blob: 764b8a4108174313c736cfc2f448ed14a59aa549 [file] [log] [blame]
// Copyright 2024 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/pipeline.h"
#include <cstdint>
#include <optional>
#include <queue>
#include <string>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/ref_counted.h"
#include "base/sequence_checker.h"
#include "base/task/sequenced_task_runner.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "components/update_client/action_runner.h"
#include "components/update_client/cancellation.h"
#include "components/update_client/component.h"
#include "components/update_client/configurator.h"
#include "components/update_client/crx_cache.h"
#include "components/update_client/op_download.h"
#include "components/update_client/op_install.h"
#include "components/update_client/op_puffin.h"
#include "components/update_client/protocol_parser.h"
#include "components/update_client/unzipper.h"
#include "components/update_client/update_client_errors.h"
#include "components/update_client/update_engine.h"
namespace update_client {
namespace {
// A `Pipeline` represents an ordered sequence of operations that the update
// server has instructed the client to perform. Operations are functions with
// two inputs:
// 1. A `FilePath`.
// 2. A completion callback, which takes a `FilePath` or error. Operations
// that don't generate a file (e.g. a "run" action) propagate their input
// path.
// Operations return a cancellation callback when run. Operations that can't
// be cancelled return `base::DoNothing` as their cancellation callback.
// The first operation in each pipeline must tolerate an empty FilePath as
// input.
using Operation = base::OnceCallback<base::OnceClosure(
const base::FilePath&,
base::OnceCallback<void(
base::expected<base::FilePath, CategorizedError>)>)>;
using RepeatingOperation = base::RepeatingCallback<base::OnceClosure(
const base::FilePath&,
base::OnceCallback<void(
base::expected<base::FilePath, CategorizedError>)>)>;
// `Pipeline` manages the flow of operations, passing the output path of
// each operation to the next one, short-circuiting on errors.
//
// Additionally, if a pipeline fails, it may fall back to a backup pipeline.
//
// `Pipeline` is refcounted. The callback passed to the currently-running
// operation holds the single ref to the pipeline, keeping it alive until
// `OpComplete` returns (after which, if there is another operation, another
// completion callback will keep it alive).
class Pipeline : public base::RefCountedThreadSafe<Pipeline> {
public:
Pipeline(
std::queue<Operation> operations,
const base::FilePath& first_path,
base::RepeatingCallback<void(const CategorizedError&)> outcome_recorder,
scoped_refptr<Pipeline> fallback);
Pipeline(const Pipeline&) = delete;
Pipeline& operator=(const Pipeline&) = delete;
// Each `Pipeline` can only be used once. Returns a cancellation callback.
base::OnceClosure Start(
base::OnceCallback<void(const CategorizedError&)> callback);
private:
friend class base::RefCountedThreadSafe<Pipeline>;
virtual ~Pipeline() = default;
void StartNext(const base::FilePath& path);
void OpComplete(base::expected<base::FilePath, CategorizedError>);
SEQUENCE_CHECKER(sequence_checker_);
std::queue<Operation> operations_;
base::FilePath first_path_;
base::RepeatingCallback<void(const CategorizedError&)> outcome_recorder_;
scoped_refptr<Pipeline> fallback_;
scoped_refptr<Cancellation> cancel_ = base::MakeRefCounted<Cancellation>();
base::OnceCallback<void(const CategorizedError&)> callback_;
};
Pipeline::Pipeline(
std::queue<Operation> operations,
const base::FilePath& first_path,
base::RepeatingCallback<void(const CategorizedError&)> outcome_recorder,
scoped_refptr<Pipeline> fallback)
: operations_(std::move(operations)),
first_path_(first_path),
outcome_recorder_(outcome_recorder),
fallback_(fallback) {}
base::OnceClosure Pipeline::Start(
base::OnceCallback<void(const CategorizedError&)> callback) {
CHECK(!callback_);
callback_ = std::move(callback);
StartNext(first_path_);
return base::BindOnce(&Cancellation::Cancel, cancel_);
}
void Pipeline::StartNext(const base::FilePath& path) {
Operation next = std::move(operations_.front());
operations_.pop();
cancel_->OnCancel(
std::move(next).Run(path, base::BindOnce(&Pipeline::OpComplete, this)));
}
void Pipeline::OpComplete(
base::expected<base::FilePath, CategorizedError> result) {
cancel_->Clear();
if (!result.has_value()) {
outcome_recorder_.Run(result.error());
if (fallback_ && !cancel_->IsCancelled()) {
// Pipeline failed, fall back to next pipeline.
cancel_->OnCancel(fallback_->Start(std::move(callback_)));
return;
}
// Pipeline failed and fallbacks exhausted or cancelled.
std::move(callback_).Run(result.error());
return;
}
if (operations_.empty()) {
// Pipeline successfully completed.
std::move(callback_).Run({});
return;
}
if (cancel_->IsCancelled()) {
// Pipeline still running, but cancelled.
std::move(callback_).Run(
{.category_ = ErrorCategory::kService,
.code_ = static_cast<int>(ServiceError::CANCELLED)});
return;
}
// Pipeline still running: start next operation.
StartNext(result.value());
}
void AssemblePipeline(
std::optional<Operation> download_diff,
std::optional<Operation> patch_diff,
Operation download_full,
RepeatingOperation install,
std::optional<RepeatingOperation> run,
base::RepeatingCallback<void(const CategorizedError&)>
diff_outcome_recorder,
base::OnceCallback<void(
base::expected<base::OnceCallback<base::OnceClosure(
base::OnceCallback<void(const CategorizedError&)>)>,
CategorizedError>)> callback,
base::expected<base::FilePath, UnpackerError> cached_installer,
base::expected<base::FilePath, UnpackerError> prev_installer) {
if (cached_installer.has_value()) {
// Skip downloading and patching; run the cached installer. No fallbacks.
std::move(callback).Run(base::BindOnce(
&Pipeline::Start,
base::MakeRefCounted<Pipeline>(
[&] {
std::queue<Operation> ops;
ops.push(base::BindOnce(install));
if (run) {
ops.push(base::BindOnce(*run));
}
return ops;
}(),
cached_installer.value(), base::DoNothing(), nullptr)));
return;
}
scoped_refptr<Pipeline> full = nullptr;
{
// Construct the full update pipeline.
// TODO(crbug.com/353249967): It's possible for the diff pipeline to have
// created and cached the full download output. Adjust the download step to
// check the cache.
std::queue<Operation> ops;
ops.push(std::move(download_full));
ops.push(base::BindOnce(install));
if (run) {
ops.push(base::BindOnce(*run));
}
full = base::MakeRefCounted<Pipeline>(std::move(ops), base::FilePath(),
base::DoNothing(), full);
}
if (download_diff && prev_installer.has_value()) {
// Do a differential update that falls back to a full update.
CHECK(patch_diff);
std::queue<Operation> ops;
ops.push(std::move(*download_diff));
ops.push(std::move(*patch_diff));
ops.push(base::BindOnce(install));
if (run) {
ops.push(base::BindOnce(*run));
}
std::move(callback).Run(base::BindOnce(
&Pipeline::Start,
base::MakeRefCounted<Pipeline>(std::move(ops), base::FilePath(),
diff_outcome_recorder, full)));
return;
}
// Else, full update only.
std::move(callback).Run(base::BindOnce(&Pipeline::Start, full));
}
} // namespace
void MakePipeline(
scoped_refptr<Configurator> config,
base::RepeatingCallback<int64_t(const base::FilePath&)> get_available_space,
bool is_foreground,
const std::string& session_id,
scoped_refptr<CrxCache> crx_cache,
crx_file::VerifierFormat crx_format,
const std::string& id,
const std::vector<uint8_t>& pk_hash,
const std::string& install_data_index,
const std::string& prev_fp,
scoped_refptr<CrxInstaller> installer,
base::RepeatingCallback<void(ComponentState)> state_tracker,
base::RepeatingCallback<void(base::Value::Dict)> event_adder,
CrxDownloader::ProgressCallback download_progress_callback,
CrxInstaller::ProgressCallback install_progress_callback,
base::RepeatingCallback<void(const CrxInstaller::Result&)>
install_complete_callback,
scoped_refptr<ActionHandler> action_handler,
base::RepeatingCallback<void(const CategorizedError&)>
diff_outcome_recorder,
const ProtocolParser::Result& result,
base::OnceCallback<void(
base::expected<base::OnceCallback<base::OnceClosure(
base::OnceCallback<void(const CategorizedError&)>)>,
CategorizedError>)> callback) {
// Run action.
std::optional<RepeatingOperation> run;
if (!result.action_run.empty()) {
run = base::BindRepeating(
[](base::RepeatingCallback<void(ComponentState)> state_tracker,
const base::FilePath& file,
base::OnceCallback<void(
base::expected<base::FilePath, CategorizedError>)>
callback) {
// Discard the input file path, and adapt callback.
state_tracker.Run(ComponentState::kRun);
return base::BindOnce(
[](const base::FilePath& file, bool success,
int error, int extra) {
// Discard any error result: RunAction failures
// don't end the pipeline.
return file;
},
file)
.Then(std::move(callback));
},
state_tracker)
.Then(base::BindRepeating(&RunAction, action_handler, installer,
result.action_run, session_id,
event_adder));
}
if (result.status == "noupdate") {
if (run) {
// The pipeline has a run action only.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback),
base::BindOnce(&Pipeline::Start,
base::MakeRefCounted<Pipeline>(
[&] {
std::queue<Operation> ops;
ops.push(*run);
return ops;
}(),
base::FilePath(),
diff_outcome_recorder, nullptr))));
return;
}
// Else, with a noupdate, there's nothing to do.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback),
base::unexpected(CategorizedError())));
return;
}
if (result.status != "ok" || result.manifest.packages.empty()) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback),
base::unexpected(CategorizedError(
{.category_ = ErrorCategory::kUpdateCheck}))));
return;
}
// Download the update.
Operation download_full =
base::BindOnce(
[](base::RepeatingCallback<void(ComponentState)> state_tracker,
const base::FilePath&,
base::OnceCallback<void(
base::expected<base::FilePath, CategorizedError>)> callback) {
// Discard the input file path, set state to downloading, pass
// `callback` on to the download.
state_tracker.Run(ComponentState::kDownloading);
return callback;
},
state_tracker)
.Then(base::BindOnce(
&DownloadOperation, config, get_available_space, is_foreground,
[&] {
std::vector<GURL> urls;
for (const auto& base_url : result.crx_urls) {
const GURL url =
base_url.Resolve(result.manifest.packages[0].name);
if (url.is_valid()) {
urls.push_back(url);
}
}
return urls;
}(),
result.manifest.packages[0].size,
result.manifest.packages[0].hash_sha256, event_adder,
download_progress_callback));
// Download and patch the diff update.
std::optional<Operation> download_diff;
std::optional<Operation> patch_diff;
if (!result.manifest.packages[0].hashdiff_sha256.empty()) {
download_diff =
base::BindOnce(
[](base::RepeatingCallback<void(ComponentState)> state_tracker,
const base::FilePath&,
base::OnceCallback<void(
base::expected<base::FilePath, CategorizedError>)>
callback) {
// Discard the input file path, pass `callback` on to the
// download.
state_tracker.Run(ComponentState::kDownloading);
return callback;
},
state_tracker)
.Then(base::BindOnce(
&DownloadOperation, config, get_available_space, is_foreground,
[&] {
std::vector<GURL> urls;
for (const auto& base_url : result.crx_diffurls) {
const GURL url =
base_url.Resolve(result.manifest.packages[0].namediff);
if (url.is_valid()) {
urls.push_back(url);
}
}
return urls;
}(),
result.manifest.packages[0].sizediff,
result.manifest.packages[0].hashdiff_sha256, event_adder,
download_progress_callback));
patch_diff = base::BindOnce(&PuffOperation, crx_cache,
config->GetPatcherFactory()->Create(),
event_adder, id, prev_fp);
}
// Install the update.
RepeatingOperation install = base::BindRepeating(
[](scoped_refptr<Configurator> config, scoped_refptr<CrxCache> crx_cache,
crx_file::VerifierFormat crx_format, const std::string& id,
const std::vector<uint8_t>& pk_hash,
scoped_refptr<CrxInstaller> installer, const std::string& run,
const std::string& arguments, const std::string& install_data,
const std::string& fingerprint,
base::RepeatingCallback<void(ComponentState)> state_tracker,
base::RepeatingCallback<void(base::Value::Dict)> event_adder,
CrxInstaller::ProgressCallback install_progress_callback,
base::RepeatingCallback<void(const CrxInstaller::Result&)>
install_complete_callback,
const base::FilePath& file,
base::OnceCallback<void(
base::expected<base::FilePath, CategorizedError>)> callback) {
state_tracker.Run(ComponentState::kUpdating);
return InstallOperation(
crx_cache, config->GetUnzipperFactory()->Create(), crx_format, id,
pk_hash, installer,
run.empty() ? nullptr
: std::make_unique<CrxInstaller::InstallParams>(
run, arguments, install_data),
fingerprint, event_adder, install_progress_callback,
base::BindOnce(
[](const base::FilePath& file,
base::RepeatingCallback<void(const CrxInstaller::Result&)>
install_complete_callback,
const CrxInstaller::Result& result)
-> base::expected<base::FilePath, CategorizedError> {
install_complete_callback.Run(result);
if (result.result.category_ != ErrorCategory::kNone) {
return base::unexpected(result.result);
}
return file;
},
file, install_complete_callback)
.Then(std::move(callback)),
file);
},
config, crx_cache, crx_format, id, pk_hash, installer,
result.manifest.run, result.manifest.arguments,
[&]() -> std::string {
if (install_data_index.empty() || result.data.empty()) {
return "";
}
const auto it = std::ranges::find(
result.data, install_data_index,
&ProtocolParser::Result::Data::install_data_index);
return it != std::end(result.data) ? it->text : "";
}(),
result.manifest.packages[0].fingerprint, state_tracker, event_adder,
install_progress_callback, install_complete_callback);
crx_cache->Get(
id, result.manifest.packages[0].fingerprint,
base::BindOnce(
[](scoped_refptr<CrxCache> crx_cache, const std::string& id,
const std::string& prev_fp,
base::OnceCallback<void(
base::expected<base::FilePath, UnpackerError>,
base::expected<base::FilePath, UnpackerError>)> callback,
base::expected<base::FilePath, UnpackerError> installer) {
crx_cache->Get(id, prev_fp,
base::BindOnce(std::move(callback), installer));
},
crx_cache, id, prev_fp,
base::BindOnce(&AssemblePipeline, std::move(download_diff),
std::move(patch_diff), std::move(download_full),
install, run, diff_outcome_recorder,
std::move(callback))));
return;
}
} // namespace update_client