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
This commit is contained in:
Thefool authored and GitHub committed 2026-10-21 15:08:23 +08:00
1 parent c776fd0ebf
commit 1ef08d5e7d
3 files changed
+44 -3

No files matched your search

+14
View File
@@ -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"
+9 -2
View File
@@ -275,8 +275,15 @@ pub async fn restart_app(app: AppHandle) -> Result<bool, String> {
/// 这里把退出清理、安装和重启串在同一个后端流程中,避免依赖旧前端继续执行。
#[tauri::command]
pub async fn install_update_and_restart(app: AppHandle) -> Result<bool, String> {
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}"))?;
+21 -1
View File
@@ -11,7 +11,8 @@
<?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>
<?endif?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<Product
Id="*"
Name="{{product_name}}"
@@ -33,11 +34,30 @@
<!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->
<Property Id="REINSTALLMODE" Value="amus" />
<!-- Never let this installer restart the machine (#8534). The in-app updater runs
`msiexec /passive /promptrestart`; with no restart option, passive mode reboots without any
prompt as soon as a file is still in use. Updaters shipped in older releases do not pass
REBOOT=ReallySuppress themselves, so the package has to carry it. Files that are still
in use are then replaced on the next restart instead. -->
<Property Id="REBOOT" Value="ReallySuppress" />
<!-- Auto launch app after installation, useful for passive mode which usually used in updates -->
<Property Id="AUTOLAUNCHAPP" Secure="yes" />
<!-- Property to forward cli args to the launched app to not lose those of the pre-update instance -->
<Property Id="LAUNCHAPPARGS" Secure="yes" />
<!-- Keep a running instance from holding the files this install replaces. AUTOLAUNCHAPP is only
passed by the in-app updater, which has already run the app's exit cleanup, so a leftover
instance is terminated there. A manual install only gets a polite close request and never
kills an instance the user may still be using.
Newer tauri-plugin-updater versions only pass AUTOLAUNCHAPP while restart_after_install is
enabled (the default). If that is ever turned off, an update install falls back to the polite
close request and nothing is terminated. -->
<util:CloseApplication Id="TerminateAppForUpdate" Target="cc-switch.exe"
CloseMessage="no" TerminateProcess="0" RebootPrompt="no">AUTOLAUNCHAPP</util:CloseApplication>
<util:CloseApplication Id="CloseAppForInstall" Target="cc-switch.exe"
CloseMessage="yes" RebootPrompt="no">NOT AUTOLAUNCHAPP</util:CloseApplication>
<!-- Whether the desktop shortcut is still there before this install runs (AppSearch runs before the old version is removed) -->
<Property Id="DESKTOPSHORTCUTEXISTS">
<DirectorySearch Id="DesktopShortcutDirSearch" Path="[DesktopFolder]" Depth="0">