OLD | NEW |
---|---|
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 // | 4 // |
5 // This file implements the Windows service controlling Me2Me host processes | 5 // This file implements the Windows service controlling Me2Me host processes |
6 // running within user sessions. | 6 // running within user sessions. |
7 | 7 |
8 #include "remoting/host/win/wts_session_process_launcher.h" | 8 #include "remoting/host/win/wts_session_process_launcher.h" |
9 | 9 |
10 #include <windows.h> | 10 #include <windows.h> |
(...skipping 29 matching lines...) Expand all Loading... | |
40 namespace { | 40 namespace { |
41 | 41 |
42 // The minimum and maximum delays between attempts to inject host process into | 42 // The minimum and maximum delays between attempts to inject host process into |
43 // a session. | 43 // a session. |
44 const int kMaxLaunchDelaySeconds = 60; | 44 const int kMaxLaunchDelaySeconds = 60; |
45 const int kMinLaunchDelaySeconds = 1; | 45 const int kMinLaunchDelaySeconds = 1; |
46 | 46 |
47 const FilePath::CharType kMe2meHostBinaryName[] = | 47 const FilePath::CharType kMe2meHostBinaryName[] = |
48 FILE_PATH_LITERAL("remoting_me2me_host.exe"); | 48 FILE_PATH_LITERAL("remoting_me2me_host.exe"); |
49 | 49 |
50 const FilePath::CharType kMe2meServiceBinaryName[] = | |
51 FILE_PATH_LITERAL("remoting_service.exe"); | |
52 | |
50 // The IPC channel name is passed to the host in the command line. | 53 // The IPC channel name is passed to the host in the command line. |
51 const char kChromotingIpcSwitchName[] = "chromoting-ipc"; | 54 const char kChromotingIpcSwitchName[] = "chromoting-ipc"; |
52 | 55 |
56 const char kElevateSwitchName[] = "elevate"; | |
57 | |
53 // The command line parameters that should be copied from the service's command | 58 // The command line parameters that should be copied from the service's command |
54 // line to the host process. | 59 // line to the host process. |
55 const char* kCopiedSwitchNames[] = { | 60 const char* kCopiedSwitchNames[] = { |
56 "auth-config", "host-config", switches::kV, switches::kVModule }; | 61 "auth-config", "host-config", switches::kV, switches::kVModule }; |
57 | 62 |
58 // The security descriptor of the Chromoting IPC channel. It gives full access | 63 // The security descriptor of the Chromoting IPC channel. It gives full access |
59 // to LocalSystem and denies access by anyone else. | 64 // to LocalSystem and denies access by anyone else. |
60 const char kChromotingChannelSecurityDescriptor[] = "O:SYG:SYD:(A;;GA;;;SY)"; | 65 const char kChromotingChannelSecurityDescriptor[] = "O:SYG:SYD:(A;;GA;;;SY)"; |
61 | 66 |
62 } // namespace | 67 } // namespace |
63 | 68 |
64 namespace remoting { | 69 namespace remoting { |
65 | 70 |
66 WtsSessionProcessLauncher::WtsSessionProcessLauncher( | 71 WtsSessionProcessLauncher::WtsSessionProcessLauncher( |
67 const base::Closure& stopped_callback, | 72 const base::Closure& stopped_callback, |
68 WtsConsoleMonitor* monitor, | 73 WtsConsoleMonitor* monitor, |
69 scoped_refptr<base::SingleThreadTaskRunner> main_message_loop, | 74 scoped_refptr<base::SingleThreadTaskRunner> main_message_loop, |
70 scoped_refptr<base::SingleThreadTaskRunner> ipc_message_loop) | 75 scoped_refptr<base::SingleThreadTaskRunner> ipc_message_loop) |
71 : Stoppable(main_message_loop, stopped_callback), | 76 : Stoppable(main_message_loop, stopped_callback), |
72 attached_(false), | 77 attached_(false), |
73 main_message_loop_(main_message_loop), | 78 main_message_loop_(main_message_loop), |
74 ipc_message_loop_(ipc_message_loop), | 79 ipc_message_loop_(ipc_message_loop), |
75 monitor_(monitor) { | 80 monitor_(monitor), |
81 job_state_(kJobUninitialized) { | |
76 monitor_->AddWtsConsoleObserver(this); | 82 monitor_->AddWtsConsoleObserver(this); |
83 | |
84 process_exit_event_.Set(CreateEvent(NULL, TRUE, FALSE, NULL)); | |
85 CHECK(process_exit_event_.IsValid()); | |
86 | |
87 // Job object initalization has to be performed on the I/O thread because | |
88 // MessageLoopForIO::RegisterJobObject() can only be called via | |
89 // MessageLoopForIO::current(). | |
Wez
2012/08/15 23:22:20
If that's the only limitation then why not move Re
alexeypa (please no reviews)
2012/08/15 23:55:50
It does not belong there (and to MessageLoopProxy
Wez
2012/08/16 00:06:33
OK, so the comment should state that job initializ
alexeypa (please no reviews)
2012/08/16 00:34:10
Done.
| |
90 ipc_message_loop_->PostTask(FROM_HERE, base::Bind( | |
91 &WtsSessionProcessLauncher::InitializeJob, | |
92 base::Unretained(this))); | |
77 } | 93 } |
78 | 94 |
79 WtsSessionProcessLauncher::~WtsSessionProcessLauncher() { | 95 WtsSessionProcessLauncher::~WtsSessionProcessLauncher() { |
80 monitor_->RemoveWtsConsoleObserver(this); | 96 monitor_->RemoveWtsConsoleObserver(this); |
81 | 97 |
82 DCHECK(!attached_); | 98 // Failing this check means that the object hasn't been shutdown correctly and |
83 DCHECK(!timer_.IsRunning()); | 99 // there still could be pending tasks posted to it. |
Wez
2012/08/15 23:22:20
nit: You're checking this here, in addition to the
alexeypa (please no reviews)
2012/08/15 23:55:50
I'm duplicating this check for a reason. First, Wt
Wez
2012/08/16 00:06:33
Right, so the comment should simply clarify that w
alexeypa (please no reviews)
2012/08/16 00:34:10
Done.
| |
100 CHECK_EQ(stoppable_state(), Stoppable::kStopped); | |
Wez
2012/08/15 23:22:20
nit: Doesn't this check belong before the RemoveWt
alexeypa (please no reviews)
2012/08/15 23:55:50
It does not really matter.
Wez
2012/08/16 00:06:33
Please move it before the RemoveWtsConsoleObserver
alexeypa (please no reviews)
2012/08/16 00:34:10
Done.
| |
101 | |
102 CHECK(!attached_); | |
103 CHECK(!timer_.IsRunning()); | |
84 } | 104 } |
85 | 105 |
86 void WtsSessionProcessLauncher::LaunchProcess() { | 106 void WtsSessionProcessLauncher::LaunchProcess() { |
87 DCHECK(main_message_loop_->BelongsToCurrentThread()); | 107 DCHECK(main_message_loop_->BelongsToCurrentThread()); |
88 DCHECK(attached_); | 108 DCHECK(attached_); |
89 DCHECK(launcher_.get() == NULL); | 109 DCHECK(launcher_.get() == NULL); |
90 DCHECK(!timer_.IsRunning()); | 110 DCHECK(!timer_.IsRunning()); |
91 DCHECK(!worker_process_.IsValid()); | 111 DCHECK(!worker_process_.IsValid()); |
92 | 112 |
93 launch_time_ = base::Time::Now(); | 113 launch_time_ = base::Time::Now(); |
94 launcher_.reset(new WorkerProcessLauncher( | 114 launcher_.reset(new WorkerProcessLauncher( |
95 this, | 115 this, |
96 base::Bind(&WtsSessionProcessLauncher::OnLauncherStopped, | 116 base::Bind(&WtsSessionProcessLauncher::OnLauncherStopped, |
97 base::Unretained(this)), | 117 base::Unretained(this)), |
98 main_message_loop_, | 118 main_message_loop_, |
99 ipc_message_loop_)); | 119 ipc_message_loop_)); |
100 launcher_->Start(kChromotingChannelSecurityDescriptor); | 120 launcher_->Start(kChromotingChannelSecurityDescriptor); |
101 } | 121 } |
102 | 122 |
103 void WtsSessionProcessLauncher::OnLauncherStopped() { | 123 void WtsSessionProcessLauncher::OnLauncherStopped() { |
104 DCHECK(main_message_loop_->BelongsToCurrentThread()); | 124 DCHECK(main_message_loop_->BelongsToCurrentThread()); |
105 | 125 |
106 DWORD exit_code; | 126 DWORD exit_code = CONTROL_C_EXIT; |
107 if (!::GetExitCodeProcess(worker_process_, &exit_code)) { | 127 if (worker_process_.IsValid()) { |
108 LOG_GETLASTERROR(INFO) | 128 if (!::GetExitCodeProcess(worker_process_, &exit_code)) { |
109 << "Failed to query the exit code of the worker process"; | 129 LOG_GETLASTERROR(INFO) |
110 exit_code = CONTROL_C_EXIT; | 130 << "Failed to query the exit code of the worker process"; |
131 exit_code = CONTROL_C_EXIT; | |
132 } | |
133 | |
134 worker_process_.Close(); | |
111 } | 135 } |
112 | 136 |
113 launcher_.reset(NULL); | 137 launcher_.reset(NULL); |
114 worker_process_.Close(); | |
115 | 138 |
116 // Do not relaunch the worker process if the caller has asked us to stop. | 139 // Do not relaunch the worker process if the caller has asked us to stop. |
117 if (stoppable_state() != Stoppable::kRunning) { | 140 if (stoppable_state() != Stoppable::kRunning) { |
118 CompleteStopping(); | 141 Stop(); |
119 return; | 142 return; |
120 } | 143 } |
121 | 144 |
122 // Stop trying to restart the worker process if its process exited due to | 145 // Stop trying to restart the worker process if its process exited due to |
123 // misconfiguration. | 146 // misconfiguration. |
124 if (kMinPermanentErrorExitCode <= exit_code && | 147 if (kMinPermanentErrorExitCode <= exit_code && |
125 exit_code <= kMaxPermanentErrorExitCode) { | 148 exit_code <= kMaxPermanentErrorExitCode) { |
126 Stop(); | 149 Stop(); |
127 return; | 150 return; |
128 } | 151 } |
(...skipping 12 matching lines...) Expand all Loading... | |
141 launch_backoff_ = std::min( | 164 launch_backoff_ = std::min( |
142 launch_backoff_, TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)); | 165 launch_backoff_, TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)); |
143 } | 166 } |
144 | 167 |
145 // Try to launch the worker process. | 168 // Try to launch the worker process. |
146 timer_.Start(FROM_HERE, launch_backoff_, | 169 timer_.Start(FROM_HERE, launch_backoff_, |
147 this, &WtsSessionProcessLauncher::LaunchProcess); | 170 this, &WtsSessionProcessLauncher::LaunchProcess); |
148 } | 171 } |
149 } | 172 } |
150 | 173 |
174 void WtsSessionProcessLauncher::OnIOCompleted( | |
175 base::MessagePumpForIO::IOContext* context, | |
176 DWORD bytes_transferred, | |
177 DWORD error) { | |
178 DCHECK(ipc_message_loop_->BelongsToCurrentThread()); | |
179 | |
180 // |bytes_transferred| is used in job object notifications to supply | |
181 // the message ID. | |
182 if (bytes_transferred == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO) | |
183 CHECK(SetEvent(process_exit_event_)); | |
184 } | |
185 | |
151 bool WtsSessionProcessLauncher::DoLaunchProcess( | 186 bool WtsSessionProcessLauncher::DoLaunchProcess( |
152 const std::string& channel_name, | 187 const std::string& channel_name, |
153 ScopedHandle* process_exit_event_out) { | 188 ScopedHandle* process_exit_event_out) { |
154 DCHECK(main_message_loop_->BelongsToCurrentThread()); | 189 DCHECK(main_message_loop_->BelongsToCurrentThread()); |
155 DCHECK(!worker_process_.IsValid()); | 190 DCHECK(!worker_process_.IsValid()); |
156 | 191 |
192 // The job object is not ready. Retry starting the host process later. | |
193 if (!job_.IsValid()) { | |
194 return false; | |
195 } | |
196 | |
157 // Construct the host binary name. | 197 // Construct the host binary name. |
158 FilePath dir_path; | 198 FilePath dir_path; |
159 if (!PathService::Get(base::DIR_EXE, &dir_path)) { | 199 if (!PathService::Get(base::DIR_EXE, &dir_path)) { |
160 LOG(ERROR) << "Failed to get the executable file name."; | 200 LOG(ERROR) << "Failed to get the executable file name."; |
161 return false; | 201 return false; |
162 } | 202 } |
163 FilePath host_binary = dir_path.Append(kMe2meHostBinaryName); | 203 FilePath host_binary = dir_path.Append(kMe2meHostBinaryName); |
204 FilePath service_binary = dir_path.Append(kMe2meServiceBinaryName); | |
164 | 205 |
165 // Create the host process command line passing the name of the IPC channel | 206 // Create the host process command line passing the name of the IPC channel |
166 // to use and copying known switches from the service's command line. | 207 // to use and copying known switches from the service's command line. |
167 CommandLine command_line(host_binary); | 208 CommandLine command_line(service_binary); |
209 command_line.AppendSwitchPath(kElevateSwitchName, host_binary); | |
168 command_line.AppendSwitchNative(kChromotingIpcSwitchName, | 210 command_line.AppendSwitchNative(kChromotingIpcSwitchName, |
169 UTF8ToWide(channel_name)); | 211 UTF8ToWide(channel_name)); |
170 command_line.CopySwitchesFrom(*CommandLine::ForCurrentProcess(), | 212 command_line.CopySwitchesFrom(*CommandLine::ForCurrentProcess(), |
171 kCopiedSwitchNames, | 213 kCopiedSwitchNames, |
172 _countof(kCopiedSwitchNames)); | 214 _countof(kCopiedSwitchNames)); |
173 | 215 |
216 CHECK(ResetEvent(process_exit_event_)); | |
217 | |
174 // Try to launch the process and attach an object watcher to the returned | 218 // Try to launch the process and attach an object watcher to the returned |
175 // handle so that we get notified when the process terminates. | 219 // handle so that we get notified when the process terminates. |
176 if (!LaunchProcessWithToken(host_binary, | 220 base::win::ScopedHandle worker_process; |
221 base::win::ScopedHandle worker_thread; | |
222 if (!LaunchProcessWithToken(service_binary, | |
177 command_line.GetCommandLineString(), | 223 command_line.GetCommandLineString(), |
178 session_token_, | 224 session_token_, |
179 &worker_process_)) { | 225 CREATE_SUSPENDED, |
226 &worker_process, | |
227 &worker_thread)) { | |
228 return false; | |
229 } | |
230 | |
231 if (!AssignProcessToJobObject(job_, worker_process)) { | |
232 LOG_GETLASTERROR(ERROR) << "Failed to assign the worker to the job object"; | |
233 TerminateProcess(worker_process, CONTROL_C_EXIT); | |
234 return false; | |
235 } | |
236 | |
237 if (!ResumeThread(worker_thread)) { | |
238 LOG_GETLASTERROR(ERROR) << "Failed to resume the worker thread"; | |
239 DoKillProcess(CONTROL_C_EXIT); | |
180 return false; | 240 return false; |
181 } | 241 } |
182 | 242 |
183 ScopedHandle process_exit_event; | 243 ScopedHandle process_exit_event; |
184 if (!DuplicateHandle(GetCurrentProcess(), | 244 if (!DuplicateHandle(GetCurrentProcess(), |
185 worker_process_, | 245 process_exit_event_, |
186 GetCurrentProcess(), | 246 GetCurrentProcess(), |
187 process_exit_event.Receive(), | 247 process_exit_event.Receive(), |
188 SYNCHRONIZE, | 248 SYNCHRONIZE, |
189 FALSE, | 249 FALSE, |
190 0)) { | 250 0)) { |
191 LOG_GETLASTERROR(ERROR) << "Failed to duplicate a handle"; | 251 LOG_GETLASTERROR(ERROR) << "Failed to duplicate a handle"; |
192 DoKillProcess(CONTROL_C_EXIT); | 252 DoKillProcess(CONTROL_C_EXIT); |
193 return false; | 253 return false; |
194 } | 254 } |
195 | 255 |
196 *process_exit_event_out = process_exit_event.Pass(); | 256 *process_exit_event_out = process_exit_event.Pass(); |
197 return true; | 257 return true; |
198 } | 258 } |
199 | 259 |
200 void WtsSessionProcessLauncher::DoKillProcess(DWORD exit_code) { | 260 void WtsSessionProcessLauncher::DoKillProcess(DWORD exit_code) { |
201 DCHECK(main_message_loop_->BelongsToCurrentThread()); | 261 DCHECK(main_message_loop_->BelongsToCurrentThread()); |
202 | 262 |
203 if (worker_process_.IsValid()) { | 263 if (job_.IsValid()) { |
204 TerminateProcess(worker_process_, exit_code); | 264 TerminateJobObject(job_, exit_code); |
205 } | 265 } |
206 } | 266 } |
207 | 267 |
208 void WtsSessionProcessLauncher::OnChannelConnected(DWORD peer_pid) { | 268 void WtsSessionProcessLauncher::OnChannelConnected(DWORD peer_pid) { |
209 DCHECK(main_message_loop_->BelongsToCurrentThread()); | 269 DCHECK(main_message_loop_->BelongsToCurrentThread()); |
210 | 270 |
211 DWORD expected_pid = GetProcessId(worker_process_); | 271 // Nothing to validate. Only processes running as SYSTEM can connect to |
212 if (peer_pid != expected_pid) { | 272 // the pipe, so if it was a malicious process the game would be over already. |
213 LOG(ERROR) | 273 |
214 << "Unexpected client connected: expected=" << expected_pid | 274 // Get a handle to the peer process so we could query its exit code later. |
215 << ", actual=" << peer_pid; | 275 // Ignore the error. Generic |CONTROL_C_EXIT| will be used if the process |
Wez
2012/08/15 23:22:20
nit: What does "will be used" mean here? You mean
alexeypa (please no reviews)
2012/08/15 23:55:50
OpenProcess can fail. I explain why this error is
Wez
2012/08/16 00:06:33
My point is just that the explanation is not clear
alexeypa (please no reviews)
2012/08/16 00:34:10
Done.
| |
216 Stop(); | 276 // cannot be opened. |
217 } | 277 worker_process_.Set(OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, peer_pid)); |
218 } | 278 } |
219 | 279 |
220 bool WtsSessionProcessLauncher::OnMessageReceived(const IPC::Message& message) { | 280 bool WtsSessionProcessLauncher::OnMessageReceived(const IPC::Message& message) { |
221 DCHECK(main_message_loop_->BelongsToCurrentThread()); | 281 DCHECK(main_message_loop_->BelongsToCurrentThread()); |
222 | 282 |
223 bool handled = true; | 283 bool handled = true; |
224 IPC_BEGIN_MESSAGE_MAP(WtsSessionProcessLauncher, message) | 284 IPC_BEGIN_MESSAGE_MAP(WtsSessionProcessLauncher, message) |
225 IPC_MESSAGE_HANDLER(ChromotingHostMsg_SendSasToConsole, | 285 IPC_MESSAGE_HANDLER(ChromotingHostMsg_SendSasToConsole, |
226 OnSendSasToConsole) | 286 OnSendSasToConsole) |
227 IPC_MESSAGE_UNHANDLED(handled = false) | 287 IPC_MESSAGE_UNHANDLED(handled = false) |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
277 } | 337 } |
278 } | 338 } |
279 | 339 |
280 void WtsSessionProcessLauncher::DoStop() { | 340 void WtsSessionProcessLauncher::DoStop() { |
281 DCHECK(main_message_loop_->BelongsToCurrentThread()); | 341 DCHECK(main_message_loop_->BelongsToCurrentThread()); |
282 | 342 |
283 if (attached_) { | 343 if (attached_) { |
284 OnSessionDetached(); | 344 OnSessionDetached(); |
285 } | 345 } |
286 | 346 |
287 if (launcher_.get() == NULL) { | 347 job_.Close(); |
288 CompleteStopping(); | 348 |
349 // Drain the completion queue to make sure all job object notification have | |
350 // been received. | |
351 if (job_state_ == kJobRunning) { | |
352 job_state_ = kJobStopping; | |
353 ipc_message_loop_->PostTask(FROM_HERE, base::Bind( | |
354 &WtsSessionProcessLauncher::DrainJobNotifications, | |
355 base::Unretained(this))); | |
356 } | |
357 | |
358 // Don't complete shutdown if |launcher_| is not completely stopped. | |
359 if (launcher_.get() != NULL) { | |
360 return; | |
361 } | |
362 | |
363 // Don't complete shutdown if the completion queue hasn't been drained. | |
364 if (job_state_ != kJobUninitialized && job_state_ != kJobStopped) { | |
365 return; | |
366 } | |
367 | |
368 CompleteStopping(); | |
369 } | |
370 | |
371 void WtsSessionProcessLauncher::DrainJobNotifications() { | |
372 DCHECK(ipc_message_loop_->BelongsToCurrentThread()); | |
373 | |
374 // DrainJobNotifications() is posted after the job object is destroyed, so | |
375 // by this time all notifications from the job object have been processed | |
376 // already. Let the main thread know that the queue has been drained. | |
377 main_message_loop_->PostTask(FROM_HERE, base::Bind( | |
378 &WtsSessionProcessLauncher::DrainJobNotificationsCompleted, | |
379 base::Unretained(this))); | |
380 } | |
381 | |
382 void WtsSessionProcessLauncher::DrainJobNotificationsCompleted() { | |
383 DCHECK(main_message_loop_->BelongsToCurrentThread()); | |
384 DCHECK_EQ(job_state_, kJobStopping); | |
385 | |
386 job_state_ = kJobStopped; | |
387 Stop(); | |
388 } | |
389 | |
390 void WtsSessionProcessLauncher::InitializeJob() { | |
391 DCHECK(ipc_message_loop_->BelongsToCurrentThread()); | |
392 | |
393 ScopedHandle job; | |
394 job.Set(CreateJobObject(NULL, NULL)); | |
395 if (!job.IsValid()) { | |
396 LOG_GETLASTERROR(ERROR) << "Failed to create a job object"; | |
397 return; | |
398 } | |
399 | |
400 // Limit the number of active processes in the job to two (the process | |
401 // performing elevation and the host) and make sure that all processes will be | |
402 // killed once the job object is destroyed. | |
403 JOBOBJECT_EXTENDED_LIMIT_INFORMATION info; | |
404 memset(&info, 0, sizeof(info)); | |
405 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_ACTIVE_PROCESS | | |
406 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; | |
407 info.BasicLimitInformation.ActiveProcessLimit = 2; | |
408 if (!SetInformationJobObject(job, | |
409 JobObjectExtendedLimitInformation, | |
410 &info, | |
411 sizeof(info))) { | |
412 LOG_GETLASTERROR(ERROR) << "Failed to set limits on the job object"; | |
413 return; | |
414 } | |
415 | |
416 // Register the job object with the completion port in the I/O thread to | |
417 // receive job notifications. | |
418 if (!MessageLoopForIO::current()->RegisterJobObject(job, this)) { | |
419 LOG_GETLASTERROR(ERROR) | |
420 << "Failed to associate the job object with a completion port"; | |
421 return; | |
422 } | |
423 | |
424 // ScopedHandle is not compatible with base::Passed, so we wrap it to a scoped | |
425 // pointer. | |
426 scoped_ptr<ScopedHandle> job_wrapper(new ScopedHandle()); | |
427 *job_wrapper = job.Pass(); | |
428 | |
429 // Let the main thread know that initialization is complete. | |
430 main_message_loop_->PostTask(FROM_HERE, base::Bind( | |
431 &WtsSessionProcessLauncher::InitializeJobCompleted, | |
432 base::Unretained(this), base::Passed(&job_wrapper))); | |
433 } | |
434 | |
435 void WtsSessionProcessLauncher::InitializeJobCompleted( | |
436 scoped_ptr<ScopedHandle> job) { | |
437 DCHECK(main_message_loop_->BelongsToCurrentThread()); | |
438 DCHECK(!job_.IsValid()); | |
439 DCHECK_EQ(job_state_, kJobUninitialized); | |
440 | |
441 if (stoppable_state() == Stoppable::kRunning) { | |
442 job_ = job->Pass(); | |
443 job_state_ = kJobRunning; | |
289 } | 444 } |
290 } | 445 } |
291 | 446 |
292 } // namespace remoting | 447 } // namespace remoting |
OLD | NEW |