From 19f851afb08bdcf7c8b2463491b72f553d18c4ae Mon Sep 17 00:00:00 2001 From: root Date: Fri, 1 May 2026 03:21:59 +0300 Subject: [PATCH] Auto-update on startup: prompt + apply + auto-restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Double-clicking drover.exe now silently checks for updates first (8s timeout, ignores network failures). If a newer release is published, a YES/NO message box asks the user; on YES we download, verify SHA256, apply via selfupdate, and re-launch the binary so the user immediately sees the new version's window. NO or no update → straight to the smoke-test window as before. Update errors show an error dialog, then continue. Implementation: - internal/updater is reused as-is — the existing CheckForUpdate + ApplyUpdate API is enough. - autoupdate_windows.go owns the dialog and re-launch logic (golang.org/x/sys/windows for MessageBoxW, os/exec for fresh process). autoupdate_other.go is the no-op stub for Linux CI. - relaunchSelf uses cmd.Start (fire and forget) — no Wait. Current process os.Exit(0) immediately so the OS doesn't keep both generations of drover running. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/drover/autoupdate_other.go | 5 ++ cmd/drover/autoupdate_windows.go | 106 +++++++++++++++++++++++++++++++ cmd/drover/main.go | 8 ++- 3 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 cmd/drover/autoupdate_other.go create mode 100644 cmd/drover/autoupdate_windows.go diff --git a/cmd/drover/autoupdate_other.go b/cmd/drover/autoupdate_other.go new file mode 100644 index 0000000..63d1f63 --- /dev/null +++ b/cmd/drover/autoupdate_other.go @@ -0,0 +1,5 @@ +//go:build !windows + +package main + +func autoUpdateOnStartup() {} diff --git a/cmd/drover/autoupdate_windows.go b/cmd/drover/autoupdate_windows.go new file mode 100644 index 0000000..26ce37c --- /dev/null +++ b/cmd/drover/autoupdate_windows.go @@ -0,0 +1,106 @@ +//go:build windows + +package main + +import ( + "context" + "fmt" + "os" + "os/exec" + "time" + "unsafe" + + "git.okcu.io/root/drover-go/internal/updater" + "golang.org/x/sys/windows" +) + +// autoUpdateOnStartup runs a non-interactive update check whenever drover.exe +// starts as a GUI app (no CLI subcommand). If an update is available, a +// Yes/No message box prompts the user; on Yes we download, verify, apply +// the update, then re-launch the binary so the new version is what they see. +// +// Network failures, server outages, and "no updates available" are silent +// fall-throughs — startup must never block on them. +func autoUpdateOnStartup() { + // Tight timeout — startup, not a long-running task. + ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) + defer cancel() + + src := updater.NewForgejoSource("git.okcu.io", "root", "drover-go", "windows-amd64.exe") + rel, hasUpdate, err := updater.CheckForUpdate(ctx, src, Version) + if err != nil || !hasUpdate { + // Silent: offline, slow network, or already up to date — none of + // these should interrupt the user. + return + } + + if !confirmUpdateDialog(rel) { + return + } + + if err := updater.ApplyUpdate(ctx, rel, nil); err != nil { + errorDialog(fmt.Sprintf("Update failed: %v", err)) + return + } + + if err := relaunchSelf(); err != nil { + errorDialog(fmt.Sprintf("Update applied but re-launch failed: %v\n\nPlease restart drover.exe manually.", err)) + return + } + + // Successfully spawned the new version — exit cleanly so it can take over. + os.Exit(0) +} + +// confirmUpdateDialog asks the user whether to apply the available update. +// Returns true on Yes (IDYES = 6). +func confirmUpdateDialog(rel *updater.Release) bool { + user32 := windows.NewLazySystemDLL("user32.dll") + messageBox := user32.NewProc("MessageBoxW") + + body := fmt.Sprintf( + "A new version is available.\n\n"+ + "Current: v%s\n"+ + "Latest: %s\n\n"+ + "Install it now? Drover-Go will restart automatically.", + Version, rel.TagName, + ) + title := "Drover-Go — Update available" + + bodyW, _ := windows.UTF16PtrFromString(body) + titleW, _ := windows.UTF16PtrFromString(title) + + // MB_YESNO | MB_ICONQUESTION | MB_SETFOREGROUND | MB_TOPMOST | MB_DEFBUTTON1 + const flags = 0x00000004 | 0x00000020 | 0x00010000 | 0x00040000 | 0x00000000 + + r, _, _ := messageBox.Call(0, uintptr(unsafe.Pointer(bodyW)), uintptr(unsafe.Pointer(titleW)), flags) + const IDYES = 6 + return r == IDYES +} + +func errorDialog(msg string) { + user32 := windows.NewLazySystemDLL("user32.dll") + messageBox := user32.NewProc("MessageBoxW") + + bodyW, _ := windows.UTF16PtrFromString(msg) + titleW, _ := windows.UTF16PtrFromString("Drover-Go — Error") + + // MB_OK | MB_ICONERROR | MB_TOPMOST + const flags = 0x00000000 | 0x00000010 | 0x00040000 + + messageBox.Call(0, uintptr(unsafe.Pointer(bodyW)), uintptr(unsafe.Pointer(titleW)), flags) +} + +// relaunchSelf starts a fresh copy of the (now-updated) executable in the +// background and returns. The caller is expected to os.Exit(0) immediately +// after — the OS handles the brief overlap fine, and the new process inherits +// nothing from us beyond the working directory and arguments. +func relaunchSelf() error { + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("locate self: %w", err) + } + cmd := exec.Command(exe, os.Args[1:]...) + cmd.Stdin, cmd.Stdout, cmd.Stderr = nil, nil, nil + return cmd.Start() +} diff --git a/cmd/drover/main.go b/cmd/drover/main.go index 7c246d0..5fec4cc 100644 --- a/cmd/drover/main.go +++ b/cmd/drover/main.go @@ -45,10 +45,12 @@ func newRootCmd() *cobra.Command { Version: fmt.Sprintf("%s (commit %s, built %s)", Version, Commit, BuildDate), SilenceUsage: true, SilenceErrors: false, - // No subcommand and no flags = end-user double-clicked the exe; - // open the smoke-test window instead of dumping CLI help to a - // console they didn't ask for. + // No subcommand and no flags = end-user double-clicked the exe. + // First do a quick update check (silent if no network or already + // current); if an update is available we prompt, apply, and + // re-launch ourselves. Then show the smoke-test window. RunE: func(cmd *cobra.Command, args []string) error { + autoUpdateOnStartup() showTestWindow() return nil },