74 lines
2.1 KiB
Go
74 lines
2.1 KiB
Go
//go:build windows
|
|
|
|
package updater
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// helperScript is rendered with fmt.Sprintf, args order:
|
|
//
|
|
// %[1]s — running binary path (source for the .old copy)
|
|
// %[2]s — .old path
|
|
// %[3]s — staged .new path
|
|
// %[4]s — running binary path (rename target)
|
|
const helperScript = `@echo off
|
|
timeout /t 3 /nobreak >nul
|
|
copy /Y "%[1]s" "%[2]s"
|
|
sc stop restic-manager-agent
|
|
:wait
|
|
sc query restic-manager-agent | find "STOPPED" >nul
|
|
if errorlevel 1 (timeout /t 1 /nobreak >nul & goto wait)
|
|
move /Y "%[3]s" "%[4]s"
|
|
sc start restic-manager-agent
|
|
del "%%~f0"
|
|
`
|
|
|
|
// Update on Windows can't overwrite the running .exe in-process
|
|
// (exclusive file lock), so we stage the new binary, write a small
|
|
// detached helper script that waits, stops the service, swaps the
|
|
// binary, and starts the service, then exit cleanly. SCM treats
|
|
// clean exits after sc stop as intentional and does not auto-restart;
|
|
// the helper's final sc start handles that.
|
|
func Update(ctx context.Context, serverURL string) error {
|
|
binPath, err := resolveOwnBinary()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stage, err := fetch(ctx, serverURL, binPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
helperPath := filepath.Join(filepath.Dir(binPath), "agent-update.cmd")
|
|
body := fmt.Sprintf(helperScript, binPath, binPath+".old", stage, binPath)
|
|
if err := os.WriteFile(helperPath, []byte(body), 0o755); err != nil {
|
|
return err
|
|
}
|
|
cmd := exec.Command("cmd.exe", "/c", helperPath)
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
HideWindow: true,
|
|
CreationFlags: 0x00000008 | 0x08000000, // DETACHED_PROCESS | CREATE_NO_WINDOW
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
slog.Info("agent self-update: helper spawned, exiting cleanly",
|
|
"binary", binPath, "helper", helperPath)
|
|
time.Sleep(200 * time.Millisecond)
|
|
os.Exit(0)
|
|
return nil // unreachable
|
|
}
|
|
|
|
// swap is unused on Windows — the helper script does the swap.
|
|
// Defined to satisfy the build (UpdateForTest references it).
|
|
func swap(_, _ string) error {
|
|
return fmt.Errorf("updater.swap not implemented on Windows; use the helper script via Update")
|
|
}
|