Clear sysproxy on core death; probe+reconnect; subscription prune; Best ignores soft UDP; Windows tray; Android protocol gate; CI workflows. Co-authored-by: Cursor <cursoragent@cursor.com>
127 lines
3.8 KiB
Go
127 lines
3.8 KiB
Go
//go:build darwin
|
|
|
|
package update
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Apply downloads the new binary or .app.zip and schedules replacement + relaunch after exit.
|
|
func Apply(ctx context.Context, manifestURL string) (string, error) {
|
|
p, err := prepareUpdate(ctx, manifestURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if p.Kind == AssetKindAppZip {
|
|
return applyAppZip(p)
|
|
}
|
|
return applyBinary(p)
|
|
}
|
|
|
|
func applyBinary(p preparedUpdate) (string, error) {
|
|
_ = os.Chmod(p.TmpPath, 0o755)
|
|
dir := filepath.Dir(p.ExePath)
|
|
scriptPath := filepath.Join(dir, "navis-update.sh")
|
|
pid := os.Getpid()
|
|
|
|
script := "#!/bin/bash\n" +
|
|
"set -e\n" +
|
|
"EXE=" + shellQuote(p.ExePath) + "\n" +
|
|
"NEW=" + shellQuote(p.TmpPath) + "\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" +
|
|
"exec \"$EXE\"\n"
|
|
|
|
if err := os.WriteFile(scriptPath, []byte(script), 0o755); 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)
|
|
}
|
|
return p.Latest, nil
|
|
}
|
|
|
|
func applyAppZip(p preparedUpdate) (string, error) {
|
|
if p.AppRoot == "" {
|
|
return "", fmt.Errorf("update: empty app root")
|
|
}
|
|
parent := filepath.Dir(p.AppRoot)
|
|
scriptPath := filepath.Join(parent, "navis-update-app.sh")
|
|
pid := os.Getpid()
|
|
identity := strings.TrimSpace(os.Getenv("NAVIS_CODESIGN_IDENTITY"))
|
|
// Never re-sign with ad-hoc "-" after replacing a shipped .app — that strips
|
|
// Developer ID / notarization. Only re-sign when a real identity is set.
|
|
doSign := identity != "" && identity != "-"
|
|
|
|
script := "#!/bin/bash\n" +
|
|
"set -e\n" +
|
|
"APP=" + shellQuote(p.AppRoot) + "\n" +
|
|
"ZIP=" + shellQuote(p.TmpPath) + "\n" +
|
|
"PID=" + strconv.Itoa(pid) + "\n" +
|
|
"PARENT=$(dirname \"$APP\")\n" +
|
|
"while kill -0 \"$PID\" 2>/dev/null; do sleep 0.4; done\n" +
|
|
"sleep 0.5\n" +
|
|
"TMP=$(mktemp -d \"$PARENT/.navis-update-XXXXXX\")\n" +
|
|
"unzip -q \"$ZIP\" -d \"$TMP\"\n" +
|
|
"NEW=$(find \"$TMP\" -maxdepth 3 -name 'Navis.app' -type d | head -n 1)\n" +
|
|
"if [ -z \"$NEW\" ] || [ ! -d \"$NEW\" ]; then echo 'Navis.app missing in zip' >&2; exit 1; fi\n" +
|
|
"rm -rf \"$APP.bak\"\n" +
|
|
"mv \"$APP\" \"$APP.bak\"\n" +
|
|
"mv \"$NEW\" \"$APP\"\n"
|
|
if doSign {
|
|
script += "ID=" + shellQuote(identity) + "\n" +
|
|
"if command -v codesign >/dev/null 2>&1; then\n" +
|
|
" codesign -s \"$ID\" --force --deep --options runtime --timestamp \"$APP\"\n" +
|
|
" codesign --verify --deep --strict \"$APP\"\n" +
|
|
"fi\n"
|
|
}
|
|
script += "xattr -cr \"$APP\" 2>/dev/null || true\n" +
|
|
"rm -rf \"$APP.bak\" \"$TMP\" \"$ZIP\" " + shellQuote(scriptPath) + "\n" +
|
|
"open \"$APP\"\n"
|
|
|
|
if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
cmd := exec.Command("/bin/bash", scriptPath)
|
|
cmd.Dir = parent
|
|
if err := cmd.Start(); err != nil {
|
|
return "", fmt.Errorf("start app updater: %w", err)
|
|
}
|
|
return p.Latest, nil
|
|
}
|
|
|
|
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")
|
|
dir := filepath.Dir(exe)
|
|
_ = os.Remove(filepath.Join(dir, ".navis-update-download"))
|
|
_ = os.Remove(filepath.Join(dir, ".navis-update-app.zip"))
|
|
if root, ok := AppBundleRoot(exe); ok {
|
|
parent := filepath.Dir(root)
|
|
_ = os.Remove(filepath.Join(parent, ".navis-update-app.zip"))
|
|
_ = os.Remove(filepath.Join(parent, "navis-update-app.sh"))
|
|
_ = os.Remove(filepath.Join(parent, "navis-update.sh"))
|
|
}
|
|
}
|