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:
Navis
2026-07-29 06:58:10 +03:00
co-authored by Cursor
parent 672979be4c
commit fce99cc393
12 changed files with 135 additions and 46 deletions
+5 -2
View File
@@ -17,7 +17,7 @@ import (
)
// CurrentVersion is the shipped client version.
const CurrentVersion = "1.8.0"
const CurrentVersion = "1.8.1"
// DefaultManifestURL is the update feed (hosted in the project git repo).
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
@@ -250,7 +250,10 @@ func prepareDownload(ctx context.Context, manifestURL string) (latest, url, sha,
if err != nil {
return "", "", "", "", "", err
}
tmp := exe + ".new"
tmp := filepath.Join(filepath.Dir(exe), ".navis-update-download")
_ = os.Remove(tmp)
// Also clear legacy leftover from older updaters.
_ = os.Remove(exe + ".new")
if err := downloadFile(ctx, st.URL, tmp); err != nil {
return "", "", "", "", "", err
}
+11
View File
@@ -49,3 +49,14 @@ func Apply(ctx context.Context, manifestURL string) (string, error) {
func shellQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
}
// CleanupStaleDownloads removes leftover update temps from failed/old runs.
func CleanupStaleDownloads() {
exe, err := os.Executable()
if err != nil {
return
}
exe, _ = filepath.Abs(exe)
_ = os.Remove(exe + ".new")
_ = os.Remove(filepath.Join(filepath.Dir(exe), ".navis-update-download"))
}
+3
View File
@@ -13,3 +13,6 @@ func Apply(ctx context.Context, manifestURL string) (string, error) {
_ = manifestURL
return "", fmt.Errorf("автообновление пока только для Windows и macOS")
}
// CleanupStaleDownloads is a no-op on unsupported platforms.
func CleanupStaleDownloads() {}
+91 -18
View File
@@ -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)