[feat] add in-app updates for desktop installations

This commit is contained in:
dijunkun
2026-10-20 20:11:12 +08:00
parent 62ba3693af
commit a625890b28
34 changed files with 1732 additions and 25 deletions
+169
View File
@@ -0,0 +1,169 @@
#include "updater/update_core.h"
#include "updater/update_platform.h"
#include "platform/single_instance.h"
#include <nlohmann/json.hpp>
#include <chrono>
#include <iostream>
#include <thread>
#ifdef _WIN32
#include <windows.h>
#include <shellapi.h>
#pragma comment(linker, "/subsystem:windows /entry:mainCRTStartup")
#endif
namespace crossdesk::updater {
namespace {
struct Request {
fs::path application;
Artifact artifact;
};
Request ReadRequest(const fs::path& job) {
const auto request = nlohmann::json::parse(ReadText(job / "request.json"));
const auto application = fs::u8path(request.at("application").get<std::string>());
if (!SupportedInstallation(application)) throw std::runtime_error("Invalid installation destination");
const auto artifact = VerifyEnvelope(request.at("envelope").get<std::string>(), PublicKey(),
PlatformKey(application), BuildVersion());
return {application, artifact};
}
int Install(const fs::path& job) {
if (!IsPrivileged()) return 2;
fs::path stage;
int result = 1;
try {
const auto request = ReadRequest(job);
stage = PrivateDirectory();
const auto package = stage / request.artifact.filename;
ExecuteInstall({
[&] {
// A privileged installer never executes the caller's writable file.
// Copy into private storage, then verify the exact copy being installed.
CopyPackage(job / request.artifact.filename, package, request.artifact);
},
[&] { WaitForApplicationExit(request.application); },
[&] { InstallPackage(package, request.application); }});
result = 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
}
RestoreServices();
if (!stage.empty()) {
std::error_code ignored;
fs::remove_all(stage, ignored);
}
return result;
}
int Coordinate(const fs::path& job) {
fs::path application;
bool committed = false;
bool success = false;
bool exited = false;
std::string error;
std::unique_ptr<MaintenanceLock> maintenance;
try {
const auto request = ReadRequest(job);
application = request.application;
auto pinned = PinHelper(ExecutablePath());
{
maintenance = std::make_unique<MaintenanceLock>(application);
VerifyPackage(job / request.artifact.filename, request.artifact);
WriteText(job / "ready", "ready\n");
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(120);
while (!fs::exists(job / "commit")) {
if (fs::exists(job / "cancel") || std::chrono::steady_clock::now() >= deadline)
throw std::runtime_error("Update cancelled before application exit");
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
committed = true;
platform::SingleInstanceGuard client;
platform::SingleInstanceGuard launcher;
const auto exit_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(45);
while (true) {
std::string lease_error;
const auto client_result = client.TryAcquire(platform::InstanceRole::kClient, lease_error);
const auto launcher_result = launcher.TryAcquire(platform::InstanceRole::kLauncher, lease_error);
if (client_result == platform::InstanceResult::kError ||
launcher_result == platform::InstanceResult::kError)
throw std::runtime_error("Cannot wait for application exit: " + lease_error);
if (client_result == platform::InstanceResult::kAcquired &&
launcher_result == platform::InstanceResult::kAcquired) break;
if (std::chrono::steady_clock::now() >= exit_deadline)
throw std::runtime_error("CrossDesk did not exit; no files were installed");
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
exited = true;
#ifdef _WIN32
// This relocated copy is held read-only across UAC and installation.
const auto installer_helper = ExecutablePath();
#else
// Use the root-owned installed helper for privileged execution.
const auto installer_helper = HelperPath(application);
#endif
const int code = RunElevated(installer_helper, job);
if (code != 0) throw std::runtime_error("Installation failed or authorization was cancelled (code " +
std::to_string(code) + ")");
success = true;
}
} catch (const std::exception& exception) { error = exception.what(); }
try {
WriteText(job / "result.json", nlohmann::json{{"success", success}, {"error", error}}.dump(2));
} catch (const std::exception&) {}
maintenance.reset();
if (committed && exited) {
try {
Restart(application);
if (success) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(90);
while (!fs::exists(job / "started") && std::chrono::steady_clock::now() < deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(200));
if (!fs::exists(job / "started")) {
success = false;
WriteText(job / "result.json", nlohmann::json{{"success", false},
{"error", "Installation finished but the new application did not confirm startup. Open CrossDesk or run the installer manually."}}.dump(2));
}
}
}
catch (const std::exception& exception) {
success = false;
WriteText(job / "result.json", nlohmann::json{{"success", false},
{"error", std::string("Could not restart CrossDesk: ") + exception.what()}}.dump(2));
}
}
if (success) {
// Keep request/result for diagnostics, remove the large download.
std::error_code ignored;
for (const auto& entry : fs::directory_iterator(job)) {
if (entry.path().extension() == ".pkg" || entry.path().extension() == ".deb" ||
(entry.path().extension() == ".exe" && entry.path() != ExecutablePath()))
fs::remove(entry.path(), ignored);
}
}
return success ? 0 : 1;
}
} // namespace
} // namespace crossdesk::updater
int main(int argc, char** argv) {
using namespace crossdesk::updater;
try {
std::vector<std::string> args;
#ifdef _WIN32
int count = 0;
auto wide = CommandLineToArgvW(GetCommandLineW(), &count);
if (!wide) return 2;
for (int i = 0; i < count; ++i) args.push_back(fs::path(wide[i]).u8string());
LocalFree(wide);
(void)argc; (void)argv;
#else
for (int i = 0; i < argc; ++i) args.emplace_back(argv[i]);
#endif
if (args.size() != 3) return 2;
const auto job = fs::u8path(args[2]);
if (!job.is_absolute() || fs::is_symlink(job) || !fs::is_directory(job)) return 2;
if (args[1] == "--install") return Install(job);
if (args[1] == "--coordinate" && OwnedJobDirectory(job)) return Coordinate(job);
} catch (const std::exception& error) { std::cerr << error.what() << '\n'; }
return 2;
}
@@ -0,0 +1,134 @@
#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);
}
} 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");
} 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}}.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();
deadline_ = std::chrono::steady_clock::now() + std::chrono::seconds(30);
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
@@ -0,0 +1,43 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _UPDATE_CONTROLLER_H_
#define _UPDATE_CONTROLLER_H_
#include "updater/update_core.h"
#include <future>
namespace crossdesk::updater {
class Controller {
public:
enum class State { Idle, Downloading, Preparing, Ready, Failed };
explicit Controller(fs::path cache);
~Controller();
bool Available(const std::string& envelope) const;
void Start(const std::string& envelope);
// Returns true only after a live helper has acknowledged the prepared job.
bool Poll();
void Cancel();
void ConfirmStartup();
State state() const { return state_; }
int progress() const { return progress_.load(); }
const std::string& error() const { return error_; }
bool busy() const { return state_ == State::Downloading || state_ == State::Preparing; }
private:
fs::path cache_, job_;
State state_ = State::Idle;
std::string error_;
mutable std::string checked_envelope_;
mutable bool checked_ = false;
mutable bool available_ = false;
std::atomic<int> progress_{0};
std::atomic<bool> cancel_{false};
std::future<fs::path> pending_;
std::chrono::steady_clock::time_point deadline_{};
};
} // namespace crossdesk::updater
#endif
+267
View File
@@ -0,0 +1,267 @@
#include "updater/update_core.h"
#include <app_version.h>
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <array>
#include <fstream>
#include <memory>
#include <stdexcept>
#include <vector>
#ifndef CROSSDESK_UPDATE_PUBLIC_KEY
#define CROSSDESK_UPDATE_PUBLIC_KEY ""
#endif
#ifndef CROSSDESK_VERSION
#define CROSSDESK_VERSION "0.0.0"
#endif
namespace crossdesk::updater {
namespace {
constexpr std::uint64_t kMaxPackageSize = 2ULL * 1024 * 1024 * 1024;
constexpr const char* kDownloadOrigin = "https://downloads.crossdesk.cn";
std::string Hex(const unsigned char* data, std::size_t length) {
constexpr char digits[] = "0123456789abcdef";
std::string result;
for (std::size_t i = 0; i < length; ++i) {
result += digits[data[i] >> 4];
result += digits[data[i] & 15];
}
return result;
}
std::vector<unsigned char> Unhex(const std::string& value, std::size_t size) {
if (value.size() != size * 2) throw std::runtime_error("Invalid signature/key size");
std::vector<unsigned char> result(size);
auto digit = [](char c) -> int {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
throw std::runtime_error("Expected lowercase hexadecimal");
};
for (std::size_t i = 0; i < size; ++i)
result[i] = static_cast<unsigned char>(digit(value[2 * i]) * 16 + digit(value[2 * i + 1]));
return result;
}
std::string ExpectedFilename(const std::string& platform, const std::string& version) {
if (version.empty() || version.find_first_not_of("0123456789.-") != std::string::npos)
throw std::runtime_error("Invalid release filename version");
if (platform == "windows-x64") return "crossdesk-win-x64-v" + version + ".exe";
if (platform == "windows-x64-portable")
return "crossdesk-win-x64-portable-v" + version + ".zip";
if (platform == "macos-x64" || platform == "macos-arm64")
return "crossdesk-" + platform + "-v" + version + ".pkg";
for (const std::string arch : {"amd64", "arm64"}) {
if (platform == "linux-" + arch)
return "crossdesk-linux-" + arch + "-v" + version + ".deb";
if (platform == "linux-" + arch + "-virtual-desktop-xfce")
return "crossdesk-virtual-desktop-xfce-linux-" + arch + "-v" + version + ".deb";
}
throw std::runtime_error("Unsupported update platform");
}
} // namespace
std::string PublicKey() { return CROSSDESK_UPDATE_PUBLIC_KEY; }
std::string BuildVersion() { return CROSSDESK_VERSION; }
std::string PathUtf8(const fs::path& path) { return path.u8string(); }
Artifact VerifyEnvelope(const std::string& envelope, const std::string& public_key,
const std::string& platform, const std::string& current_version) {
if (envelope.size() > 65536) throw std::runtime_error("Update envelope is too large");
const auto wrapper = nlohmann::json::parse(envelope);
const auto payload = wrapper.at("payload").get<std::string>();
const auto signature = Unhex(wrapper.at("signature").get<std::string>(), 64);
const auto key_bytes = Unhex(public_key, 32);
std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key(
EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, key_bytes.data(), key_bytes.size()),
EVP_PKEY_free);
std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> context(EVP_MD_CTX_new(), EVP_MD_CTX_free);
if (!key || !context || EVP_DigestVerifyInit(context.get(), nullptr, nullptr, nullptr, key.get()) != 1 ||
EVP_DigestVerify(context.get(), signature.data(), signature.size(),
reinterpret_cast<const unsigned char*>(payload.data()), payload.size()) != 1)
throw std::runtime_error("Update signature verification failed");
const auto value = nlohmann::json::parse(payload);
if (value.at("schema") != 1) throw std::runtime_error("Unsupported update schema");
Artifact artifact;
artifact.platform = value.at("platform").get<std::string>();
artifact.version = value.at("version").get<std::string>();
artifact.filename = value.at("filename").get<std::string>();
artifact.url = value.at("url").get<std::string>();
artifact.sha256 = value.at("sha256").get<std::string>();
if (!value.at("size").is_number_unsigned()) throw std::runtime_error("Invalid package size");
artifact.size = value.at("size").get<std::uint64_t>();
if (artifact.platform != platform || platform.find("portable") != std::string::npos ||
!IsValidAppVersion(current_version) || !IsValidAppVersion(artifact.version) ||
!IsNewerVersion(current_version, artifact.version))
throw std::runtime_error("Update platform/version does not match this installation");
if (!artifact.size || artifact.size > kMaxPackageSize ||
artifact.filename != ExpectedFilename(platform, artifact.version) ||
artifact.url != std::string(kDownloadOrigin) + "/" + artifact.filename)
throw std::runtime_error("Invalid update package metadata");
Unhex(artifact.sha256, 32);
return artifact;
}
std::string PlatformKey(const fs::path& executable, bool portable) {
#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__))
(void)executable;
return portable ? "windows-x64-portable" : "windows-x64";
#elif defined(__APPLE__)
(void)executable; (void)portable;
#if defined(__aarch64__) || defined(__arm64__)
return "macos-arm64";
#else
return "macos-x64";
#endif
#elif defined(__linux__) && !defined(__OHOS__)
(void)portable;
#if defined(__aarch64__)
std::string key = "linux-arm64";
#elif defined(__x86_64__)
std::string key = "linux-amd64";
#else
return {};
#endif
std::ifstream marker(executable.parent_path() / "package-name");
std::string package;
if (!std::getline(marker, package)) return {};
if (package == "crossdesk-virtual-desktop-xfce") key += "-virtual-desktop-xfce";
else if (package != "crossdesk") return {};
return key;
#else
(void)executable; (void)portable;
return {};
#endif
}
std::string DownloadUrl(const std::string& manifest, const std::string& platform,
const std::string& version) {
try {
const auto download = nlohmann::json::parse(manifest).at("downloads").at(platform);
const auto filename = ExpectedFilename(platform, version);
const auto url = std::string(kDownloadOrigin) + "/" + filename;
if (download.at("version") == version && download.at("filename") == filename &&
download.at("url") == url) return url;
} catch (const std::exception&) {}
return "https://crossdesk.cn";
}
std::string SignedDownload(const std::string& manifest, const std::string& platform) {
try {
return nlohmann::json::parse(manifest).at("downloads").at(platform).at("update").dump();
} catch (const std::exception&) { return {}; }
}
std::string Sha256(const std::string& value) {
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int size = 0;
if (EVP_Digest(value.data(), value.size(), digest, &size, EVP_sha256(), nullptr) != 1)
throw std::runtime_error("SHA-256 failed");
return Hex(digest, size);
}
void VerifyPackage(const fs::path& file, const Artifact& artifact) {
std::ifstream input(file, std::ios::binary);
std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> context(EVP_MD_CTX_new(), EVP_MD_CTX_free);
if (!input || !context || EVP_DigestInit_ex(context.get(), EVP_sha256(), nullptr) != 1)
throw std::runtime_error("Cannot read update package");
std::array<char, 65536> buffer{};
std::uint64_t total = 0;
while (input.read(buffer.data(), buffer.size()) || input.gcount()) {
const auto size = static_cast<std::size_t>(input.gcount());
total += size;
if (total > artifact.size || EVP_DigestUpdate(context.get(), buffer.data(), size) != 1)
throw std::runtime_error("Update package size/hash failed");
}
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int size = 0;
if (!input.eof() || total != artifact.size ||
EVP_DigestFinal_ex(context.get(), digest, &size) != 1 || Hex(digest, size) != artifact.sha256)
throw std::runtime_error("Update package verification failed");
}
void DownloadPackage(const Artifact& artifact, const fs::path& destination,
std::atomic<int>& progress, const std::atomic<bool>& cancel) {
httplib::Client client(kDownloadOrigin);
client.set_connection_timeout(10);
client.set_read_timeout(30);
client.set_write_timeout(10);
client.set_max_timeout(30 * 60 * 1000);
client.set_follow_location(false);
#if defined(__linux__)
for (const char* bundle : {"/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt"}) {
if (fs::exists(bundle)) { client.set_ca_cert_path(bundle); break; }
}
#endif
std::ofstream file(destination, std::ios::binary | std::ios::trunc);
if (!file) throw std::runtime_error("Cannot create update download");
std::uint64_t received = 0;
const auto response = client.Get(("/" + artifact.filename).c_str(),
[&](const httplib::Response& headers) {
return headers.status == 200 && !cancel.load();
},
[&](const char* data, std::size_t length) {
if (cancel.load() || length > artifact.size - received) return false;
file.write(data, static_cast<std::streamsize>(length));
received += length;
progress.store(static_cast<int>(received * 100 / artifact.size));
return static_cast<bool>(file);
});
file.close();
if (!response || response->status != 200 || !file || received != artifact.size || cancel.load())
throw std::runtime_error(cancel.load() ? "Update cancelled" : "Update download failed");
VerifyPackage(destination, artifact);
}
void CopyPackage(const fs::path& source, const fs::path& destination, const Artifact& artifact) {
std::ifstream input(source, std::ios::binary);
std::ofstream output(destination, std::ios::binary | std::ios::trunc);
if (!input || !output) throw std::runtime_error("Cannot stage installation package");
std::array<char, 65536> buffer{};
std::uint64_t copied = 0;
while (input.read(buffer.data(), buffer.size()) || input.gcount()) {
const auto length = static_cast<std::uint64_t>(input.gcount());
if (length > artifact.size - copied) throw std::runtime_error("Installation package is too large");
output.write(buffer.data(), input.gcount());
if (!output) throw std::runtime_error("Cannot write installation package");
copied += length;
}
output.close();
if (!input.eof() || !output || copied != artifact.size)
throw std::runtime_error("Installation package is incomplete");
VerifyPackage(destination, artifact);
}
std::string ReadText(const fs::path& file, std::size_t maximum) {
std::ifstream input(file, std::ios::binary);
if (!input) throw std::runtime_error("Cannot read update metadata");
std::string value(maximum + 1, '\0');
input.read(value.data(), static_cast<std::streamsize>(value.size()));
value.resize(static_cast<std::size_t>(input.gcount()));
if (value.size() > maximum || input.bad()) throw std::runtime_error("Invalid update metadata size");
return value;
}
void WriteText(const fs::path& file, const std::string& text) {
std::ofstream output(file, std::ios::binary | std::ios::trunc);
output << text;
output.close();
if (!output) throw std::runtime_error("Cannot write update state");
}
std::string RandomId() {
unsigned char bytes[16];
if (RAND_bytes(bytes, sizeof(bytes)) != 1) throw std::runtime_error("Cannot generate update ID");
return Hex(bytes, sizeof(bytes));
}
void ExecuteInstall(const InstallActions& actions) {
actions.verify();
actions.wait_for_exit();
actions.install();
}
} // namespace crossdesk::updater
+61
View File
@@ -0,0 +1,61 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _UPDATE_CORE_H_
#define _UPDATE_CORE_H_
#include <atomic>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <string>
namespace crossdesk::updater {
namespace fs = std::filesystem;
struct Artifact {
std::string platform;
std::string version;
std::string filename;
std::string url;
std::string sha256;
std::uint64_t size = 0;
};
// The envelope contains the exact signed UTF-8 payload and a hex Ed25519 signature.
// Never reserialize the payload before verifying it.
Artifact VerifyEnvelope(const std::string& envelope, const std::string& public_key,
const std::string& platform,
const std::string& current_version);
std::string PublicKey();
std::string BuildVersion();
std::string PlatformKey(const fs::path& executable, bool portable = false);
std::string DownloadUrl(const std::string& manifest, const std::string& platform,
const std::string& version);
std::string SignedDownload(const std::string& manifest, const std::string& platform);
std::string Sha256(const std::string& value);
void VerifyPackage(const fs::path& file, const Artifact& artifact);
void CopyPackage(const fs::path& source, const fs::path& destination, const Artifact& artifact);
void DownloadPackage(const Artifact& artifact, const fs::path& destination,
std::atomic<int>& progress, const std::atomic<bool>& cancel);
std::string ReadText(const fs::path& file, std::size_t maximum = 65536);
void WriteText(const fs::path& file, const std::string& text);
fs::path PrivateDirectory();
std::string RandomId();
// ABI boundary between the C++17 core and C++20 GUI (char8_t).
std::string PathUtf8(const fs::path& path);
// Files and installation operations are injectable so failure ordering is tested
// without invoking an actual installer.
struct InstallActions {
std::function<void()> verify;
std::function<void()> wait_for_exit;
std::function<void()> install;
};
void ExecuteInstall(const InstallActions& actions);
} // namespace crossdesk::updater
#endif
@@ -0,0 +1,43 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _UPDATE_PLATFORM_H_
#define _UPDATE_PLATFORM_H_
#include "updater/update_core.h"
#include <memory>
#include <vector>
namespace crossdesk::updater {
fs::path ExecutablePath();
fs::path HelperPath(const fs::path& application);
bool SupportedInstallation(const fs::path& application);
bool IsPrivileged();
bool OwnedJobDirectory(const fs::path& job);
void Launch(const fs::path& executable, const std::vector<std::string>& arguments);
int Run(const fs::path& executable, const std::vector<std::string>& arguments,
bool elevate = false);
int RunElevated(const fs::path& executable, const fs::path& job);
void WaitForApplicationExit(const fs::path& application);
void InstallPackage(const fs::path& package, const fs::path& application);
void Restart(const fs::path& application);
void RestoreServices();
// Pin relocated helper files against changes during elevation (Windows).
std::shared_ptr<void> PinHelper(const fs::path& helper);
class MaintenanceLock {
public:
explicit MaintenanceLock(const fs::path& application);
~MaintenanceLock();
MaintenanceLock(const MaintenanceLock&) = delete;
MaintenanceLock& operator=(const MaintenanceLock&) = delete;
private:
std::intptr_t handle_ = -1;
};
bool MaintenanceActive(const fs::path& application);
} // namespace crossdesk::updater
#endif