Release 1.7.1: download-only updates and remove netsh/startup network probes for AV FPs.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -842,8 +842,8 @@
|
||||
updateBtn.addEventListener("click", () => withBusy(async () => {
|
||||
try {
|
||||
setMeta("Скачивание обновления…");
|
||||
await applyUpdate();
|
||||
setMeta("Обновление установлено, перезапуск…", "ok");
|
||||
const msg = await applyUpdate();
|
||||
setMeta(String(msg || "Готово"), "ok");
|
||||
} catch (e) { setMeta(String(e), "err"); }
|
||||
}));
|
||||
|
||||
|
||||
@@ -4,9 +4,7 @@ package sysproxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
@@ -87,7 +85,7 @@ func (c *windowsController) Enable(httpHostPort string) error {
|
||||
if err := notifyInternetSettingsChanged(); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = setWinHTTPProxy(httpHostPort)
|
||||
// Skip netsh winhttp — spawning netsh is a common AV heuristic; WinINET covers browsers.
|
||||
|
||||
c.enabled = true
|
||||
return nil
|
||||
@@ -120,7 +118,6 @@ func (c *windowsController) Disable() error {
|
||||
}
|
||||
|
||||
_ = notifyInternetSettingsChanged()
|
||||
_ = resetWinHTTPProxy()
|
||||
|
||||
c.enabled = false
|
||||
return nil
|
||||
@@ -139,18 +136,3 @@ func notifyInternetSettingsChanged() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setWinHTTPProxy(httpHostPort string) error {
|
||||
cmd := exec.Command("netsh", "winhttp", "set", "proxy", httpHostPort,
|
||||
"bypass-list=localhost;127.*;10.*;192.168.*;<local>")
|
||||
const createNoWindow = 0x08000000
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func resetWinHTTPProxy() error {
|
||||
cmd := exec.Command("netsh", "winhttp", "reset", "proxy")
|
||||
const createNoWindow = 0x08000000
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -17,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// CurrentVersion is the shipped client version.
|
||||
const CurrentVersion = "1.7.0"
|
||||
const CurrentVersion = "1.7.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"
|
||||
@@ -226,51 +225,3 @@ func fileSHA256(path string) (string, error) {
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func prepareDownload(ctx context.Context, manifestURL string) (latest, url, sha, exePath, tmpPath string, err error) {
|
||||
st, err := Check(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")
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", "", "", "", "", err
|
||||
}
|
||||
exe, err = filepath.Abs(exe)
|
||||
if err != nil {
|
||||
return "", "", "", "", "", err
|
||||
}
|
||||
tmp := exe + ".new"
|
||||
if err := downloadFile(ctx, st.URL, tmp); err != nil {
|
||||
return "", "", "", "", "", err
|
||||
}
|
||||
wantSHA := st.SHA256
|
||||
if wantSHA == "" {
|
||||
if man, err := fetchManifest(ctx, manifestURL); err == nil {
|
||||
if _, sha2, ok := man.AssetFor(PlatformKey()); ok {
|
||||
wantSHA = sha2
|
||||
}
|
||||
}
|
||||
}
|
||||
if wantSHA != "" {
|
||||
sum, err := fileSHA256(tmp)
|
||||
if err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", "", "", "", "", err
|
||||
}
|
||||
if !strings.EqualFold(sum, wantSHA) {
|
||||
_ = os.Remove(tmp)
|
||||
return "", "", "", "", "", fmt.Errorf("checksum mismatch")
|
||||
}
|
||||
}
|
||||
return st.Latest, st.URL, wantSHA, exe, tmp, nil
|
||||
}
|
||||
|
||||
@@ -8,43 +8,88 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Apply downloads the new binary and schedules replacement after exit (macOS).
|
||||
// Apply downloads the new binary into ~/Downloads (no silent self-replace).
|
||||
func Apply(ctx context.Context, manifestURL string) (string, error) {
|
||||
latest, _, _, exe, tmp, err := prepareDownload(ctx, manifestURL)
|
||||
st, err := Check(ctx, manifestURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_ = os.Chmod(tmp, 0o755)
|
||||
dir := filepath.Dir(exe)
|
||||
scriptPath := filepath.Join(dir, "navis-update.sh")
|
||||
pid := os.Getpid()
|
||||
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")
|
||||
}
|
||||
|
||||
script := "#!/bin/bash\n" +
|
||||
"set -e\n" +
|
||||
"EXE=" + shellQuote(exe) + "\n" +
|
||||
"NEW=" + shellQuote(tmp) + "\n" +
|
||||
"PID=" + strconv.Itoa(pid) + "\n" +
|
||||
"while kill -0 \"$PID\" 2>/dev/null; do sleep 0.4; done\n" +
|
||||
"sleep 0.5\n" +
|
||||
"mv -f \"$NEW\" \"$EXE\"\n" +
|
||||
"chmod +x \"$EXE\"\n" +
|
||||
"rm -f " + shellQuote(scriptPath) + "\n"
|
||||
|
||||
if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cmd := exec.Command("/bin/bash", scriptPath)
|
||||
cmd.Dir = dir
|
||||
if err := cmd.Start(); err != nil {
|
||||
return "", fmt.Errorf("start updater: %w", err)
|
||||
dir := filepath.Join(home, "Downloads")
|
||||
_ = os.MkdirAll(dir, 0o755)
|
||||
dest := filepath.Join(dir, fmt.Sprintf("Navis-%s-darwin-%s", sanitizeDarwinVer(st.Latest), runtime.GOARCH))
|
||||
tmp := dest + ".part"
|
||||
_ = os.Remove(tmp)
|
||||
_ = os.Remove(dest)
|
||||
|
||||
if err := downloadFile(ctx, st.URL, tmp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return latest, nil
|
||||
if st.SHA256 != "" {
|
||||
sum, err := fileSHA256(tmp)
|
||||
if err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", err
|
||||
}
|
||||
if sum != "" && !eqFold(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(dest, 0o755)
|
||||
_ = exec.Command("open", "-R", dest).Start()
|
||||
return fmt.Sprintf("Скачано v%s:\n%s\nЗакройте Navis и запустите новый файл.", st.Latest, dest), nil
|
||||
}
|
||||
|
||||
func shellQuote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||
func sanitizeDarwinVer(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)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return "update"
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func eqFold(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
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build !windows
|
||||
|
||||
package update
|
||||
|
||||
// MaybeFinishUpdate is Windows-only.
|
||||
func MaybeFinishUpdate(args []string) bool {
|
||||
_ = args
|
||||
return false
|
||||
}
|
||||
@@ -5,116 +5,113 @@ package update
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/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).
|
||||
// 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).
|
||||
func Apply(ctx context.Context, manifestURL string) (string, error) {
|
||||
latest, _, _, exe, tmp, err := prepareDownload(ctx, manifestURL)
|
||||
st, err := Check(ctx, manifestURL)
|
||||
if 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
|
||||
if !st.Available {
|
||||
if st.Error != "" {
|
||||
return "", fmt.Errorf("%s", st.Error)
|
||||
}
|
||||
_ = os.Remove(tmp)
|
||||
return "", fmt.Errorf("обновление не найдено")
|
||||
}
|
||||
if !allowedDownloadURL(st.URL) {
|
||||
return "", fmt.Errorf("URL обновления должен быть на git.evilfox.cc или files.de4ima.uk")
|
||||
}
|
||||
_ = os.Chmod(pending, 0o755)
|
||||
|
||||
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)
|
||||
dir, err := downloadsDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return latest, nil
|
||||
_ = 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 {
|
||||
_ = os.Remove(tmp)
|
||||
return "", err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
_ = revealInExplorer(dest)
|
||||
return fmt.Sprintf("Скачано v%s:\n%s\nЗакройте Navis и запустите новый файл.", st.Latest, dest), 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 {
|
||||
func downloadsDir() (string, error) {
|
||||
if u := os.Getenv("USERPROFILE"); u != "" {
|
||||
return filepath.Join(u, "Downloads"), nil
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, "Downloads"), nil
|
||||
}
|
||||
|
||||
func revealInExplorer(path string) error {
|
||||
path, err := filepath.Abs(path)
|
||||
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)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return "update"
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func equalFoldHex(a, b string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
pid, err := strconv.Atoi(args[1])
|
||||
if err != nil || pid <= 0 {
|
||||
return true
|
||||
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
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user