| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #include "remoting/host/host_script_object.h" | |
| 6 | |
| 7 #include "base/bind.h" | |
| 8 #include "base/message_loop.h" | |
| 9 #include "base/task.h" | |
| 10 #include "base/threading/platform_thread.h" | |
| 11 #include "remoting/base/auth_token_util.h" | |
| 12 #include "remoting/host/chromoting_host.h" | |
| 13 #include "remoting/host/chromoting_host_context.h" | |
| 14 #include "remoting/host/host_config.h" | |
| 15 #include "remoting/host/host_key_pair.h" | |
| 16 #include "remoting/host/host_plugin_utils.h" | |
| 17 #include "remoting/host/in_memory_host_config.h" | |
| 18 #include "remoting/host/register_support_host_request.h" | |
| 19 #include "remoting/host/support_access_verifier.h" | |
| 20 | |
| 21 namespace remoting { | |
| 22 | |
| 23 // Supported Javascript interface: | |
| 24 // readonly attribute string accessCode; | |
| 25 // readonly attribute int state; | |
| 26 // | |
| 27 // state: { | |
| 28 // DISCONNECTED, | |
| 29 // REQUESTED_ACCESS_CODE, | |
| 30 // RECEIVED_ACCESS_CODE, | |
| 31 // CONNECTED, | |
| 32 // AFFIRMING_CONNECTION, | |
| 33 // ERROR, | |
| 34 // } | |
| 35 // | |
| 36 // attribute Function void logDebugInfo(string); | |
| 37 // attribute Function void onStateChanged(); | |
| 38 // | |
| 39 // // The |auth_service_with_token| parameter should be in the format | |
| 40 // // "auth_service:auth_token". An example would be "oauth2:1/2a3912vd". | |
| 41 // void connect(string uid, string auth_service_with_token); | |
| 42 // void disconnect(); | |
| 43 | |
| 44 namespace { | |
| 45 | |
| 46 const char* kAttrNameAccessCode = "accessCode"; | |
| 47 const char* kAttrNameState = "state"; | |
| 48 const char* kAttrNameLogDebugInfo = "logDebugInfo"; | |
| 49 const char* kAttrNameOnStateChanged = "onStateChanged"; | |
| 50 const char* kFuncNameConnect = "connect"; | |
| 51 const char* kFuncNameDisconnect = "disconnect"; | |
| 52 | |
| 53 // States. | |
| 54 const char* kAttrNameDisconnected = "DISCONNECTED"; | |
| 55 const char* kAttrNameRequestedAccessCode = "REQUESTED_ACCESS_CODE"; | |
| 56 const char* kAttrNameReceivedAccessCode = "RECEIVED_ACCESS_CODE"; | |
| 57 const char* kAttrNameConnected = "CONNECTED"; | |
| 58 const char* kAttrNameAffirmingConnection = "AFFIRMING_CONNECTION"; | |
| 59 const char* kAttrNameError = "ERROR"; | |
| 60 | |
| 61 const int kMaxLoginAttempts = 5; | |
| 62 | |
| 63 } // namespace | |
| 64 | |
| 65 HostNPScriptObject::HostNPScriptObject(NPP plugin, NPObject* parent) | |
| 66 : plugin_(plugin), | |
| 67 parent_(parent), | |
| 68 state_(kDisconnected), | |
| 69 log_debug_info_func_(NULL), | |
| 70 on_state_changed_func_(NULL), | |
| 71 np_thread_id_(base::PlatformThread::CurrentId()), | |
| 72 failed_login_attempts_(0), | |
| 73 disconnected_event_(true, false) { | |
| 74 VLOG(2) << "HostNPScriptObject"; | |
| 75 host_context_.SetUITaskPostFunction(base::Bind( | |
| 76 &HostNPScriptObject::PostTaskToNPThread, base::Unretained(this))); | |
| 77 } | |
| 78 | |
| 79 HostNPScriptObject::~HostNPScriptObject() { | |
| 80 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 81 | |
| 82 // Disconnect synchronously. We cannot disconnect asynchronously | |
| 83 // here because |host_context_| needs to be stopped on the plugin | |
| 84 // thread, but the plugin thread may not exist after the instance | |
| 85 // is destroyed. | |
| 86 destructing_.Set(); | |
| 87 disconnected_event_.Reset(); | |
| 88 DisconnectInternal(); | |
| 89 disconnected_event_.Wait(); | |
| 90 | |
| 91 host_context_.Stop(); | |
| 92 if (log_debug_info_func_) { | |
| 93 g_npnetscape_funcs->releaseobject(log_debug_info_func_); | |
| 94 } | |
| 95 if (on_state_changed_func_) { | |
| 96 g_npnetscape_funcs->releaseobject(on_state_changed_func_); | |
| 97 } | |
| 98 } | |
| 99 | |
| 100 bool HostNPScriptObject::Init() { | |
| 101 VLOG(2) << "Init"; | |
| 102 // TODO(wez): This starts a bunch of threads, which might fail. | |
| 103 host_context_.Start(); | |
| 104 return true; | |
| 105 } | |
| 106 | |
| 107 bool HostNPScriptObject::HasMethod(const std::string& method_name) { | |
| 108 VLOG(2) << "HasMethod " << method_name; | |
| 109 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 110 return (method_name == kFuncNameConnect || | |
| 111 method_name == kFuncNameDisconnect); | |
| 112 } | |
| 113 | |
| 114 bool HostNPScriptObject::InvokeDefault(const NPVariant* args, | |
| 115 uint32_t argCount, | |
| 116 NPVariant* result) { | |
| 117 VLOG(2) << "InvokeDefault"; | |
| 118 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 119 SetException("exception during default invocation"); | |
| 120 return false; | |
| 121 } | |
| 122 | |
| 123 bool HostNPScriptObject::Invoke(const std::string& method_name, | |
| 124 const NPVariant* args, | |
| 125 uint32_t argCount, | |
| 126 NPVariant* result) { | |
| 127 VLOG(2) << "Invoke " << method_name; | |
| 128 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 129 if (method_name == kFuncNameConnect) { | |
| 130 return Connect(args, argCount, result); | |
| 131 } else if (method_name == kFuncNameDisconnect) { | |
| 132 return Disconnect(args, argCount, result); | |
| 133 } else { | |
| 134 SetException("Invoke: unknown method " + method_name); | |
| 135 return false; | |
| 136 } | |
| 137 } | |
| 138 | |
| 139 bool HostNPScriptObject::HasProperty(const std::string& property_name) { | |
| 140 VLOG(2) << "HasProperty " << property_name; | |
| 141 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 142 return (property_name == kAttrNameAccessCode || | |
| 143 property_name == kAttrNameState || | |
| 144 property_name == kAttrNameLogDebugInfo || | |
| 145 property_name == kAttrNameOnStateChanged || | |
| 146 property_name == kAttrNameDisconnected || | |
| 147 property_name == kAttrNameRequestedAccessCode || | |
| 148 property_name == kAttrNameReceivedAccessCode || | |
| 149 property_name == kAttrNameConnected || | |
| 150 property_name == kAttrNameAffirmingConnection || | |
| 151 property_name == kAttrNameError); | |
| 152 } | |
| 153 | |
| 154 bool HostNPScriptObject::GetProperty(const std::string& property_name, | |
| 155 NPVariant* result) { | |
| 156 VLOG(2) << "GetProperty " << property_name; | |
| 157 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 158 if (!result) { | |
| 159 SetException("GetProperty: NULL result"); | |
| 160 return false; | |
| 161 } | |
| 162 | |
| 163 if (property_name == kAttrNameOnStateChanged) { | |
| 164 OBJECT_TO_NPVARIANT(on_state_changed_func_, *result); | |
| 165 return true; | |
| 166 } else if (property_name == kAttrNameLogDebugInfo) { | |
| 167 OBJECT_TO_NPVARIANT(log_debug_info_func_, *result); | |
| 168 return true; | |
| 169 } else if (property_name == kAttrNameState) { | |
| 170 INT32_TO_NPVARIANT(state_, *result); | |
| 171 return true; | |
| 172 } else if (property_name == kAttrNameAccessCode) { | |
| 173 *result = NPVariantFromString(access_code_); | |
| 174 return true; | |
| 175 } else if (property_name == kAttrNameDisconnected) { | |
| 176 INT32_TO_NPVARIANT(kDisconnected, *result); | |
| 177 return true; | |
| 178 } else if (property_name == kAttrNameRequestedAccessCode) { | |
| 179 INT32_TO_NPVARIANT(kRequestedAccessCode, *result); | |
| 180 return true; | |
| 181 } else if (property_name == kAttrNameReceivedAccessCode) { | |
| 182 INT32_TO_NPVARIANT(kReceivedAccessCode, *result); | |
| 183 return true; | |
| 184 } else if (property_name == kAttrNameConnected) { | |
| 185 INT32_TO_NPVARIANT(kConnected, *result); | |
| 186 return true; | |
| 187 } else if (property_name == kAttrNameAffirmingConnection) { | |
| 188 INT32_TO_NPVARIANT(kAffirmingConnection, *result); | |
| 189 return true; | |
| 190 } else if (property_name == kAttrNameError) { | |
| 191 INT32_TO_NPVARIANT(kError, *result); | |
| 192 return true; | |
| 193 } else { | |
| 194 SetException("GetProperty: unsupported property " + property_name); | |
| 195 return false; | |
| 196 } | |
| 197 } | |
| 198 | |
| 199 bool HostNPScriptObject::SetProperty(const std::string& property_name, | |
| 200 const NPVariant* value) { | |
| 201 VLOG(2) << "SetProperty " << property_name; | |
| 202 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 203 | |
| 204 if (property_name == kAttrNameOnStateChanged) { | |
| 205 if (NPVARIANT_IS_OBJECT(*value)) { | |
| 206 if (on_state_changed_func_) { | |
| 207 g_npnetscape_funcs->releaseobject(on_state_changed_func_); | |
| 208 } | |
| 209 on_state_changed_func_ = NPVARIANT_TO_OBJECT(*value); | |
| 210 if (on_state_changed_func_) { | |
| 211 g_npnetscape_funcs->retainobject(on_state_changed_func_); | |
| 212 } | |
| 213 return true; | |
| 214 } else { | |
| 215 SetException("SetProperty: unexpected type for property " + | |
| 216 property_name); | |
| 217 } | |
| 218 return false; | |
| 219 } | |
| 220 | |
| 221 if (property_name == kAttrNameLogDebugInfo) { | |
| 222 if (NPVARIANT_IS_OBJECT(*value)) { | |
| 223 if (log_debug_info_func_) { | |
| 224 g_npnetscape_funcs->releaseobject(log_debug_info_func_); | |
| 225 } | |
| 226 log_debug_info_func_ = NPVARIANT_TO_OBJECT(*value); | |
| 227 if (log_debug_info_func_) { | |
| 228 g_npnetscape_funcs->retainobject(log_debug_info_func_); | |
| 229 } | |
| 230 return true; | |
| 231 } else { | |
| 232 SetException("SetProperty: unexpected type for property " + | |
| 233 property_name); | |
| 234 } | |
| 235 return false; | |
| 236 } | |
| 237 | |
| 238 return false; | |
| 239 } | |
| 240 | |
| 241 bool HostNPScriptObject::RemoveProperty(const std::string& property_name) { | |
| 242 VLOG(2) << "RemoveProperty " << property_name; | |
| 243 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 244 return false; | |
| 245 } | |
| 246 | |
| 247 bool HostNPScriptObject::Enumerate(std::vector<std::string>* values) { | |
| 248 VLOG(2) << "Enumerate"; | |
| 249 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 250 const char* entries[] = { | |
| 251 kAttrNameAccessCode, | |
| 252 kAttrNameState, | |
| 253 kAttrNameLogDebugInfo, | |
| 254 kAttrNameOnStateChanged, | |
| 255 kFuncNameConnect, | |
| 256 kFuncNameDisconnect, | |
| 257 kAttrNameDisconnected, | |
| 258 kAttrNameRequestedAccessCode, | |
| 259 kAttrNameReceivedAccessCode, | |
| 260 kAttrNameConnected, | |
| 261 kAttrNameAffirmingConnection, | |
| 262 kAttrNameError | |
| 263 }; | |
| 264 for (size_t i = 0; i < arraysize(entries); ++i) { | |
| 265 values->push_back(entries[i]); | |
| 266 } | |
| 267 return true; | |
| 268 } | |
| 269 | |
| 270 void HostNPScriptObject::OnSignallingConnected(SignalStrategy* signal_strategy, | |
| 271 const std::string& full_jid) { | |
| 272 OnStateChanged(kConnected); | |
| 273 } | |
| 274 | |
| 275 void HostNPScriptObject::OnSignallingDisconnected() { | |
| 276 } | |
| 277 | |
| 278 void HostNPScriptObject::OnAccessDenied() { | |
| 279 DCHECK_EQ(MessageLoop::current(), host_context_.network_message_loop()); | |
| 280 | |
| 281 ++failed_login_attempts_; | |
| 282 if (failed_login_attempts_ == kMaxLoginAttempts) | |
| 283 DisconnectInternal(); | |
| 284 } | |
| 285 | |
| 286 void HostNPScriptObject::OnAuthenticatedClientsChanged(int clients_connected) { | |
| 287 DCHECK_NE(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 288 OnStateChanged(clients_connected ? kConnected : kDisconnected); | |
| 289 } | |
| 290 | |
| 291 void HostNPScriptObject::OnShutdown() { | |
| 292 DCHECK_EQ(MessageLoop::current(), host_context_.main_message_loop()); | |
| 293 | |
| 294 OnStateChanged(kDisconnected); | |
| 295 } | |
| 296 | |
| 297 // string uid, string auth_token | |
| 298 bool HostNPScriptObject::Connect(const NPVariant* args, | |
| 299 uint32_t arg_count, | |
| 300 NPVariant* result) { | |
| 301 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 302 | |
| 303 LogDebugInfo("Connecting..."); | |
| 304 | |
| 305 if (arg_count != 2) { | |
| 306 SetException("connect: bad number of arguments"); | |
| 307 return false; | |
| 308 } | |
| 309 | |
| 310 std::string uid = StringFromNPVariant(args[0]); | |
| 311 if (uid.empty()) { | |
| 312 SetException("connect: bad uid argument"); | |
| 313 return false; | |
| 314 } | |
| 315 | |
| 316 std::string auth_service_with_token = StringFromNPVariant(args[1]); | |
| 317 std::string auth_token; | |
| 318 std::string auth_service; | |
| 319 ParseAuthTokenWithService(auth_service_with_token, &auth_token, | |
| 320 &auth_service); | |
| 321 if (auth_token.empty()) { | |
| 322 SetException("connect: auth_service_with_token argument has empty token"); | |
| 323 return false; | |
| 324 } | |
| 325 | |
| 326 ConnectInternal(uid, auth_token, auth_service); | |
| 327 | |
| 328 return true; | |
| 329 } | |
| 330 | |
| 331 void HostNPScriptObject::ConnectInternal( | |
| 332 const std::string& uid, | |
| 333 const std::string& auth_token, | |
| 334 const std::string& auth_service) { | |
| 335 if (MessageLoop::current() != host_context_.main_message_loop()) { | |
| 336 host_context_.main_message_loop()->PostTask( | |
| 337 FROM_HERE, | |
| 338 NewRunnableMethod(this, &HostNPScriptObject::ConnectInternal, | |
| 339 uid, auth_token, auth_service)); | |
| 340 return; | |
| 341 } | |
| 342 // Store the supplied user ID and token to the Host configuration. | |
| 343 scoped_refptr<MutableHostConfig> host_config = new InMemoryHostConfig(); | |
| 344 host_config->SetString(kXmppLoginConfigPath, uid); | |
| 345 host_config->SetString(kXmppAuthTokenConfigPath, auth_token); | |
| 346 host_config->SetString(kXmppAuthServiceConfigPath, auth_service); | |
| 347 | |
| 348 // Create an access verifier and fetch the host secret. | |
| 349 scoped_ptr<SupportAccessVerifier> access_verifier; | |
| 350 access_verifier.reset(new SupportAccessVerifier()); | |
| 351 | |
| 352 // Generate a key pair for the Host to use. | |
| 353 // TODO(wez): Move this to the worker thread. | |
| 354 HostKeyPair host_key_pair; | |
| 355 host_key_pair.Generate(); | |
| 356 host_key_pair.Save(host_config); | |
| 357 | |
| 358 // Request registration of the host for support. | |
| 359 scoped_ptr<RegisterSupportHostRequest> register_request( | |
| 360 new RegisterSupportHostRequest()); | |
| 361 if (!register_request->Init( | |
| 362 host_config.get(), | |
| 363 base::Bind(&HostNPScriptObject::OnReceivedSupportID, | |
| 364 base::Unretained(this), | |
| 365 access_verifier.get()))) { | |
| 366 OnStateChanged(kDisconnected); | |
| 367 return; | |
| 368 } | |
| 369 | |
| 370 // Create the Host. | |
| 371 scoped_refptr<ChromotingHost> host = | |
| 372 ChromotingHost::Create(&host_context_, host_config, | |
| 373 access_verifier.release()); | |
| 374 host->AddStatusObserver(this); | |
| 375 host->AddStatusObserver(register_request.get()); | |
| 376 host->set_it2me(true); | |
| 377 | |
| 378 // Nothing went wrong, so lets save the host, config and request. | |
| 379 host_ = host; | |
| 380 host_config_ = host_config; | |
| 381 register_request_.reset(register_request.release()); | |
| 382 | |
| 383 // Start the Host. | |
| 384 host_->Start(); | |
| 385 | |
| 386 OnStateChanged(kRequestedAccessCode); | |
| 387 return; | |
| 388 } | |
| 389 | |
| 390 bool HostNPScriptObject::Disconnect(const NPVariant* args, | |
| 391 uint32_t arg_count, | |
| 392 NPVariant* result) { | |
| 393 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 394 if (arg_count != 0) { | |
| 395 SetException("disconnect: bad number of arguments"); | |
| 396 return false; | |
| 397 } | |
| 398 | |
| 399 DisconnectInternal(); | |
| 400 | |
| 401 return true; | |
| 402 } | |
| 403 | |
| 404 void HostNPScriptObject::DisconnectInternal() { | |
| 405 if (MessageLoop::current() != host_context_.main_message_loop()) { | |
| 406 host_context_.main_message_loop()->PostTask( | |
| 407 FROM_HERE, | |
| 408 NewRunnableMethod(this, &HostNPScriptObject::DisconnectInternal)); | |
| 409 return; | |
| 410 } | |
| 411 | |
| 412 if (!host_) { | |
| 413 disconnected_event_.Signal(); | |
| 414 return; | |
| 415 } | |
| 416 | |
| 417 host_->Shutdown( | |
| 418 NewRunnableMethod(this, &HostNPScriptObject::OnShutdownFinished)); | |
| 419 } | |
| 420 | |
| 421 void HostNPScriptObject::OnShutdownFinished() { | |
| 422 DCHECK_EQ(MessageLoop::current(), host_context_.main_message_loop()); | |
| 423 | |
| 424 host_ = NULL; | |
| 425 register_request_.reset(); | |
| 426 host_config_ = NULL; | |
| 427 disconnected_event_.Signal(); | |
| 428 } | |
| 429 | |
| 430 void HostNPScriptObject::OnReceivedSupportID( | |
| 431 SupportAccessVerifier* access_verifier, | |
| 432 bool success, | |
| 433 const std::string& support_id) { | |
| 434 CHECK_NE(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 435 | |
| 436 if (!success) { | |
| 437 // TODO(wez): Replace the success/fail flag with full error reporting. | |
| 438 DisconnectInternal(); | |
| 439 return; | |
| 440 } | |
| 441 | |
| 442 // Inform the AccessVerifier of our Support-Id, for authentication. | |
| 443 access_verifier->OnIT2MeHostRegistered(success, support_id); | |
| 444 | |
| 445 // Combine the Support Id with the Host Id to make the Access Code. | |
| 446 // TODO(wez): Locking, anyone? | |
| 447 access_code_ = support_id + "-" + access_verifier->host_secret(); | |
| 448 | |
| 449 // Let the caller know that life is good. | |
| 450 OnStateChanged(kReceivedAccessCode); | |
| 451 } | |
| 452 | |
| 453 void HostNPScriptObject::OnStateChanged(State state) { | |
| 454 if (destructing_.IsSet()) { | |
| 455 return; | |
| 456 } | |
| 457 | |
| 458 if (!host_context_.IsUIThread()) { | |
| 459 host_context_.PostToUIThread( | |
| 460 FROM_HERE, | |
| 461 NewRunnableMethod(this, &HostNPScriptObject::OnStateChanged, state)); | |
| 462 return; | |
| 463 } | |
| 464 state_ = state; | |
| 465 if (on_state_changed_func_) { | |
| 466 VLOG(2) << "Calling state changed " << state; | |
| 467 bool is_good = CallJSFunction(on_state_changed_func_, NULL, 0, NULL); | |
| 468 LOG_IF(ERROR, !is_good) << "OnStateChanged failed"; | |
| 469 } | |
| 470 } | |
| 471 | |
| 472 void HostNPScriptObject::LogDebugInfo(const std::string& message) { | |
| 473 if (!host_context_.IsUIThread()) { | |
| 474 host_context_.PostToUIThread( | |
| 475 FROM_HERE, | |
| 476 NewRunnableMethod(this, &HostNPScriptObject::LogDebugInfo, message)); | |
| 477 return; | |
| 478 } | |
| 479 if (log_debug_info_func_) { | |
| 480 NPVariant* arg = new NPVariant(); | |
| 481 LOG(INFO) << "Logging: " << message; | |
| 482 STRINGZ_TO_NPVARIANT(message.c_str(), *arg); | |
| 483 bool is_good = CallJSFunction(log_debug_info_func_, arg, 1, NULL); | |
| 484 LOG_IF(ERROR, !is_good) << "LogDebugInfo failed"; | |
| 485 } | |
| 486 } | |
| 487 | |
| 488 void HostNPScriptObject::SetException(const std::string& exception_string) { | |
| 489 CHECK_EQ(base::PlatformThread::CurrentId(), np_thread_id_); | |
| 490 g_npnetscape_funcs->setexception(parent_, exception_string.c_str()); | |
| 491 LogDebugInfo(exception_string); | |
| 492 } | |
| 493 | |
| 494 bool HostNPScriptObject::CallJSFunction(NPObject* func, | |
| 495 const NPVariant* args, | |
| 496 uint32_t argCount, | |
| 497 NPVariant* result) { | |
| 498 NPVariant np_result; | |
| 499 bool is_good = func->_class->invokeDefault(func, args, argCount, &np_result); | |
| 500 if (is_good) { | |
| 501 if (result) { | |
| 502 *result = np_result; | |
| 503 } else { | |
| 504 g_npnetscape_funcs->releasevariantvalue(&np_result); | |
| 505 } | |
| 506 } | |
| 507 return is_good; | |
| 508 } | |
| 509 | |
| 510 void HostNPScriptObject::PostTaskToNPThread( | |
| 511 const tracked_objects::Location& from_here, Task* task) { | |
| 512 // The NPAPI functions cannot make use of |from_here|, but this method is | |
| 513 // passed as a callback to ChromotingHostContext, so it needs to have the | |
| 514 // appropriate signature. | |
| 515 | |
| 516 // Can be called from any thread. | |
| 517 g_npnetscape_funcs->pluginthreadasynccall(plugin_, | |
| 518 &NPTaskSpringboard, | |
| 519 task); | |
| 520 } | |
| 521 | |
| 522 void HostNPScriptObject::NPTaskSpringboard(void* task) { | |
| 523 Task* real_task = reinterpret_cast<Task*>(task); | |
| 524 real_task->Run(); | |
| 525 delete real_task; | |
| 526 } | |
| 527 | |
| 528 } // namespace remoting | |
| OLD | NEW |