Release 1.7.0: reduce AV false positives (no bat updater, no silent core download, app manifest).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-07-28 09:09:56 +03:00
co-authored by Cursor
parent 740517dbfb
commit 29643e2157
22 changed files with 212 additions and 87 deletions
+2 -1
View File
@@ -8,5 +8,6 @@ import (
)
func applySysProcAttr(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
const createNoWindow = 0x08000000
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
}
+3 -1
View File
@@ -8,5 +8,7 @@ import (
)
func applySysProcAttr(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
// CREATE_NO_WINDOW — quieter than HideWindow for console child processes.
const createNoWindow = 0x08000000
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
}
+4 -2
View File
@@ -143,12 +143,14 @@ func notifyInternetSettingsChanged() error {
func setWinHTTPProxy(httpHostPort string) error {
cmd := exec.Command("netsh", "winhttp", "set", "proxy", httpHostPort,
"bypass-list=localhost;127.*;10.*;192.168.*;<local>")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
const createNoWindow = 0x08000000
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
return cmd.Run()
}
func resetWinHTTPProxy() error {
cmd := exec.Command("netsh", "winhttp", "reset", "proxy")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
const createNoWindow = 0x08000000
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
return cmd.Run()
}
+1 -1
View File
@@ -17,7 +17,7 @@ import (
)
// CurrentVersion is the shipped client version.
const CurrentVersion = "1.6.1"
const CurrentVersion = "1.7.0"
// 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"
+9
View File
@@ -0,0 +1,9 @@
//go:build !windows
package update
// MaybeFinishUpdate is Windows-only.
func MaybeFinishUpdate(args []string) bool {
_ = args
return false
}
+95 -25
View File
@@ -5,46 +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 exe and schedules replacement after exit (Windows).
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) {
latest, _, _, exe, tmp, err := prepareDownload(ctx, manifestURL)
if err != nil {
return "", err
}
dir := filepath.Dir(exe)
bat := filepath.Join(dir, "navis-update.bat")
script := "@echo off\r\n" +
"setlocal EnableExtensions\r\n" +
"cd /d \"" + dir + "\"\r\n" +
"set \"EXE=" + exe + "\"\r\n" +
"set \"NEW=" + tmp + "\"\r\n" +
":wait\r\n" +
"ping -n 2 127.0.0.1 >nul\r\n" +
"del /f /q \"%EXE%\" >nul 2>&1\r\n" +
"if exist \"%EXE%\" goto wait\r\n" +
"move /y \"%NEW%\" \"%EXE%\" >nul\r\n" +
"if not exist \"%EXE%\" exit /b 1\r\n" +
"start \"\" \"%EXE%\"\r\n" +
"exit /b 0\r\n"
if err := os.WriteFile(bat, []byte(script), 0o755); 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 "", err2
}
_ = os.Remove(tmp)
}
_ = os.Chmod(pending, 0o755)
cmd := exec.Command("cmd.exe", "/C", bat)
cmd.Dir = dir
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x00000008 | 0x00000200, // DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
}
cmd := exec.Command(pending, finishUpdateFlag, strconv.Itoa(os.Getpid()), exe)
cmd.Dir = filepath.Dir(exe)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false}
if err := cmd.Start(); err != nil {
return "", fmt.Errorf("start updater: %w", err)
}
return latest, 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
}
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)
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 {
// Still locked — wait a bit more.
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 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()
}