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>
210 lines
4.9 KiB
Go
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()
|
|
}
|