Files
crossdesk/apps/desktop/src/updater/update_controller.cpp
T

147 lines
6.0 KiB
C++

#include "updater/update_controller.h"
#include "updater/update_platform.h"
#include "common/filesystem_utf8.h"
#include <nlohmann/json.hpp>
#include <stdexcept>
namespace crossdesk::updater {
Controller::Controller(fs::path cache) : cache_(std::move(cache)) {
// A previous result survives a restart. Incomplete jobs are never replayed.
try {
const auto previous = PathFromUtf8(ReadText(cache_ / "last-update", 32768));
if (!fs::exists(previous / "result.json")) {
state_ = State::Failed;
error_ = "The previous update was interrupted. Download and retry.\n" + PathUtf8(previous);
return;
}
const auto result = nlohmann::json::parse(ReadText(previous / "result.json"));
if (!result.value("success", false)) {
state_ = State::Failed;
error_ = result.value("error", std::string("Update did not complete")) +
"\n" + PathUtf8(previous);
} else if (fs::exists(previous / "started")) {
// Retire receipts confirmed by older clients too, before temporary
// storage is cleaned by the OS and a successful update looks interrupted.
fs::remove(cache_ / "last-update");
}
} catch (const std::exception&) {}
}
void Controller::ConfirmStartup() {
try {
const auto previous = PathFromUtf8(ReadText(cache_ / "last-update", 32768));
const auto result = nlohmann::json::parse(ReadText(previous / "result.json"));
if (!result.value("success", false)) return;
const auto request = nlohmann::json::parse(ReadText(previous / "request.json"));
const auto artifact = VerifyEnvelope(request.at("envelope").get<std::string>(), PublicKey(),
PlatformKey(ExecutablePath()), "0.0.0");
auto built = BuildVersion();
if (!built.empty() && built[0] == 'v') built.erase(0, 1);
if (artifact.version == built) {
WriteText(previous / "started", "started\n");
// Acknowledge the coordinator first. Successful startup no longer needs
// a persistent pointer into a temporary job directory.
fs::remove(cache_ / "last-update");
}
} catch (const std::exception&) {}
}
Controller::~Controller() {
Cancel();
if (pending_.valid()) {
try {
const auto prepared = pending_.get();
WriteText(prepared / "cancel", "cancel\n");
} catch (const std::exception&) {}
}
}
bool Controller::Available(const std::string& envelope) const {
if (checked_ && checked_envelope_ == envelope) return available_;
checked_ = true;
checked_envelope_ = envelope;
available_ = false;
try {
const auto application = ExecutablePath();
if (!SupportedInstallation(application)) return false;
VerifyEnvelope(envelope, PublicKey(), PlatformKey(application), BuildVersion());
available_ = true;
return available_;
} catch (const std::exception&) { return false; }
}
void Controller::Start(const std::string& envelope) {
if (busy() || state_ == State::Ready) return;
error_.clear();
progress_ = 0;
cancel_ = false;
state_ = State::Downloading;
try {
pending_ = std::async(std::launch::async, [this, envelope] {
const auto application = ExecutablePath();
if (!SupportedInstallation(application)) throw std::runtime_error("Unsupported installation");
const auto artifact = VerifyEnvelope(envelope, PublicKey(), PlatformKey(application), BuildVersion());
const auto job = PrivateDirectory();
try {
WriteText(job / "request.json", nlohmann::json{
{"application", PathUtf8(application)}, {"envelope", envelope},
{"restart_arguments", RestartArguments()}}.dump());
DownloadPackage(artifact, job / artifact.filename, progress_, cancel_);
if (cancel_) throw std::runtime_error("Update cancelled");
// Relocate the coordinator so package installation can replace the
// installed helper too. Its crypto/runtime dependencies are static.
const auto helper = job / HelperPath(application).filename();
fs::copy_file(HelperPath(application), helper);
fs::permissions(helper, fs::perms::owner_all);
Launch(helper, {"--coordinate", PathUtf8(job)});
return job;
} catch (...) {
// Only this freshly created directory is owned by this worker.
std::error_code ignored;
fs::remove_all(job, ignored);
throw;
}
});
} catch (const std::exception& exception) {
error_ = exception.what();
state_ = State::Failed;
}
}
bool Controller::Poll() {
try {
if (state_ == State::Downloading && pending_.valid() &&
pending_.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
job_ = pending_.get();
// POSIX readiness includes the system authorization dialog and staging
// the verified package; keep the GUI available while the user approves.
deadline_ = std::chrono::steady_clock::now() + std::chrono::minutes(5);
state_ = State::Preparing;
}
if (state_ != State::Preparing) return false;
if (cancel_) throw std::runtime_error("Update cancelled");
if (fs::exists(job_ / "result.json")) {
const auto result = nlohmann::json::parse(ReadText(job_ / "result.json"));
throw std::runtime_error(result.value("error", std::string("Update preparation failed")));
}
if (fs::exists(job_ / "ready")) {
WriteText(cache_ / "last-update", PathUtf8(job_));
WriteText(job_ / "commit", "install\n");
state_ = State::Ready;
return true;
}
if (std::chrono::steady_clock::now() >= deadline_)
throw std::runtime_error("Update helper did not become ready");
} catch (const std::exception& exception) {
const bool cancelled = cancel_.load();
if (state_ == State::Preparing) Cancel();
error_ = exception.what();
state_ = cancelled ? State::Idle : State::Failed;
if (cancelled) error_.clear();
}
return false;
}
void Controller::Cancel() {
cancel_ = true;
if (state_ == State::Preparing && !job_.empty()) {
try { WriteText(job_ / "cancel", "cancel\n"); } catch (const std::exception&) {}
}
}
} // namespace crossdesk::updater