121 lines
2.8 KiB
Go
121 lines
2.8 KiB
Go
//go:build windows
|
|
|
|
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).
|
|
func Apply(ctx context.Context, manifestURL string) (string, error) {
|
|
latest, _, _, exe, tmp, err := prepareDownload(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
|
|
}
|
|
_ = os.Remove(tmp)
|
|
}
|
|
_ = os.Chmod(pending, 0o755)
|
|
|
|
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
|
|
}
|
|
|
|
// 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 {
|
|
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()
|
|
}
|