//go:build windows package update import ( "context" "fmt" "io" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "time" "golang.org/x/sys/windows" ) const finishUpdateFlag = "--navis-finish-update" // Apply downloads the new build and replaces the running Navis.exe after this process exits. 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 "", fmt.Errorf("prepare updater: %w", err2) } _ = os.Remove(tmp) } _ = os.Chmod(pending, 0o755) _ = os.Remove(exe + ".new") cmd := exec.Command(pending, finishUpdateFlag, strconv.Itoa(os.Getpid()), exe) cmd.Dir = filepath.Dir(exe) const ( createNewProcessGroup = 0x00000200 detachedProcess = 0x00000008 createNoWindow = 0x08000000 ) cmd.SysProcAttr = &syscall.SysProcAttr{ CreationFlags: createNewProcessGroup | detachedProcess | createNoWindow, HideWindow: true, } if err := cmd.Start(); err != nil { _ = os.Remove(pending) return "", fmt.Errorf("start updater: %w", err) } _ = cmd.Process.Release() return latest, nil } // MaybeFinishUpdate handles: Navis-pending.exe --navis-finish-update // Replaces targetExe in-place with this binary, then relaunches it. 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) // Only allow replacing Navis.exe in the same directory as this pending updater. if !safeUpdateTarget(self, target) { return true } waitPIDExit(uint32(pid), 120*time.Second) time.Sleep(400 * time.Millisecond) if err := replaceExecutable(self, target); err != nil { time.Sleep(2 * time.Second) if err2 := replaceExecutable(self, target); err2 != nil { return true } } _ = os.Remove(target + ".bak") _ = os.Remove(target + ".new") _ = os.Remove(self) cmd := exec.Command(target) cmd.Dir = filepath.Dir(target) _ = cmd.Start() return true } func safeUpdateTarget(self, target string) bool { selfDir := filepath.Clean(filepath.Dir(self)) targetDir := filepath.Clean(filepath.Dir(target)) if selfDir != targetDir { return false } base := strings.ToLower(filepath.Base(target)) return base == "navis.exe" } // CleanupStaleDownloads removes leftover update temps from failed/old runs. func CleanupStaleDownloads() { exe, err := os.Executable() if err != nil { return } exe, _ = filepath.Abs(exe) dir := filepath.Dir(exe) base := filepath.Base(exe) _ = os.Remove(exe + ".new") _ = os.Remove(filepath.Join(dir, base+".new")) _ = os.Remove(filepath.Join(dir, "Navis-update.part")) _ = os.Remove(filepath.Join(dir, ".navis-update-download")) } func replaceExecutable(src, dst string) error { backup := dst + ".bak" _ = os.Remove(backup) var last error for i := 0; i < 40; i++ { if err := os.Rename(dst, backup); err == nil { last = nil break } else { last = err time.Sleep(250 * time.Millisecond) } } if last != nil { staged := dst + ".stage" _ = os.Remove(staged) if err := copyFile(src, staged); err != nil { return fmt.Errorf("stage new exe: %w", err) } if err := moveFileReplace(staged, dst); err != nil { _ = os.Remove(staged) return fmt.Errorf("replace locked exe: %w (also: %v)", err, last) } return nil } if err := copyFile(src, dst); err != nil { _ = os.Rename(backup, dst) return err } _ = os.Chmod(dst, 0o755) _ = os.Remove(backup) return nil } func moveFileReplace(src, dst string) error { from, err := windows.UTF16PtrFromString(src) if err != nil { return err } to, err := windows.UTF16PtrFromString(dst) if err != nil { return err } const ( moveFileReplaceExisting = 0x1 moveFileWriteThrough = 0x8 ) return windows.MoveFileEx(from, to, moveFileReplaceExisting|moveFileWriteThrough) } 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() }