| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/update_client/update_engine.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/barrier_callback.h" |
| #include "base/check_op.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/strings/strcat.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/uuid.h" |
| #include "base/values.h" |
| #include "base/version.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/update_client/component.h" |
| #include "components/update_client/configurator.h" |
| #include "components/update_client/crx_cache.h" |
| #include "components/update_client/crx_update_item.h" |
| #include "components/update_client/persisted_data.h" |
| #include "components/update_client/protocol_parser.h" |
| #include "components/update_client/update_checker.h" |
| #include "components/update_client/update_client.h" |
| #include "components/update_client/update_client_errors.h" |
| #include "components/update_client/utils.h" |
| |
| namespace update_client { |
| |
| namespace { |
| |
| base::Value::Dict MakeEvent(UpdateClient::PingParams ping_params, |
| const base::Version& previous_version) { |
| base::Value::Dict event; |
| event.Set("eventtype", ping_params.event_type); |
| event.Set("eventresult", ping_params.result); |
| if (ping_params.error_code) { |
| event.Set("errorcode", ping_params.error_code); |
| } |
| if (ping_params.extra_code1) { |
| event.Set("extracode1", ping_params.extra_code1); |
| } |
| if (!ping_params.app_command_id.empty()) { |
| event.Set("appcommandid", ping_params.app_command_id); |
| } |
| event.Set("previousversion", previous_version.GetString()); |
| return event; |
| } |
| |
| } // namespace |
| |
| UpdateContext::UpdateContext( |
| scoped_refptr<Configurator> config, |
| bool is_foreground, |
| bool is_install, |
| const std::vector<std::string>& ids, |
| UpdateClient::CrxStateChangeCallback crx_state_change_callback, |
| UpdateEngine::Callback callback, |
| PersistedData* persisted_data, |
| bool is_update_check_only, |
| base::RepeatingCallback<int64_t(const base::FilePath&)> get_available_space) |
| : config(config), |
| is_foreground(is_foreground), |
| is_install(is_install), |
| ids(ids), |
| crx_state_change_callback(crx_state_change_callback), |
| callback(std::move(callback)), |
| session_id(base::StrCat( |
| {"{", base::Uuid::GenerateRandomV4().AsLowercaseString(), "}"})), |
| persisted_data(persisted_data), |
| is_update_check_only(is_update_check_only), |
| get_available_space(get_available_space) { |
| for (const auto& id : ids) { |
| components.insert( |
| std::make_pair(id, std::make_unique<Component>(*this, id))); |
| } |
| } |
| |
| UpdateContext::~UpdateContext() = default; |
| |
| UpdateEngine::UpdateEngine( |
| scoped_refptr<Configurator> config, |
| UpdateChecker::Factory update_checker_factory, |
| scoped_refptr<PingManager> ping_manager, |
| const UpdateClient::CrxStateChangeCallback& notify_observers_callback) |
| : config_(config), |
| update_checker_factory_(update_checker_factory), |
| ping_manager_(ping_manager), |
| notify_observers_callback_(notify_observers_callback) {} |
| |
| UpdateEngine::~UpdateEngine() = default; |
| |
| base::RepeatingClosure UpdateEngine::Update( |
| bool is_foreground, |
| bool is_install, |
| const std::vector<std::string>& ids, |
| UpdateClient::CrxDataCallback crx_data_callback, |
| UpdateClient::CrxStateChangeCallback crx_state_change_callback, |
| Callback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return InvokeOperation(is_foreground, /*is_update_check_only=*/false, |
| is_install, ids, std::move(crx_data_callback), |
| std::move(crx_state_change_callback), |
| std::move(callback)); |
| } |
| |
| void UpdateEngine::CheckForUpdate( |
| bool is_foreground, |
| const std::string& id, |
| UpdateClient::CrxDataCallback crx_data_callback, |
| UpdateClient::CrxStateChangeCallback crx_state_change_callback, |
| Callback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| InvokeOperation(is_foreground, /*is_update_check_only=*/true, |
| /*is_install=*/false, {id}, std::move(crx_data_callback), |
| std::move(crx_state_change_callback), std::move(callback)); |
| } |
| |
| base::RepeatingClosure UpdateEngine::InvokeOperation( |
| bool is_foreground, |
| bool is_update_check_only, |
| bool is_install, |
| const std::vector<std::string>& ids, |
| UpdateClient::CrxDataCallback crx_data_callback, |
| UpdateClient::CrxStateChangeCallback crx_state_change_callback, |
| Callback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (ids.empty()) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(callback), Error::INVALID_ARGUMENT)); |
| return base::DoNothing(); |
| } |
| |
| if (IsThrottled(is_foreground)) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), Error::RETRY_LATER)); |
| return base::DoNothing(); |
| } |
| |
| scoped_refptr<UpdateContext> update_context = |
| base::MakeRefCounted<UpdateContext>( |
| config_, is_foreground, is_install, ids, |
| crx_state_change_callback |
| ? base::BindRepeating( |
| [](UpdateClient::CrxStateChangeCallback a, |
| UpdateClient::CrxStateChangeCallback b, |
| const CrxUpdateItem& item) { |
| a.Run(item); |
| b.Run(item); |
| }, |
| crx_state_change_callback, notify_observers_callback_) |
| : notify_observers_callback_, |
| std::move(callback), config_->GetPersistedData(), |
| is_update_check_only); |
| CHECK(!update_context->session_id.empty()); |
| |
| const auto [unused, inserted] = update_contexts_.insert( |
| std::make_pair(update_context->session_id, update_context)); |
| CHECK(inserted); |
| |
| // Calls out to get the corresponding CrxComponent data for the components. |
| std::move(crx_data_callback) |
| .Run(ids, |
| base::BindOnce(&UpdateEngine::StartOperation, this, update_context)); |
| return is_update_check_only |
| ? base::DoNothing() |
| : base::BindRepeating( |
| [](scoped_refptr<UpdateContext> context) { |
| context->is_cancelled = true; |
| for (const auto& entry : context->components) { |
| entry.second->Cancel(); |
| } |
| }, |
| update_context); |
| } |
| |
| void UpdateEngine::StartOperation( |
| scoped_refptr<UpdateContext> update_context, |
| const std::vector<std::optional<CrxComponent>>& crx_components) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (crx_components.size() != update_context->ids.size()) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(update_context->callback), |
| Error::BAD_CRX_DATA_CALLBACK)); |
| return; |
| } |
| |
| for (size_t i = 0; i != update_context->ids.size(); ++i) { |
| const auto& id = update_context->ids[i]; |
| |
| CHECK_EQ(update_context->components[id]->state(), ComponentState::kNew); |
| |
| const auto crx_component = crx_components[i]; |
| if (crx_component && (id == GetCrxComponentID(*crx_component))) { |
| // This component can be checked for updates. |
| auto& component = update_context->components[id]; |
| component->set_crx_component(*crx_component); |
| component->set_previous_version(component->crx_component()->version); |
| component->set_previous_fp(component->crx_component()->fingerprint); |
| update_context->components_to_check_for_updates.push_back(id); |
| } else { |
| // |CrxDataCallback| did not return a CrxComponent instance for this |
| // component, which most likely, has been uninstalled. This component |
| // is going to be transitioned to an error state when the its |Handle| |
| // method is called later on by the engine. |
| update_context->component_queue.push(id); |
| } |
| } |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(update_context->components_to_check_for_updates.empty() |
| ? &UpdateEngine::HandleComponent |
| : &UpdateEngine::DoUpdateCheck, |
| this, update_context)); |
| } |
| |
| void UpdateEngine::DoUpdateCheck(scoped_refptr<UpdateContext> update_context) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(update_context); |
| |
| // Make the components transition from |kNew| to |kChecking| state. |
| for (const auto& id : update_context->components_to_check_for_updates) { |
| update_context->components[id]->Handle(base::DoNothing()); |
| } |
| |
| update_context->update_checker = update_checker_factory_.Run(config_); |
| |
| update_context->update_checker->CheckForUpdates( |
| update_context, config_->ExtraRequestParams(), |
| base::BindOnce(&UpdateEngine::UpdateCheckResultsAvailable, this, |
| update_context)); |
| } |
| |
| void UpdateEngine::UpdateCheckResultsAvailable( |
| scoped_refptr<UpdateContext> update_context, |
| std::optional<ProtocolParser::Results> results, |
| ErrorCategory error_category, |
| int error, |
| int retry_after_sec) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(update_context); |
| |
| update_context->retry_after_sec = retry_after_sec; |
| |
| // Only positive values for throttle_sec are effective. 0 means that no |
| // throttling occurs and it resets the throttle. |
| // Negative values are not trusted and are ignored. |
| constexpr int kMaxRetryAfterSec = 24 * 60 * 60; // 24 hours. |
| const int throttle_sec = |
| std::min(update_context->retry_after_sec, kMaxRetryAfterSec); |
| if (throttle_sec >= 0) { |
| config_->GetPersistedData()->SetThrottleUpdatesUntil( |
| throttle_sec ? base::Time::Now() + base::Seconds(throttle_sec) |
| : base::Time()); |
| } |
| |
| update_context->update_check_error = error; |
| |
| auto complete = base::BarrierCallback<bool>( |
| update_context->components_to_check_for_updates.size(), |
| base::BindOnce([](const std::vector<bool>&) {}) |
| .Then(base::BindOnce(&UpdateEngine::UpdateCheckComplete, this, |
| update_context))); |
| if (error) { |
| CHECK(!results); |
| for (const auto& id : update_context->components_to_check_for_updates) { |
| CHECK_EQ(1u, update_context->components.count(id)); |
| auto& component = update_context->components.at(id); |
| component->SetUpdateCheckResult(std::nullopt, ErrorCategory::kUpdateCheck, |
| error, complete); |
| } |
| return; |
| } |
| |
| CHECK(results); |
| CHECK_EQ(0, error); |
| |
| std::map<std::string, ProtocolParser::App> id_to_result; |
| for (const auto& result : results->apps) { |
| id_to_result[result.app_id] = result; |
| } |
| |
| for (const auto& id : update_context->components_to_check_for_updates) { |
| CHECK_EQ(1u, update_context->components.count(id)); |
| auto& component = update_context->components.at(id); |
| const auto& it = id_to_result.find(id); |
| if (it != id_to_result.end()) { |
| const auto& result = it->second; |
| const auto& [category, protocol_error] = [](const std::string& status) { |
| // "ok" and "noupdate" are non-error cases. |
| if (status == "ok" || status == "noupdate") { |
| return std::make_pair(ErrorCategory::kNone, ProtocolError::NONE); |
| } |
| // Some app status literals can be folded down as an updatecheck status. |
| if (status == "error-unknownApplication") { |
| return std::make_pair(ErrorCategory::kUpdateCheck, |
| ProtocolError::UNKNOWN_APPLICATION); |
| } |
| if (status == "restricted") { |
| return std::make_pair(ErrorCategory::kUpdateCheck, |
| ProtocolError::RESTRICTED_APPLICATION); |
| } |
| if (status == "error-invalidAppId") { |
| return std::make_pair(ErrorCategory::kUpdateCheck, |
| ProtocolError::INVALID_APPID); |
| } |
| if (status == "error-osnotsupported") { |
| return std::make_pair(ErrorCategory::kUpdateCheck, |
| ProtocolError::OS_NOT_SUPPORTED); |
| } |
| if (status == "error-hwnotsupported") { |
| return std::make_pair(ErrorCategory::kUpdateCheck, |
| ProtocolError::HW_NOT_SUPPORTED); |
| } |
| if (status == "error-hash") { |
| return std::make_pair(ErrorCategory::kUpdateCheck, |
| ProtocolError::NO_HASH); |
| } |
| if (status == "error-unsupportedprotocol") { |
| return std::make_pair(ErrorCategory::kUpdateCheck, |
| ProtocolError::UNSUPPORTED_PROTOCOL); |
| } |
| if (status == "error-internal") { |
| return std::make_pair(ErrorCategory::kUpdateCheck, |
| ProtocolError::INTERNAL); |
| } |
| if (status == "error-inexpressible") { |
| return std::make_pair(ErrorCategory::kUpdateCheck, |
| ProtocolError::INEXPRESSIBLE); |
| } |
| // Otherwise, this is an unknown status. |
| return std::make_pair(ErrorCategory::kUpdateCheck, |
| ProtocolError::UNKNOWN_ERROR); |
| }(result.status); |
| component->SetUpdateCheckResult( |
| result, category, static_cast<int>(protocol_error), complete); |
| } else { |
| component->SetUpdateCheckResult( |
| std::nullopt, ErrorCategory::kUpdateCheck, |
| static_cast<int>(ProtocolError::UPDATE_RESPONSE_NOT_FOUND), complete); |
| } |
| } |
| } |
| |
| void UpdateEngine::UpdateCheckComplete( |
| scoped_refptr<UpdateContext> update_context) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(update_context); |
| |
| for (const auto& id : update_context->components_to_check_for_updates) { |
| update_context->component_queue.push(id); |
| |
| // Handle the |kChecking| state and transition the component to the |
| // next state, depending on the update check results. |
| CHECK_EQ(1u, update_context->components.count(id)); |
| auto& component = update_context->components.at(id); |
| CHECK_EQ(component->state(), ComponentState::kChecking); |
| component->Handle(base::DoNothing()); |
| } |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&UpdateEngine::HandleComponent, this, update_context)); |
| } |
| |
| void UpdateEngine::HandleComponent( |
| scoped_refptr<UpdateContext> update_context) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(update_context); |
| |
| auto& queue = update_context->component_queue; |
| |
| if (queue.empty()) { |
| const Error error = update_context->update_check_error |
| ? Error::UPDATE_CHECK_ERROR |
| : Error::NONE; |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&UpdateEngine::UpdateComplete, this, |
| update_context, error)); |
| return; |
| } |
| |
| const auto& id = queue.front(); |
| CHECK_EQ(1u, update_context->components.count(id)); |
| const auto& component = update_context->components.at(id); |
| CHECK(component); |
| |
| auto& next_update_delay = update_context->next_update_delay; |
| if (!next_update_delay.is_zero() && component->IsUpdateAvailable()) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&UpdateEngine::HandleComponent, this, update_context), |
| next_update_delay); |
| next_update_delay = base::TimeDelta(); |
| return; |
| } |
| |
| component->Handle(base::BindOnce(&UpdateEngine::HandleComponentComplete, this, |
| update_context)); |
| } |
| |
| void UpdateEngine::HandleComponentComplete( |
| scoped_refptr<UpdateContext> update_context) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(update_context); |
| |
| auto& queue = update_context->component_queue; |
| CHECK(!queue.empty()); |
| |
| const auto& id = queue.front(); |
| CHECK_EQ(1u, update_context->components.count(id)); |
| const auto& component = update_context->components.at(id); |
| CHECK(component); |
| |
| base::OnceClosure callback = |
| base::BindOnce(&UpdateEngine::HandleComponent, this, update_context); |
| if (component->IsHandled()) { |
| update_context->next_update_delay = component->GetUpdateDuration(); |
| queue.pop(); |
| if (!component->events().empty()) { |
| CHECK(component->crx_component()); |
| ping_manager_->SendPing(component->session_id(), |
| *component->crx_component(), |
| component->GetEvents(), std::move(callback)); |
| return; |
| } |
| } |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE, |
| std::move(callback)); |
| } |
| |
| void UpdateEngine::UpdateComplete(scoped_refptr<UpdateContext> update_context, |
| Error error) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(update_context); |
| |
| const auto num_erased = update_contexts_.erase(update_context->session_id); |
| CHECK_EQ(1u, num_erased); |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(update_context->callback), error)); |
| } |
| |
| bool UpdateEngine::GetUpdateState(const std::string& id, |
| CrxUpdateItem* update_item) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| for (const auto& context : update_contexts_) { |
| const auto& components = context.second->components; |
| const auto it = components.find(id); |
| if (it != components.end()) { |
| *update_item = it->second->GetCrxUpdateItem(); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool UpdateEngine::IsThrottled(bool is_foreground) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| base::Time throttle_updates_until = |
| config_->GetPersistedData()->GetThrottleUpdatesUntil(); |
| |
| if (is_foreground || throttle_updates_until.is_null()) { |
| return false; |
| } |
| |
| const auto now(base::Time::Now()); |
| |
| // Throttle the calls in the interval (t - 1 day, t) to limit the effect of |
| // unset clocks or clock drift. |
| return throttle_updates_until - base::Days(1) < now && |
| now < throttle_updates_until; |
| } |
| |
| void UpdateEngine::SendPing(const CrxComponent& crx_component, |
| UpdateClient::PingParams ping_params, |
| Callback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| std::vector<base::Value::Dict> events; |
| events.push_back(MakeEvent(ping_params, crx_component.version)); |
| ping_manager_->SendPing( |
| base::StrCat( |
| {"{", base::Uuid::GenerateRandomV4().AsLowercaseString(), "}"}), |
| crx_component, std::move(events), |
| base::BindOnce(std::move(callback), Error::NONE)); |
| } |
| |
| } // namespace update_client |