|
|
|
@@ -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 <pid> <targetExe>
|
|
|
|
|
// 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)
|
|
|
|
|