| 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 #include "remoting/protocol/jingle_session.h" | 5 #include "remoting/protocol/jingle_session.h" |
| 6 | 6 |
| 7 #include "base/bind.h" | 7 #include "base/bind.h" |
| 8 #include "base/rand_util.h" | 8 #include "base/rand_util.h" |
| 9 #include "base/stl_util.h" | 9 #include "base/stl_util.h" |
| 10 #include "base/string_number_conversions.h" | 10 #include "base/string_number_conversions.h" |
| 11 #include "base/time.h" |
| 11 #include "remoting/base/constants.h" | 12 #include "remoting/base/constants.h" |
| 12 #include "remoting/jingle_glue/iq_sender.h" | 13 #include "remoting/jingle_glue/iq_sender.h" |
| 13 #include "remoting/protocol/authenticator.h" | 14 #include "remoting/protocol/authenticator.h" |
| 14 #include "remoting/protocol/channel_authenticator.h" | 15 #include "remoting/protocol/channel_authenticator.h" |
| 15 #include "remoting/protocol/content_description.h" | 16 #include "remoting/protocol/content_description.h" |
| 16 #include "remoting/protocol/jingle_messages.h" | 17 #include "remoting/protocol/jingle_messages.h" |
| 17 #include "remoting/protocol/jingle_session_manager.h" | 18 #include "remoting/protocol/jingle_session_manager.h" |
| 18 #include "remoting/protocol/session_config.h" | 19 #include "remoting/protocol/session_config.h" |
| 19 #include "third_party/libjingle/source/talk/p2p/base/candidate.h" | 20 #include "third_party/libjingle/source/talk/p2p/base/candidate.h" |
| 20 #include "third_party/libjingle/source/talk/xmllite/xmlelement.h" | 21 #include "third_party/libjingle/source/talk/xmllite/xmlelement.h" |
| 21 | 22 |
| 22 using buzz::XmlElement; | 23 using buzz::XmlElement; |
| 23 | 24 |
| 24 namespace remoting { | 25 namespace remoting { |
| 25 namespace protocol { | 26 namespace protocol { |
| 26 | 27 |
| 27 namespace { | 28 namespace { |
| 28 // Delay after candidate creation before sending transport-info | 29 // Delay after candidate creation before sending transport-info |
| 29 // message. This is neccessary to be able to pack multiple candidates | 30 // message. This is neccessary to be able to pack multiple candidates |
| 30 // into one transport-info messages. The value needs to be greater | 31 // into one transport-info messages. The value needs to be greater |
| 31 // than zero because ports are opened asynchronously in the browser | 32 // than zero because ports are opened asynchronously in the browser |
| 32 // process. | 33 // process. |
| 33 const int kTransportInfoSendDelayMs = 2; | 34 const int kTransportInfoSendDelayMs = 2; |
| 34 | 35 |
| 36 // How long we should wait for a response from the other end. This |
| 37 // value is used for all requests include |session-initiate| and |
| 38 // |transport-info|. |
| 39 const int kMessageResponseTimeoutSeconds = 10; |
| 40 |
| 35 Session::Error AuthRejectionReasonToError( | 41 Session::Error AuthRejectionReasonToError( |
| 36 Authenticator::RejectionReason reason) { | 42 Authenticator::RejectionReason reason) { |
| 37 switch (reason) { | 43 switch (reason) { |
| 38 case Authenticator::INVALID_CREDENTIALS: | 44 case Authenticator::INVALID_CREDENTIALS: |
| 39 return Session::AUTHENTICATION_FAILED; | 45 return Session::AUTHENTICATION_FAILED; |
| 40 case Authenticator::PROTOCOL_ERROR: | 46 case Authenticator::PROTOCOL_ERROR: |
| 41 return Session::INCOMPATIBLE_PROTOCOL; | 47 return Session::INCOMPATIBLE_PROTOCOL; |
| 42 } | 48 } |
| 43 NOTREACHED(); | 49 NOTREACHED(); |
| 44 return Session::UNKNOWN_ERROR; | 50 return Session::UNKNOWN_ERROR; |
| 45 } | 51 } |
| 46 | 52 |
| 47 } // namespace | 53 } // namespace |
| 48 | 54 |
| 49 JingleSession::JingleSession(JingleSessionManager* session_manager) | 55 JingleSession::JingleSession(JingleSessionManager* session_manager) |
| 50 : session_manager_(session_manager), | 56 : session_manager_(session_manager), |
| 51 state_(INITIALIZING), | 57 state_(INITIALIZING), |
| 52 error_(OK), | 58 error_(OK), |
| 53 config_is_set_(false) { | 59 config_is_set_(false) { |
| 54 } | 60 } |
| 55 | 61 |
| 56 JingleSession::~JingleSession() { | 62 JingleSession::~JingleSession() { |
| 63 STLDeleteContainerPointers(pending_requests_.begin(), |
| 64 pending_requests_.end()); |
| 57 STLDeleteContainerPairSecondPointers(channels_.begin(), channels_.end()); | 65 STLDeleteContainerPairSecondPointers(channels_.begin(), channels_.end()); |
| 58 session_manager_->SessionDestroyed(this); | 66 session_manager_->SessionDestroyed(this); |
| 59 } | 67 } |
| 60 | 68 |
| 61 void JingleSession::SetStateChangeCallback( | 69 void JingleSession::SetStateChangeCallback( |
| 62 const StateChangeCallback& callback) { | 70 const StateChangeCallback& callback) { |
| 63 DCHECK(CalledOnValidThread()); | 71 DCHECK(CalledOnValidThread()); |
| 64 DCHECK(!callback.is_null()); | 72 DCHECK(!callback.is_null()); |
| 65 state_change_callback_ = callback; | 73 state_change_callback_ = callback; |
| 66 } | 74 } |
| (...skipping 28 matching lines...) Expand all Loading... |
| 95 // enough entropy. In the worst case connection will fail when two | 103 // enough entropy. In the worst case connection will fail when two |
| 96 // clients generate the same session ID concurrently. | 104 // clients generate the same session ID concurrently. |
| 97 session_id_ = base::Int64ToString(base::RandGenerator(kint64max)); | 105 session_id_ = base::Int64ToString(base::RandGenerator(kint64max)); |
| 98 | 106 |
| 99 // Send session-initiate message. | 107 // Send session-initiate message. |
| 100 JingleMessage message(peer_jid_, JingleMessage::SESSION_INITIATE, | 108 JingleMessage message(peer_jid_, JingleMessage::SESSION_INITIATE, |
| 101 session_id_); | 109 session_id_); |
| 102 message.description.reset( | 110 message.description.reset( |
| 103 new ContentDescription(candidate_config_->Clone(), | 111 new ContentDescription(candidate_config_->Clone(), |
| 104 authenticator_->GetNextMessage())); | 112 authenticator_->GetNextMessage())); |
| 105 initiate_request_ = session_manager_->iq_sender()->SendIq( | 113 SendMessage(message); |
| 106 message.ToXml(), | |
| 107 base::Bind(&JingleSession::OnSessionInitiateResponse, | |
| 108 base::Unretained(this))); | |
| 109 | 114 |
| 110 SetState(CONNECTING); | 115 SetState(CONNECTING); |
| 111 } | 116 } |
| 112 | 117 |
| 113 void JingleSession::InitializeIncomingConnection( | 118 void JingleSession::InitializeIncomingConnection( |
| 114 const JingleMessage& initiate_message, | 119 const JingleMessage& initiate_message, |
| 115 scoped_ptr<Authenticator> authenticator) { | 120 scoped_ptr<Authenticator> authenticator) { |
| 116 DCHECK(CalledOnValidThread()); | 121 DCHECK(CalledOnValidThread()); |
| 117 DCHECK(initiate_message.description.get()); | 122 DCHECK(initiate_message.description.get()); |
| 118 DCHECK(authenticator.get()); | 123 DCHECK(authenticator.get()); |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 151 JingleMessage message(peer_jid_, JingleMessage::SESSION_ACCEPT, | 156 JingleMessage message(peer_jid_, JingleMessage::SESSION_ACCEPT, |
| 152 session_id_); | 157 session_id_); |
| 153 | 158 |
| 154 scoped_ptr<buzz::XmlElement> auth_message; | 159 scoped_ptr<buzz::XmlElement> auth_message; |
| 155 if (authenticator_->state() == Authenticator::MESSAGE_READY) | 160 if (authenticator_->state() == Authenticator::MESSAGE_READY) |
| 156 auth_message = authenticator_->GetNextMessage(); | 161 auth_message = authenticator_->GetNextMessage(); |
| 157 | 162 |
| 158 message.description.reset( | 163 message.description.reset( |
| 159 new ContentDescription(CandidateSessionConfig::CreateFrom(config_), | 164 new ContentDescription(CandidateSessionConfig::CreateFrom(config_), |
| 160 auth_message.Pass())); | 165 auth_message.Pass())); |
| 161 initiate_request_ = session_manager_->iq_sender()->SendIq( | 166 SendMessage(message); |
| 162 message.ToXml(), | |
| 163 base::Bind(&JingleSession::OnSessionInitiateResponse, | |
| 164 base::Unretained(this))); | |
| 165 | 167 |
| 166 // Update state. | 168 // Update state. |
| 167 SetState(CONNECTED); | 169 SetState(CONNECTED); |
| 168 | 170 |
| 169 if (authenticator_->state() == Authenticator::ACCEPTED) { | 171 if (authenticator_->state() == Authenticator::ACCEPTED) { |
| 170 SetState(AUTHENTICATED); | 172 SetState(AUTHENTICATED); |
| 171 } else { | 173 } else { |
| 172 DCHECK_EQ(authenticator_->state(), Authenticator::WAITING_MESSAGE); | 174 DCHECK_EQ(authenticator_->state(), Authenticator::WAITING_MESSAGE); |
| 173 } | 175 } |
| 174 | 176 |
| 175 return; | 177 return; |
| 176 } | 178 } |
| 177 | 179 |
| 178 void JingleSession::OnSessionInitiateResponse( | |
| 179 const buzz::XmlElement* response) { | |
| 180 const std::string& type = response->Attr(buzz::QName("", "type")); | |
| 181 if (type != "result") { | |
| 182 LOG(ERROR) << "Received error in response to session-initiate message: \"" | |
| 183 << response->Str() | |
| 184 << "\". Terminating the session."; | |
| 185 | |
| 186 // TODO(sergeyu): There may be different reasons for error | |
| 187 // here. Parse the response stanza to find failure reason. | |
| 188 CloseInternal(PEER_IS_OFFLINE); | |
| 189 } | |
| 190 } | |
| 191 | |
| 192 void JingleSession::CreateStreamChannel( | 180 void JingleSession::CreateStreamChannel( |
| 193 const std::string& name, | 181 const std::string& name, |
| 194 const StreamChannelCallback& callback) { | 182 const StreamChannelCallback& callback) { |
| 195 DCHECK(!channels_[name]); | 183 DCHECK(!channels_[name]); |
| 196 | 184 |
| 197 scoped_ptr<ChannelAuthenticator> channel_authenticator = | 185 scoped_ptr<ChannelAuthenticator> channel_authenticator = |
| 198 authenticator_->CreateChannelAuthenticator(); | 186 authenticator_->CreateChannelAuthenticator(); |
| 199 scoped_ptr<StreamTransport> channel = | 187 scoped_ptr<StreamTransport> channel = |
| 200 session_manager_->transport_factory_->CreateStreamTransport(); | 188 session_manager_->transport_factory_->CreateStreamTransport(); |
| 201 channel->Initialize(name, session_manager_->transport_config_, | 189 channel->Initialize(name, session_manager_->transport_config_, |
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 275 route.local_address); | 263 route.local_address); |
| 276 } | 264 } |
| 277 } | 265 } |
| 278 | 266 |
| 279 void JingleSession::OnTransportDeleted(Transport* transport) { | 267 void JingleSession::OnTransportDeleted(Transport* transport) { |
| 280 ChannelsMap::iterator it = channels_.find(transport->name()); | 268 ChannelsMap::iterator it = channels_.find(transport->name()); |
| 281 DCHECK_EQ(it->second, transport); | 269 DCHECK_EQ(it->second, transport); |
| 282 channels_.erase(it); | 270 channels_.erase(it); |
| 283 } | 271 } |
| 284 | 272 |
| 273 void JingleSession::SendMessage(const JingleMessage& message) { |
| 274 scoped_ptr<IqRequest> request = session_manager_->iq_sender()->SendIq( |
| 275 message.ToXml(), |
| 276 base::Bind(&JingleSession::OnMessageResponse, |
| 277 base::Unretained(this), message.action)); |
| 278 if (request.get()) { |
| 279 request->SetTimeout( |
| 280 base::TimeDelta::FromSeconds(kMessageResponseTimeoutSeconds)); |
| 281 pending_requests_.push_back(request.release()); |
| 282 } else { |
| 283 LOG(ERROR) << "Failed to send a " |
| 284 << JingleMessage::GetActionName(message.action) << " message"; |
| 285 } |
| 286 } |
| 287 |
| 288 void JingleSession::OnMessageResponse( |
| 289 JingleMessage::ActionType request_type, |
| 290 IqRequest* request, |
| 291 const buzz::XmlElement* response) { |
| 292 Error error = OK; |
| 293 |
| 294 std::string type_str = JingleMessage::GetActionName(request_type); |
| 295 |
| 296 if (!response) { |
| 297 LOG(ERROR) << type_str << " request timed out."; |
| 298 // Most likely the session-initiate timeout indicates a problem |
| 299 // with the signaling. |
| 300 error = UNKNOWN_ERROR; |
| 301 } else { |
| 302 const std::string& type = response->Attr(buzz::QName("", "type")); |
| 303 if (type != "result") { |
| 304 LOG(ERROR) << "Received error in response to " << type_str |
| 305 << " message: \"" << response->Str() |
| 306 << "\". Terminating the session."; |
| 307 |
| 308 switch (request_type) { |
| 309 case JingleMessage::SESSION_INFO: |
| 310 // session-info is used for the new authentication protocol, |
| 311 // and wasn't previously supported. |
| 312 error = INCOMPATIBLE_PROTOCOL; |
| 313 |
| 314 default: |
| 315 // TODO(sergeyu): There may be different reasons for error |
| 316 // here. Parse the response stanza to find failure reason. |
| 317 error = PEER_IS_OFFLINE; |
| 318 } |
| 319 } |
| 320 } |
| 321 |
| 322 CleanupPendingRequests(request); |
| 323 |
| 324 if (error != OK) { |
| 325 CloseInternal(error); |
| 326 } |
| 327 } |
| 328 |
| 329 void JingleSession::CleanupPendingRequests(IqRequest* request) { |
| 330 DCHECK(!pending_requests_.empty()); |
| 331 DCHECK(request); |
| 332 |
| 333 // This method is called whenever a response to |request| is |
| 334 // received. Here we delete that request and all requests that were |
| 335 // sent before it. The idea here is that if we send messages A, B |
| 336 // and C and then suddenly receive response to C then it means that |
| 337 // either A and B messages or the corresponding response messages |
| 338 // were somehow lost. E.g. that may happen when the client switches |
| 339 // from one network to another. The best way to handle that case is |
| 340 // to ignore errors and timeouts for A and B by deleting the |
| 341 // corresponding IqRequest objects. |
| 342 while (!pending_requests_.empty() && pending_requests_.front() != request) { |
| 343 delete pending_requests_.front(); |
| 344 pending_requests_.pop_front(); |
| 345 } |
| 346 |
| 347 // Delete the |request| itself. |
| 348 DCHECK_EQ(request, pending_requests_.front()); |
| 349 delete request; |
| 350 if (!pending_requests_.empty()) |
| 351 pending_requests_.pop_front(); |
| 352 } |
| 353 |
| 285 void JingleSession::OnIncomingMessage(const JingleMessage& message, | 354 void JingleSession::OnIncomingMessage(const JingleMessage& message, |
| 286 const ReplyCallback& reply_callback) { | 355 const ReplyCallback& reply_callback) { |
| 287 DCHECK(CalledOnValidThread()); | 356 DCHECK(CalledOnValidThread()); |
| 288 | 357 |
| 289 if (message.from != peer_jid_) { | 358 if (message.from != peer_jid_) { |
| 290 // Ignore messages received from a different Jid. | 359 // Ignore messages received from a different Jid. |
| 291 reply_callback.Run(JingleMessageReply::INVALID_SID); | 360 reply_callback.Run(JingleMessageReply::INVALID_SID); |
| 292 return; | 361 return; |
| 293 } | 362 } |
| 294 | 363 |
| (...skipping 149 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 444 return true; | 513 return true; |
| 445 } | 514 } |
| 446 | 515 |
| 447 void JingleSession::ProcessAuthenticationStep() { | 516 void JingleSession::ProcessAuthenticationStep() { |
| 448 DCHECK_EQ(state_, CONNECTED); | 517 DCHECK_EQ(state_, CONNECTED); |
| 449 | 518 |
| 450 if (authenticator_->state() == Authenticator::MESSAGE_READY) { | 519 if (authenticator_->state() == Authenticator::MESSAGE_READY) { |
| 451 JingleMessage message(peer_jid_, JingleMessage::SESSION_INFO, session_id_); | 520 JingleMessage message(peer_jid_, JingleMessage::SESSION_INFO, session_id_); |
| 452 message.info = authenticator_->GetNextMessage(); | 521 message.info = authenticator_->GetNextMessage(); |
| 453 DCHECK(message.info.get()); | 522 DCHECK(message.info.get()); |
| 454 | 523 SendMessage(message); |
| 455 session_info_request_ = session_manager_->iq_sender()->SendIq( | |
| 456 message.ToXml(), base::Bind( | |
| 457 &JingleSession::OnSessionInfoResponse, | |
| 458 base::Unretained(this))); | |
| 459 } | 524 } |
| 460 DCHECK_NE(authenticator_->state(), Authenticator::MESSAGE_READY); | 525 DCHECK_NE(authenticator_->state(), Authenticator::MESSAGE_READY); |
| 461 | 526 |
| 462 if (authenticator_->state() == Authenticator::ACCEPTED) { | 527 if (authenticator_->state() == Authenticator::ACCEPTED) { |
| 463 SetState(AUTHENTICATED); | 528 SetState(AUTHENTICATED); |
| 464 } else if (authenticator_->state() == Authenticator::REJECTED) { | 529 } else if (authenticator_->state() == Authenticator::REJECTED) { |
| 465 CloseInternal(AuthRejectionReasonToError( | 530 CloseInternal(AuthRejectionReasonToError( |
| 466 authenticator_->rejection_reason())); | 531 authenticator_->rejection_reason())); |
| 467 } | 532 } |
| 468 } | 533 } |
| 469 | 534 |
| 470 void JingleSession::OnSessionInfoResponse(const buzz::XmlElement* response) { | |
| 471 const std::string& type = response->Attr(buzz::QName("", "type")); | |
| 472 if (type != "result") { | |
| 473 LOG(ERROR) << "Received error in response to session-info message: \"" | |
| 474 << response->Str() | |
| 475 << "\". Terminating the session."; | |
| 476 CloseInternal(INCOMPATIBLE_PROTOCOL); | |
| 477 } | |
| 478 } | |
| 479 | |
| 480 void JingleSession::OnTransportInfoResponse(const buzz::XmlElement* response) { | |
| 481 const std::string& type = response->Attr(buzz::QName("", "type")); | |
| 482 if (type != "result") { | |
| 483 LOG(ERROR) << "Received error in response to session-initiate message: \"" | |
| 484 << response->Str() | |
| 485 << "\". Terminating the session."; | |
| 486 | |
| 487 if (state_ == CONNECTING) { | |
| 488 CloseInternal(PEER_IS_OFFLINE); | |
| 489 } else { | |
| 490 // Host has disconnected without sending session-terminate message. | |
| 491 CloseInternal(OK); | |
| 492 } | |
| 493 } | |
| 494 } | |
| 495 | |
| 496 void JingleSession::SendTransportInfo() { | 535 void JingleSession::SendTransportInfo() { |
| 497 JingleMessage message(peer_jid_, JingleMessage::TRANSPORT_INFO, session_id_); | 536 JingleMessage message(peer_jid_, JingleMessage::TRANSPORT_INFO, session_id_); |
| 498 message.candidates.swap(pending_candidates_); | 537 message.candidates.swap(pending_candidates_); |
| 499 transport_info_request_ = session_manager_->iq_sender()->SendIq( | 538 SendMessage(message); |
| 500 message.ToXml(), base::Bind( | |
| 501 &JingleSession::OnTransportInfoResponse, | |
| 502 base::Unretained(this))); | |
| 503 } | 539 } |
| 504 | 540 |
| 505 | |
| 506 void JingleSession::CloseInternal(Error error) { | 541 void JingleSession::CloseInternal(Error error) { |
| 507 DCHECK(CalledOnValidThread()); | 542 DCHECK(CalledOnValidThread()); |
| 508 | 543 |
| 509 if (state_ == CONNECTING || state_ == CONNECTED || state_ == AUTHENTICATED) { | 544 if (state_ == CONNECTING || state_ == CONNECTED || state_ == AUTHENTICATED) { |
| 510 // Send session-terminate message with the appropriate error code. | 545 // Send session-terminate message with the appropriate error code. |
| 511 JingleMessage::Reason reason; | 546 JingleMessage::Reason reason; |
| 512 switch (error) { | 547 switch (error) { |
| 513 case OK: | 548 case OK: |
| 514 reason = JingleMessage::SUCCESS; | 549 reason = JingleMessage::SUCCESS; |
| 515 break; | 550 break; |
| 516 case SESSION_REJECTED: | 551 case SESSION_REJECTED: |
| 517 case AUTHENTICATION_FAILED: | 552 case AUTHENTICATION_FAILED: |
| 518 reason = JingleMessage::DECLINE; | 553 reason = JingleMessage::DECLINE; |
| 519 break; | 554 break; |
| 520 case INCOMPATIBLE_PROTOCOL: | 555 case INCOMPATIBLE_PROTOCOL: |
| 521 reason = JingleMessage::INCOMPATIBLE_PARAMETERS; | 556 reason = JingleMessage::INCOMPATIBLE_PARAMETERS; |
| 522 break; | 557 break; |
| 523 default: | 558 default: |
| 524 reason = JingleMessage::GENERAL_ERROR; | 559 reason = JingleMessage::GENERAL_ERROR; |
| 525 } | 560 } |
| 526 | 561 |
| 527 JingleMessage message(peer_jid_, JingleMessage::SESSION_TERMINATE, | 562 JingleMessage message(peer_jid_, JingleMessage::SESSION_TERMINATE, |
| 528 session_id_); | 563 session_id_); |
| 529 message.reason = reason; | 564 message.reason = reason; |
| 530 session_manager_->iq_sender()->SendIq( | 565 SendMessage(message); |
| 531 message.ToXml(), IqSender::ReplyCallback()); | |
| 532 } | 566 } |
| 533 | 567 |
| 534 error_ = error; | 568 error_ = error; |
| 535 | 569 |
| 536 if (state_ != FAILED && state_ != CLOSED) { | 570 if (state_ != FAILED && state_ != CLOSED) { |
| 537 if (error != OK) { | 571 if (error != OK) { |
| 538 SetState(FAILED); | 572 SetState(FAILED); |
| 539 } else { | 573 } else { |
| 540 SetState(CLOSED); | 574 SetState(CLOSED); |
| 541 } | 575 } |
| 542 } | 576 } |
| 543 } | 577 } |
| 544 | 578 |
| 545 void JingleSession::SetState(State new_state) { | 579 void JingleSession::SetState(State new_state) { |
| 546 DCHECK(CalledOnValidThread()); | 580 DCHECK(CalledOnValidThread()); |
| 547 | 581 |
| 548 if (new_state != state_) { | 582 if (new_state != state_) { |
| 549 DCHECK_NE(state_, CLOSED); | 583 DCHECK_NE(state_, CLOSED); |
| 550 DCHECK_NE(state_, FAILED); | 584 DCHECK_NE(state_, FAILED); |
| 551 | 585 |
| 552 state_ = new_state; | 586 state_ = new_state; |
| 553 if (!state_change_callback_.is_null()) | 587 if (!state_change_callback_.is_null()) |
| 554 state_change_callback_.Run(new_state); | 588 state_change_callback_.Run(new_state); |
| 555 } | 589 } |
| 556 } | 590 } |
| 557 | 591 |
| 558 } // namespace protocol | 592 } // namespace protocol |
| 559 } // namespace remoting | 593 } // namespace remoting |
| OLD | NEW |