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/wts_session_process_launcher_win.h" | 8 #include "remoting/host/wts_session_process_launcher_win.h" |
9 | 9 |
10 #include <windows.h> | 10 #include <windows.h> |
11 #include <sddl.h> | 11 #include <sddl.h> |
12 #include <limits> | |
13 | 12 |
| 13 #include "base/bind.h" |
| 14 #include "base/bind_helpers.h" |
14 #include "base/logging.h" | 15 #include "base/logging.h" |
15 #include "base/process_util.h" | 16 #include "base/process_util.h" |
16 #include "base/rand_util.h" | 17 #include "base/rand_util.h" |
17 #include "base/string16.h" | 18 #include "base/string16.h" |
18 #include "base/stringprintf.h" | 19 #include "base/stringprintf.h" |
19 #include "base/threading/thread.h" | 20 #include "base/threading/thread.h" |
20 #include "base/utf_string_conversions.h" | 21 #include "base/utf_string_conversions.h" |
21 #include "base/win/scoped_handle.h" | 22 #include "base/win/scoped_handle.h" |
22 #include "ipc/ipc_channel_proxy.h" | 23 #include "ipc/ipc_channel_proxy.h" |
23 #include "ipc/ipc_message.h" | 24 #include "ipc/ipc_message.h" |
24 #include "ipc/ipc_message_macros.h" | 25 #include "ipc/ipc_message_macros.h" |
25 | 26 |
26 #include "remoting/host/chromoting_messages.h" | 27 #include "remoting/host/chromoting_messages.h" |
27 #include "remoting/host/sas_injector.h" | 28 #include "remoting/host/sas_injector.h" |
| 29 #include "remoting/host/worker_process_launcher.h" |
28 #include "remoting/host/wts_console_monitor_win.h" | 30 #include "remoting/host/wts_console_monitor_win.h" |
29 | 31 |
30 using base::win::ScopedHandle; | 32 using base::win::ScopedHandle; |
31 using base::TimeDelta; | 33 using base::TimeDelta; |
32 | 34 |
33 namespace { | 35 namespace { |
34 | 36 |
35 // The minimum and maximum delays between attempts to inject host process into | 37 // The minimum and maximum delays between attempts to inject host process into |
36 // a session. | 38 // a session. |
37 const int kMaxLaunchDelaySeconds = 60; | 39 const int kMaxLaunchDelaySeconds = 60; |
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
130 sizeof(new_session_id))) { | 132 sizeof(new_session_id))) { |
131 LOG_GETLASTERROR(ERROR) << | 133 LOG_GETLASTERROR(ERROR) << |
132 "Failed to change session ID of a token"; | 134 "Failed to change session ID of a token"; |
133 return false; | 135 return false; |
134 } | 136 } |
135 | 137 |
136 token_out->Set(session_token.Take()); | 138 token_out->Set(session_token.Take()); |
137 return true; | 139 return true; |
138 } | 140 } |
139 | 141 |
140 // Generates random channel ID. | |
141 // N.B. Stolen from src/content/common/child_process_host_impl.cc | |
142 string16 GenerateRandomChannelId(void* instance) { | |
143 return base::StringPrintf(ASCIIToUTF16("%d.%p.%d").c_str(), | |
144 base::GetCurrentProcId(), instance, | |
145 base::RandInt(0, std::numeric_limits<int>::max())); | |
146 } | |
147 | |
148 // Creates the server end of the Chromoting IPC channel. | |
149 // N.B. This code is based on IPC::Channel's implementation. | |
150 bool CreatePipeForIpcChannel(void* instance, | |
151 string16* channel_name_out, | |
152 ScopedHandle* pipe_out) { | |
153 // Create security descriptor for the channel. | |
154 SECURITY_ATTRIBUTES security_attributes; | |
155 security_attributes.nLength = sizeof(security_attributes); | |
156 security_attributes.bInheritHandle = FALSE; | |
157 | |
158 ULONG security_descriptor_length = 0; | |
159 if (!ConvertStringSecurityDescriptorToSecurityDescriptorA( | |
160 kChromotingChannelSecurityDescriptor, | |
161 SDDL_REVISION_1, | |
162 reinterpret_cast<PSECURITY_DESCRIPTOR*>( | |
163 &security_attributes.lpSecurityDescriptor), | |
164 &security_descriptor_length)) { | |
165 LOG_GETLASTERROR(ERROR) << | |
166 "Failed to create a security descriptor for the Chromoting IPC channel"; | |
167 return false; | |
168 } | |
169 | |
170 // Generate a random channel name. | |
171 string16 channel_name(GenerateRandomChannelId(instance)); | |
172 | |
173 // Convert it to the pipe name. | |
174 string16 pipe_name(ASCIIToUTF16(kChromePipeNamePrefix)); | |
175 pipe_name.append(channel_name); | |
176 | |
177 // Create the server end of the pipe. This code should match the code in | |
178 // IPC::Channel with exception of passing a non-default security descriptor. | |
179 HANDLE pipe = CreateNamedPipeW(pipe_name.c_str(), | |
180 PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | | |
181 FILE_FLAG_FIRST_PIPE_INSTANCE, | |
182 PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, | |
183 1, | |
184 IPC::Channel::kReadBufferSize, | |
185 IPC::Channel::kReadBufferSize, | |
186 5000, | |
187 &security_attributes); | |
188 if (pipe == INVALID_HANDLE_VALUE) { | |
189 LOG_GETLASTERROR(ERROR) << | |
190 "Failed to create the server end of the Chromoting IPC channel"; | |
191 LocalFree(security_attributes.lpSecurityDescriptor); | |
192 return false; | |
193 } | |
194 | |
195 LocalFree(security_attributes.lpSecurityDescriptor); | |
196 | |
197 *channel_name_out = channel_name; | |
198 pipe_out->Set(pipe); | |
199 return true; | |
200 } | |
201 | |
202 // Launches |binary| in the security context of the supplied |user_token|. | 142 // Launches |binary| in the security context of the supplied |user_token|. |
203 bool LaunchProcessAsUser(const FilePath& binary, | 143 bool LaunchProcessAsUser(const FilePath& binary, |
204 const string16& command_line, | 144 const string16& command_line, |
205 HANDLE user_token, | 145 HANDLE user_token, |
206 base::Process* process_out) { | 146 base::Process* process_out) { |
207 string16 application_name = binary.value(); | 147 string16 application_name = binary.value(); |
208 string16 desktop = ASCIIToUTF16(kDefaultDesktopName); | 148 string16 desktop = ASCIIToUTF16(kDefaultDesktopName); |
209 | 149 |
210 PROCESS_INFORMATION process_info; | 150 PROCESS_INFORMATION process_info; |
211 STARTUPINFOW startup_info; | 151 STARTUPINFOW startup_info; |
(...skipping 23 matching lines...) Expand all Loading... |
235 return true; | 175 return true; |
236 } | 176 } |
237 | 177 |
238 } // namespace | 178 } // namespace |
239 | 179 |
240 namespace remoting { | 180 namespace remoting { |
241 | 181 |
242 WtsSessionProcessLauncher::WtsSessionProcessLauncher( | 182 WtsSessionProcessLauncher::WtsSessionProcessLauncher( |
243 WtsConsoleMonitor* monitor, | 183 WtsConsoleMonitor* monitor, |
244 const FilePath& host_binary, | 184 const FilePath& host_binary, |
245 base::Thread* io_thread) | 185 base::MessageLoopProxy* ipc_message_loop) |
246 : host_binary_(host_binary), | 186 : host_binary_(host_binary), |
247 io_thread_(io_thread), | 187 ipc_message_loop_(ipc_message_loop), |
248 monitor_(monitor), | 188 monitor_(monitor) { |
249 state_(StateDetached) { | |
250 monitor_->AddWtsConsoleObserver(this); | 189 monitor_->AddWtsConsoleObserver(this); |
251 } | 190 } |
252 | 191 |
253 WtsSessionProcessLauncher::~WtsSessionProcessLauncher() { | 192 WtsSessionProcessLauncher::~WtsSessionProcessLauncher() { |
254 DCHECK(state_ == StateDetached); | |
255 DCHECK(!timer_.IsRunning()); | 193 DCHECK(!timer_.IsRunning()); |
256 DCHECK(process_.handle() == NULL); | |
257 DCHECK(process_watcher_.GetWatchedObject() == NULL); | |
258 DCHECK(chromoting_channel_.get() == NULL); | |
259 | 194 |
260 monitor_->RemoveWtsConsoleObserver(this); | 195 monitor_->RemoveWtsConsoleObserver(this); |
261 } | 196 } |
262 | 197 |
| 198 // Creates the server end of the Chromoting IPC channel. |
| 199 // N.B. This code is based on IPC::Channel's implementation. |
| 200 bool WtsSessionProcessLauncher::CreateChannelHandle( |
| 201 const std::string& channel_name, |
| 202 IPC::ChannelHandle* channel_handle_out) { |
| 203 |
| 204 // Create security descriptor for the channel. |
| 205 SECURITY_ATTRIBUTES security_attributes; |
| 206 security_attributes.nLength = sizeof(security_attributes); |
| 207 security_attributes.bInheritHandle = FALSE; |
| 208 |
| 209 ULONG security_descriptor_length = 0; |
| 210 if (!ConvertStringSecurityDescriptorToSecurityDescriptorA( |
| 211 kChromotingChannelSecurityDescriptor, |
| 212 SDDL_REVISION_1, |
| 213 reinterpret_cast<PSECURITY_DESCRIPTOR*>( |
| 214 &security_attributes.lpSecurityDescriptor), |
| 215 &security_descriptor_length)) { |
| 216 LOG_GETLASTERROR(ERROR) << |
| 217 "Failed to create a security descriptor for the Chromoting IPC channel"; |
| 218 return false; |
| 219 } |
| 220 |
| 221 // Convert it to the pipe name. |
| 222 string16 pipe_name(ASCIIToUTF16(kChromePipeNamePrefix)); |
| 223 pipe_name.append(ASCIIToUTF16(channel_name)); |
| 224 |
| 225 // Create the server end of the pipe. This code should match the code in |
| 226 // IPC::Channel with exception of passing a non-default security descriptor. |
| 227 ipc_pipe_.Set(CreateNamedPipeW(pipe_name.c_str(), |
| 228 PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | |
| 229 FILE_FLAG_FIRST_PIPE_INSTANCE, |
| 230 PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, |
| 231 1, |
| 232 IPC::Channel::kReadBufferSize, |
| 233 IPC::Channel::kReadBufferSize, |
| 234 5000, |
| 235 &security_attributes)); |
| 236 if (!ipc_pipe_.IsValid()) { |
| 237 LOG_GETLASTERROR(ERROR) << |
| 238 "Failed to create the server end of the Chromoting IPC channel"; |
| 239 LocalFree(security_attributes.lpSecurityDescriptor); |
| 240 return false; |
| 241 } |
| 242 |
| 243 LocalFree(security_attributes.lpSecurityDescriptor); |
| 244 |
| 245 *channel_handle_out = IPC::ChannelHandle(ipc_pipe_.Get()); |
| 246 return true; |
| 247 |
| 248 } |
| 249 |
| 250 bool WtsSessionProcessLauncher::LaunchWorker(const std::string& channel_name, |
| 251 base::Process* process_out) { |
| 252 |
| 253 string16 command_line = |
| 254 base::StringPrintf(ASCIIToUTF16(kHostProcessCommandLineFormat).c_str(), |
| 255 host_binary_.value().c_str(), |
| 256 ASCIIToUTF16(channel_name).c_str()); |
| 257 |
| 258 return LaunchProcessAsUser(host_binary_, command_line, session_token_, |
| 259 process_out); |
| 260 } |
| 261 |
263 void WtsSessionProcessLauncher::LaunchProcess() { | 262 void WtsSessionProcessLauncher::LaunchProcess() { |
264 DCHECK(state_ == StateStarting); | |
265 DCHECK(!timer_.IsRunning()); | 263 DCHECK(!timer_.IsRunning()); |
266 DCHECK(process_.handle() == NULL); | |
267 DCHECK(process_watcher_.GetWatchedObject() == NULL); | |
268 DCHECK(chromoting_channel_.get() == NULL); | |
269 | 264 |
270 launch_time_ = base::Time::Now(); | 265 launch_time_ = base::Time::Now(); |
| 266 launcher_ = WorkerProcessLauncher::Create(ipc_message_loop_.get()); |
| 267 bool success = |
| 268 launcher_->Start( |
| 269 this, |
| 270 base::Bind(&WtsSessionProcessLauncher::CreateChannelHandle, |
| 271 base::Unretained(this)), |
| 272 base::Bind(&WtsSessionProcessLauncher::LaunchWorker, |
| 273 base::Unretained(this))); |
271 | 274 |
272 string16 channel_name; | 275 // The pipe handle has been duplicated by the IPC::Channel. We can close our |
273 ScopedHandle pipe; | 276 // copy now. |
274 if (CreatePipeForIpcChannel(this, &channel_name, &pipe)) { | 277 ipc_pipe_.Close(); |
275 // Wrap the pipe into an IPC channel. | |
276 chromoting_channel_.reset(new IPC::ChannelProxy( | |
277 IPC::ChannelHandle(pipe.Get()), | |
278 IPC::Channel::MODE_SERVER, | |
279 this, | |
280 io_thread_->message_loop_proxy().get())); | |
281 | 278 |
282 string16 command_line = | 279 if (!success) { |
283 base::StringPrintf(ASCIIToUTF16(kHostProcessCommandLineFormat).c_str(), | 280 // Something went wrong. Try to launch the host again later. The attempts |
284 host_binary_.value().c_str(), | 281 // rate is limited by exponential backoff. |
285 channel_name.c_str()); | 282 launch_backoff_ = std::max(launch_backoff_ * 2, |
286 | 283 TimeDelta::FromSeconds(kMinLaunchDelaySeconds)); |
287 // Try to launch the process and attach an object watcher to the returned | 284 launch_backoff_ = std::min(launch_backoff_, |
288 // handle so that we get notified when the process terminates. | 285 TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)); |
289 if (LaunchProcessAsUser(host_binary_, command_line, session_token_, | 286 timer_.Start(FROM_HERE, launch_backoff_, |
290 &process_)) { | 287 this, &WtsSessionProcessLauncher::LaunchProcess); |
291 if (process_watcher_.StartWatching(process_.handle(), this)) { | |
292 state_ = StateAttached; | |
293 return; | |
294 } else { | |
295 LOG(ERROR) << "Failed to arm the process watcher."; | |
296 process_.Terminate(0); | |
297 process_.Close(); | |
298 } | |
299 } | |
300 | |
301 chromoting_channel_.reset(); | |
302 } | 288 } |
303 | |
304 // Something went wrong. Try to launch the host again later. The attempts rate | |
305 // is limited by exponential backoff. | |
306 launch_backoff_ = std::max(launch_backoff_ * 2, | |
307 TimeDelta::FromSeconds(kMinLaunchDelaySeconds)); | |
308 launch_backoff_ = std::min(launch_backoff_, | |
309 TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)); | |
310 timer_.Start(FROM_HERE, launch_backoff_, | |
311 this, &WtsSessionProcessLauncher::LaunchProcess); | |
312 } | 289 } |
313 | 290 |
314 void WtsSessionProcessLauncher::OnObjectSignaled(HANDLE object) { | 291 bool WtsSessionProcessLauncher::OnMessageReceived(const IPC::Message& message) { |
315 DCHECK(state_ == StateAttached); | 292 bool handled = true; |
| 293 IPC_BEGIN_MESSAGE_MAP(WtsSessionProcessLauncher, message) |
| 294 IPC_MESSAGE_HANDLER(ChromotingHostMsg_SendSasToConsole, |
| 295 OnSendSasToConsole) |
| 296 IPC_MESSAGE_UNHANDLED(handled = false) |
| 297 IPC_END_MESSAGE_MAP() |
| 298 return handled; |
| 299 } |
| 300 |
| 301 void WtsSessionProcessLauncher::OnChannelError() { |
316 DCHECK(!timer_.IsRunning()); | 302 DCHECK(!timer_.IsRunning()); |
317 DCHECK(process_.handle() != NULL); | |
318 DCHECK(process_watcher_.GetWatchedObject() == NULL); | |
319 DCHECK(chromoting_channel_.get() != NULL); | |
320 | |
321 // The host process has been terminated for some reason. The handle can now be | |
322 // closed. | |
323 process_.Close(); | |
324 chromoting_channel_.reset(); | |
325 | 303 |
326 // Expand the backoff interval if the process has died quickly or reset it if | 304 // Expand the backoff interval if the process has died quickly or reset it if |
327 // it was up longer than the maximum backoff delay. | 305 // the process was up longer than the maximum backoff delay. |
328 base::TimeDelta delta = base::Time::Now() - launch_time_; | 306 base::TimeDelta delta = base::Time::Now() - launch_time_; |
329 if (delta < base::TimeDelta() || | 307 if (delta < base::TimeDelta() || |
330 delta >= base::TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)) { | 308 delta >= base::TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)) { |
331 launch_backoff_ = base::TimeDelta(); | 309 launch_backoff_ = base::TimeDelta(); |
332 } else { | 310 } else { |
333 launch_backoff_ = std::max(launch_backoff_ * 2, | 311 launch_backoff_ = std::max(launch_backoff_ * 2, |
334 TimeDelta::FromSeconds(kMinLaunchDelaySeconds)); | 312 TimeDelta::FromSeconds(kMinLaunchDelaySeconds)); |
335 launch_backoff_ = std::min(launch_backoff_, | 313 launch_backoff_ = std::min(launch_backoff_, |
336 TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)); | 314 TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)); |
337 } | 315 } |
338 | 316 |
339 // Try to restart the host. | 317 // Try to restart the host. |
340 state_ = StateStarting; | |
341 timer_.Start(FROM_HERE, launch_backoff_, | 318 timer_.Start(FROM_HERE, launch_backoff_, |
342 this, &WtsSessionProcessLauncher::LaunchProcess); | 319 this, &WtsSessionProcessLauncher::LaunchProcess); |
343 } | 320 } |
344 | 321 |
345 bool WtsSessionProcessLauncher::OnMessageReceived(const IPC::Message& message) { | 322 void WtsSessionProcessLauncher::OnSendSasToConsole() { |
346 bool handled = true; | 323 if (sas_injector_.get() == NULL) { |
347 IPC_BEGIN_MESSAGE_MAP(WtsSessionProcessLauncher, message) | 324 sas_injector_ = SasInjector::Create(); |
348 IPC_MESSAGE_HANDLER(ChromotingHostMsg_SendSasToConsole, | 325 } |
349 OnSendSasToConsole) | |
350 IPC_MESSAGE_UNHANDLED(handled = false) | |
351 IPC_END_MESSAGE_MAP() | |
352 return handled; | |
353 } | |
354 | 326 |
355 void WtsSessionProcessLauncher::OnSendSasToConsole() { | 327 if (sas_injector_.get() != NULL) { |
356 if (state_ == StateAttached) { | 328 sas_injector_->InjectSas(); |
357 if (sas_injector_.get() == NULL) { | |
358 sas_injector_ = SasInjector::Create(); | |
359 } | |
360 | |
361 if (sas_injector_.get() != NULL) { | |
362 sas_injector_->InjectSas(); | |
363 } | |
364 } | 329 } |
365 } | 330 } |
366 | 331 |
367 void WtsSessionProcessLauncher::OnSessionAttached(uint32 session_id) { | 332 void WtsSessionProcessLauncher::OnSessionAttached(uint32 session_id) { |
368 DCHECK(state_ == StateDetached); | |
369 DCHECK(!timer_.IsRunning()); | 333 DCHECK(!timer_.IsRunning()); |
370 DCHECK(process_.handle() == NULL); | |
371 DCHECK(process_watcher_.GetWatchedObject() == NULL); | |
372 DCHECK(chromoting_channel_.get() == NULL); | |
373 | 334 |
374 // Temporarily enable the SE_TCB_NAME privilege. The privileged token is | 335 // Temporarily enable the SE_TCB_NAME privilege. The privileged token is |
375 // created as needed and kept for later reuse. | 336 // created as needed and kept for later reuse. |
376 if (privileged_token_.Get() == NULL) { | 337 if (privileged_token_.Get() == NULL) { |
377 if (!CreatePrivilegedToken(&privileged_token_)) { | 338 if (!CreatePrivilegedToken(&privileged_token_)) { |
378 return; | 339 return; |
379 } | 340 } |
380 } | 341 } |
381 | 342 |
382 if (!ImpersonateLoggedOnUser(privileged_token_)) { | 343 if (!ImpersonateLoggedOnUser(privileged_token_)) { |
383 LOG_GETLASTERROR(ERROR) << | 344 LOG_GETLASTERROR(ERROR) << |
384 "Failed to impersonate the privileged token"; | 345 "Failed to impersonate the privileged token"; |
385 return; | 346 return; |
386 } | 347 } |
387 | 348 |
388 // While the SE_TCB_NAME privilege is enabled, create a session token for | 349 // While the SE_TCB_NAME privilege is enabled, create a session token for |
389 // the launched process. | 350 // the launched process. |
390 bool result = CreateSessionToken(session_id, &session_token_); | 351 bool result = CreateSessionToken(session_id, &session_token_); |
391 | 352 |
392 // Revert to the default token. The default token is sufficient to call | 353 // Revert to the default token. The default token is sufficient to call |
393 // CreateProcessAsUser() successfully. | 354 // CreateProcessAsUser() successfully. |
394 CHECK(RevertToSelf()); | 355 CHECK(RevertToSelf()); |
395 | 356 |
396 if (!result) | |
397 return; | |
398 | |
399 // Now try to launch the host. | 357 // Now try to launch the host. |
400 state_ = StateStarting; | 358 if (result) { |
401 LaunchProcess(); | 359 LaunchProcess(); |
| 360 } |
402 } | 361 } |
403 | 362 |
404 void WtsSessionProcessLauncher::OnSessionDetached() { | 363 void WtsSessionProcessLauncher::OnSessionDetached() { |
405 DCHECK(state_ == StateDetached || | 364 launch_backoff_ = base::TimeDelta(); |
406 state_ == StateStarting || | 365 timer_.Stop(); |
407 state_ == StateAttached); | 366 launcher_.reset(); |
408 | |
409 switch (state_) { | |
410 case StateDetached: | |
411 DCHECK(!timer_.IsRunning()); | |
412 DCHECK(process_.handle() == NULL); | |
413 DCHECK(process_watcher_.GetWatchedObject() == NULL); | |
414 DCHECK(chromoting_channel_.get() == NULL); | |
415 break; | |
416 | |
417 case StateStarting: | |
418 DCHECK(timer_.IsRunning()); | |
419 DCHECK(process_.handle() == NULL); | |
420 DCHECK(process_watcher_.GetWatchedObject() == NULL); | |
421 DCHECK(chromoting_channel_.get() == NULL); | |
422 | |
423 timer_.Stop(); | |
424 launch_backoff_ = base::TimeDelta(); | |
425 state_ = StateDetached; | |
426 break; | |
427 | |
428 case StateAttached: | |
429 DCHECK(!timer_.IsRunning()); | |
430 DCHECK(process_.handle() != NULL); | |
431 DCHECK(process_watcher_.GetWatchedObject() != NULL); | |
432 DCHECK(chromoting_channel_.get() != NULL); | |
433 | |
434 process_watcher_.StopWatching(); | |
435 process_.Terminate(0); | |
436 process_.Close(); | |
437 chromoting_channel_.reset(); | |
438 state_ = StateDetached; | |
439 break; | |
440 } | |
441 } | 367 } |
442 | 368 |
443 } // namespace remoting | 369 } // namespace remoting |
OLD | NEW |