summaryrefslogtreecommitdiffstats
path: root/chrome/browser
diff options
context:
space:
mode:
authoratwilson <atwilson@chromium.org>2015-02-27 09:13:03 -0800
committerCommit bot <commit-bot@chromium.org>2015-02-27 17:14:05 +0000
commit0fabd51a7b69707cc9faf02c004aabc3b4f4b1da (patch)
tree1c123739fe863db9efbe5623902c6847e4a449ac /chrome/browser
parent93c53be1cb22bfaec6b5c201ddd6150a813acaf6 (diff)
downloadchromium_src-0fabd51a7b69707cc9faf02c004aabc3b4f4b1da.zip
chromium_src-0fabd51a7b69707cc9faf02c004aabc3b4f4b1da.tar.gz
chromium_src-0fabd51a7b69707cc9faf02c004aabc3b4f4b1da.tar.bz2
Added support for monitoring heartbeats over the GCM channel.
Wired up the HeartbeatEnabled and HeartbeatFrequency policies. Added new HeartbeatScheduler class that registers with the GCMDriver() and sends heartbeats over the GCM channel to the management server. BUG=430908 Review URL: https://codereview.chromium.org/941713003 Cr-Commit-Position: refs/heads/master@{#318471}
Diffstat (limited to 'chrome/browser')
-rw-r--r--chrome/browser/browser_process_impl.cc4
-rw-r--r--chrome/browser/chromeos/ownership/owner_settings_service_chromeos.cc2
-rw-r--r--chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.cc20
-rw-r--r--chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.h8
-rw-r--r--chrome/browser/chromeos/policy/device_policy_decoder_chromeos.cc21
-rw-r--r--chrome/browser/chromeos/policy/heartbeat_scheduler.cc376
-rw-r--r--chrome/browser/chromeos/policy/heartbeat_scheduler.h142
-rw-r--r--chrome/browser/chromeos/policy/heartbeat_scheduler_unittest.cc254
-rw-r--r--chrome/browser/chromeos/settings/device_settings_provider.cc23
-rw-r--r--chrome/browser/chromeos/settings/device_settings_provider_unittest.cc40
10 files changed, 884 insertions, 6 deletions
diff --git a/chrome/browser/browser_process_impl.cc b/chrome/browser/browser_process_impl.cc
index 9fd1a60..2f86717 100644
--- a/chrome/browser/browser_process_impl.cc
+++ b/chrome/browser/browser_process_impl.cc
@@ -291,7 +291,9 @@ void BrowserProcessImpl::StartTearDown() {
#if defined(ENABLE_CONFIGURATION_POLICY)
// The policy providers managed by |browser_policy_connector_| need to shut
- // down while the IO and FILE threads are still alive.
+ // down while the IO and FILE threads are still alive. The monitoring
+ // framework owned by |browser_policy_connector_| relies on |gcm_driver_|, so
+ // this must be shutdown before |gcm_driver_| below.
if (browser_policy_connector_)
browser_policy_connector_->Shutdown();
#endif
diff --git a/chrome/browser/chromeos/ownership/owner_settings_service_chromeos.cc b/chrome/browser/chromeos/ownership/owner_settings_service_chromeos.cc
index cc071b5..48a81cb 100644
--- a/chrome/browser/chromeos/ownership/owner_settings_service_chromeos.cc
+++ b/chrome/browser/chromeos/ownership/owner_settings_service_chromeos.cc
@@ -643,6 +643,8 @@ void OwnerSettingsServiceChromeOS::UpdateDeviceSettings(
// kAccountsPrefTransferSAMLCookies
// kDeviceAttestationEnabled
// kDeviceOwner
+ // kHeartbeatEnabled
+ // kHeartbeatFrequency
// kReleaseChannelDelegated
// kReportDeviceActivityTimes
// kReportDeviceBootMode
diff --git a/chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.cc b/chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.cc
index 91f687e..5f54015 100644
--- a/chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.cc
+++ b/chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.cc
@@ -14,12 +14,14 @@
#include "base/prefs/pref_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
+#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/attestation/attestation_policy_observer.h"
#include "chrome/browser/chromeos/login/enrollment/auto_enrollment_controller.h"
#include "chrome/browser/chromeos/login/startup_utils.h"
#include "chrome/browser/chromeos/policy/device_cloud_policy_store_chromeos.h"
#include "chrome/browser/chromeos/policy/device_status_collector.h"
#include "chrome/browser/chromeos/policy/enterprise_install_attributes.h"
+#include "chrome/browser/chromeos/policy/heartbeat_scheduler.h"
#include "chrome/browser/chromeos/policy/server_backed_state_keys_broker.h"
#include "chrome/browser/chromeos/policy/status_uploader.h"
#include "chrome/common/pref_names.h"
@@ -186,6 +188,7 @@ bool DeviceCloudPolicyManagerChromeOS::IsSharkRequisition() const {
void DeviceCloudPolicyManagerChromeOS::Shutdown() {
status_uploader_.reset();
+ heartbeat_scheduler_.reset();
state_keys_update_subscription_.reset();
CloudPolicyManager::Shutdown();
}
@@ -240,11 +243,19 @@ void DeviceCloudPolicyManagerChromeOS::StartConnection(
attestation_policy_observer_.reset(
new chromeos::attestation::AttestationPolicyObserver(client()));
- // Enable device reporting for enterprise enrolled devices. We want to do this
- // even if management is currently inactive, in case management is turned
- // back on in a future policy fetch.
- if (install_attributes->IsEnterpriseDevice())
+ // Enable device reporting and status monitoring for enterprise enrolled
+ // devices. We want to create these objects for enrolled devices, even if
+ // monitoring is currently inactive, in case monitoring is turned back on in
+ // a future policy fetch - the classes themselves track the current state of
+ // the monitoring settings and only perform monitoring if it is active.
+ if (install_attributes->IsEnterpriseDevice()) {
CreateStatusUploader();
+ heartbeat_scheduler_.reset(
+ new HeartbeatScheduler(g_browser_process->gcm_driver(),
+ install_attributes->GetDomain(),
+ install_attributes->GetDeviceId(),
+ task_runner_));
+ }
NotifyConnected();
}
@@ -263,6 +274,7 @@ void DeviceCloudPolicyManagerChromeOS::Unregister(
void DeviceCloudPolicyManagerChromeOS::Disconnect() {
status_uploader_.reset();
+ heartbeat_scheduler_.reset();
core()->Disconnect();
NotifyDisconnected();
diff --git a/chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.h b/chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.h
index 775e6f2..2035c5e 100644
--- a/chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.h
+++ b/chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.h
@@ -35,6 +35,7 @@ namespace policy {
class DeviceCloudPolicyStoreChromeOS;
class EnterpriseInstallAttributes;
+class HeartbeatScheduler;
class StatusUploader;
// CloudPolicyManager specialization for device policy on Chrome OS.
@@ -50,7 +51,8 @@ class DeviceCloudPolicyManagerChromeOS : public CloudPolicyManager {
using UnregisterCallback = base::Callback<void(bool)>;
- // |task_runner| is the runner for policy refresh tasks.
+ // |task_runner| is the runner for policy refresh, heartbeat, and status
+ // upload tasks.
DeviceCloudPolicyManagerChromeOS(
scoped_ptr<DeviceCloudPolicyStoreChromeOS> store,
const scoped_refptr<base::SequencedTaskRunner>& task_runner,
@@ -127,6 +129,10 @@ class DeviceCloudPolicyManagerChromeOS : public CloudPolicyManager {
// state.
scoped_ptr<StatusUploader> status_uploader_;
+ // Helper object that handles sending heartbeats over the GCM channel to
+ // the server, to monitor connectivity.
+ scoped_ptr<HeartbeatScheduler> heartbeat_scheduler_;
+
// The TaskRunner used to do device status uploads.
scoped_refptr<base::SequencedTaskRunner> task_runner_;
diff --git a/chrome/browser/chromeos/policy/device_policy_decoder_chromeos.cc b/chrome/browser/chromeos/policy/device_policy_decoder_chromeos.cc
index 0ba51d8..5492114 100644
--- a/chrome/browser/chromeos/policy/device_policy_decoder_chromeos.cc
+++ b/chrome/browser/chromeos/policy/device_policy_decoder_chromeos.cc
@@ -413,6 +413,27 @@ void DecodeReportingPolicies(const em::ChromeDeviceSettingsProto& policy,
NULL);
}
}
+
+ if (policy.has_device_heartbeat_settings()) {
+ const em::DeviceHeartbeatSettingsProto& container(
+ policy.device_heartbeat_settings());
+ if (container.has_heartbeat_enabled()) {
+ policies->Set(key::kHeartbeatEnabled,
+ POLICY_LEVEL_MANDATORY,
+ POLICY_SCOPE_MACHINE,
+ new base::FundamentalValue(
+ container.heartbeat_enabled()),
+ NULL);
+ }
+ if (container.has_heartbeat_frequency()) {
+ policies->Set(key::kHeartbeatFrequency,
+ POLICY_LEVEL_MANDATORY,
+ POLICY_SCOPE_MACHINE,
+ DecodeIntegerValue(
+ container.heartbeat_frequency()).release(),
+ NULL);
+ }
+ }
}
void DecodeAutoUpdatePolicies(const em::ChromeDeviceSettingsProto& policy,
diff --git a/chrome/browser/chromeos/policy/heartbeat_scheduler.cc b/chrome/browser/chromeos/policy/heartbeat_scheduler.cc
new file mode 100644
index 0000000..74dd1e0
--- /dev/null
+++ b/chrome/browser/chromeos/policy/heartbeat_scheduler.cc
@@ -0,0 +1,376 @@
+// Copyright (c) 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/chromeos/policy/heartbeat_scheduler.h"
+
+#include <string>
+
+#include "base/command_line.h"
+#include "base/location.h"
+#include "base/sequenced_task_runner.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/time/time.h"
+#include "chrome/common/chrome_switches.h"
+#include "components/gcm_driver/gcm_driver.h"
+
+namespace {
+const int kMinHeartbeatIntervalMs = 30 * 1000; // 30 seconds
+const int kMaxHeartbeatIntervalMs = 24 * 60 * 60 * 1000; // 24 hours
+
+// Our sender ID we send up with all of our GCM messages.
+const char* kHeartbeatGCMAppID = "com.google.chromeos.monitoring";
+
+// The default destination we send our GCM messages to.
+const char* kHeartbeatGCMDestinationID = "1013309121859";
+const char* kHeartbeatGCMSenderSuffix = "@google.com";
+
+const char* kMonitoringMessageTypeKey = "type";
+const char* kHeartbeatTimestampKey = "timestamp";
+const char* kHeartbeatDomainNameKey = "domain_name";
+const char* kHeartbeatDeviceIDKey = "device_id";
+const char* kHeartbeatTypeValue = "hb";
+
+// If we get an error registering with GCM, try again in two minutes.
+const int64 kRegistrationRetryDelayMs = 2 * 60 * 1000;
+
+// Returns the destination ID for GCM heartbeats.
+std::string GetDestinationID() {
+ std::string receiver_id = kHeartbeatGCMDestinationID;
+ if (base::CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kMonitoringDestinationID)) {
+ receiver_id = base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
+ switches::kMonitoringDestinationID);
+ }
+ return receiver_id;
+}
+
+} // namespace
+
+namespace policy {
+
+const int64 HeartbeatScheduler::kDefaultHeartbeatIntervalMs =
+ 2 * 60 * 1000; // 2 minutes
+
+// Helper class used to manage GCM registration (handles retrying after
+// errors, etc).
+class HeartbeatRegistrationHelper {
+ public:
+ typedef base::Callback<void(const std::string& registration_id)>
+ RegistrationHelperCallback;
+
+ HeartbeatRegistrationHelper(
+ gcm::GCMDriver* gcm_driver,
+ const scoped_refptr<base::SequencedTaskRunner>& task_runner);
+
+ void Register(const RegistrationHelperCallback& callback);
+
+ private:
+ void AttemptRegistration();
+
+ // Callback invoked once a registration attempt has finished.
+ void OnRegisterAttemptComplete(const std::string& registration_id,
+ gcm::GCMClient::Result result);
+
+ // GCMDriver to use to register.
+ gcm::GCMDriver* const gcm_driver_;
+
+ // Callback to invoke when we have completed GCM registration.
+ RegistrationHelperCallback callback_;
+
+ // TaskRunner used for scheduling retry attempts.
+ const scoped_refptr<base::SequencedTaskRunner> task_runner_;
+
+ // Should remain the last member so it will be destroyed first and
+ // invalidate all weak pointers.
+ base::WeakPtrFactory<HeartbeatRegistrationHelper> weak_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(HeartbeatRegistrationHelper);
+};
+
+HeartbeatRegistrationHelper::HeartbeatRegistrationHelper(
+ gcm::GCMDriver* gcm_driver,
+ const scoped_refptr<base::SequencedTaskRunner>& task_runner)
+ : gcm_driver_(gcm_driver),
+ task_runner_(task_runner),
+ weak_factory_(this) {
+}
+
+void HeartbeatRegistrationHelper::Register(
+ const RegistrationHelperCallback& callback) {
+ // Should only call Register() once.
+ DCHECK(callback_.is_null());
+ callback_ = callback;
+ AttemptRegistration();
+}
+
+void HeartbeatRegistrationHelper::AttemptRegistration() {
+ std::vector<std::string> destinations;
+ destinations.push_back(GetDestinationID());
+ gcm_driver_->Register(
+ kHeartbeatGCMAppID,
+ destinations,
+ base::Bind(&HeartbeatRegistrationHelper::OnRegisterAttemptComplete,
+ weak_factory_.GetWeakPtr()));
+}
+
+void HeartbeatRegistrationHelper::OnRegisterAttemptComplete(
+ const std::string& registration_id, gcm::GCMClient::Result result) {
+ DVLOG(1) << "Received Register() response: " << result;
+ // TODO(atwilson): Track GCM errors via UMA (http://crbug.com/459238).
+ switch (result) {
+ case gcm::GCMClient::SUCCESS:
+ {
+ // Copy the callback, because the callback may free this object and
+ // we don't want to free the callback object and any bound variables
+ // until the callback exits.
+ RegistrationHelperCallback callback = callback_;
+ callback.Run(registration_id);
+ // This helper may be freed now, so do not access any member variables
+ // after this point.
+ return;
+ }
+
+ case gcm::GCMClient::NETWORK_ERROR:
+ case gcm::GCMClient::SERVER_ERROR:
+ // Transient error - try again after a delay.
+ task_runner_->PostDelayedTask(
+ FROM_HERE,
+ base::Bind(&HeartbeatRegistrationHelper::AttemptRegistration,
+ weak_factory_.GetWeakPtr()),
+ base::TimeDelta::FromMilliseconds(kRegistrationRetryDelayMs));
+ break;
+
+ case gcm::GCMClient::INVALID_PARAMETER:
+ case gcm::GCMClient::UNKNOWN_ERROR:
+ case gcm::GCMClient::GCM_DISABLED:
+ // No need to bother retrying in the case of one of these fatal errors.
+ // This means that heartbeats will remain disabled until the next
+ // restart.
+ DLOG(ERROR) << "Fatal GCM Registration error: " << result;
+ break;
+
+ case gcm::GCMClient::ASYNC_OPERATION_PENDING:
+ case gcm::GCMClient::TTL_EXCEEDED:
+ default:
+ NOTREACHED() << "Unexpected GCMDriver::Register() result: " << result;
+ break;
+ }
+}
+
+HeartbeatScheduler::HeartbeatScheduler(
+ gcm::GCMDriver* driver,
+ const std::string& enrollment_domain,
+ const std::string& device_id,
+ const scoped_refptr<base::SequencedTaskRunner>& task_runner)
+ : task_runner_(task_runner),
+ enrollment_domain_(enrollment_domain),
+ device_id_(device_id),
+ heartbeat_enabled_(false),
+ heartbeat_interval_(base::TimeDelta::FromMilliseconds(
+ kDefaultHeartbeatIntervalMs)),
+ gcm_driver_(driver),
+ weak_factory_(this) {
+ // If no GCMDriver (e.g. this is loaded as part of an unrelated unit test)
+ // do nothing as no heartbeats can be sent.
+ if (!gcm_driver_)
+ return;
+
+ heartbeat_frequency_observer_ =
+ chromeos::CrosSettings::Get()->AddSettingsObserver(
+ chromeos::kHeartbeatFrequency,
+ base::Bind(&HeartbeatScheduler::RefreshHeartbeatSettings,
+ base::Unretained(this)));
+
+ heartbeat_enabled_observer_ =
+ chromeos::CrosSettings::Get()->AddSettingsObserver(
+ chromeos::kHeartbeatEnabled,
+ base::Bind(&HeartbeatScheduler::RefreshHeartbeatSettings,
+ base::Unretained(this)));
+
+ // Update the heartbeat frequency from settings. This will trigger a
+ // heartbeat as appropriate once the settings have been refreshed.
+ RefreshHeartbeatSettings();
+}
+
+void HeartbeatScheduler::RefreshHeartbeatSettings() {
+ // Attempt to fetch the current value of the reporting settings.
+ // If trusted values are not available, register this function to be called
+ // back when they are available.
+ chromeos::CrosSettings* settings = chromeos::CrosSettings::Get();
+ if (chromeos::CrosSettingsProvider::TRUSTED != settings->PrepareTrustedValues(
+ base::Bind(&HeartbeatScheduler::RefreshHeartbeatSettings,
+ weak_factory_.GetWeakPtr()))) {
+ return;
+ }
+
+ // CrosSettings are trusted - update our cached settings (we cache the
+ // value because CrosSettings can become untrusted at arbitrary times and we
+ // want to use the last trusted value).
+ int frequency;
+ if (settings->GetInteger(chromeos::kHeartbeatFrequency, &frequency))
+ heartbeat_interval_ = EnsureValidHeartbeatInterval(
+ base::TimeDelta::FromMilliseconds(frequency));
+
+ bool enabled;
+ if (settings->GetBoolean(chromeos::kHeartbeatEnabled, &enabled))
+ heartbeat_enabled_ = enabled;
+
+ if (!heartbeat_enabled_) {
+ // Heartbeats are no longer enabled - cancel our callback and any
+ // outstanding registration attempts and disconnect from GCM so the
+ // connection can be shut down. If heartbeats are re-enabled later, we
+ // will re-register with GCM.
+ heartbeat_callback_.Cancel();
+ ShutdownGCM();
+ } else {
+ // Schedule a new upload with the new frequency.
+ ScheduleNextHeartbeat();
+ }
+
+ DVLOG(1) << "heartbeat enabled: " << heartbeat_enabled_;
+ DVLOG(1) << "heartbeat frequency: " << heartbeat_interval_;
+}
+
+void HeartbeatScheduler::ShutdownGCM() {
+ registration_helper_.reset();
+ registration_id_.clear();
+ if (registered_app_handler_) {
+ registered_app_handler_ = false;
+ gcm_driver_->RemoveAppHandler(kHeartbeatGCMAppID);
+ }
+}
+
+base::TimeDelta HeartbeatScheduler::EnsureValidHeartbeatInterval(
+ const base::TimeDelta& interval) {
+ const base::TimeDelta min = base::TimeDelta::FromMilliseconds(
+ kMinHeartbeatIntervalMs);
+ const base::TimeDelta max = base::TimeDelta::FromMilliseconds(
+ kMaxHeartbeatIntervalMs);
+ if (interval < min) {
+ DLOG(WARNING) << "Invalid heartbeat interval: " << interval;
+ return min;
+ }
+ if (interval > max) {
+ DLOG(WARNING) << "Invalid heartbeat interval: " << interval;
+ return max;
+ }
+ return interval;
+}
+
+void HeartbeatScheduler::ScheduleNextHeartbeat() {
+ // Do nothing if heartbeats are disabled.
+ if (!heartbeat_enabled_)
+ return;
+
+ if (registration_id_.empty()) {
+ // We are not registered with the GCM service yet, so kick off registration.
+ if (!registration_helper_) {
+ // Add ourselves as an AppHandler - this is required in order to setup
+ // a GCM connection.
+ registered_app_handler_ = true;
+ gcm_driver_->AddAppHandler(kHeartbeatGCMAppID, this);
+ registration_helper_.reset(new HeartbeatRegistrationHelper(
+ gcm_driver_, task_runner_));
+ registration_helper_->Register(
+ base::Bind(&HeartbeatScheduler::OnRegistrationComplete,
+ weak_factory_.GetWeakPtr()));
+ }
+ return;
+ }
+
+ // Calculate when to fire off the next update (if it should have already
+ // happened, this yields a TimeDelta of 0).
+ base::TimeDelta delay = std::max(
+ last_heartbeat_ + heartbeat_interval_ - base::Time::NowFromSystemTime(),
+ base::TimeDelta());
+
+ heartbeat_callback_.Reset(base::Bind(&HeartbeatScheduler::SendHeartbeat,
+ base::Unretained(this)));
+ task_runner_->PostDelayedTask(
+ FROM_HERE, heartbeat_callback_.callback(), delay);
+}
+
+void HeartbeatScheduler::OnRegistrationComplete(
+ const std::string& registration_id) {
+ DCHECK(!registration_id.empty());
+ registration_helper_.reset();
+ registration_id_ = registration_id;
+
+ // Now that GCM registration is complete, start sending heartbeats.
+ ScheduleNextHeartbeat();
+}
+
+void HeartbeatScheduler::SendHeartbeat() {
+ DCHECK(!registration_id_.empty());
+ if (!gcm_driver_ || !heartbeat_enabled_)
+ return;
+
+ gcm::GCMClient::OutgoingMessage message;
+ message.time_to_live = heartbeat_interval_.InSeconds();
+ // Just use the current timestamp as the message ID - if the user changes the
+ // time and we send a message with the same ID that we previously used, no
+ // big deal (the new message will replace the old, which is the behavior we
+ // want anyway, per:
+ // https://developer.chrome.com/apps/cloudMessaging#send_messages
+ message.id = base::Int64ToString(
+ base::Time::NowFromSystemTime().ToInternalValue());
+ message.data[kMonitoringMessageTypeKey] = kHeartbeatTypeValue;
+ message.data[kHeartbeatTimestampKey] = base::Int64ToString(
+ base::Time::NowFromSystemTime().ToJavaTime());
+ message.data[kHeartbeatDomainNameKey] = enrollment_domain_;
+ message.data[kHeartbeatDeviceIDKey] = device_id_;
+ gcm_driver_->Send(kHeartbeatGCMAppID,
+ GetDestinationID() + kHeartbeatGCMSenderSuffix,
+ message,
+ base::Bind(&HeartbeatScheduler::OnHeartbeatSent,
+ weak_factory_.GetWeakPtr()));
+}
+
+void HeartbeatScheduler::OnHeartbeatSent(const std::string& message_id,
+ gcm::GCMClient::Result result) {
+ DVLOG(1) << "Monitoring heartbeat sent - result = " << result;
+ // Don't care if the result was successful or not - just schedule the next
+ // heartbeat.
+ DLOG_IF(ERROR, result != gcm::GCMClient::SUCCESS) <<
+ "Error sending monitoring heartbeat: " << result;
+ last_heartbeat_ = base::Time::NowFromSystemTime();
+ ScheduleNextHeartbeat();
+}
+
+HeartbeatScheduler::~HeartbeatScheduler() {
+ ShutdownGCM();
+}
+
+void HeartbeatScheduler::ShutdownHandler() {
+ // This should never be called, because BrowserProcessImpl::StartTearDown()
+ // should shutdown the BrowserPolicyConnector (which destroys this object)
+ // before the GCMDriver. Our goal is to make sure that this object is always
+ // shutdown before GCMDriver is shut down, rather than trying to handle the
+ // case when GCMDriver goes away.
+ NOTREACHED() << "HeartbeatScheduler should be destroyed before GCMDriver";
+}
+
+void HeartbeatScheduler::OnMessage(
+ const std::string& app_id,
+ const gcm::GCMClient::IncomingMessage& message) {
+ // Should never be called because we don't get any incoming messages
+ // for our app ID.
+ NOTREACHED() << "Received incoming message for " << app_id;
+}
+
+void HeartbeatScheduler::OnMessagesDeleted(const std::string& app_id) {
+}
+
+void HeartbeatScheduler::OnSendError(
+ const std::string& app_id,
+ const gcm::GCMClient::SendErrorDetails& details) {
+ // Ignore send errors - we already are notified above in OnHeartbeatSent().
+}
+
+void HeartbeatScheduler::OnSendAcknowledged(const std::string& app_id,
+ const std::string& message_id) {
+ DVLOG(1) << "Heartbeat sent with message_id: " << message_id;
+}
+
+} // namespace policy
diff --git a/chrome/browser/chromeos/policy/heartbeat_scheduler.h b/chrome/browser/chromeos/policy/heartbeat_scheduler.h
new file mode 100644
index 0000000..d516eb0
--- /dev/null
+++ b/chrome/browser/chromeos/policy/heartbeat_scheduler.h
@@ -0,0 +1,142 @@
+// Copyright (c) 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CHROMEOS_POLICY_HEARTBEAT_SCHEDULER_H_
+#define CHROME_BROWSER_CHROMEOS_POLICY_HEARTBEAT_SCHEDULER_H_
+
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/cancelable_callback.h"
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "chrome/browser/chromeos/settings/cros_settings.h"
+#include "components/gcm_driver/gcm_app_handler.h"
+#include "components/gcm_driver/gcm_client.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace gcm {
+class GCMDriver;
+}
+
+namespace policy {
+
+class EnterpriseInstallAttributes;
+class HeartbeatRegistrationHelper;
+
+// Class responsible for periodically sending heartbeats to the policy service
+// for monitoring device connectivity.
+class HeartbeatScheduler : public gcm::GCMAppHandler {
+ public:
+ // Default interval for how often we send up a heartbeat.
+ static const int64 kDefaultHeartbeatIntervalMs;
+
+ // Constructor. |driver| can be null for tests.
+ HeartbeatScheduler(
+ gcm::GCMDriver* driver,
+ const std::string& enrollment_domain,
+ const std::string& device_id,
+ const scoped_refptr<base::SequencedTaskRunner>& task_runner);
+
+ ~HeartbeatScheduler() override;
+
+ // Returns the time of the last heartbeat, or Time(0) if no heartbeat
+ // has ever happened.
+ base::Time last_heartbeat() const { return last_heartbeat_; }
+
+ // GCMAppHandler overrides.
+ void ShutdownHandler() override;
+ void OnMessage(const std::string& app_id,
+ const gcm::GCMClient::IncomingMessage& message) override;
+ void OnMessagesDeleted(const std::string& app_id) override;
+ void OnSendError(const std::string& app_id,
+ const gcm::GCMClient::SendErrorDetails& details) override;
+ void OnSendAcknowledged(const std::string& app_id,
+ const std::string& message_id) override;
+
+ private:
+ // Callback invoked periodically to send a heartbeat to the policy service.
+ void SendHeartbeat();
+
+ // Invoked after GCM registration has successfully completed.
+ void OnRegistrationComplete(const std::string& registration_id);
+
+ // Invoked after a heartbeat has been sent to the server.
+ void OnHeartbeatSent(const std::string& message_id,
+ gcm::GCMClient::Result result);
+
+ // Helper method that figures out when the next heartbeat should
+ // be scheduled.
+ void ScheduleNextHeartbeat();
+
+ // Updates the heartbeat-enabled status and frequency from settings and
+ // schedules the next heartbeat.
+ void RefreshHeartbeatSettings();
+
+ // Ensures that the passed interval is within a valid range (not too large or
+ // too small).
+ base::TimeDelta EnsureValidHeartbeatInterval(const base::TimeDelta& interval);
+
+ // Shuts down our GCM connection (called when heartbeats are disabled).
+ void ShutdownGCM();
+
+ // TaskRunner used for scheduling heartbeats.
+ const scoped_refptr<base::SequencedTaskRunner> task_runner_;
+
+ // The domain that this device is enrolled to.
+ const std::string enrollment_domain_;
+
+ // The device_id for this device - sent up with the enrollment domain with
+ // heartbeats to identify the device to the server.
+ const std::string device_id_;
+
+ // True if heartbeats are enabled. Kept cached in this object because
+ // CrosSettings can switch to an untrusted state temporarily, and we want
+ // to use the last-known trusted values.
+ bool heartbeat_enabled_;
+
+ // Cached copy of the current heartbeat interval, in milliseconds.
+ base::TimeDelta heartbeat_interval_;
+
+ // Observers to changes in the heartbeat settings.
+ scoped_ptr<chromeos::CrosSettings::ObserverSubscription>
+ heartbeat_frequency_observer_;
+ scoped_ptr<chromeos::CrosSettings::ObserverSubscription>
+ heartbeat_enabled_observer_;
+
+ // The time the last heartbeat was sent.
+ base::Time last_heartbeat_;
+
+ // Callback invoked via a delay to send a heartbeat.
+ base::CancelableClosure heartbeat_callback_;
+
+ // The GCMDriver used to send heartbeat messages.
+ gcm::GCMDriver* const gcm_driver_;
+
+ // The GCM registration ID - if empty, we are not registered yet.
+ std::string registration_id_;
+
+ // If true, we are already registered with GCM and should unregister when
+ // destroyed.
+ bool registered_app_handler_ = false;
+
+ // Helper class to manage registering with the GCM server, including
+ // retries, etc.
+ scoped_ptr<HeartbeatRegistrationHelper> registration_helper_;
+
+ // Note: This should remain the last member so it'll be destroyed and
+ // invalidate the weak pointers before any other members are destroyed.
+ base::WeakPtrFactory<HeartbeatScheduler> weak_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(HeartbeatScheduler);
+};
+
+} // namespace policy
+
+#endif // CHROME_BROWSER_CHROMEOS_POLICY_HEARTBEAT_SCHEDULER_H_
diff --git a/chrome/browser/chromeos/policy/heartbeat_scheduler_unittest.cc b/chrome/browser/chromeos/policy/heartbeat_scheduler_unittest.cc
new file mode 100644
index 0000000..7a1a05c
--- /dev/null
+++ b/chrome/browser/chromeos/policy/heartbeat_scheduler_unittest.cc
@@ -0,0 +1,254 @@
+// Copyright (c) 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/chromeos/policy/heartbeat_scheduler.h"
+
+#include "base/strings/string_number_conversions.h"
+#include "base/test/test_simple_task_runner.h"
+#include "chrome/browser/chromeos/settings/cros_settings.h"
+#include "chrome/browser/chromeos/settings/device_settings_service.h"
+#include "chrome/browser/chromeos/settings/stub_cros_settings_provider.h"
+#include "chromeos/settings/cros_settings_names.h"
+#include "components/gcm_driver/fake_gcm_driver.h"
+#include "content/public/test/test_utils.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ::testing::_;
+using ::testing::SaveArg;
+
+namespace {
+const char* const kFakeEnrollmentDomain = "example.com";
+const char* const kFakeDeviceId = "fake_device_id";
+const char* const kHeartbeatGCMAppID = "com.google.chromeos.monitoring";
+
+class MockGCMDriver : public testing::StrictMock<gcm::FakeGCMDriver> {
+ public:
+ MockGCMDriver() {
+ }
+
+ ~MockGCMDriver() override {
+ }
+
+ MOCK_METHOD2(RegisterImpl,
+ void(const std::string&, const std::vector<std::string>&));
+ MOCK_METHOD3(SendImpl,
+ void(const std::string&, const std::string&,
+ const gcm::GCMClient::OutgoingMessage& message));
+
+
+ // Helper function to complete a registration previously started by
+ // Register().
+ void CompleteRegistration(const std::string& app_id,
+ gcm::GCMClient::Result result) {
+ RegisterFinished(app_id, "registration_id", result);
+ }
+
+ // Helper function to complete a send operation previously started by
+ // Send().
+ void CompleteSend(const std::string& app_id,
+ const std::string& message_id,
+ gcm::GCMClient::Result result) {
+ SendFinished(app_id, message_id, result);
+ }
+};
+
+class HeartbeatSchedulerTest : public testing::Test {
+ public:
+ HeartbeatSchedulerTest()
+ : task_runner_(new base::TestSimpleTaskRunner()),
+ scheduler_(
+ &gcm_driver_, kFakeEnrollmentDomain, kFakeDeviceId, task_runner_) {
+ }
+
+ void SetUp() override {
+ // Swap out the DeviceSettingsProvider with our stub settings provider
+ // so we can set values for the heartbeat frequency.
+ chromeos::CrosSettings* cros_settings = chromeos::CrosSettings::Get();
+ device_settings_provider_ =
+ cros_settings->GetProvider(chromeos::kReportDeviceVersionInfo);
+ EXPECT_TRUE(device_settings_provider_);
+ EXPECT_TRUE(
+ cros_settings->RemoveSettingsProvider(device_settings_provider_));
+ cros_settings->AddSettingsProvider(&stub_settings_provider_);
+ }
+
+ void TearDown() override {
+ content::RunAllBlockingPoolTasksUntilIdle();
+ // Restore the real DeviceSettingsProvider.
+ chromeos::CrosSettings* cros_settings = chromeos::CrosSettings::Get();
+ EXPECT_TRUE(cros_settings->RemoveSettingsProvider(
+ &stub_settings_provider_));
+ cros_settings->AddSettingsProvider(device_settings_provider_);
+ }
+
+ void CheckPendingTaskDelay(base::Time last_heartbeat,
+ base::TimeDelta expected_delay) {
+ EXPECT_FALSE(last_heartbeat.is_null());
+ base::Time now = base::Time::NowFromSystemTime();
+ EXPECT_GE(now, last_heartbeat);
+ base::TimeDelta actual_delay = task_runner_->NextPendingTaskDelay();
+
+ // NextPendingTaskDelay() returns the exact original delay value the task
+ // was posted with. The heartbeat task would have been calculated to fire at
+ // |last_heartbeat| + |expected_delay|, but we don't know the exact time
+ // when the task was posted (if it was a couple of milliseconds after
+ // |last_heartbeat|, then |actual_delay| would be a couple of milliseconds
+ // smaller than |expected_delay|.
+ //
+ // We do know that the task was posted sometime between |last_heartbeat|
+ // and |now|, so we know that 0 <= |expected_delay| - |actual_delay| <=
+ // |now| - |last_heartbeat|.
+ base::TimeDelta delta = expected_delay - actual_delay;
+ EXPECT_LE(base::TimeDelta(), delta);
+ EXPECT_GE(now - last_heartbeat, delta);
+ }
+
+ base::MessageLoop loop_;
+
+ // Helpers used to mock out cros settings.
+ chromeos::ScopedTestDeviceSettingsService test_device_settings_service_;
+ chromeos::ScopedTestCrosSettings test_cros_settings_;
+ chromeos::CrosSettingsProvider* device_settings_provider_;
+ chromeos::StubCrosSettingsProvider stub_settings_provider_;
+
+ MockGCMDriver gcm_driver_;
+
+ // TaskRunner used to run individual tests.
+ scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
+
+ // The HeartbeatScheduler instance under test.
+ policy::HeartbeatScheduler scheduler_;
+};
+
+TEST_F(HeartbeatSchedulerTest, Basic) {
+ // Just makes sure we can spin up and shutdown the scheduler with
+ // heartbeats disabled.
+ chromeos::CrosSettings::Get()->SetBoolean(
+ chromeos::kHeartbeatEnabled, false);
+ ASSERT_TRUE(task_runner_->GetPendingTasks().empty());
+}
+
+TEST_F(HeartbeatSchedulerTest, PermanentlyFailedGCMRegistration) {
+ // If heartbeats are enabled, we should register with GCMDriver.
+ EXPECT_CALL(gcm_driver_, RegisterImpl(kHeartbeatGCMAppID, _));
+ chromeos::CrosSettings::Get()->SetBoolean(
+ chromeos::kHeartbeatEnabled, true);
+ gcm_driver_.CompleteRegistration(
+ kHeartbeatGCMAppID, gcm::GCMClient::GCM_DISABLED);
+
+ // There should be no heartbeat tasks pending, because registration failed.
+ ASSERT_TRUE(task_runner_->GetPendingTasks().empty());
+}
+
+TEST_F(HeartbeatSchedulerTest, TemporarilyFailedGCMRegistration) {
+ EXPECT_CALL(gcm_driver_, RegisterImpl(kHeartbeatGCMAppID, _));
+ chromeos::CrosSettings::Get()->SetBoolean(
+ chromeos::kHeartbeatEnabled, true);
+ gcm_driver_.CompleteRegistration(
+ kHeartbeatGCMAppID, gcm::GCMClient::SERVER_ERROR);
+ testing::Mock::VerifyAndClearExpectations(&gcm_driver_);
+
+ // Should have a pending task to try registering again.
+ ASSERT_FALSE(task_runner_->GetPendingTasks().empty());
+ EXPECT_CALL(gcm_driver_, RegisterImpl(kHeartbeatGCMAppID, _));
+ task_runner_->RunPendingTasks();
+ testing::Mock::VerifyAndClearExpectations(&gcm_driver_);
+
+ // Once we have successfully registered, we should send a heartbeat.
+ EXPECT_CALL(gcm_driver_, SendImpl(kHeartbeatGCMAppID, _, _));
+ gcm_driver_.CompleteRegistration(
+ kHeartbeatGCMAppID, gcm::GCMClient::SUCCESS);
+ task_runner_->RunPendingTasks();
+}
+
+TEST_F(HeartbeatSchedulerTest, ChangeHeartbeatFrequency) {
+ EXPECT_CALL(gcm_driver_, RegisterImpl(kHeartbeatGCMAppID, _));
+ chromeos::CrosSettings::Get()->SetBoolean(
+ chromeos::kHeartbeatEnabled, true);
+ gcm_driver_.CompleteRegistration(
+ kHeartbeatGCMAppID, gcm::GCMClient::SUCCESS);
+
+ EXPECT_EQ(1U, task_runner_->GetPendingTasks().size());
+ // Should have a heartbeat task posted with zero delay on startup.
+ EXPECT_EQ(base::TimeDelta(), task_runner_->NextPendingTaskDelay());
+ testing::Mock::VerifyAndClearExpectations(&gcm_driver_);
+
+ const int new_delay = 1234*1000; // 1234 seconds.
+ chromeos::CrosSettings::Get()->SetInteger(chromeos::kHeartbeatFrequency,
+ new_delay);
+ // Now run pending heartbeat task, should send a heartbeat.
+ gcm::GCMClient::OutgoingMessage message;
+ EXPECT_CALL(gcm_driver_, SendImpl(kHeartbeatGCMAppID, _, _))
+ .WillOnce(SaveArg<2>(&message));
+ task_runner_->RunPendingTasks();
+ EXPECT_TRUE(task_runner_->GetPendingTasks().empty());
+
+ // Complete sending a message - we should queue up the next heartbeat
+ // even if the previous attempt failed.
+ gcm_driver_.CompleteSend(
+ kHeartbeatGCMAppID, message.id, gcm::GCMClient::SERVER_ERROR);
+ EXPECT_EQ(1U, task_runner_->GetPendingTasks().size());
+ CheckPendingTaskDelay(scheduler_.last_heartbeat(),
+ base::TimeDelta::FromMilliseconds(new_delay));
+}
+
+TEST_F(HeartbeatSchedulerTest, DisableHeartbeats) {
+ // Makes sure that we can disable heartbeats on the fly.
+ EXPECT_CALL(gcm_driver_, RegisterImpl(kHeartbeatGCMAppID, _));
+ chromeos::CrosSettings::Get()->SetBoolean(
+ chromeos::kHeartbeatEnabled, true);
+ gcm::GCMClient::OutgoingMessage message;
+ EXPECT_CALL(gcm_driver_, SendImpl(kHeartbeatGCMAppID, _, _))
+ .WillOnce(SaveArg<2>(&message));
+ gcm_driver_.CompleteRegistration(
+ kHeartbeatGCMAppID, gcm::GCMClient::SUCCESS);
+ // Should have a heartbeat task posted.
+ EXPECT_EQ(1U, task_runner_->GetPendingTasks().size());
+ task_runner_->RunPendingTasks();
+
+ // Complete sending a message - we should queue up the next heartbeat.
+ gcm_driver_.CompleteSend(
+ kHeartbeatGCMAppID, message.id, gcm::GCMClient::SUCCESS);
+
+ // Should have a new heartbeat task posted.
+ ASSERT_EQ(1U, task_runner_->GetPendingTasks().size());
+ CheckPendingTaskDelay(
+ scheduler_.last_heartbeat(),
+ base::TimeDelta::FromMilliseconds(
+ policy::HeartbeatScheduler::kDefaultHeartbeatIntervalMs));
+ testing::Mock::VerifyAndClearExpectations(&gcm_driver_);
+
+ // Now disable heartbeats. Should get no more heartbeats sent.
+ chromeos::CrosSettings::Get()->SetBoolean(
+ chromeos::kHeartbeatEnabled, false);
+ task_runner_->RunPendingTasks();
+ EXPECT_TRUE(task_runner_->GetPendingTasks().empty());
+}
+
+TEST_F(HeartbeatSchedulerTest, CheckMessageContents) {
+ gcm::GCMClient::OutgoingMessage message;
+ EXPECT_CALL(gcm_driver_, RegisterImpl(kHeartbeatGCMAppID, _));
+ EXPECT_CALL(gcm_driver_, SendImpl(kHeartbeatGCMAppID, _, _))
+ .WillOnce(SaveArg<2>(&message));
+ chromeos::CrosSettings::Get()->SetBoolean(
+ chromeos::kHeartbeatEnabled, true);
+ gcm_driver_.CompleteRegistration(
+ kHeartbeatGCMAppID, gcm::GCMClient::SUCCESS);
+ task_runner_->RunPendingTasks();
+
+ // Heartbeats should have a time-to-live equivalent to the heartbeat frequency
+ // so we don't have more than one heartbeat queued at a time.
+ EXPECT_EQ(policy::HeartbeatScheduler::kDefaultHeartbeatIntervalMs/1000,
+ message.time_to_live);
+
+ // Check the values in the message payload.
+ EXPECT_EQ("hb", message.data["type"]);
+ int64 timestamp;
+ EXPECT_TRUE(base::StringToInt64(message.data["timestamp"], &timestamp));
+ EXPECT_EQ(kFakeEnrollmentDomain, message.data["domain_name"]);
+ EXPECT_EQ(kFakeDeviceId, message.data["device_id"]);
+}
+
+} // namespace
diff --git a/chrome/browser/chromeos/settings/device_settings_provider.cc b/chrome/browser/chromeos/settings/device_settings_provider.cc
index 13b1f6d..5721d8a 100644
--- a/chrome/browser/chromeos/settings/device_settings_provider.cc
+++ b/chrome/browser/chromeos/settings/device_settings_provider.cc
@@ -56,6 +56,8 @@ const char* const kKnownSettings[] = {
kAttestationForContentProtectionEnabled,
kDeviceAttestationEnabled,
kDeviceOwner,
+ kHeartbeatEnabled,
+ kHeartbeatFrequency,
kPolicyMissingMitigationMode,
kReleaseChannel,
kReleaseChannelDelegated,
@@ -326,6 +328,26 @@ void DecodeReportingPolicies(
}
}
+void DecodeHeartbeatPolicies(
+ const em::ChromeDeviceSettingsProto& policy,
+ PrefValueMap* new_values_cache) {
+ if (!policy.has_device_heartbeat_settings())
+ return;
+
+ const em::DeviceHeartbeatSettingsProto& heartbeat_policy =
+ policy.device_heartbeat_settings();
+ if (heartbeat_policy.has_heartbeat_enabled()) {
+ new_values_cache->SetBoolean(
+ kHeartbeatEnabled,
+ heartbeat_policy.heartbeat_enabled());
+ }
+ if (heartbeat_policy.has_heartbeat_frequency()) {
+ new_values_cache->SetInteger(
+ kHeartbeatFrequency,
+ heartbeat_policy.heartbeat_frequency());
+ }
+}
+
void DecodeGenericPolicies(
const em::ChromeDeviceSettingsProto& policy,
PrefValueMap* new_values_cache) {
@@ -587,6 +609,7 @@ void DeviceSettingsProvider::UpdateValuesCache(
DecodeNetworkPolicies(settings, &new_values_cache);
DecodeAutoUpdatePolicies(settings, &new_values_cache);
DecodeReportingPolicies(settings, &new_values_cache);
+ DecodeHeartbeatPolicies(settings, &new_values_cache);
DecodeGenericPolicies(settings, &new_values_cache);
DecodeDeviceState(policy_data, &new_values_cache);
diff --git a/chrome/browser/chromeos/settings/device_settings_provider_unittest.cc b/chrome/browser/chromeos/settings/device_settings_provider_unittest.cc
index 5f8fdbf..824f615 100644
--- a/chrome/browser/chromeos/settings/device_settings_provider_unittest.cc
+++ b/chrome/browser/chromeos/settings/device_settings_provider_unittest.cc
@@ -88,6 +88,33 @@ class DeviceSettingsProviderTest : public DeviceSettingsTestBase {
Mock::VerifyAndClearExpectations(this);
}
+ // Helper routine to enable/disable all reporting settings in policy.
+ void SetHeartbeatSettings(bool enable_heartbeat, int frequency) {
+ EXPECT_CALL(*this, SettingChanged(_)).Times(AtLeast(1));
+ em::DeviceHeartbeatSettingsProto* proto =
+ device_policy_.payload().mutable_device_heartbeat_settings();
+ proto->set_heartbeat_enabled(enable_heartbeat);
+ proto->set_heartbeat_frequency(frequency);
+ device_policy_.Build();
+ device_settings_test_helper_.set_policy_blob(device_policy_.GetBlob());
+ ReloadDeviceSettings();
+ Mock::VerifyAndClearExpectations(this);
+ }
+
+ // Helper routine to ensure all heartbeat policies have been correctly
+ // decoded.
+ void VerifyHeartbeatSettings(bool expected_enable_state,
+ int expected_frequency) {
+
+ const base::FundamentalValue expected_enabled_value(expected_enable_state);
+ EXPECT_TRUE(base::Value::Equals(provider_->Get(kHeartbeatEnabled),
+ &expected_enabled_value));
+
+ const base::FundamentalValue expected_frequency_value(expected_frequency);
+ EXPECT_TRUE(base::Value::Equals(provider_->Get(kHeartbeatFrequency),
+ &expected_frequency_value));
+ }
+
// Helper routine to ensure all reporting policies have been correctly
// decoded.
void VerifyReportingSettings(bool expected_enable_state,
@@ -405,4 +432,17 @@ TEST_F(DeviceSettingsProviderTest, DecodeReportingSettings) {
VerifyReportingSettings(false, status_frequency);
}
+TEST_F(DeviceSettingsProviderTest, DecodeHeartbeatSettings) {
+ // Turn on heartbeats and verify that the heartbeat settings have been
+ // decoded correctly.
+ const int heartbeat_frequency = 50000;
+ SetHeartbeatSettings(true, heartbeat_frequency);
+ VerifyHeartbeatSettings(true, heartbeat_frequency);
+
+ // Turn off all reporting and verify that the settings are decoded
+ // correctly.
+ SetHeartbeatSettings(false, heartbeat_frequency);
+ VerifyHeartbeatSettings(false, heartbeat_frequency);
+}
+
} // namespace chromeos