blob: a56a15a8784b163600ce711434614ff388aecd30 [file] [log] [blame]
// Copyright 2014 The Chromium OS 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 "buffet/device_registration_info.h"
#include <memory>
#include <set>
#include <utility>
#include <vector>
#include <base/bind.h>
#include <base/json/json_writer.h>
#include <base/message_loop/message_loop.h>
#include <base/values.h>
#include <chromeos/bind_lambda.h>
#include <chromeos/data_encoding.h>
#include <chromeos/errors/error_codes.h>
#include <chromeos/http/http_utils.h>
#include <chromeos/key_value_store.h>
#include <chromeos/mime_utils.h>
#include <chromeos/strings/string_utils.h>
#include <chromeos/url_utils.h>
#include "buffet/commands/cloud_command_proxy.h"
#include "buffet/commands/command_definition.h"
#include "buffet/commands/command_manager.h"
#include "buffet/commands/schema_constants.h"
#include "buffet/device_registration_storage_keys.h"
#include "buffet/org.chromium.Buffet.Manager.h"
#include "buffet/states/state_manager.h"
#include "buffet/utils.h"
const char buffet::kErrorDomainOAuth2[] = "oauth2";
const char buffet::kErrorDomainGCD[] = "gcd";
const char buffet::kErrorDomainGCDServer[] = "gcd_server";
namespace buffet {
namespace storage_keys {
// Persistent keys
const char kRefreshToken[] = "refresh_token";
const char kDeviceId[] = "device_id";
const char kRobotAccount[] = "robot_account";
const char kName[] = "name";
const char kDescription[] = "description";
const char kLocation[] = "location";
} // namespace storage_keys
} // namespace buffet
namespace {
const int kMaxStartDeviceRetryDelayMinutes{1};
const int64_t kMinStartDeviceRetryDelaySeconds{5};
std::pair<std::string, std::string> BuildAuthHeader(
const std::string& access_token_type,
const std::string& access_token) {
std::string authorization =
chromeos::string_utils::Join(" ", access_token_type, access_token);
return {chromeos::http::request_header::kAuthorization, authorization};
}
inline void SetUnexpectedError(chromeos::ErrorPtr* error) {
chromeos::Error::AddTo(error, FROM_HERE, buffet::kErrorDomainGCD,
"unexpected_response", "Unexpected GCD error");
}
void ParseGCDError(const base::DictionaryValue* json,
chromeos::ErrorPtr* error) {
const base::Value* list_value = nullptr;
const base::ListValue* error_list = nullptr;
if (!json->Get("error.errors", &list_value) ||
!list_value->GetAsList(&error_list)) {
SetUnexpectedError(error);
return;
}
for (size_t i = 0; i < error_list->GetSize(); i++) {
const base::Value* error_value = nullptr;
const base::DictionaryValue* error_object = nullptr;
if (!error_list->Get(i, &error_value) ||
!error_value->GetAsDictionary(&error_object)) {
SetUnexpectedError(error);
continue;
}
std::string error_code, error_message;
if (error_object->GetString("reason", &error_code) &&
error_object->GetString("message", &error_message)) {
chromeos::Error::AddTo(error, FROM_HERE, buffet::kErrorDomainGCDServer,
error_code, error_message);
} else {
SetUnexpectedError(error);
}
}
}
std::string BuildURL(const std::string& url,
const std::vector<std::string>& subpaths,
const chromeos::data_encoding::WebParamList& params) {
std::string result = chromeos::url::CombineMultiple(url, subpaths);
return chromeos::url::AppendQueryParams(result, params);
}
void IgnoreCloudError(const chromeos::Error*) {
}
void IgnoreCloudErrorWithCallback(const base::Closure& cb,
const chromeos::Error*) {
cb.Run();
}
void IgnoreCloudResult(const base::DictionaryValue&) {
}
void IgnoreCloudResultWithCallback(const base::Closure& cb,
const base::DictionaryValue&) {
cb.Run();
}
} // anonymous namespace
namespace buffet {
DeviceRegistrationInfo::DeviceRegistrationInfo(
const std::shared_ptr<CommandManager>& command_manager,
const std::shared_ptr<StateManager>& state_manager,
std::unique_ptr<BuffetConfig> config,
const std::shared_ptr<chromeos::http::Transport>& transport,
const std::shared_ptr<StorageInterface>& state_store,
bool xmpp_enabled,
org::chromium::Buffet::ManagerAdaptor* manager)
: transport_{transport},
storage_{state_store},
command_manager_{command_manager},
state_manager_{state_manager},
config_{std::move(config)},
xmpp_enabled_{xmpp_enabled},
manager_{manager} {
OnConfigChanged();
command_changed_callback_token_ = command_manager_->AddOnCommandDefChanged(
base::Bind(&DeviceRegistrationInfo::OnCommandDefsChanged,
weak_factory_.GetWeakPtr()));
}
DeviceRegistrationInfo::~DeviceRegistrationInfo() = default;
std::pair<std::string, std::string>
DeviceRegistrationInfo::GetAuthorizationHeader() const {
return BuildAuthHeader("Bearer", access_token_);
}
std::string DeviceRegistrationInfo::GetServiceURL(
const std::string& subpath,
const chromeos::data_encoding::WebParamList& params) const {
return BuildURL(config_->service_url(), {subpath}, params);
}
std::string DeviceRegistrationInfo::GetDeviceURL(
const std::string& subpath,
const chromeos::data_encoding::WebParamList& params) const {
CHECK(!device_id_.empty()) << "Must have a valid device ID";
return BuildURL(config_->service_url(),
{"devices", device_id_, subpath}, params);
}
std::string DeviceRegistrationInfo::GetOAuthURL(
const std::string& subpath,
const chromeos::data_encoding::WebParamList& params) const {
return BuildURL(config_->oauth_url(), {subpath}, params);
}
const std::string& DeviceRegistrationInfo::GetDeviceId() const {
return device_id_;
}
bool DeviceRegistrationInfo::Load() {
// Set kInvalidCredentials to trigger on_status_changed_ callback.
registration_status_ = RegistrationStatus::kInvalidCredentials;
SetRegistrationStatus(RegistrationStatus::kUnconfigured);
auto value = storage_->Load();
const base::DictionaryValue* dict = nullptr;
if (!value || !value->GetAsDictionary(&dict))
return false;
// Read all available data before failing.
std::string name;
if (dict->GetString(storage_keys::kName, &name) && !name.empty())
config_->set_name(name);
std::string description;
if (dict->GetString(storage_keys::kDescription, &description))
config_->set_description(description);
std::string location;
if (dict->GetString(storage_keys::kLocation, &location))
config_->set_location(location);
dict->GetString(storage_keys::kRefreshToken, &refresh_token_);
dict->GetString(storage_keys::kRobotAccount, &device_robot_account_);
std::string device_id;
if (dict->GetString(storage_keys::kDeviceId, &device_id))
SetDeviceId(device_id);
OnConfigChanged();
if (HaveRegistrationCredentials(nullptr)) {
// Wait a significant amount of time for local daemons to publish their
// state to Buffet before publishing it to the cloud.
// TODO(wiley) We could do a lot of things here to either expose this
// timeout as a configurable knob or allow local
// daemons to signal that their state is up to date so that
// we need not wait for them.
ScheduleStartDevice(base::TimeDelta::FromSeconds(5));
}
return true;
}
bool DeviceRegistrationInfo::Save() const {
base::DictionaryValue dict;
dict.SetString(storage_keys::kRefreshToken, refresh_token_);
dict.SetString(storage_keys::kDeviceId, device_id_);
dict.SetString(storage_keys::kRobotAccount, device_robot_account_);
dict.SetString(storage_keys::kName, config_->name());
dict.SetString(storage_keys::kDescription, config_->description());
dict.SetString(storage_keys::kLocation, config_->location());
return storage_->Save(&dict);
}
void DeviceRegistrationInfo::ScheduleStartDevice(const base::TimeDelta& later) {
SetRegistrationStatus(RegistrationStatus::kConnecting);
base::MessageLoop* current = base::MessageLoop::current();
if (!current)
return; // Assume we're in unittests
base::TimeDelta max_delay =
base::TimeDelta::FromMinutes(kMaxStartDeviceRetryDelayMinutes);
base::TimeDelta min_delay =
base::TimeDelta::FromSeconds(kMinStartDeviceRetryDelaySeconds);
base::TimeDelta retry_delay = later * 2;
if (retry_delay > max_delay) { retry_delay = max_delay; }
if (retry_delay < min_delay) { retry_delay = min_delay; }
current->PostDelayedTask(
FROM_HERE,
base::Bind(&DeviceRegistrationInfo::StartDevice,
weak_factory_.GetWeakPtr(), nullptr,
retry_delay),
later);
}
bool DeviceRegistrationInfo::CheckRegistration(chromeos::ErrorPtr* error) {
return HaveRegistrationCredentials(error) &&
MaybeRefreshAccessToken(error);
}
bool DeviceRegistrationInfo::HaveRegistrationCredentials(
chromeos::ErrorPtr* error) {
const bool have_credentials = !refresh_token_.empty() &&
!device_id_.empty() &&
!device_robot_account_.empty();
VLOG(1) << "Device registration record "
<< ((have_credentials) ? "found" : "not found.");
if (!have_credentials)
chromeos::Error::AddTo(error, FROM_HERE, kErrorDomainGCD,
"device_not_registered",
"No valid device registration record found");
return have_credentials;
}
std::unique_ptr<base::DictionaryValue>
DeviceRegistrationInfo::ParseOAuthResponse(chromeos::http::Response* response,
chromeos::ErrorPtr* error) {
int code = 0;
auto resp = chromeos::http::ParseJsonResponse(response, &code, error);
if (resp && code >= chromeos::http::status_code::BadRequest) {
std::string error_code, error_message;
if (!resp->GetString("error", &error_code)) {
error_code = "unexpected_response";
}
if (error_code == "invalid_grant") {
LOG(INFO) << "The device's registration has been revoked.";
SetRegistrationStatus(RegistrationStatus::kInvalidCredentials);
}
// I have never actually seen an error_description returned.
if (!resp->GetString("error_description", &error_message)) {
error_message = "Unexpected OAuth error";
}
chromeos::Error::AddTo(error, FROM_HERE, buffet::kErrorDomainOAuth2,
error_code, error_message);
return std::unique_ptr<base::DictionaryValue>();
}
return resp;
}
bool DeviceRegistrationInfo::MaybeRefreshAccessToken(
chromeos::ErrorPtr* error) {
LOG(INFO) << "Checking access token expiration.";
if (!access_token_.empty() &&
!access_token_expiration_.is_null() &&
access_token_expiration_ > base::Time::Now()) {
LOG(INFO) << "Access token is still valid.";
return true;
}
return RefreshAccessToken(error);
}
bool DeviceRegistrationInfo::RefreshAccessToken(
chromeos::ErrorPtr* error) {
LOG(INFO) << "Refreshing access token.";
auto response = chromeos::http::PostFormDataAndBlock(GetOAuthURL("token"), {
{"refresh_token", refresh_token_},
{"client_id", config_->client_id()},
{"client_secret", config_->client_secret()},
{"grant_type", "refresh_token"},
}, {}, transport_, error);
if (!response)
return false;
auto json = ParseOAuthResponse(response.get(), error);
if (!json)
return false;
int expires_in = 0;
if (!json->GetString("access_token", &access_token_) ||
!json->GetInteger("expires_in", &expires_in) ||
access_token_.empty() ||
expires_in <= 0) {
LOG(ERROR) << "Access token unavailable.";
chromeos::Error::AddTo(error, FROM_HERE, kErrorDomainOAuth2,
"unexpected_server_response",
"Access token unavailable");
return false;
}
access_token_expiration_ = base::Time::Now() +
base::TimeDelta::FromSeconds(expires_in);
LOG(INFO) << "Access token is refreshed for additional " << expires_in
<< " seconds.";
StartXmpp();
return true;
}
void DeviceRegistrationInfo::StartXmpp() {
if (!xmpp_enabled_) {
LOG(WARNING) << "XMPP support disabled by flag.";
return;
}
// If no MessageLoop assume we're in unittests.
if (!base::MessageLoop::current()) {
LOG(INFO) << "No MessageLoop, not starting XMPP";
return;
}
if (!fd_watcher_.StopWatchingFileDescriptor()) {
LOG(WARNING) << "Failed to stop the previous watcher";
return;
}
std::unique_ptr<XmppConnection> connection(new XmppConnection());
if (!connection->Initialize()) {
LOG(WARNING) << "Failed to connect to XMPP server";
return;
}
xmpp_client_.reset(new XmppClient(device_robot_account_, access_token_,
std::move(connection)));
if (!base::MessageLoopForIO::current()->WatchFileDescriptor(
xmpp_client_->GetFileDescriptor(), true /* persistent */,
base::MessageLoopForIO::WATCH_READ, &fd_watcher_, this)) {
LOG(WARNING) << "Failed to watch XMPP file descriptor";
return;
}
xmpp_client_->StartStream();
}
void DeviceRegistrationInfo::OnFileCanReadWithoutBlocking(int fd) {
if (!xmpp_client_ || xmpp_client_->GetFileDescriptor() != fd)
return;
if (!xmpp_client_->Read()) {
// Authentication failed or the socket was closed.
if (!fd_watcher_.StopWatchingFileDescriptor())
LOG(WARNING) << "Failed to stop the watcher";
return;
}
}
std::unique_ptr<base::DictionaryValue>
DeviceRegistrationInfo::BuildDeviceResource(chromeos::ErrorPtr* error) {
// Limit only to commands that are visible to the cloud.
auto commands = command_manager_->GetCommandDictionary().GetCommandsAsJson(
[](const CommandDefinition* def) { return def->GetVisibility().cloud; },
true, error);
if (!commands)
return nullptr;
std::unique_ptr<base::DictionaryValue> state =
state_manager_->GetStateValuesAsJson(error);
if (!state)
return nullptr;
std::unique_ptr<base::DictionaryValue> resource{new base::DictionaryValue};
if (!device_id_.empty())
resource->SetString("id", device_id_);
resource->SetString("name", config_->name());
if (!config_->description().empty())
resource->SetString("description", config_->description());
if (!config_->location().empty())
resource->SetString("location", config_->location());
resource->SetString("modelManifestId", config_->model_id());
resource->SetString("deviceKind", config_->device_kind());
resource->SetString("channel.supportedType", "xmpp");
resource->Set("commandDefs", commands.release());
resource->Set("state", state.release());
return resource;
}
std::unique_ptr<base::DictionaryValue> DeviceRegistrationInfo::GetDeviceInfo(
chromeos::ErrorPtr* error) {
if (!CheckRegistration(error))
return std::unique_ptr<base::DictionaryValue>();
// TODO(antonm): Switch to DoCloudRequest later.
auto response = chromeos::http::GetAndBlock(
GetDeviceURL(), {GetAuthorizationHeader()}, transport_, error);
int status_code = 0;
std::unique_ptr<base::DictionaryValue> json =
chromeos::http::ParseJsonResponse(response.get(), &status_code, error);
if (json) {
if (status_code >= chromeos::http::status_code::BadRequest) {
LOG(WARNING) << "Failed to retrieve the device info. Response code = "
<< status_code;
ParseGCDError(json.get(), error);
return std::unique_ptr<base::DictionaryValue>();
}
}
return json;
}
namespace {
bool GetWithDefault(const std::map<std::string, std::string>& source,
const std::string& key,
const std::string& default_value,
std::string* output) {
auto it = source.find(key);
if (it == source.end()) {
*output = default_value;
return false;
}
*output = it->second;
return true;
}
} // namespace
std::string DeviceRegistrationInfo::RegisterDevice(
const std::map<std::string, std::string>& params,
chromeos::ErrorPtr* error) {
std::string ticket_id;
if (!GetWithDefault(params, "ticket_id", "", &ticket_id)) {
chromeos::Error::AddTo(error, FROM_HERE, kErrorDomainBuffet,
"missing_parameter",
"Need ticket_id parameter for RegisterDevice()");
return std::string();
}
// These fields are optional, and will default to values from the manufacturer
// supplied config.
std::string name;
GetWithDefault(params, storage_keys::kName, config_->name(), &name);
std::string description;
GetWithDefault(params, storage_keys::kDescription, config_->description(),
&description);
std::string location;
GetWithDefault(params, storage_keys::kLocation, config_->location(),
&location);
if (!UpdateDeviceInfo(name, description, location, error))
return std::string();
std::unique_ptr<base::DictionaryValue> device_draft =
BuildDeviceResource(error);
if (!device_draft)
return std::string();
base::DictionaryValue req_json;
req_json.SetString("id", ticket_id);
req_json.SetString("oauthClientId", config_->client_id());
req_json.Set("deviceDraft", device_draft.release());
auto url = GetServiceURL("registrationTickets/" + ticket_id,
{{"key", config_->api_key()}});
std::unique_ptr<chromeos::http::Response> response =
chromeos::http::PatchJsonAndBlock(url, &req_json, {}, transport_, error);
auto json_resp = chromeos::http::ParseJsonResponse(response.get(), nullptr,
error);
if (!json_resp)
return std::string();
if (!response->IsSuccessful()) {
ParseGCDError(json_resp.get(), error);
return std::string();
}
url = GetServiceURL("registrationTickets/" + ticket_id +
"/finalize?key=" + config_->api_key());
response = chromeos::http::SendRequestWithNoDataAndBlock(
chromeos::http::request_type::kPost, url, {}, transport_, error);
if (!response)
return std::string();
json_resp = chromeos::http::ParseJsonResponse(response.get(), nullptr, error);
if (!json_resp)
return std::string();
if (!response->IsSuccessful()) {
ParseGCDError(json_resp.get(), error);
return std::string();
}
std::string auth_code;
std::string device_id;
if (!json_resp->GetString("robotAccountEmail", &device_robot_account_) ||
!json_resp->GetString("robotAccountAuthorizationCode", &auth_code) ||
!json_resp->GetString("deviceDraft.id", &device_id)) {
chromeos::Error::AddTo(error, FROM_HERE, kErrorDomainGCD,
"unexpected_response",
"Device account missing in response");
return std::string();
}
SetDeviceId(device_id);
// Now get access_token and refresh_token
response = chromeos::http::PostFormDataAndBlock(GetOAuthURL("token"), {
{"code", auth_code},
{"client_id", config_->client_id()},
{"client_secret", config_->client_secret()},
{"redirect_uri", "oob"},
{"scope", "https://www.googleapis.com/auth/clouddevices"},
{"grant_type", "authorization_code"}
}, {}, transport_, error);
if (!response)
return std::string();
json_resp = ParseOAuthResponse(response.get(), error);
int expires_in = 0;
if (!json_resp ||
!json_resp->GetString("access_token", &access_token_) ||
!json_resp->GetString("refresh_token", &refresh_token_) ||
!json_resp->GetInteger("expires_in", &expires_in) ||
access_token_.empty() ||
refresh_token_.empty() ||
expires_in <= 0) {
chromeos::Error::AddTo(error, FROM_HERE,
kErrorDomainGCD, "unexpected_response",
"Device access_token missing in response");
return std::string();
}
access_token_expiration_ = base::Time::Now() +
base::TimeDelta::FromSeconds(expires_in);
Save();
StartXmpp();
// We're going to respond with our success immediately and we'll StartDevice
// shortly after.
ScheduleStartDevice(base::TimeDelta::FromSeconds(0));
return device_id_;
}
namespace {
template <class T>
void PostToCallback(base::Callback<void(const T&)> callback,
std::unique_ptr<T> value) {
auto cb = [callback] (T* result) {
callback.Run(*result);
};
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(cb, base::Owned(value.release())));
}
using ResponsePtr = std::unique_ptr<chromeos::http::Response>;
void SendRequestWithRetries(
const std::string& method,
const std::string& url,
const std::string& data,
const std::string& mime_type,
const chromeos::http::HeaderList& headers,
std::shared_ptr<chromeos::http::Transport> transport,
int num_retries,
const chromeos::http::SuccessCallback& success_callback,
const chromeos::http::ErrorCallback& error_callback) {
auto on_failure =
[method, url, data, mime_type, headers, transport, num_retries,
success_callback, error_callback](int request_id,
const chromeos::Error* error) {
if (num_retries > 0) {
SendRequestWithRetries(method, url, data, mime_type,
headers, transport, num_retries - 1,
success_callback, error_callback);
} else {
error_callback.Run(request_id, error);
}
};
auto on_success =
[on_failure, success_callback, error_callback](int request_id,
ResponsePtr response) {
int status_code = response->GetStatusCode();
if (status_code >= chromeos::http::status_code::Continue &&
status_code < chromeos::http::status_code::BadRequest) {
success_callback.Run(request_id, std::move(response));
return;
}
// TODO(antonm): Should add some useful information to error.
LOG(WARNING) << "Request failed. Response code = " << status_code;
chromeos::ErrorPtr error;
chromeos::Error::AddTo(&error, FROM_HERE, chromeos::errors::http::kDomain,
std::to_string(status_code),
response->GetStatusText());
if (status_code >= chromeos::http::status_code::InternalServerError &&
status_code < 600) {
// Request was valid, but server failed, retry.
// TODO(antonm): Implement exponential backoff.
// TODO(antonm): Reconsider status codes, maybe only some require
// retry.
// TODO(antonm): Support Retry-After header.
on_failure(request_id, error.get());
} else {
error_callback.Run(request_id, error.get());
}
};
chromeos::http::SendRequest(method, url, data.c_str(), data.size(),
mime_type, headers, transport,
base::Bind(on_success),
base::Bind(on_failure));
}
} // namespace
void DeviceRegistrationInfo::DoCloudRequest(
const std::string& method,
const std::string& url,
const base::DictionaryValue* body,
const CloudRequestCallback& success_callback,
const CloudRequestErrorCallback& error_callback) {
// TODO(antonm): Add reauthorization on access token expiration (do not
// forget about 5xx when fetching new access token).
// TODO(antonm): Add support for device removal.
std::string data;
if (body)
base::JSONWriter::Write(body, &data);
const std::string mime_type{chromeos::mime::AppendParameter(
chromeos::mime::application::kJson,
chromeos::mime::parameters::kCharset,
"utf-8")};
auto status_cb = base::Bind(&DeviceRegistrationInfo::SetRegistrationStatus,
weak_factory_.GetWeakPtr());
auto request_cb = [success_callback, error_callback, status_cb](
int request_id, ResponsePtr response) {
status_cb.Run(RegistrationStatus::kConnected);
chromeos::ErrorPtr error;
std::unique_ptr<base::DictionaryValue> json_resp{
chromeos::http::ParseJsonResponse(response.get(), nullptr, &error)};
if (!json_resp) {
error_callback.Run(error.get());
return;
}
success_callback.Run(*json_resp);
};
auto error_cb =
[error_callback](int request_id, const chromeos::Error* error) {
error_callback.Run(error);
};
auto transport = transport_;
auto error_callackback_with_reauthorization = base::Bind(
[method, url, data, mime_type, transport, request_cb, error_cb,
status_cb](DeviceRegistrationInfo* self, int request_id,
const chromeos::Error* error) {
status_cb.Run(RegistrationStatus::kConnecting);
if (error->HasError(
chromeos::errors::http::kDomain,
std::to_string(chromeos::http::status_code::Denied))) {
chromeos::ErrorPtr reauthorization_error;
// Forcibly refresh the access token.
if (!self->RefreshAccessToken(&reauthorization_error)) {
// TODO(antonm): Check if the device has been actually removed.
error_cb(request_id, reauthorization_error.get());
return;
}
SendRequestWithRetries(method, url, data, mime_type,
{self->GetAuthorizationHeader()}, transport, 7,
base::Bind(request_cb), base::Bind(error_cb));
} else {
error_cb(request_id, error);
}
},
base::Unretained(this));
SendRequestWithRetries(method, url,
data, mime_type,
{GetAuthorizationHeader()},
transport,
7,
base::Bind(request_cb),
error_callackback_with_reauthorization);
}
void DeviceRegistrationInfo::StartDevice(
chromeos::ErrorPtr* error,
const base::TimeDelta& retry_delay) {
if (!HaveRegistrationCredentials(error))
return;
auto handle_start_device_failure_cb = base::Bind(
&IgnoreCloudErrorWithCallback,
base::Bind(&DeviceRegistrationInfo::ScheduleStartDevice,
weak_factory_.GetWeakPtr(),
retry_delay));
// "Starting" a device just means that we:
// 1) push an updated device resource
// 2) fetch an initial set of outstanding commands
// 3) abort any commands that we've previously marked as "in progress"
// or as being in an error state.
// 4) Initiate periodic polling for commands.
auto periodically_poll_commands_cb = base::Bind(
&DeviceRegistrationInfo::PeriodicallyPollCommands,
weak_factory_.GetWeakPtr());
auto abort_commands_cb = base::Bind(
&DeviceRegistrationInfo::AbortLimboCommands,
weak_factory_.GetWeakPtr(),
periodically_poll_commands_cb);
auto fetch_commands_cb = base::Bind(
&DeviceRegistrationInfo::FetchCommands,
weak_factory_.GetWeakPtr(),
abort_commands_cb,
handle_start_device_failure_cb);
UpdateDeviceResource(fetch_commands_cb, handle_start_device_failure_cb);
}
bool DeviceRegistrationInfo::UpdateDeviceInfo(const std::string& name,
const std::string& description,
const std::string& location,
chromeos::ErrorPtr* error) {
if (name.empty()) {
chromeos::Error::AddTo(error, FROM_HERE, kErrorDomainBuffet,
"invalid_parameter", "Empty device name");
return false;
}
config_->set_name(name);
config_->set_description(description);
config_->set_location(location);
OnConfigChanged();
if (HaveRegistrationCredentials(nullptr)) {
UpdateDeviceResource(base::Bind(&base::DoNothing),
base::Bind(&IgnoreCloudError));
}
return true;
}
void DeviceRegistrationInfo::UpdateCommand(
const std::string& command_id,
const base::DictionaryValue& command_patch,
const base::Closure& on_success,
const base::Closure& on_error) {
DoCloudRequest(
chromeos::http::request_type::kPatch,
GetServiceURL("commands/" + command_id),
&command_patch,
base::Bind(&IgnoreCloudResultWithCallback, on_success),
base::Bind(&IgnoreCloudErrorWithCallback, on_error));
}
void DeviceRegistrationInfo::UpdateDeviceResource(
const base::Closure& on_success,
const CloudRequestErrorCallback& on_failure) {
VLOG(1) << "Updating GCD server with CDD...";
std::unique_ptr<base::DictionaryValue> device_resource =
BuildDeviceResource(nullptr);
if (!device_resource)
return;
DoCloudRequest(
chromeos::http::request_type::kPut,
GetDeviceURL(),
device_resource.get(),
base::Bind(&IgnoreCloudResultWithCallback, on_success),
on_failure);
}
namespace {
void HandleFetchCommandsResult(
const base::Callback<void(const base::ListValue&)>& callback,
const base::DictionaryValue& json) {
const base::ListValue* commands{nullptr};
if (!json.GetList("commands", &commands)) {
VLOG(1) << "No commands in the response.";
}
const base::ListValue empty;
callback.Run(commands ? *commands : empty);
}
} // namespace
void DeviceRegistrationInfo::FetchCommands(
const base::Callback<void(const base::ListValue&)>& on_success,
const CloudRequestErrorCallback& on_failure) {
DoCloudRequest(
chromeos::http::request_type::kGet,
GetServiceURL("commands/queue", {{"deviceId", device_id_}}),
nullptr,
base::Bind(&HandleFetchCommandsResult, on_success),
on_failure);
}
void DeviceRegistrationInfo::AbortLimboCommands(
const base::Closure& callback, const base::ListValue& commands) {
const size_t size{commands.GetSize()};
for (size_t i = 0; i < size; ++i) {
const base::DictionaryValue* command{nullptr};
if (!commands.GetDictionary(i, &command)) {
LOG(WARNING) << "No command resource at " << i;
continue;
}
std::string command_state;
if (!command->GetString("state", &command_state)) {
LOG(WARNING) << "Command with no state at " << i;
continue;
}
if (command_state != "error" &&
command_state != "inProgress" &&
command_state != "paused") {
// It's not a limbo command, ignore.
continue;
}
std::string command_id;
if (!command->GetString("id", &command_id)) {
LOG(WARNING) << "Command with no ID at " << i;
continue;
}
std::unique_ptr<base::DictionaryValue> command_copy{command->DeepCopy()};
command_copy->SetString("state", "aborted");
// TODO(wiley) We could consider handling this error case more gracefully.
DoCloudRequest(
chromeos::http::request_type::kPut,
GetServiceURL("commands/" + command_id),
command_copy.get(),
base::Bind(&IgnoreCloudResult), base::Bind(&IgnoreCloudError));
}
base::MessageLoop::current()->PostTask(FROM_HERE, callback);
}
void DeviceRegistrationInfo::PeriodicallyPollCommands() {
VLOG(1) << "Poll commands";
command_poll_timer_.Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(config_->polling_period_ms()),
base::Bind(&DeviceRegistrationInfo::FetchCommands,
base::Unretained(this),
base::Bind(&DeviceRegistrationInfo::PublishCommands,
base::Unretained(this)),
base::Bind(&IgnoreCloudError)));
// TODO(antonm): Use better trigger: when StateManager registers new updates,
// it should call closure which will post a task, probably with some
// throttling, to publish state updates.
state_push_timer_.Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(config_->polling_period_ms()),
base::Bind(&DeviceRegistrationInfo::PublishStateUpdates,
base::Unretained(this)));
}
void DeviceRegistrationInfo::PublishCommands(const base::ListValue& commands) {
const CommandDictionary& command_dictionary =
command_manager_->GetCommandDictionary();
const size_t size{commands.GetSize()};
for (size_t i = 0; i < size; ++i) {
const base::DictionaryValue* command{nullptr};
if (!commands.GetDictionary(i, &command)) {
LOG(WARNING) << "No command resource at " << i;
continue;
}
auto command_instance = CommandInstance::FromJson(
command, commands::attributes::kCommand_Visibility_Cloud,
command_dictionary, nullptr);
if (!command_instance) {
LOG(WARNING) << "Failed to parse a command";
continue;
}
// TODO(antonm): Properly process cancellation of commands.
if (!command_manager_->FindCommand(command_instance->GetID())) {
std::unique_ptr<CommandProxyInterface> cloud_proxy{
new CloudCommandProxy(command_instance.get(), this)};
command_instance->AddProxy(std::move(cloud_proxy));
command_manager_->AddCommand(std::move(command_instance));
}
}
}
void DeviceRegistrationInfo::PublishStateUpdates() {
VLOG(1) << "PublishStateUpdates";
const std::vector<StateChange> state_changes{
state_manager_->GetAndClearRecordedStateChanges()};
if (state_changes.empty())
return;
std::unique_ptr<base::ListValue> patches{new base::ListValue};
for (const auto& state_change : state_changes) {
std::unique_ptr<base::DictionaryValue> patch{new base::DictionaryValue};
patch->SetString("timeMs",
std::to_string(state_change.timestamp.ToJavaTime()));
std::unique_ptr<base::DictionaryValue> changes{new base::DictionaryValue};
for (const auto& pair : state_change.changed_properties) {
auto value = pair.second->ToJson(nullptr);
if (!value) {
return;
}
// The key in |pair.first| is the full property name in format
// "package.property_name", so must use DictionaryValue::Set() instead of
// DictionaryValue::SetWithoutPathExpansion to recreate the JSON
// property tree properly.
changes->Set(pair.first, value.release());
}
patch->Set("patch", changes.release());
patches->Append(patch.release());
}
base::DictionaryValue body;
body.SetString("requestTimeMs",
std::to_string(base::Time::Now().ToJavaTime()));
body.Set("patches", patches.release());
DoCloudRequest(
chromeos::http::request_type::kPost,
GetDeviceURL("patchState"),
&body,
base::Bind(&IgnoreCloudResult), base::Bind(&IgnoreCloudError));
}
void DeviceRegistrationInfo::SetRegistrationStatus(
RegistrationStatus new_status) {
registration_status_ = new_status;
if (manager_)
manager_->SetStatus(StatusToString(registration_status_));
VLOG_IF(1, new_status != registration_status_)
<< "Changing registration status to " << StatusToString(new_status);
}
void DeviceRegistrationInfo::SetDeviceId(const std::string& device_id) {
device_id_ = device_id;
if (manager_)
manager_->SetDeviceId(device_id_);
}
void DeviceRegistrationInfo::OnConfigChanged() {
if (!manager_)
return;
manager_->SetOemName(config_->oem_name());
manager_->SetModelName(config_->model_name());
manager_->SetModelId(config_->model_id());
manager_->SetName(config_->name());
manager_->SetDescription(config_->description());
manager_->SetLocation(config_->location());
}
void DeviceRegistrationInfo::OnCommandDefsChanged() {
VLOG(1) << "CommandDefinitionChanged notification received";
if (!HaveRegistrationCredentials(nullptr))
return;
UpdateDeviceResource(base::Bind(&base::DoNothing),
base::Bind(&IgnoreCloudError));
}
} // namespace buffet