From e45350319c8af44c4122a0dd29aba5c50e892aad Mon Sep 17 00:00:00 2001 From: dijunkun Date: Wed, 21 Oct 2026 03:10:40 +0800 Subject: [PATCH] [fix] authorize POSIX updates before closing CrossDesk --- apps/desktop/src/updater/main.cpp | 50 ++++++++++++++----- .../desktop/src/updater/update_controller.cpp | 4 +- docs/UPDATER.md | 17 ++++--- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src/updater/main.cpp b/apps/desktop/src/updater/main.cpp index d518c4db..a81d6be0 100644 --- a/apps/desktop/src/updater/main.cpp +++ b/apps/desktop/src/updater/main.cpp @@ -27,6 +27,18 @@ Request ReadRequest(const fs::path& job) { return {application, artifact}; } +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; @@ -41,7 +53,19 @@ int Install(const fs::path& job) { // Copy into private storage, then verify the exact copy being installed. CopyPackage(job / request.artifact.filename, package, request.artifact); }, - [&] { WaitForApplicationExit(request.application); }, + [&] { +#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) { @@ -69,14 +93,20 @@ int Coordinate(const fs::path& job) { { maintenance = std::make_unique(application); VerifyPackage(job / request.artifact.filename, request.artifact); +#ifdef _WIN32 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)); - } + 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); @@ -96,12 +126,8 @@ int Coordinate(const fs::path& job) { 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); + const int code = RunElevated(ExecutablePath(), job); #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; diff --git a/apps/desktop/src/updater/update_controller.cpp b/apps/desktop/src/updater/update_controller.cpp index f4821d5a..7b06dbd5 100644 --- a/apps/desktop/src/updater/update_controller.cpp +++ b/apps/desktop/src/updater/update_controller.cpp @@ -99,7 +99,9 @@ bool Controller::Poll() { 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); + // 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; diff --git a/docs/UPDATER.md b/docs/UPDATER.md index 081d905e..56e412f8 100644 --- a/docs/UPDATER.md +++ b/docs/UPDATER.md @@ -12,11 +12,15 @@ Windows 安装版、macOS PKG 安装版和 Linux DEB 安装版共用 1. 用户点击“安装更新”并确认连接和文件传输将中断。 2. 主程序异步下载,显示进度,并验证签名、大小和 SHA-256。 -3. 升级器从私有暂存目录启动,取得安装目录对应的维护锁并确认就绪。 -4. 主程序正常退出,升级器等待同一用户的 GUI 和守护进程释放实例锁。 -5. 升级器请求系统安装授权。取消授权会尝试重新打开原程序。 -6. 获得授权的安装进程再次验证签名,将包复制到只有管理员可写的 - 私有目录,再验证该副本。确认相关程序退出后,调用平台安装器。 +3. 升级器从私有暂存目录启动,取得安装目录对应的维护锁。 +4. macOS/Linux 先请求系统授权,再由提权后的安装进程再次验证签名、 + 将包复制到只有管理员可写的私有目录并验证副本。完成这些步骤后才 + 通知主程序退出;等待授权期间远程连接继续运行,取消授权不会关闭 + 主程序。准备阶段最多等待五分钟。 +5. 主程序收到就绪通知后提交安装并正常退出。Windows 保持原有流程: + 先等待同一用户的 GUI 和守护进程退出,再启动提权安装进程验证副本。 +6. 安装进程确认相关程序退出后调用平台安装器。macOS/Linux 未收到 + 主程序的安装提交、收到取消通知或等待提交超时时,不执行安装。 7. 升级器以原调用者身份重启程序。新程序进入 GUI 事件循环后确认启动; 90 秒内未收到确认会记录失败。启动确认不依赖远程服务器连通性。 @@ -106,7 +110,8 @@ xmake b crossdesk ## 失败处理与验证 -下载、验签或准备失败时不关闭主程序。安装失败或授权取消后尝试重启。 +下载、验签或准备失败时不关闭主程序。主程序退出后的安装失败会尝试 +重启;macOS/Linux 在退出前取消授权时保留原程序和连接。 `last-update` 位于应用缓存目录,指向私有暂存目录;`request.json` 和 `result.json` 保留升级版本及结果,未完成的任务不会自动重放。成功启动后 删除大型安装包,保留少量诊断文件。失败任务保留下载包便于排查。