Files
M4andCursor 041cbb1250 Release 2.7.3.1: harden proxy recovery, updates and local API.
Restore orphaned system proxy after crash, require update SHA-256, add macOS /api auth token, fix UDP ping false positives, HTTPS-only subscriptions, and keep the UI responsive during connect.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 18:48:54 +03:00

210 lines
4.9 KiB
Go

//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 <pid> <targetExe>
// 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()
}