Files
magpie/internal/update/install_windows_test.go
yetone 88cccc184e update: on Windows, an exe something holds open for a moment still goes aside for the new version, and one held for good says why in full, with the release page a click away (freecss on Discord: "0.1.628 已下载 · couldn't move magpie-windows-amd64.exe aside to put the new version in: rename C:\Us…") — InstallBinary renamed the running exe to .old once and gave up on the first error, which Go prints as "rename <exe> <exe>.old: <reason>", two full paths ahead of the reason, and the version row cut it off on one line, so what failed couldn't be read. A leftover .old was already handled (RemoveOld, then .old-2…, TestInstallBinaryBesideARunningOld), and Windows renames a running image (it is mapped with FILE_SHARE_DELETE), so the likely cause is another program holding the exe open without delete sharing — an antivirus scan, OneDrive or a similar sync client — or a folder that refuses the change (access denied); which one isn't known until the full message is read. moveAside now tries again for about 3 s (0.1, 0.2, 0.4, 0.8, 1.5 s apart), clearing a read-only attribute after the first failure, with a fresh .old name each try; a missing exe isn't retried. When it still can't, the message is the reason without the paths and what to do: ERROR_SHARING_VIOLATION/ERROR_LOCK_VIOLATION says another program has it open (often an antivirus or OneDrive), access denied names the folder Windows won't let magpie change, anything else says to download the new version; the download is kept for another try, as before. The version row wraps a failed install's or download's reason (.sub.wraps) with the whole text as its tooltip, and a ready-but-failed install offers Download, which opens the release page. The Windows test box wasn't reachable from this session (ssh key refused), so nothing ran on Windows; TestInstallBinaryHeldOpen (windows-only: an exe held open without FILE_SHARE_DELETE for 0.5 s still installs; held for good, the error says another program has it open and the download is kept) was cross-compiled with GOOS=windows go test -c but not run. TestMoveAsideTriesAgain and TestMoveAsideSaysWhy (portable, with a fake rename) and the new update-check.test.cjs case (webkit, en and zh: the reason is not cut off, its tooltip is the full text, Download opens the release page, a later state is one line again) fail without the change; Chromium couldn't launch here (its Playwright cache has no ICU data). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet, go test -tags nogui ./... pass (TestFXInState failed once on a TempDir cleanup race, passes on the rerun; untouched).
2026-10-02 13:26:54 +08:00

94 lines
2.9 KiB
Go

package update
import (
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
)
// What the last update moved aside can still be running (a `magpie serve`
// started before it), and then can't be removed or replaced: the running
// exe goes beside it and the new version still goes in.
func TestInstallBinaryBesideARunningOld(t *testing.T) {
ping := filepath.Join(os.Getenv("SystemRoot"), "System32", "PING.EXE")
b, err := os.ReadFile(ping)
if err != nil {
t.Skip("no PING.EXE:", err)
}
dir := t.TempDir()
exe := filepath.Join(dir, "magpie.exe")
for _, f := range []string{exe, exe + ".old"} {
if err := os.WriteFile(f, b, 0o755); err != nil {
t.Fatal(err)
}
cmd := exec.Command(f, "-n", "30", "127.0.0.1")
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { cmd.Process.Kill(); cmd.Wait() })
}
if os.Remove(exe+".old") == nil {
t.Fatal("a running .old could be removed; the test proves nothing")
}
staged := exe + ".new"
os.WriteFile(staged, []byte("new version"), 0o755)
if err := InstallBinary(staged, exe); err != nil {
t.Fatal(err)
}
if got, _ := os.ReadFile(exe); string(got) != "new version" {
t.Fatalf("exe is not the new version: %d bytes", len(got))
}
if _, err := os.Stat(exe + ".old-2"); err != nil {
t.Fatal("the running exe was not moved beside the old:", err)
}
}
// holdOpen opens path as an antivirus scan does: reading, without letting
// it be renamed or removed (no FILE_SHARE_DELETE).
func holdOpen(t *testing.T, path string) syscall.Handle {
p, _ := syscall.UTF16PtrFromString(path)
h, err := syscall.CreateFile(p, syscall.GENERIC_READ, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE, nil, syscall.OPEN_EXISTING, 0, 0)
if err != nil {
t.Fatal(err)
}
return h
}
// Held open for a moment, the exe still goes aside and the new version in;
// held for good, the update says what holds it up and keeps the download.
func TestInstallBinaryHeldOpen(t *testing.T) {
dir := t.TempDir()
exe := filepath.Join(dir, "magpie-windows-amd64.exe")
staged := exe + ".new"
os.WriteFile(exe, []byte("old version"), 0o755)
os.WriteFile(staged, []byte("new version"), 0o755)
h := holdOpen(t, exe)
if os.Rename(exe, exe+".x") == nil {
t.Fatal("a held exe could be renamed; the test proves nothing")
}
go func() { time.Sleep(500 * time.Millisecond); syscall.CloseHandle(h) }()
if err := InstallBinary(staged, exe); err != nil {
t.Fatal(err)
}
if got, _ := os.ReadFile(exe); string(got) != "new version" {
t.Fatalf("exe is %q", got)
}
os.WriteFile(staged, []byte("newer version"), 0o755)
h = holdOpen(t, exe)
defer syscall.CloseHandle(h)
err := InstallBinary(staged, exe)
if err == nil || !strings.Contains(err.Error(), "another program has it open") {
t.Fatalf("held for good: %v", err)
}
t.Log(err)
if _, err := os.Stat(staged); err != nil {
t.Fatal("the download was not kept for another try:", err)
}
}