From b83122a9ebcf527c3b84d447956f4098aa89b647 Mon Sep 17 00:00:00 2001 From: "fgorski@chromium.org" Date: Wed, 22 Jan 2014 21:29:29 +0000 Subject: [PATCH] GCM Client initialization * hooking up MCSClient, Connection factory, User List and others. * basic message dispatching. (will be moved to another pack. BUG=284553 Review URL: https://codereview.chromium.org/133273002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@246413 0039d316-1c4b-4281-b951-d872f2087c98 --- chrome/browser/services/gcm/gcm_client_factory.cc | 3 +- google_apis/gcm/engine/mcs_client.cc | 86 ++++---- google_apis/gcm/engine/mcs_client.h | 43 ++-- google_apis/gcm/engine/mcs_client_unittest.cc | 118 +++++++--- google_apis/gcm/gcm_client_impl.cc | 253 +++++++++++++++++++++- google_apis/gcm/gcm_client_impl.h | 135 +++++++++++- google_apis/gcm/tools/mcs_probe.cc | 62 +++--- 7 files changed, 571 insertions(+), 129 deletions(-) diff --git a/chrome/browser/services/gcm/gcm_client_factory.cc b/chrome/browser/services/gcm/gcm_client_factory.cc index eb4ce1d96dfd..fa00880b6f12 100644 --- a/chrome/browser/services/gcm/gcm_client_factory.cc +++ b/chrome/browser/services/gcm/gcm_client_factory.cc @@ -44,7 +44,8 @@ GCMClient* GCMClientFactory::GetClient() { worker_pool->GetSequencedTaskRunnerWithShutdownBehavior( worker_pool->GetSequenceToken(), base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)); - client->Initialize(gcm_store_path, blocking_task_runner); + // TODO(jianli): proper initialization in progress. + // client->Initialize(gcm_store_path, blocking_task_runner); g_gcm_client_initialized = true; } return client; diff --git a/google_apis/gcm/engine/mcs_client.cc b/google_apis/gcm/engine/mcs_client.cc index 34e756c7c98b..41bb4d0877cb 100644 --- a/google_apis/gcm/engine/mcs_client.cc +++ b/google_apis/gcm/engine/mcs_client.cc @@ -106,12 +106,14 @@ MCSClient::~MCSClient() { } void MCSClient::Initialize( - const InitializationCompleteCallback& initialization_callback, + const ErrorCallback& error_callback, const OnMessageReceivedCallback& message_received_callback, const OnMessageSentCallback& message_sent_callback, const GCMStore::LoadResult& load_result) { DCHECK_EQ(state_, UNINITIALIZED); - initialization_callback_ = initialization_callback; + + state_ = LOADED; + mcs_error_callback_ = error_callback; message_received_callback_ = message_received_callback; message_sent_callback_ = message_sent_callback; @@ -124,26 +126,20 @@ void MCSClient::Initialize( weak_ptr_factory_.GetWeakPtr())); connection_handler_ = connection_factory_->GetConnectionHandler(); - // TODO(fgorski): Likely this whole check will be done outside in GCMClient. - if (!load_result.success) { - state_ = UNINITIALIZED; - LOG(ERROR) << "Failed to load/create RMQ state. Not connecting."; - return; - } - - state_ = LOADED; stream_id_out_ = 1; // Login request is hardcoded to id 1. - // TODO(fgorski): android_id and secutiry_token will be moved to GCMClient. - if (load_result.device_android_id == 0 || - load_result.device_security_token == 0) { + android_id_ = load_result.device_android_id; + security_token_ = load_result.device_security_token; + + if (android_id_ == 0) { DVLOG(1) << "No device credentials found, assuming new client."; - initialization_callback_.Run(true, 0, 0); + // No need to try and load RMQ data in that case. return; } - android_id_ = load_result.device_android_id; - security_token_ = load_result.device_security_token; + // |android_id_| is non-zero, so should |security_token_|. + DCHECK_NE(0u, security_token_) << "Security token invalid, while android id" + << " is non-zero."; DVLOG(1) << "RMQ Load finished with " << load_result.incoming_messages.size() << " incoming acks pending and " @@ -161,13 +157,15 @@ void MCSClient::Initialize( uint64 timestamp = 0; if (!base::StringToUint64(iter->first, ×tamp)) { LOG(ERROR) << "Invalid restored message."; + // TODO(fgorski): Error: data unreadable + mcs_error_callback_.Run(); return; } // Check if the TTL has expired for this message. if (HasTTLExpired(*iter->second, clock_)) { expired_ttl_ids.push_back(iter->first); - message_sent_callback_.Run("TTL expired for " + iter->first); + NotifyMessageSendStatus(*iter->second, TTL_EXCEEDED); delete iter->second; continue; } @@ -193,39 +191,34 @@ void MCSClient::Initialize( packet_info->persistent_id = base::Uint64ToString(iter->first); to_send_.push_back(make_linked_ptr(packet_info)); } - - // TODO(fgorski): that is likely the only place where the initialization - // callback could be used. - initialization_callback_.Run(true, android_id_, security_token_); } void MCSClient::Login(uint64 android_id, uint64 security_token) { + DCHECK_EQ(state_, LOADED); + DCHECK(android_id_ == 0 || android_id_ == android_id); + DCHECK(security_token_ == 0 || security_token_ == security_token); + if (android_id != android_id_ && security_token != security_token_) { DCHECK(android_id); DCHECK(security_token); - DCHECK(restored_unackeds_server_ids_.empty()); android_id_ = android_id; security_token_ = security_token; - gcm_store_->SetDeviceCredentials( - android_id_, - security_token_, - base::Bind(&MCSClient::OnGCMUpdateFinished, - weak_ptr_factory_.GetWeakPtr())); } state_ = CONNECTING; connection_factory_->Connect(); + DCHECK(restored_unackeds_server_ids_.empty()); } void MCSClient::SendMessage(const MCSMessage& message) { int ttl = GetTTL(message.GetProtobuf()); DCHECK_GE(ttl, 0); if (to_send_.size() > kMaxSendQueueSize) { - message_sent_callback_.Run("Message queue full."); + NotifyMessageSendStatus(message.GetProtobuf(), QUEUE_SIZE_LIMIT_REACHED); return; } if (message.size() > kMaxMessageBytes) { - message_sent_callback_.Run("Message too large."); + NotifyMessageSendStatus(message.GetProtobuf(), MESSAGE_TOO_LARGE); return; } @@ -245,12 +238,13 @@ void MCSClient::SendMessage(const MCSMessage& message) { *(packet_info->protobuf)), base::Bind(&MCSClient::OnGCMUpdateFinished, weak_ptr_factory_.GetWeakPtr()))) { - message_sent_callback_.Run("Message queue full."); + NotifyMessageSendStatus(message.GetProtobuf(), + APP_QUEUE_SIZE_LIMIT_REACHED); return; } } else if (!connection_factory_->IsEndpointReachable()) { DVLOG(1) << "No active connection, dropping message."; - message_sent_callback_.Run("TTL expired"); + NotifyMessageSendStatus(message.GetProtobuf(), NO_CONNECTION_ON_ZERO_TTL); return; } to_send_.push_back(make_linked_ptr(packet_info)); @@ -324,7 +318,7 @@ void MCSClient::ResetStateAndBuildLoginRequest( // message from the persistent store. if (!packet->persistent_id.empty()) expired_ttl_ids.push_back(packet->persistent_id); - message_sent_callback_.Run("TTL expired"); + NotifyMessageSendStatus(*packet->protobuf, TTL_EXCEEDED); } } @@ -372,7 +366,7 @@ void MCSClient::MaybeSendMessage() { if (HasTTLExpired(*packet->protobuf, clock_)) { DCHECK(!packet->persistent_id.empty()); DVLOG(1) << "Dropping expired message " << packet->persistent_id << "."; - message_sent_callback_.Run("TTL expired for " + packet->persistent_id); + NotifyMessageSendStatus(*packet->protobuf, TTL_EXCEEDED); gcm_store_->RemoveOutgoingMessage( packet->persistent_id, base::Bind(&MCSClient::OnGCMUpdateFinished, @@ -532,7 +526,7 @@ void MCSClient::HandlePacketFromWire( state_ = UNINITIALIZED; DVLOG(1) << " Error code: " << login_response->error().code(); DVLOG(1) << " Error message: " << login_response->error().message(); - initialization_callback_.Run(false, 0, 0); + mcs_error_callback_.Run(); return; } @@ -641,6 +635,7 @@ void MCSClient::HandleStreamAck(StreamId last_stream_id_received) { const MCSPacketInternal& outgoing_packet = to_resend_.front(); acked_outgoing_persistent_ids.push_back(outgoing_packet->persistent_id); acked_outgoing_stream_ids.push_back(outgoing_packet->stream_id); + NotifyMessageSendStatus(*outgoing_packet->protobuf, SUCCESS); to_resend_.pop_front(); } @@ -663,6 +658,7 @@ void MCSClient::HandleSelectiveAck(const PersistentIdList& id_list) { for (; iter != id_list.end() && !to_resend_.empty(); ++iter) { const MCSPacketInternal& outgoing_packet = to_resend_.front(); DCHECK_EQ(outgoing_packet->persistent_id, *iter); + NotifyMessageSendStatus(*outgoing_packet->protobuf, SUCCESS); // No need to re-acknowledge any server messages this message already // acknowledged. @@ -678,6 +674,7 @@ void MCSClient::HandleSelectiveAck(const PersistentIdList& id_list) { for (; iter != id_list.end() && !to_send_.empty(); ++iter) { const MCSPacketInternal& outgoing_packet = to_send_.front(); DCHECK_EQ(outgoing_packet->persistent_id, *iter); + NotifyMessageSendStatus(*outgoing_packet->protobuf, SUCCESS); // No need to re-acknowledge any server messages this message already // acknowledged. @@ -706,12 +703,6 @@ void MCSClient::HandleSelectiveAck(const PersistentIdList& id_list) { } void MCSClient::HandleServerConfirmedReceipt(StreamId device_stream_id) { - // TODO(zea): use a message id the sender understands. - base::MessageLoop::current()->PostTask( - FROM_HERE, - base::Bind(message_sent_callback_, - "Message " + base::UintToString(device_stream_id) + " sent.")); - PersistentIdList acked_incoming_ids; for (std::map::iterator iter = acked_server_ids_.begin(); @@ -739,4 +730,19 @@ void MCSClient::OnConnectionResetByHeartbeat() { connection_factory_->SignalConnectionReset(); } +void MCSClient::NotifyMessageSendStatus( + const google::protobuf::MessageLite& protobuf, + MessageSendStatus status) { + if (GetMCSProtoTag(protobuf) != kDataMessageStanzaTag) + return; + + const mcs_proto::DataMessageStanza* data_message_stanza = + reinterpret_cast(&protobuf); + message_sent_callback_.Run( + data_message_stanza->device_user_id(), + data_message_stanza->category(), + data_message_stanza->id(), + status); +} + } // namespace gcm diff --git a/google_apis/gcm/engine/mcs_client.h b/google_apis/gcm/engine/mcs_client.h index 906b97ae1bdf..38ba29871525 100644 --- a/google_apis/gcm/engine/mcs_client.h +++ b/google_apis/gcm/engine/mcs_client.h @@ -51,22 +51,35 @@ class GCM_EXPORT MCSClient { CONNECTED, // Connected and running. }; - // Callback for informing MCSClient status. It is valid for this to be - // invoked more than once if a permanent error is encountered after a - // successful login was initiated. - typedef base::Callback< - void(bool success, - uint64 restored_android_id, - uint64 restored_security_token)> InitializationCompleteCallback; + enum MessageSendStatus { + // Message sent succcessfully. + SUCCESS, + // Message not saved, because total queue size limit reached. + QUEUE_SIZE_LIMIT_REACHED, + // Messgae not saved, because app queue size limit reached. + APP_QUEUE_SIZE_LIMIT_REACHED, + // Message too large to send. + MESSAGE_TOO_LARGE, + // Message not send becuase of TTL = 0 and no working connection. + NO_CONNECTION_ON_ZERO_TTL, + // Message exceeded TTL. + TTL_EXCEEDED + }; + + // Callback for MCSClient's error conditions. + // TODO(fgorski): Keeping it as a callback with intention to add meaningful + // error information. + typedef base::Callback ErrorCallback; // Callback when a message is received. typedef base::Callback OnMessageReceivedCallback; // Callback when a message is sent (and receipt has been acknowledged by // the MCS endpoint). - // TODO(zea): pass some sort of structure containing more details about - // send failures. - typedef base::Callback - OnMessageSentCallback; + typedef base::Callback< + void(int64 user_serial_number, + const std::string& app_id, + const std::string& message_id, + MessageSendStatus status)> OnMessageSentCallback; MCSClient(base::Clock* clock, ConnectionFactory* connection_factory, @@ -82,7 +95,7 @@ class GCM_EXPORT MCSClient { // |success == true|. /// If an error loading the GCM store is encountered, // |initialization_callback| will be invoked with |success == false|. - void Initialize(const InitializationCompleteCallback& initialization_callback, + void Initialize(const ErrorCallback& initialization_callback, const OnMessageReceivedCallback& message_received_callback, const OnMessageSentCallback& message_sent_callback, const GCMStore::LoadResult& load_result); @@ -165,6 +178,10 @@ class GCM_EXPORT MCSClient { // Helper for the heartbeat manager to signal a connection reset. void OnConnectionResetByHeartbeat(); + // Runs the message_sent_callback_ with send |status| of the |protobuf|. + void NotifyMessageSendStatus(const google::protobuf::MessageLite& protobuf, + MessageSendStatus status); + // Clock for enforcing TTL. Passed in for testing. base::Clock* const clock_; @@ -172,7 +189,7 @@ class GCM_EXPORT MCSClient { State state_; // Callbacks for owner. - InitializationCompleteCallback initialization_callback_; + ErrorCallback mcs_error_callback_; OnMessageReceivedCallback message_received_callback_; OnMessageSentCallback message_sent_callback_; diff --git a/google_apis/gcm/engine/mcs_client_unittest.cc b/google_apis/gcm/engine/mcs_client_unittest.cc index e5b7ce767915..7ba590487dc8 100644 --- a/google_apis/gcm/engine/mcs_client_unittest.cc +++ b/google_apis/gcm/engine/mcs_client_unittest.cc @@ -9,6 +9,7 @@ #include "base/run_loop.h" #include "base/strings/string_number_conversions.h" #include "base/test/simple_test_clock.h" +#include "components/webdata/encryptor/encryptor.h" #include "google_apis/gcm/base/mcs_util.h" #include "google_apis/gcm/engine/fake_connection_factory.h" #include "google_apis/gcm/engine/fake_connection_handler.h" @@ -37,12 +38,14 @@ const int kTTLValue = 5 * 60; // 5 minutes. // Helper for building arbitrary data messages. MCSMessage BuildDataMessage(const std::string& from, const std::string& category, + const std::string& message_id, int last_stream_id_received, - const std::string persistent_id, + const std::string& persistent_id, int ttl, uint64 sent, int queued) { mcs_proto::DataMessageStanza data_message; + data_message.set_id(message_id); data_message.set_from(from); data_message.set_category(category); data_message.set_last_stream_id_received(last_stream_id_received); @@ -79,6 +82,7 @@ class MCSClientTest : public testing::Test { void BuildMCSClient(); void InitializeClient(); + void StoreCredentials(); void LoginClient(const std::vector& acknowledged_ids); base::SimpleTestClock* clock() { return &clock_; } @@ -91,6 +95,11 @@ class MCSClientTest : public testing::Test { uint64 restored_security_token() const { return restored_security_token_; } MCSMessage* received_message() const { return received_message_.get(); } std::string sent_message_id() const { return sent_message_id_;} + MCSClient::MessageSendStatus message_send_status() const { + return message_send_status_; + } + + void SetDeviceCredentialsCallback(bool success); FakeConnectionHandler* GetFakeHandler() const; @@ -98,11 +107,12 @@ class MCSClientTest : public testing::Test { void PumpLoop(); private: - void InitializationCallback(bool success, - uint64 restored_android_id, - uint64 restored_security_token); + void ErrorCallback(); void MessageReceivedCallback(const MCSMessage& message); - void MessageSentCallback(const std::string& message_id); + void MessageSentCallback(int64 user_serial_number, + const std::string& app_id, + const std::string& message_id, + MCSClient::MessageSendStatus status); base::SimpleTestClock clock_; @@ -118,16 +128,23 @@ class MCSClientTest : public testing::Test { uint64 restored_security_token_; scoped_ptr received_message_; std::string sent_message_id_; + MCSClient::MessageSendStatus message_send_status_; }; MCSClientTest::MCSClientTest() : run_loop_(new base::RunLoop()), - init_success_(false), + init_success_(true), restored_android_id_(0), - restored_security_token_(0) { + restored_security_token_(0), + message_send_status_(MCSClient::SUCCESS) { EXPECT_TRUE(temp_directory_.CreateUniqueTempDir()); run_loop_.reset(new base::RunLoop()); + // On OSX, prevent the Keychain permissions popup during unit tests. +#if defined(OS_MACOSX) + Encryptor::UseMockKeychain(true); +#endif + // Advance the clock to a non-zero time. clock_.Advance(base::TimeDelta::FromSeconds(1)); } @@ -147,12 +164,12 @@ void MCSClientTest::InitializeClient() { gcm_store_->Load(base::Bind( &MCSClient::Initialize, base::Unretained(mcs_client_.get()), - base::Bind(&MCSClientTest::InitializationCallback, + base::Bind(&MCSClientTest::ErrorCallback, base::Unretained(this)), base::Bind(&MCSClientTest::MessageReceivedCallback, base::Unretained(this)), base::Bind(&MCSClientTest::MessageSentCallback, base::Unretained(this)))); - run_loop_->Run(); + run_loop_->RunUntilIdle(); run_loop_.reset(new base::RunLoop()); } @@ -170,6 +187,15 @@ void MCSClientTest::LoginClient( run_loop_.reset(new base::RunLoop()); } +void MCSClientTest::StoreCredentials() { + gcm_store_->SetDeviceCredentials( + kAndroidId, kSecurityToken, + base::Bind(&MCSClientTest::SetDeviceCredentialsCallback, + base::Unretained(this))); + run_loop_->Run(); + run_loop_.reset(new base::RunLoop()); +} + FakeConnectionHandler* MCSClientTest::GetFakeHandler() const { return reinterpret_cast( connection_factory_.GetConnectionHandler()); @@ -185,13 +211,9 @@ void MCSClientTest::PumpLoop() { run_loop_.reset(new base::RunLoop()); } -void MCSClientTest::InitializationCallback(bool success, - uint64 restored_android_id, - uint64 restored_security_token) { - init_success_ = success; - restored_android_id_ = restored_android_id; - restored_security_token_ = restored_security_token; - DVLOG(1) << "Initialization callback invoked, killing loop."; +void MCSClientTest::ErrorCallback() { + init_success_ = false; + DVLOG(1) << "Error callback invoked, killing loop."; run_loop_->Quit(); } @@ -201,9 +223,18 @@ void MCSClientTest::MessageReceivedCallback(const MCSMessage& message) { run_loop_->Quit(); } -void MCSClientTest::MessageSentCallback(const std::string& message_id) { +void MCSClientTest::MessageSentCallback(int64 user_serial_number, + const std::string& app_id, + const std::string& message_id, + MCSClient::MessageSendStatus status) { DVLOG(1) << "Message sent callback invoked, killing loop."; sent_message_id_ = message_id; + message_send_status_ = status; + run_loop_->Quit(); +} + +void MCSClientTest::SetDeviceCredentialsCallback(bool success) { + ASSERT_TRUE(success); run_loop_->Quit(); } @@ -211,8 +242,6 @@ void MCSClientTest::MessageSentCallback(const std::string& message_id) { TEST_F(MCSClientTest, InitializeNew) { BuildMCSClient(); InitializeClient(); - EXPECT_EQ(0U, restored_android_id()); - EXPECT_EQ(0U, restored_security_token()); EXPECT_TRUE(init_success()); } @@ -224,10 +253,9 @@ TEST_F(MCSClientTest, InitializeExisting) { LoginClient(std::vector()); // Rebuild the client, to reload from the GCM store. + StoreCredentials(); BuildMCSClient(); InitializeClient(); - EXPECT_EQ(kAndroidId, restored_android_id()); - EXPECT_EQ(kSecurityToken, restored_security_token()); EXPECT_TRUE(init_success()); } @@ -258,7 +286,7 @@ TEST_F(MCSClientTest, SendMessageNoRMQ) { BuildMCSClient(); InitializeClient(); LoginClient(std::vector()); - MCSMessage message(BuildDataMessage("from", "category", 1, "", 0, 1, 0)); + MCSMessage message(BuildDataMessage("from", "category", "X", 1, "", 0, 1, 0)); GetFakeHandler()->ExpectOutgoingMessage(message); mcs_client()->SendMessage(message); EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); @@ -271,12 +299,13 @@ TEST_F(MCSClientTest, SendMessageNoRMQWhileDisconnected) { InitializeClient(); EXPECT_TRUE(sent_message_id().empty()); - MCSMessage message(BuildDataMessage("from", "category", 1, "", 0, 1, 0)); + MCSMessage message(BuildDataMessage("from", "category", "X", 1, "", 0, 1, 0)); mcs_client()->SendMessage(message); // Message sent callback should be invoked, but no message should actually // be sent. - EXPECT_FALSE(sent_message_id().empty()); + EXPECT_EQ("X", sent_message_id()); + EXPECT_EQ(MCSClient::NO_CONNECTION_ON_ZERO_TTL, message_send_status()); EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); } @@ -286,7 +315,7 @@ TEST_F(MCSClientTest, SendMessageRMQ) { InitializeClient(); LoginClient(std::vector()); MCSMessage message( - BuildDataMessage("from", "category", 1, "1", kTTLValue, 1, 0)); + BuildDataMessage("from", "category", "X", 1, "1", kTTLValue, 1, 0)); GetFakeHandler()->ExpectOutgoingMessage(message); mcs_client()->SendMessage(message); EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); @@ -300,7 +329,7 @@ TEST_F(MCSClientTest, SendMessageRMQWhileDisconnected) { LoginClient(std::vector()); GetFakeHandler()->set_fail_send(true); MCSMessage message( - BuildDataMessage("from", "category", 1, "1", kTTLValue, 1, 0)); + BuildDataMessage("from", "category", "X", 1, "1", kTTLValue, 1, 0)); // The initial (failed) send. GetFakeHandler()->ExpectOutgoingMessage(message); @@ -312,6 +341,7 @@ TEST_F(MCSClientTest, SendMessageRMQWhileDisconnected) { // The second (re)send. MCSMessage message2(BuildDataMessage("from", "category", + "X", 1, "1", kTTLValue, @@ -336,7 +366,7 @@ TEST_F(MCSClientTest, SendMessageRMQOnRestart) { LoginClient(std::vector()); GetFakeHandler()->set_fail_send(true); MCSMessage message( - BuildDataMessage("from", "category", 1, "1", kTTLValue, 1, 0)); + BuildDataMessage("from", "category", "X", 1, "1", kTTLValue, 1, 0)); // The initial (failed) send. GetFakeHandler()->ExpectOutgoingMessage(message); @@ -345,12 +375,14 @@ TEST_F(MCSClientTest, SendMessageRMQOnRestart) { EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); // Rebuild the client, which should resend the old message. + StoreCredentials(); BuildMCSClient(); InitializeClient(); clock()->Advance(base::TimeDelta::FromSeconds(kTTLValue - 1)); MCSMessage message2(BuildDataMessage("from", "category", + "X", 1, "1", kTTLValue, @@ -374,10 +406,12 @@ TEST_F(MCSClientTest, SendMessageRMQWithStreamAck) { MCSMessage message( BuildDataMessage("from", "category", + "X", 1, base::IntToString(i), kTTLValue, - 1, 0)); + 1, + 0)); GetFakeHandler()->ExpectOutgoingMessage(message); mcs_client()->SendMessage(message); } @@ -392,6 +426,7 @@ TEST_F(MCSClientTest, SendMessageRMQWithStreamAck) { WaitForMCSEvent(); // Reconnect and ensure no messages are resent. + StoreCredentials(); BuildMCSClient(); InitializeClient(); LoginClient(std::vector()); @@ -412,6 +447,7 @@ TEST_F(MCSClientTest, SendMessageRMQAckOnReconnect) { MCSMessage message( BuildDataMessage("from", "category", + id_list.back(), 1, id_list.back(), kTTLValue, @@ -424,6 +460,7 @@ TEST_F(MCSClientTest, SendMessageRMQAckOnReconnect) { // Rebuild the client, and receive an acknowledgment for the messages as // part of the login response. + StoreCredentials(); BuildMCSClient(); InitializeClient(); LoginClient(std::vector()); @@ -431,7 +468,6 @@ TEST_F(MCSClientTest, SendMessageRMQAckOnReconnect) { GetFakeHandler()->ReceiveMessage( MCSMessage(kIqStanzaTag, ack.PassAs())); - WaitForMCSEvent(); EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); } @@ -450,6 +486,7 @@ TEST_F(MCSClientTest, SendMessageRMQPartialAckOnReconnect) { MCSMessage message( BuildDataMessage("from", "category", + id_list.back(), 1, id_list.back(), kTTLValue, @@ -462,6 +499,7 @@ TEST_F(MCSClientTest, SendMessageRMQPartialAckOnReconnect) { // Rebuild the client, and receive an acknowledgment for the messages as // part of the login response. + StoreCredentials(); BuildMCSClient(); InitializeClient(); LoginClient(std::vector()); @@ -477,6 +515,7 @@ TEST_F(MCSClientTest, SendMessageRMQPartialAckOnReconnect) { MCSMessage message( BuildDataMessage("from", "category", + remaining_ids[i - 1], 2, remaining_ids[i - 1], kTTLValue, @@ -488,6 +527,7 @@ TEST_F(MCSClientTest, SendMessageRMQPartialAckOnReconnect) { MCSMessage(kIqStanzaTag, ack.PassAs())); WaitForMCSEvent(); + PumpLoop(); EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); } @@ -505,6 +545,7 @@ TEST_F(MCSClientTest, AckOnLogin) { MCSMessage message( BuildDataMessage("from", "category", + "X", 1, id_list.back(), kTTLValue, @@ -516,6 +557,7 @@ TEST_F(MCSClientTest, AckOnLogin) { } // Restart the client. + StoreCredentials(); BuildMCSClient(); InitializeClient(); LoginClient(id_list); @@ -535,13 +577,13 @@ TEST_F(MCSClientTest, AckOnSend) { MCSMessage message( BuildDataMessage("from", "category", + id_list.back(), 1, id_list.back(), kTTLValue, 1, 0)); GetFakeHandler()->ReceiveMessage(message); - WaitForMCSEvent(); PumpLoop(); } @@ -549,6 +591,7 @@ TEST_F(MCSClientTest, AckOnSend) { MCSMessage message( BuildDataMessage("from", "category", + "X", kMessageBatchSize + 1, "1", kTTLValue, @@ -579,6 +622,7 @@ TEST_F(MCSClientTest, AckWhenLimitReachedWithHeartbeat) { MCSMessage message( BuildDataMessage("from", "category", + id_list.back(), 1, id_list.back(), kTTLValue, @@ -605,10 +649,11 @@ TEST_F(MCSClientTest, AckWhenLimitReachedWithHeartbeat) { GetFakeHandler()->ReceiveMessage( MCSMessage(kHeartbeatPingTag, heartbeat.PassAs())); - WaitForMCSEvent(); + PumpLoop(); EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); // Rebuild the client. Nothing should be sent on login. + StoreCredentials(); BuildMCSClient(); InitializeClient(); LoginClient(std::vector()); @@ -622,7 +667,7 @@ TEST_F(MCSClientTest, ExpiredTTLOnSend) { InitializeClient(); LoginClient(std::vector()); MCSMessage message( - BuildDataMessage("from", "category", 1, "1", kTTLValue, 1, 0)); + BuildDataMessage("from", "category", "X", 1, "1", kTTLValue, 1, 0)); // Advance time to after the TTL. clock()->Advance(base::TimeDelta::FromSeconds(kTTLValue + 2)); @@ -630,7 +675,8 @@ TEST_F(MCSClientTest, ExpiredTTLOnSend) { mcs_client()->SendMessage(message); // No messages should be sent, but the callback should still be invoked. - EXPECT_FALSE(sent_message_id().empty()); + EXPECT_EQ("X", sent_message_id()); + EXPECT_EQ(MCSClient::TTL_EXCEEDED, message_send_status()); EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); } @@ -640,7 +686,7 @@ TEST_F(MCSClientTest, ExpiredTTLOnRestart) { LoginClient(std::vector()); GetFakeHandler()->set_fail_send(true); MCSMessage message( - BuildDataMessage("from", "category", 1, "1", kTTLValue, 1, 0)); + BuildDataMessage("from", "category", "X", 1, "1", kTTLValue, 1, 0)); // The initial (failed) send. GetFakeHandler()->ExpectOutgoingMessage(message); @@ -651,11 +697,13 @@ TEST_F(MCSClientTest, ExpiredTTLOnRestart) { // Move the clock forward and rebuild the client, which should fail the // message send on restart. clock()->Advance(base::TimeDelta::FromSeconds(kTTLValue + 2)); + StoreCredentials(); BuildMCSClient(); InitializeClient(); LoginClient(std::vector()); PumpLoop(); - EXPECT_FALSE(sent_message_id().empty()); + EXPECT_EQ("X", sent_message_id()); + EXPECT_EQ(MCSClient::TTL_EXCEEDED, message_send_status()); EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived()); } diff --git a/google_apis/gcm/gcm_client_impl.cc b/google_apis/gcm/gcm_client_impl.cc index eea2a7507a60..e03211f98e18 100644 --- a/google_apis/gcm/gcm_client_impl.cc +++ b/google_apis/gcm/gcm_client_impl.cc @@ -4,31 +4,205 @@ #include "google_apis/gcm/gcm_client_impl.h" +#include "base/bind.h" #include "base/files/file_path.h" +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" #include "base/sequenced_task_runner.h" -#include "google_apis/gcm/engine/gcm_store.h" +#include "google_apis/gcm/base/mcs_message.h" +#include "google_apis/gcm/base/mcs_util.h" +#include "google_apis/gcm/engine/checkin_request.h" +#include "google_apis/gcm/engine/connection_factory_impl.h" #include "google_apis/gcm/engine/gcm_store_impl.h" +#include "google_apis/gcm/engine/mcs_client.h" #include "google_apis/gcm/engine/user_list.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "net/http/http_network_session.h" +#include "url/gurl.h" namespace gcm { -GCMClientImpl::GCMClientImpl() { +namespace { +const char kMCSEndpoint[] = "https://mtalk.google.com:5228"; +} // namespace + +GCMClientImpl::GCMClientImpl() + : state_(UNINITIALIZED), + url_request_context_getter_(NULL), + pending_checkins_deleter_(&pending_checkins_) { } GCMClientImpl::~GCMClientImpl() { } void GCMClientImpl::Initialize( + const checkin_proto::ChromeBuildProto& chrome_build_proto, const base::FilePath& path, - scoped_refptr blocking_task_runner) { - gcm_store_.reset(new GCMStoreImpl(false, - path, - blocking_task_runner)); + scoped_refptr blocking_task_runner, + const scoped_refptr& + url_request_context_getter) { + DCHECK_EQ(UNINITIALIZED, state_); + DCHECK(url_request_context_getter); + + chrome_build_proto_.CopyFrom(chrome_build_proto); + url_request_context_getter_ = url_request_context_getter; + + gcm_store_.reset(new GCMStoreImpl(false, path, blocking_task_runner)); + gcm_store_->Load(base::Bind(&GCMClientImpl::OnLoadCompleted, + base::Unretained(this))); user_list_.reset(new UserList(gcm_store_.get())); + connection_factory_.reset(new ConnectionFactoryImpl(GURL(kMCSEndpoint), + network_session_, + net_log_.net_log())); + mcs_client_.reset(new MCSClient(&clock_, + connection_factory_.get(), + gcm_store_.get())); + state_ = LOADING; +} + +void GCMClientImpl::OnLoadCompleted(const GCMStore::LoadResult& result) { + DCHECK_EQ(LOADING, state_); + + if (!result.success) { + ResetState(); + return; + } + + user_list_->Initialize(result.serial_number_mappings); + + device_checkin_info_.android_id = result.device_android_id; + device_checkin_info_.secret = result.device_security_token; + InitializeMCSClient(result); + if (!device_checkin_info_.IsValid()) { + device_checkin_info_.Reset(); + state_ = INITIAL_DEVICE_CHECKIN; + StartCheckin(0, device_checkin_info_); + } else { + state_ = READY; + StartMCSLogin(); + } +} + +void GCMClientImpl::InitializeMCSClient(const GCMStore::LoadResult& result) { + mcs_client_->Initialize( + base::Bind(&GCMClientImpl::OnMCSError, base::Unretained(this)), + base::Bind(&GCMClientImpl::OnMessageReceivedFromMCS, + base::Unretained(this)), + base::Bind(&GCMClientImpl::OnMessageSentToMCS, base::Unretained(this)), + result); +} + +void GCMClientImpl::OnFirstTimeDeviceCheckinCompleted( + const CheckinInfo& checkin_info) { + DCHECK(!device_checkin_info_.IsValid()); + + state_ = READY; + device_checkin_info_.android_id = checkin_info.android_id; + device_checkin_info_.secret = checkin_info.secret; + gcm_store_->SetDeviceCredentials( + checkin_info.android_id, checkin_info.secret, + base::Bind(&GCMClientImpl::SetDeviceCredentialsCallback, + base::Unretained(this))); + StartMCSLogin(); +} + +void GCMClientImpl::StartMCSLogin() { + DCHECK_EQ(READY, state_); + DCHECK(device_checkin_info_.IsValid()); + mcs_client_->Login(device_checkin_info_.android_id, + device_checkin_info_.secret); +} + +void GCMClientImpl::ResetState() { + state_ = UNINITIALIZED; + // TODO(fgorski): reset all of the necessart objects and start over. +} + +void GCMClientImpl::StartCheckin(int64 user_serial_number, + const CheckinInfo& checkin_info) { + DCHECK_EQ(0U, pending_checkins_.count(user_serial_number)); + CheckinRequest* checkin_request = + new CheckinRequest( + base::Bind(&GCMClientImpl::OnCheckinCompleted, + base::Unretained(this), + user_serial_number), + chrome_build_proto_, + user_serial_number, + checkin_info.android_id, + checkin_info.secret, + url_request_context_getter_); + pending_checkins_[user_serial_number] = checkin_request; + checkin_request->Start(); +} + +void GCMClientImpl::OnCheckinCompleted(int64 user_serial_number, + uint64 android_id, + uint64 security_token) { + CheckinInfo checkin_info; + checkin_info.android_id = android_id; + checkin_info.secret = security_token; + + // Delete the checkin request. + PendingCheckins::iterator iter = pending_checkins_.find(user_serial_number); + DCHECK(iter != pending_checkins_.end()); + delete iter->second; + pending_checkins_.erase(iter); + + if (user_serial_number == 0) { + OnDeviceCheckinCompleted(checkin_info); + return; + } + + Delegate* delegate = user_list_->GetDelegateBySerialNumber( + user_serial_number); + // TODO(fgorski): Add a reasonable Result here. It is possible that we are + // missing the right parameter on the CheckinRequest level. + delegate->OnCheckInFinished(checkin_info, SUCCESS); +} + +void GCMClientImpl::OnDeviceCheckinCompleted(const CheckinInfo& checkin_info) { + if (!checkin_info.IsValid()) { + // TODO(fgorski): Trigger a retry logic here. (no need to start over). + return; + } + + if (state_ == INITIAL_DEVICE_CHECKIN) { + OnFirstTimeDeviceCheckinCompleted(checkin_info); + } else { + DCHECK_EQ(READY, state_); + if (device_checkin_info_.android_id != checkin_info.android_id || + device_checkin_info_.secret != checkin_info.secret) { + ResetState(); + } else { + // TODO(fgorski): Reset the checkin timer. + } + } +} + +void GCMClientImpl::SetDeviceCredentialsCallback(bool success) { + // TODO(fgorski): This is one of the signals that store needs a rebuild. + DCHECK(success); } void GCMClientImpl::SetUserDelegate(const std::string& username, Delegate* delegate) { + DCHECK(!username.empty()); + DCHECK(delegate); + user_list_->SetDelegate( + username, + delegate, + base::Bind(&GCMClientImpl::SetDelegateCompleted, base::Unretained(this))); +} + +void GCMClientImpl::SetDelegateCompleted(const std::string& username, + int64 user_serial_number) { + Delegate* delegate = user_list_->GetDelegateByUsername(username); + DCHECK(delegate); + if (state_ == READY) { + delegate->OnLoadingCompleted(); + return; + } } void GCMClientImpl::CheckIn(const std::string& username) { @@ -51,7 +225,72 @@ void GCMClientImpl::Send(const std::string& username, } bool GCMClientImpl::IsLoading() const { - return false; + return state_ != READY; +} + +void GCMClientImpl::OnMessageReceivedFromMCS(const gcm::MCSMessage& message) { + // We need to do the message parsing here and then dispatch it to the right + // delegate related to that message + switch (message.tag()) { + case kLoginResponseTag: + DVLOG(1) << "Login response received by GCM Client. Ignoring."; + return; + case kDataMessageStanzaTag: + DVLOG(1) << "A downstream message received. Processing..."; + HandleIncomingMessage(message); + return; + default: + NOTREACHED() << "Message with unexpected tag received by GCMClient"; + return; + } +} + +void GCMClientImpl::OnMessageSentToMCS(int64 user_serial_number, + const std::string& app_id, + const std::string& message_id, + MCSClient::MessageSendStatus status) { + // TODO(fgorski): This is only a placeholder, it likely has to change the + // arguments to be able to identify the user and app. +} + +void GCMClientImpl::OnMCSError() { + // TODO(fgorski): For now it replaces the initialization method. Long term it + // should have an error or status passed in. +} + +void GCMClientImpl::HandleIncomingMessage(const gcm::MCSMessage& message) { + const mcs_proto::DataMessageStanza& data_message_stanza = + reinterpret_cast( + message.GetProtobuf()); + IncomingMessage incoming_message; + for (int i = 0; i < data_message_stanza.app_data_size(); ++i) { + incoming_message.data[data_message_stanza.app_data(i).key()] = + data_message_stanza.app_data(i).value(); + } + + int64 user_serial_number = data_message_stanza.device_user_id(); + Delegate* delegate = + user_list_->GetDelegateBySerialNumber(user_serial_number); + if (delegate) { + DVLOG(1) << "Found delegate for serial number: " << user_serial_number; + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&GCMClientImpl::NotifyDelegateOnMessageReceived, + base::Unretained(this), + delegate, + data_message_stanza.category(), + incoming_message)); + } else { + DVLOG(1) << "Delegate for serial number: " << user_serial_number + << " not found."; + } +} + +void GCMClientImpl::NotifyDelegateOnMessageReceived( + GCMClient::Delegate* delegate, + const std::string& app_id, + const IncomingMessage& incoming_message) { + delegate->OnMessageReceived(app_id, incoming_message); } } // namespace gcm diff --git a/google_apis/gcm/gcm_client_impl.h b/google_apis/gcm/gcm_client_impl.h index 4a32f772250c..7d1856e8ed60 100644 --- a/google_apis/gcm/gcm_client_impl.h +++ b/google_apis/gcm/gcm_client_impl.h @@ -5,29 +5,54 @@ #ifndef GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_ #define GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_ +#include +#include + #include "base/compiler_specific.h" #include "base/memory/ref_counted.h" -#include "base/memory/scoped_ptr.h" +#include "base/stl_util.h" +#include "base/time/default_clock.h" +#include "google_apis/gcm/base/mcs_message.h" +#include "google_apis/gcm/engine/gcm_store.h" +#include "google_apis/gcm/engine/mcs_client.h" #include "google_apis/gcm/gcm_client.h" +#include "google_apis/gcm/protocol/android_checkin.pb.h" +#include "net/base/net_log.h" +#include "net/url_request/url_request_context_getter.h" namespace base { class FilePath; class SequencedTaskRunner; } // namespace base +namespace net { +class HttpNetworkSession; +class URLRequestContextGetter; +} + namespace gcm { -class GCMStore; +class CheckinRequest; +class ConnectionFactory; +class GCMClientImplTest; class UserList; +// Implements the GCM Client. It is used to coordinate MCS Client (communication +// with MCS) and other pieces of GCM infrastructure like Registration and +// Checkins. It also allows for registering user delegates that host +// applications that send and receive messages. class GCM_EXPORT GCMClientImpl : public GCMClient { public: GCMClientImpl(); virtual ~GCMClientImpl(); + // Begins initialization of the GCM Client. void Initialize( + const checkin_proto::ChromeBuildProto& chrome_build_proto, const base::FilePath& path, - scoped_refptr blocking_task_runner); + scoped_refptr blocking_task_runner, + const scoped_refptr& + url_request_context_getter); // Overridden from GCMClient: virtual void SetUserDelegate(const std::string& username, @@ -46,9 +71,113 @@ class GCM_EXPORT GCMClientImpl : public GCMClient { virtual bool IsLoading() const OVERRIDE; private: + // State representation of the GCMClient. + enum State { + // Uninitialized. + UNINITIALIZED, + // GCM store loading is in progress. + LOADING, + // Initial device checkin is in progress. + INITIAL_DEVICE_CHECKIN, + // Ready to accept requests. + READY, + }; + + // Collection of pending checkin requests. Keys are serial numbers of the + // users as assigned by the user_list_. Values are pending checkin requests to + // obtain android IDs and security tokens for the users. + typedef std::map PendingCheckins; + + friend class GCMClientImplTest; + + // Callbacks for the MCSClient. + // Receives messages and dispatches them to relevant user delegates. + void OnMessageReceivedFromMCS(const gcm::MCSMessage& message); + // Receives confirmation of sent messages or information about errors. + void OnMessageSentToMCS(int64 user_serial_number, + const std::string& app_id, + const std::string& message_id, + MCSClient::MessageSendStatus status); + // Receives information about mcs_client_ errors. + void OnMCSError(); + + // Runs after GCM Store load is done to trigger continuation of the + // initialization. + void OnLoadCompleted(const GCMStore::LoadResult& result); + // Initializes mcs_client_, which handles the connection to MCS. + void InitializeMCSClient(const GCMStore::LoadResult& result); + // Complets the first time device checkin. + void OnFirstTimeDeviceCheckinCompleted(const CheckinInfo& checkin_info); + // Starts a login on mcs_client_. + void StartMCSLogin(); + // Resets state to before initialization. + void ResetState(); + + // Startes a checkin request for a user with specified |serial_number|. + // Checkin info can be invalid, in which case it is considered a first time + // checkin. + void StartCheckin(int64 user_serial_number, + const CheckinInfo& checkin_info); + // Completes the checkin request for the specified |serial_number|. + // |android_id| and |security_token| are expected to be non-zero or an error + // is triggered. Function also cleans up the pending checkin. + void OnCheckinCompleted(int64 user_serial_number, + uint64 android_id, + uint64 security_token); + // Completes the checkin request for a device (serial number of 0). + void OnDeviceCheckinCompleted(const CheckinInfo& checkin_info); + + // Callback for persisting device credentials in the |gcm_store_|. + void SetDeviceCredentialsCallback(bool success); + + // Callback for setting a delegate on a |user_list_|. Informs that the + // delegate with matching |username| was assigned a |user_serial_number|. + void SetDelegateCompleted(const std::string& username, + int64 user_serial_number); + + // Handles incoming data message and dispatches it the a relevant user + // delegate. + void HandleIncomingMessage(const gcm::MCSMessage& message); + + // Fires OnMessageReceived event on |delegate|, with specified |app_id| and + // |incoming_message|. + void NotifyDelegateOnMessageReceived(GCMClient::Delegate* delegate, + const std::string& app_id, + const IncomingMessage& incoming_message); + + // State of the GCM Client Implementation. + State state_; + + // Device checkin info (android ID and security token used by device). + CheckinInfo device_checkin_info_; + + // Clock used for timing of retry logic. + base::DefaultClock clock_; + + // Information about the chrome build. + // TODO(fgorski): Check if it can be passed in constructor and made const. + checkin_proto::ChromeBuildProto chrome_build_proto_; + + // Persistent data store for keeping device credentials, messages and user to + // serial number mappings. scoped_ptr gcm_store_; + + // Keeps the mappings of user's serial numbers and assigns new serial numbers + // once a user delegate is added for the first time. scoped_ptr user_list_; + scoped_refptr network_session_; + net::BoundNetLog net_log_; + scoped_ptr connection_factory_; + scoped_refptr url_request_context_getter_; + + // Controls receiving and sending of packets and reliable message queueing. + scoped_ptr mcs_client_; + + // Currently pending checkins. GCMClientImpl owns the CheckinRequests. + PendingCheckins pending_checkins_; + STLValueDeleter pending_checkins_deleter_; + DISALLOW_COPY_AND_ASSIGN(GCMClientImpl); }; diff --git a/google_apis/gcm/tools/mcs_probe.cc b/google_apis/gcm/tools/mcs_probe.cc index d6d12d575dc6..f2528ef252c0 100644 --- a/google_apis/gcm/tools/mcs_probe.cc +++ b/google_apis/gcm/tools/mcs_probe.cc @@ -89,8 +89,14 @@ void MessageReceivedCallback(const MCSMessage& message) { } } -void MessageSentCallback(const std::string& local_id) { - LOG(INFO) << "Message sent. Status: " << local_id; +void MessageSentCallback(int64 user_serial_number, + const std::string& app_id, + const std::string& message_id, + MCSClient::MessageSendStatus status) { + LOG(INFO) << "Message sent. Serial number: " << user_serial_number + << " Application ID: " << app_id + << " Message ID: " << message_id + << " Message send status: " << status; } // Needed to use a real host resolver. @@ -175,9 +181,8 @@ class MCSProbe { void InitializeNetworkState(); void BuildNetworkSession(); - void InitializationCallback(bool success, - uint64 restored_android_id, - uint64 restored_security_token); + void LoadCallback(const GCMStore::LoadResult& load_result); + void ErrorCallback(); void OnCheckInCompleted(uint64 android_id, uint64 secret); base::DefaultClock clock_; @@ -270,15 +275,29 @@ void MCSProbe::Start() { connection_factory_.get(), gcm_store_.get())); run_loop_.reset(new base::RunLoop()); - gcm_store_->Load(base::Bind( - &MCSClient::Initialize, - base::Unretained(mcs_client_.get()), - base::Bind(&MCSProbe::InitializationCallback, base::Unretained(this)), - base::Bind(&MessageReceivedCallback), - base::Bind(&MessageSentCallback))); + gcm_store_->Load(base::Bind(&MCSProbe::LoadCallback, + base::Unretained(this))); run_loop_->Run(); } +void MCSProbe::LoadCallback(const GCMStore::LoadResult& load_result) { + DCHECK(load_result.success); + android_id_ = load_result.device_android_id; + secret_ = load_result.device_security_token; + mcs_client_->Initialize( + base::Bind(&MCSProbe::ErrorCallback, base::Unretained(this)), + base::Bind(&MessageReceivedCallback), + base::Bind(&MessageSentCallback), + load_result); + + if (!android_id_ || !secret_) { + CheckIn(); + return; + } + + mcs_client_->Login(android_id_, secret_); +} + void MCSProbe::InitializeNetworkState() { FILE* log_file = NULL; if (command_line_.HasSwitch(kLogFileSwitch)) { @@ -345,25 +364,8 @@ void MCSProbe::BuildNetworkSession() { network_session_ = new net::HttpNetworkSession(session_params); } -void MCSProbe::InitializationCallback(bool success, - uint64 restored_android_id, - uint64 restored_security_token) { - LOG(INFO) << "Initialization " << (success ? "success!" : "failure!"); - if (restored_android_id && restored_security_token) { - LOG(INFO) << "Restored device check-in info."; - android_id_ = restored_android_id; - secret_ = restored_security_token; - } - if (!success) - return; - - if (!android_id_ || !secret_) { - CheckIn(); - return; - } - - LOG(INFO) << "MCS login initiated."; - mcs_client_->Login(android_id_, secret_); +void MCSProbe::ErrorCallback() { + LOG(INFO) << "MCS error happened"; } void MCSProbe::CheckIn() { -- 2.11.4.GIT