Fix Windows auto-update to replace Navis.exe in place (1.8.1).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,8 +18,7 @@ import (
|
||||
|
||||
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).
|
||||
// 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 {
|
||||
@@ -30,24 +29,34 @@ func Apply(ctx context.Context, manifestURL string) (string, error) {
|
||||
if err := os.Rename(tmp, pending); err != nil {
|
||||
if err2 := copyFile(tmp, pending); err2 != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", err2
|
||||
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 createNoWindow = 0x08000000
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
|
||||
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>
|
||||
// Returns true if this process was the updater and should exit.
|
||||
// Replaces targetExe in-place with this binary, then relaunches it.
|
||||
func MaybeFinishUpdate(args []string) bool {
|
||||
if len(args) < 3 || args[0] != finishUpdateFlag {
|
||||
return false
|
||||
@@ -64,21 +73,18 @@ func MaybeFinishUpdate(args []string) bool {
|
||||
self, _ = filepath.Abs(self)
|
||||
target, _ = filepath.Abs(target)
|
||||
|
||||
waitPIDExit(uint32(pid), 90*time.Second)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
waitPIDExit(uint32(pid), 120*time.Second)
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
|
||||
backup := target + ".bak"
|
||||
_ = os.Remove(backup)
|
||||
if err := os.Rename(target, backup); err != nil {
|
||||
if err := replaceExecutable(self, target); err != nil {
|
||||
time.Sleep(2 * time.Second)
|
||||
_ = os.Rename(target, backup)
|
||||
if err2 := replaceExecutable(self, target); err2 != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if err := copyFile(self, target); err != nil {
|
||||
_ = os.Rename(backup, target)
|
||||
return true
|
||||
}
|
||||
_ = os.Chmod(target, 0o755)
|
||||
_ = os.Remove(backup)
|
||||
|
||||
_ = os.Remove(target + ".bak")
|
||||
_ = os.Remove(target + ".new")
|
||||
_ = os.Remove(self)
|
||||
|
||||
cmd := exec.Command(target)
|
||||
@@ -87,6 +93,73 @@ func MaybeFinishUpdate(args []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user