From 1ef08d5e7d598db24c7e8f277d889681960edbbb Mon Sep 17 00:00:00 2001 From: Thefool Date: Wed, 21 Oct 2026 15:08:23 +0800 Subject: [PATCH] fix(windows): stop in-app updates from rebooting the machine (#8628) The MSI now carries REBOOT=ReallySuppress, so Windows Installer never restarts the machine when a file is in use during an in-app update, including updates driven by older updaters. The updater also passes REBOOT=ReallySuppress, a leftover instance is closed on the updater path, and the release workflow fails if the built MSI lacks the property. Fixes #8534 --- .github/workflows/release.yml | 14 ++++++++++++++ src-tauri/src/commands/settings.rs | 11 +++++++++-- src-tauri/wix/per-user-main.wxs | 22 +++++++++++++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 03010ac39..b526ac02a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -230,6 +230,20 @@ jobs: $msi = Get-ChildItem -Path (Join-Path $targetRoot 'bundle') -Recurse -Include *.msi -ErrorAction SilentlyContinue | Select-Object -First 1 } if ($null -ne $msi) { + # #8534:旧版本的应用内更新器以 `msiexec /passive /promptrestart` 安装、不带任何 REBOOT + # 选项,文件被占用时会不经提示直接重启系统,所以 MSI 自身必须带 REBOOT=ReallySuppress。 + $installer = New-Object -ComObject WindowsInstaller.Installer + $db = $installer.GetType().InvokeMember('OpenDatabase', 'InvokeMethod', $null, $installer, @($msi.FullName, 0)) + $view = $db.GetType().InvokeMember('OpenView', 'InvokeMethod', $null, $db, @('SELECT `Value` FROM `Property` WHERE `Property`=''REBOOT''')) + $view.GetType().InvokeMember('Execute', 'InvokeMethod', $null, $view, $null) | Out-Null + $record = $view.GetType().InvokeMember('Fetch', 'InvokeMethod', $null, $view, $null) + $reboot = if ($null -ne $record) { $record.GetType().InvokeMember('StringData', 'GetProperty', $null, $record, 1) } else { $null } + $view.GetType().InvokeMember('Close', 'InvokeMethod', $null, $view, $null) | Out-Null + if ($reboot -ne 'ReallySuppress') { + throw "MSI Property table must set REBOOT=ReallySuppress (found: '$reboot') - see #8534" + } + Write-Host "MSI REBOOT property: $reboot" + $dest = "CC-Switch-$VERSION-Windows$assetSuffix.msi" Copy-Item $msi.FullName (Join-Path release-assets $dest) Write-Host "Installer copied: $dest" diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index 5902d0dd9..276c32838 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -275,8 +275,15 @@ pub async fn restart_app(app: AppHandle) -> Result { /// 这里把退出清理、安装和重启串在同一个后端流程中,避免依赖旧前端继续执行。 #[tauri::command] pub async fn install_update_and_restart(app: AppHandle) -> Result { - let updater = app - .updater_builder() + let builder = app.updater_builder(); + // Windows 上 updater 以 `msiexec /passive /promptrestart` 安装 MSI。passive 没有模态框、 + // 未指定重启选项时,安装目录里有文件被占用,Windows Installer 会不经确认直接重启系统 + // (#8534)。`/promptrestart` 只对应 REBOOTPROMPT,挡不住这一点,因此显式追加 + // REBOOT=ReallySuppress(即 `/norestart`)。旧版本的 updater 不带这个参数, + // 由 MSI 模板自身的 REBOOT 属性兜底。 + #[cfg(target_os = "windows")] + let builder = builder.installer_arg("REBOOT=ReallySuppress"); + let updater = builder .build() .map_err(|e| format!("初始化更新器失败: {e}"))?; diff --git a/src-tauri/wix/per-user-main.wxs b/src-tauri/wix/per-user-main.wxs index acc51404c..2dc129df4 100644 --- a/src-tauri/wix/per-user-main.wxs +++ b/src-tauri/wix/per-user-main.wxs @@ -11,7 +11,8 @@ - + + + + + + AUTOLAUNCHAPP + NOT AUTOLAUNCHAPP +