Release 1.8.0: auto-update with restart and AmneziaWG 2.0 support.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-07-29 06:45:57 +03:00
co-authored by Cursor
parent cd0b3f4707
commit 672979be4c
27 changed files with 1396 additions and 228 deletions
+85 -82
View File
@@ -5,113 +5,116 @@ package update
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"syscall"
"time"
"golang.org/x/sys/windows"
)
// Apply downloads the new build into the user Downloads folder.
// It does NOT replace the running exe or spawn a self-updater (AV false-positive pattern).
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).
func Apply(ctx context.Context, manifestURL string) (string, error) {
st, err := Check(ctx, manifestURL)
latest, _, _, exe, tmp, err := prepareDownload(ctx, manifestURL)
if err != nil {
return "", err
}
if !st.Available {
if st.Error != "" {
return "", fmt.Errorf("%s", st.Error)
}
return "", fmt.Errorf("обновление не найдено")
}
if !allowedDownloadURL(st.URL) {
return "", fmt.Errorf("URL обновления должен быть на git.evilfox.cc или files.de4ima.uk")
}
dir, err := downloadsDir()
if err != nil {
return "", err
}
_ = os.MkdirAll(dir, 0o755)
dest := filepath.Join(dir, fmt.Sprintf("Navis-%s-windows-amd64.exe", sanitizeFileVer(st.Latest)))
tmp := dest + ".part"
_ = os.Remove(tmp)
_ = os.Remove(dest)
if err := downloadFile(ctx, st.URL, tmp); err != nil {
return "", err
}
if st.SHA256 != "" {
sum, err := fileSHA256(tmp)
if err != nil {
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 "", err
return "", err2
}
if !equalFoldHex(sum, st.SHA256) {
_ = os.Remove(tmp)
return "", fmt.Errorf("checksum mismatch")
}
}
if err := os.Rename(tmp, dest); err != nil {
_ = os.Remove(tmp)
return "", err
}
_ = os.Chmod(pending, 0o755)
_ = revealInExplorer(dest)
return fmt.Sprintf("Скачано v%s:\n%s\nЗакройте Navis и запустите новый файл.", st.Latest, dest), nil
cmd := exec.Command(pending, finishUpdateFlag, strconv.Itoa(os.Getpid()), exe)
cmd.Dir = filepath.Dir(exe)
const createNoWindow = 0x08000000
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
if err := cmd.Start(); err != nil {
return "", fmt.Errorf("start updater: %w", err)
}
return latest, nil
}
func downloadsDir() (string, error) {
if u := os.Getenv("USERPROFILE"); u != "" {
return filepath.Join(u, "Downloads"), nil
// MaybeFinishUpdate handles: Navis-pending.exe --navis-finish-update <pid> <targetExe>
// Returns true if this process was the updater and should exit.
func MaybeFinishUpdate(args []string) bool {
if len(args) < 3 || args[0] != finishUpdateFlag {
return false
}
home, err := os.UserHomeDir()
pid, err := strconv.Atoi(args[1])
if err != nil || pid <= 0 {
return true
}
target := args[2]
self, err := os.Executable()
if err != nil {
return "", err
return true
}
return filepath.Join(home, "Downloads"), nil
self, _ = filepath.Abs(self)
target, _ = filepath.Abs(target)
waitPIDExit(uint32(pid), 90*time.Second)
time.Sleep(500 * time.Millisecond)
backup := target + ".bak"
_ = os.Remove(backup)
if err := os.Rename(target, backup); err != nil {
time.Sleep(2 * time.Second)
_ = os.Rename(target, backup)
}
if err := copyFile(self, target); err != nil {
_ = os.Rename(backup, target)
return true
}
_ = os.Chmod(target, 0o755)
_ = os.Remove(backup)
_ = os.Remove(self)
cmd := exec.Command(target)
cmd.Dir = filepath.Dir(target)
_ = cmd.Start()
return true
}
func revealInExplorer(path string) error {
path, err := filepath.Abs(path)
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
}
cmd := exec.Command("explorer.exe", "/select,", path)
const createNoWindow = 0x08000000
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
return cmd.Start()
}
func sanitizeFileVer(v string) string {
out := make([]rune, 0, len(v))
for _, r := range v {
switch {
case r >= '0' && r <= '9', r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r == '.', r == '-':
out = append(out, r)
}
defer in.Close()
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
if err != nil {
return err
}
if len(out) == 0 {
return "update"
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return string(out)
}
func equalFoldHex(a, b string) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
ca, cb := a[i], b[i]
if ca >= 'A' && ca <= 'F' {
ca += 'a' - 'A'
}
if cb >= 'A' && cb <= 'F' {
cb += 'a' - 'A'
}
if ca != cb {
return false
}
}
return true
return out.Close()
}