diff --git a/cmd/vpnapp/main_windows.go b/cmd/vpnapp/main_windows.go index ec7b8bb..946d3fa 100644 --- a/cmd/vpnapp/main_windows.go +++ b/cmd/vpnapp/main_windows.go @@ -61,6 +61,7 @@ func main() { if update.MaybeFinishUpdate(os.Args[1:]) { return } + update.CleanupStaleDownloads() log.SetFlags(log.LstdFlags | log.Lshortfile) @@ -342,11 +343,9 @@ func (a *app) applyUpdate() (string, error) { return "", err } _ = a.mgr.Disconnect() + // Hard-exit so Windows unlocks Navis.exe; do not wait on webview.Terminate (can hang). go func() { - time.Sleep(400 * time.Millisecond) - if a.webview != nil { - a.webview.Terminate() - } + time.Sleep(300 * time.Millisecond) os.Exit(0) }() return "Устанавливается v" + latest + "… Navis перезапустится.", nil diff --git a/dist/navis-release/Navis.exe b/dist/navis-release/Navis.exe index d105aee..4f910f0 100644 Binary files a/dist/navis-release/Navis.exe and b/dist/navis-release/Navis.exe differ diff --git a/dist/navis-release/darwin-amd64/Navis b/dist/navis-release/darwin-amd64/Navis index 0e451e9..e8c11ed 100644 Binary files a/dist/navis-release/darwin-amd64/Navis and b/dist/navis-release/darwin-amd64/Navis differ diff --git a/dist/navis-release/darwin-arm64/Navis b/dist/navis-release/darwin-arm64/Navis index cb272fc..c259c66 100644 Binary files a/dist/navis-release/darwin-arm64/Navis and b/dist/navis-release/darwin-arm64/Navis differ diff --git a/dist/navis-release/update.json b/dist/navis-release/update.json index 583e942..745cb66 100644 --- a/dist/navis-release/update.json +++ b/dist/navis-release/update.json @@ -1,28 +1,28 @@ { - "version": "1.8.0", - "notes": "Автообновление с перезапуском + AmneziaWG 2.0 (вставка .conf / awg://)", + "version": "1.8.1", + "notes": "Исправлено автообновление: замена текущего Navis.exe на месте без .new", "platform": "windows-amd64", "os": "windows", "arch": "amd64", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", - "sha256": "cc656a2c9c026c1c0f068eb7b05785965d7b7bc9fc42b5b13f0e6e7ada3dde97", + "sha256": "7265ace71fdd58b86c0c9acf8ee05d3f0182623201987c9d119d9d1ab328f500", "mandatory": false, "platforms": { "windows-amd64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", - "sha256": "cc656a2c9c026c1c0f068eb7b05785965d7b7bc9fc42b5b13f0e6e7ada3dde97", + "sha256": "7265ace71fdd58b86c0c9acf8ee05d3f0182623201987c9d119d9d1ab328f500", "os": "windows", "arch": "amd64" }, "darwin-arm64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis", - "sha256": "e7df0d9d7dd6d9ad53768ffb250bedd8c1dcca7ba085847737dd378bf2aadbd6", + "sha256": "9a1f9d99b930cde479f9d28ef37b0f1472ab033f6e9f28da5781617639c17d54", "os": "darwin", "arch": "arm64" }, "darwin-amd64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis", - "sha256": "21583e6085e7d4d1b5800c21f5de356dc0f1c68c9658e58c0bd6557a390325cf", + "sha256": "9c036f9022761a117021af37778ecd293a3fc2b7438eaf26f804bb01ef4943f1", "os": "darwin", "arch": "amd64" } diff --git a/dist/update.json b/dist/update.json index 583e942..745cb66 100644 --- a/dist/update.json +++ b/dist/update.json @@ -1,28 +1,28 @@ { - "version": "1.8.0", - "notes": "Автообновление с перезапуском + AmneziaWG 2.0 (вставка .conf / awg://)", + "version": "1.8.1", + "notes": "Исправлено автообновление: замена текущего Navis.exe на месте без .new", "platform": "windows-amd64", "os": "windows", "arch": "amd64", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", - "sha256": "cc656a2c9c026c1c0f068eb7b05785965d7b7bc9fc42b5b13f0e6e7ada3dde97", + "sha256": "7265ace71fdd58b86c0c9acf8ee05d3f0182623201987c9d119d9d1ab328f500", "mandatory": false, "platforms": { "windows-amd64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", - "sha256": "cc656a2c9c026c1c0f068eb7b05785965d7b7bc9fc42b5b13f0e6e7ada3dde97", + "sha256": "7265ace71fdd58b86c0c9acf8ee05d3f0182623201987c9d119d9d1ab328f500", "os": "windows", "arch": "amd64" }, "darwin-arm64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis", - "sha256": "e7df0d9d7dd6d9ad53768ffb250bedd8c1dcca7ba085847737dd378bf2aadbd6", + "sha256": "9a1f9d99b930cde479f9d28ef37b0f1472ab033f6e9f28da5781617639c17d54", "os": "darwin", "arch": "arm64" }, "darwin-amd64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis", - "sha256": "21583e6085e7d4d1b5800c21f5de356dc0f1c68c9658e58c0bd6557a390325cf", + "sha256": "9c036f9022761a117021af37778ecd293a3fc2b7438eaf26f804bb01ef4943f1", "os": "darwin", "arch": "amd64" } diff --git a/internal/update/update.go b/internal/update/update.go index 0c60853..af6b4ce 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -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 } diff --git a/internal/update/update_darwin.go b/internal/update/update_darwin.go index cd92ee9..0790282 100644 --- a/internal/update/update_darwin.go +++ b/internal/update/update_darwin.go @@ -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")) +} diff --git a/internal/update/update_other.go b/internal/update/update_other.go index c7e4a33..2ec2e5b 100644 --- a/internal/update/update_other.go +++ b/internal/update/update_other.go @@ -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() {} diff --git a/internal/update/update_windows.go b/internal/update/update_windows.go index 4879bd1..cf11548 100644 --- a/internal/update/update_windows.go +++ b/internal/update/update_windows.go @@ -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 -// 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) diff --git a/server/update.json b/server/update.json index 583e942..745cb66 100644 --- a/server/update.json +++ b/server/update.json @@ -1,28 +1,28 @@ { - "version": "1.8.0", - "notes": "Автообновление с перезапуском + AmneziaWG 2.0 (вставка .conf / awg://)", + "version": "1.8.1", + "notes": "Исправлено автообновление: замена текущего Navis.exe на месте без .new", "platform": "windows-amd64", "os": "windows", "arch": "amd64", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", - "sha256": "cc656a2c9c026c1c0f068eb7b05785965d7b7bc9fc42b5b13f0e6e7ada3dde97", + "sha256": "7265ace71fdd58b86c0c9acf8ee05d3f0182623201987c9d119d9d1ab328f500", "mandatory": false, "platforms": { "windows-amd64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", - "sha256": "cc656a2c9c026c1c0f068eb7b05785965d7b7bc9fc42b5b13f0e6e7ada3dde97", + "sha256": "7265ace71fdd58b86c0c9acf8ee05d3f0182623201987c9d119d9d1ab328f500", "os": "windows", "arch": "amd64" }, "darwin-arm64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis", - "sha256": "e7df0d9d7dd6d9ad53768ffb250bedd8c1dcca7ba085847737dd378bf2aadbd6", + "sha256": "9a1f9d99b930cde479f9d28ef37b0f1472ab033f6e9f28da5781617639c17d54", "os": "darwin", "arch": "arm64" }, "darwin-amd64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis", - "sha256": "21583e6085e7d4d1b5800c21f5de356dc0f1c68c9658e58c0bd6557a390325cf", + "sha256": "9c036f9022761a117021af37778ecd293a3fc2b7438eaf26f804bb01ef4943f1", "os": "darwin", "arch": "amd64" } diff --git a/versioninfo.json b/versioninfo.json index 44d9888..d5aa168 100644 --- a/versioninfo.json +++ b/versioninfo.json @@ -1,7 +1,7 @@ { "FixedFileInfo": { - "FileVersion": { "Major": 1, "Minor": 8, "Patch": 0, "Build": 0 }, - "ProductVersion": { "Major": 1, "Minor": 8, "Patch": 0, "Build": 0 }, + "FileVersion": { "Major": 1, "Minor": 8, "Patch": 1, "Build": 0 }, + "ProductVersion": { "Major": 1, "Minor": 8, "Patch": 1, "Build": 0 }, "FileFlagsMask": "3f", "FileFlags": "00", "FileOS": "40004", @@ -11,12 +11,12 @@ "StringFileInfo": { "CompanyName": "EvilFox", "FileDescription": "Navis VPN client (NaiveProxy / Hysteria 2 / AmneziaWG 2.0)", - "FileVersion": "1.8.0.0", + "FileVersion": "1.8.1.0", "InternalName": "Navis", "LegalCopyright": "Copyright (c) EvilFox", "OriginalFilename": "Navis.exe", "ProductName": "Navis", - "ProductVersion": "1.8.0.0", + "ProductVersion": "1.8.1.0", "Comments": "Open-source VPN/proxy client. https://evilfox.win/" }, "VarFileInfo": {