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

250 lines
8.5 KiB
Go

//go:build windows
package app
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
func TestBuildWindowsLaunchCommandHidesConsoleWindow(t *testing.T) {
cmd := buildWindowsLaunchCommand(
`C:\tmp\gonavi-update\update.ps1`,
windowsUpdateLaunchContext{StagedDir: `C:\tmp\gonavi-update`},
)
if cmd.SysProcAttr == nil {
t.Fatalf("expected Windows update launcher to configure SysProcAttr")
}
if !cmd.SysProcAttr.HideWindow {
t.Fatalf("expected Windows update launcher to hide the console window")
}
if cmd.SysProcAttr.CreationFlags&windowsCreateNoWindow == 0 {
t.Fatalf("expected Windows update launcher to set CREATE_NO_WINDOW, flags=%#x", cmd.SysProcAttr.CreationFlags)
}
}
func TestFindOtherWindowsUpdateInstancesMatchesExecutablePath(t *testing.T) {
executable, err := os.Executable()
if err != nil {
t.Fatalf("os.Executable returned error: %v", err)
}
instances, err := findOtherWindowsUpdateInstances([]string{executable}, -1)
if err != nil {
t.Fatalf("findOtherWindowsUpdateInstances returned error: %v", err)
}
for _, instance := range instances {
if instance.PID == uint32(os.Getpid()) {
return
}
}
t.Fatalf("expected current executable process %d to be detected, got %#v", os.Getpid(), instances)
}
func TestCloseWindowsUpdateInstancesTerminatesProcessesWithoutWindows(t *testing.T) {
helperPath := filepath.Join(t.TempDir(), "GoNavi.exe")
build := exec.Command("go", "build", "-ldflags=-H=windowsgui", "-o", helperPath, "./testdata/windows_update_helper")
if output, err := build.CombinedOutput(); err != nil {
t.Fatalf("build update helper: %v\n%s", err, output)
}
command := exec.Command(helperPath)
if err := command.Start(); err != nil {
t.Fatalf("start update helper: %v", err)
}
t.Cleanup(func() {
if command.Process != nil {
_ = command.Process.Kill()
}
})
time.Sleep(150 * time.Millisecond)
process := windowsUpdateProcess{PID: uint32(command.Process.Pid), Executable: helperPath}
if err := closeWindowsUpdateInstances([]windowsUpdateProcess{process}); err != nil {
t.Fatalf("closeWindowsUpdateInstances returned error: %v", err)
}
_ = command.Wait()
instances, err := findOtherWindowsUpdateInstances([]string{helperPath}, -1)
if err != nil {
t.Fatalf("findOtherWindowsUpdateInstances after close: %v", err)
}
if len(instances) != 0 {
t.Fatalf("helper still running after close: %#v", instances)
}
}
func TestCloseWindowsUpdateInstancesRejectsChangedExecutableIdentity(t *testing.T) {
helperPath := filepath.Join(t.TempDir(), "GoNavi.exe")
build := exec.Command("go", "build", "-ldflags=-H=windowsgui", "-o", helperPath, "./testdata/windows_update_helper")
if output, err := build.CombinedOutput(); err != nil {
t.Fatalf("build update helper: %v\n%s", err, output)
}
command := exec.Command(helperPath)
if err := command.Start(); err != nil {
t.Fatalf("start update helper: %v", err)
}
t.Cleanup(func() {
if command.Process != nil {
_ = command.Process.Kill()
_, _ = command.Process.Wait()
}
})
time.Sleep(150 * time.Millisecond)
staleIdentity := windowsUpdateProcess{
PID: uint32(command.Process.Pid),
Executable: filepath.Join(filepath.Dir(helperPath), "Different.exe"),
}
err := closeWindowsUpdateInstances([]windowsUpdateProcess{staleIdentity})
if err == nil || !strings.Contains(err.Error(), "executable changed") {
t.Fatalf("identity mismatch error = %v, want executable changed", err)
}
instances, findErr := findOtherWindowsUpdateInstances([]string{helperPath}, -1)
if findErr != nil {
t.Fatalf("find helper after rejected close: %v", findErr)
}
if len(instances) != 1 || instances[0].PID != uint32(command.Process.Pid) {
t.Fatalf("helper was not preserved after identity mismatch: %#v", instances)
}
}
func TestInstallUpdateAndRestartClosesOtherTargetInstances(t *testing.T) {
dir := t.TempDir()
currentTarget := filepath.Join(dir, "GoNavi-dev-old-Windows-Amd64-Portable.exe")
newTarget := filepath.Join(dir, "GoNavi-dev-new-Windows-Amd64-Portable.exe")
if err := os.WriteFile(newTarget, []byte("12345678"), 0o644); err != nil {
t.Fatalf("WriteFile returned error: %v", err)
}
app := NewApp()
app.SetLanguage("en-US")
app.updateState.staged = &stagedUpdate{
Channel: updateChannelDev,
Version: "dev-new",
AssetName: filepath.Base(newTarget),
FilePath: newTarget,
StagedDir: filepath.Join(dir, ".gonavi-update-windows-dev-new"),
InstallMode: updateInstallModePortable,
PackageType: updatePackageTypePortable,
AutoRelaunch: true,
}
originalResolveInstallTarget := updateResolveInstallTarget
originalFindOtherInstances := updateFindOtherWindowsInstances
originalCloseInstances := updateCloseWindowsInstances
originalAcquireMaintenance := updateAcquireWindowsMaintenance
originalLaunchInstallScript := updateLaunchInstallScript
originalQuitSleep := updateQuitSleep
originalExitProcess := updateExitProcess
t.Cleanup(func() {
updateResolveInstallTarget = originalResolveInstallTarget
updateFindOtherWindowsInstances = originalFindOtherInstances
updateCloseWindowsInstances = originalCloseInstances
updateAcquireWindowsMaintenance = originalAcquireMaintenance
updateLaunchInstallScript = originalLaunchInstallScript
updateQuitSleep = originalQuitSleep
updateExitProcess = originalExitProcess
})
updateResolveInstallTarget = func() string { return currentTarget }
updateAcquireWindowsMaintenance = func(string) (windowsUpdateMaintenanceLease, error) {
return windowsUpdateMaintenanceLease{Name: `Global\GoNavi-Update-Test`}, nil
}
var checkedTargets []string
findCalls := 0
updateFindOtherWindowsInstances = func(targets []string, currentPID int) ([]windowsUpdateProcess, error) {
findCalls++
checkedTargets = append([]string(nil), targets...)
if currentPID != os.Getpid() {
t.Fatalf("current PID = %d, want %d", currentPID, os.Getpid())
}
if findCalls == 1 {
return []windowsUpdateProcess{{PID: 4321, Executable: newTarget}}, nil
}
return nil, nil
}
closed := false
updateCloseWindowsInstances = func(processes []windowsUpdateProcess) error {
closed = len(processes) == 1 && processes[0].PID == 4321 && processes[0].Executable == newTarget
return nil
}
launched := false
updateLaunchInstallScript = func(*stagedUpdate) error {
launched = true
return nil
}
quitFinished := make(chan struct{}, 1)
updateQuitSleep = func(time.Duration) {}
updateExitProcess = func(int) { quitFinished <- struct{}{} }
result := app.InstallUpdateAndRestart(true)
if !result.Success {
t.Fatalf("expected confirmed update to close other instances and launch, got %#v", result)
}
if !closed {
t.Fatal("confirmed update did not close the discovered GoNavi instance")
}
if !launched {
t.Fatal("update launcher did not start after other instances closed")
}
if len(checkedTargets) != 2 || checkedTargets[0] != currentTarget || checkedTargets[1] != newTarget {
t.Fatalf("checked targets = %#v, want current and final target", checkedTargets)
}
if findCalls != 2 {
t.Fatalf("find calls = %d, want discovery and post-close verification", findCalls)
}
select {
case <-quitFinished:
case <-time.After(time.Second):
t.Fatal("timed out waiting for updater-controlled quit goroutine")
}
}
func TestInstallUpdateAndRestartRequiresCloseConfirmationOnWindows(t *testing.T) {
dir := t.TempDir()
packagePath := filepath.Join(dir, "GoNavi-Installer.msi")
if err := os.WriteFile(packagePath, []byte("fake msi"), 0o644); err != nil {
t.Fatalf("WriteFile MSI: %v", err)
}
app := NewApp()
app.SetLanguage("en-US")
app.updateState.staged = &stagedUpdate{
Version: "1.2.3",
AssetName: filepath.Base(packagePath),
FilePath: packagePath,
StagedDir: dir,
InstallMode: updateInstallModeMSI,
PackageType: updatePackageTypeMSI,
AutoRelaunch: true,
InstallLogPath: filepath.Join(dir, "update.log"),
}
originalResolveMode := updateResolveInstallMode
originalLaunch := updateLaunchInstallScript
t.Cleanup(func() {
updateResolveInstallMode = originalResolveMode
updateLaunchInstallScript = originalLaunch
})
updateResolveInstallMode = func() updateInstallMode { return updateInstallModeMSI }
launched := false
updateLaunchInstallScript = func(*stagedUpdate) error {
launched = true
return nil
}
result := app.InstallUpdateAndRestart(false)
if result.Success {
t.Fatalf("update without close confirmation unexpectedly succeeded: %#v", result)
}
if launched {
t.Fatal("update launcher must not start before close-all confirmation")
}
if !strings.Contains(result.Message, "current GoNavi installation") {
t.Fatalf("missing close confirmation message: %q", result.Message)
}
}