//go:build windows package update import ( "context" "fmt" "io" "os" "os/exec" "path/filepath" "strconv" "syscall" "time" "golang.org/x/sys/windows" ) const finishUpdateFlag = "--navis-finish-update" // Apply downloads the new exe and starts it to replace the running binary after exit. // Avoids writing .bat self-replacer scripts (common AV false-positive pattern). func Apply(ctx context.Context, manifestURL string) (string, error) { latest, _, _, exe, tmp, err := prepareDownload(ctx, manifestURL) if err != nil { return "", err } pending := filepath.Join(filepath.Dir(exe), "Navis-pending.exe") _ = os.Remove(pending) if err := os.Rename(tmp, pending); err != nil { if err2 := copyFile(tmp, pending); err2 != nil { _ = os.Remove(tmp) return "", err2 } _ = os.Remove(tmp) } _ = os.Chmod(pending, 0o755) cmd := exec.Command(pending, finishUpdateFlag, strconv.Itoa(os.Getpid()), exe) cmd.Dir = filepath.Dir(exe) cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false} if err := cmd.Start(); err != nil { return "", fmt.Errorf("start updater: %w", err) } return latest, nil } // MaybeFinishUpdate handles: Navis-pending.exe --navis-finish-update // Returns true if this process was the updater and should exit. func MaybeFinishUpdate(args []string) bool { if len(args) < 3 || args[0] != finishUpdateFlag { return false } pid, err := strconv.Atoi(args[1]) if err != nil || pid <= 0 { return true } target := args[2] self, err := os.Executable() if err != nil { return true } self, _ = filepath.Abs(self) target, _ = filepath.Abs(target) waitPIDExit(uint32(pid), 90*time.Second) time.Sleep(500 * time.Millisecond) backup := target + ".bak" _ = os.Remove(backup) if err := os.Rename(target, backup); err != nil { // Still locked — wait a bit more. time.Sleep(2 * time.Second) _ = os.Rename(target, backup) } if err := copyFile(self, target); err != nil { _ = os.Rename(backup, target) return true } _ = os.Chmod(target, 0o755) _ = os.Remove(backup) _ = os.Remove(self) cmd := exec.Command(target) cmd.Dir = filepath.Dir(target) _ = cmd.Start() return true } func waitPIDExit(pid uint32, timeout time.Duration) { const synchronize = 0x00100000 h, err := windows.OpenProcess(synchronize, false, pid) if err != nil { time.Sleep(1500 * time.Millisecond) return } defer windows.CloseHandle(h) ms := uint32(timeout / time.Millisecond) if ms == 0 { ms = 1000 } _, _ = windows.WaitForSingleObject(h, ms) } func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755) if err != nil { return err } defer out.Close() if _, err := io.Copy(out, in); err != nil { return err } return out.Close() }