Files
MyGoNavi/internal/app/windows_msi_update_script_test.go
Syngnat 60d172254d 🐛 fix(update): 安装前确认并安全关闭当前安装的全部实例
- 安装更新前先确认关闭当前安装目录下的 GoNavi 实例,并继续执行未保存 SQL 退出保护
- 按可执行文件路径枚举并复核进程身份,优雅关闭超时后再强制终止,避免 PID 复用误杀
- 通过全局维护事件阻止更新期间启动新实例,并由 PowerShell 更新器完成跨权限无空窗交接
- 同步更新前端交互、多语言提示、Wails 绑定及 Windows 发布资产校验
2026-07-21 10:55:08 +08:00

124 lines
5.5 KiB
Go

package app
import (
"path/filepath"
"strings"
"testing"
)
func TestBuildWindowsMSIUpdatePowerShellScriptInstallsRelaunchesAndCleans(t *testing.T) {
script := buildWindowsMSIUpdatePowerShellScript()
mustContain := []string{
`function Save-GoNaviDesktopShortcutState`,
`function Remove-GoNaviDesktopShortcutsForTarget`,
`function Restore-GoNaviDesktopShortcutState`,
`function Send-ShellItemUpdatedNotification`,
`SHChangeNotify`,
`function Repair-LegacyGoNaviTaskbarPins`,
`while (Get-Process -Id $HostProcessId -ErrorAction SilentlyContinue)`,
`$DesktopShortcutState = Save-GoNaviDesktopShortcutState -TargetPath $Target -BackupDirectory $StagedDir`,
`if (-not $DesktopShortcutState.Succeeded)`,
`$DesktopShortcutInstallValue = $DesktopShortcutState.InstallValue`,
`Start-Process -FilePath $MSIExecPath -Verb RunAs`,
`'INSTALLFOLDER=' + (Quote-NativeArgument $TargetDir)`,
`'INSTALLDESKTOPSHORTCUT=' + $DesktopShortcutInstallValue`,
`'/passive'`,
`'/norestart'`,
`'/L*v'`,
`$InstallerExitCode -notin @(0, 1641, 3010)`,
`if (-not (Restore-GoNaviDesktopShortcutState -State $DesktopShortcutState -OnlyForeign))`,
`Repair-LegacyGoNaviTaskbarPins -TargetPath $Target`,
`function Release-UpdateMaintenanceLock`,
`[Threading.EventWaitHandle]::OpenExisting($MaintenanceEventName)`,
`[void]$HandoffEvent.Set()`,
`update maintenance lock could not be released before relaunch`,
`Start-Process -FilePath $Target -WorkingDirectory $TargetDir`,
`Remove-UpdateArtifact $Source`,
`MSI package retained for manual install`,
`previous application relaunched after MSI failure`,
}
for _, token := range mustContain {
if !strings.Contains(script, token) {
t.Fatalf("MSI updater missing %q\n%s", token, script)
}
}
if strings.Index(script, `Remove-UpdateArtifact $Source`) < strings.Index(script, `Start-Process -FilePath $Target -WorkingDirectory $TargetDir`) {
t.Fatalf("MSI package must be removed only after relaunch\n%s", script)
}
desktopStateIndex := strings.Index(script, `$DesktopShortcutState = Save-GoNaviDesktopShortcutState -TargetPath $Target -BackupDirectory $StagedDir`)
installerIndex := strings.Index(script, `Start-Process -FilePath $MSIExecPath -Verb RunAs`)
if desktopStateIndex < 0 || desktopStateIndex > installerIndex {
t.Fatalf("desktop shortcut state must be captured before MSI starts\n%s", script)
}
repairIndex := strings.Index(script, `Repair-LegacyGoNaviTaskbarPins -TargetPath $Target`)
releaseIndex := strings.Index(script, `if (-not (Release-UpdateMaintenanceLock))`)
relaunchIndex := strings.Index(script, `Start-Process -FilePath $Target -WorkingDirectory $TargetDir`)
if repairIndex < installerIndex || repairIndex > relaunchIndex {
t.Fatalf("legacy taskbar pins must be repaired after install and before relaunch\n%s", script)
}
if releaseIndex < repairIndex || releaseIndex > relaunchIndex {
t.Fatalf("maintenance lock must be released after install repair and before relaunch\n%s", script)
}
for _, r := range script {
if r > 0x7f {
t.Fatalf("MSI updater must remain ASCII-only, found %q", r)
}
}
}
func TestBuildWindowsMSILaunchCommandPreservesPathsInEnvironment(t *testing.T) {
context := windowsMSIUpdateLaunchContext{
SourcePath: `C:\Users\tester\AppData\Local\GoNavi 100%\GoNavi-Installer.msi`,
TargetPath: `D:\software ! 100% & portable\GoNavi.exe`,
StagedDir: `C:\Users\tester\AppData\Local\GoNavi 100%\stage`,
LogPath: `C:\Users\tester\AppData\Local\GoNavi 100%\stage\update.log`,
MSILogPath: `C:\Users\tester\AppData\Local\GoNavi 100%\stage\msi.log`,
MSIExecPath: `C:\Windows\System32\msiexec.exe`,
MaintenanceEventName: `Global\GoNavi-Update-Test`,
HandoffEventName: `Local\GoNavi-Update-Handoff-Test`,
PID: 12345,
}
cmd := buildWindowsMSILaunchCommand(filepath.Join(context.StagedDir, "update-msi.ps1"), context)
want := map[string]string{
"GONAVI_UPDATE_SOURCE": context.SourcePath,
"GONAVI_UPDATE_TARGET": context.TargetPath,
"GONAVI_UPDATE_STAGED_DIR": context.StagedDir,
"GONAVI_UPDATE_LOG_PATH": context.LogPath,
"GONAVI_UPDATE_MSI_LOG_PATH": context.MSILogPath,
"GONAVI_UPDATE_MSIEXEC_PATH": context.MSIExecPath,
"GONAVI_UPDATE_MAINTENANCE_EVENT_NAME": context.MaintenanceEventName,
"GONAVI_UPDATE_HANDOFF_EVENT_NAME": context.HandoffEventName,
"GONAVI_UPDATE_PID": "12345",
}
got := make(map[string]string, len(want))
for _, item := range cmd.Env {
name, value, ok := strings.Cut(item, "=")
if ok {
if _, exists := want[name]; exists {
got[name] = value
}
}
}
for name, value := range want {
if got[name] != value {
t.Fatalf("environment %s = %q, want %q", name, got[name], value)
}
}
}
func TestResolveWindowsMSIExecPathPrefersExplicitOverride(t *testing.T) {
values := map[string]string{
"GONAVI_UPDATE_MSIEXEC_PATH": `D:\\tools\\fake-msiexec.exe`,
"SystemRoot": `C:\\Windows`,
}
got := resolveWindowsMSIExecPath(func(name string) string { return values[name] })
if got != values["GONAVI_UPDATE_MSIEXEC_PATH"] {
t.Fatalf("msiexec path = %q, want override %q", got, values["GONAVI_UPDATE_MSIEXEC_PATH"])
}
delete(values, "GONAVI_UPDATE_MSIEXEC_PATH")
wantSystem := filepath.Join(values["SystemRoot"], "System32", "msiexec.exe")
if got := resolveWindowsMSIExecPath(func(name string) string { return values[name] }); got != wantSystem {
t.Fatalf("msiexec path = %q, want SystemRoot path %q", got, wantSystem)
}
}