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

200 lines
8.0 KiB
C++

#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;
std::vector<std::string> restart_arguments;
};
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,
request.value("restart_arguments", std::vector<std::string>{})};
}
void WaitForCommit(const fs::path& job) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(120);
while (true) {
if (fs::exists(job / "cancel"))
throw std::runtime_error("Update cancelled before application exit");
if (fs::exists(job / "commit")) return;
if (std::chrono::steady_clock::now() >= deadline)
throw std::runtime_error("Application did not confirm update installation");
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
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);
},
[&] {
#ifndef _WIN32
// Authorization and staging finish while the GUI can still service
// the remote connection. An empty directory publishes readiness
// atomically without writing through a caller-controlled file.
if (fs::exists(job / "cancel"))
throw std::runtime_error("Update cancelled before application exit");
if (!fs::create_directory(job / "ready"))
throw std::runtime_error("Update readiness was already published");
WaitForCommit(job);
#endif
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;
std::vector<std::string> restart_arguments;
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;
restart_arguments = request.restart_arguments;
auto pinned = PinHelper(ExecutablePath());
{
maintenance = std::make_unique<MaintenanceLock>(application);
VerifyPackage(job / request.artifact.filename, request.artifact);
#ifdef _WIN32
WriteText(job / "ready", "ready\n");
WaitForCommit(job);
committed = true;
#else
// The root-owned helper publishes ready only after authorization and
// verification of its private package copy, then waits for the GUI's
// commit before touching the installation.
const int code = RunElevated(HelperPath(application), job);
committed = fs::exists(job / "commit");
if (!committed)
throw std::runtime_error("Update preparation failed or authorization was cancelled (code " +
std::to_string(code) + ")");
#endif
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 int code = RunElevated(ExecutablePath(), job);
#endif
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, restart_arguments);
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;
}