diff --git a/cmd/drover/autoupdate_windows.go b/cmd/drover/autoupdate_windows.go index 26ce37c..4d76e3c 100644 --- a/cmd/drover/autoupdate_windows.go +++ b/cmd/drover/autoupdate_windows.go @@ -14,32 +14,35 @@ import ( "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. +// autoUpdateOnStartup silently checks for and applies updates whenever +// drover.exe starts as a GUI app (no CLI subcommand). Chrome-style: no +// prompt, no progress bar, no questions — if an update is available we +// download, verify, apply, and re-launch. The user just sees the new +// version's window appear instead of the old one. // -// Network failures, server outages, and "no updates available" are silent -// fall-throughs — startup must never block on them. +// Network failures, server outages, slow downloads, and "no updates +// available" are silent fall-throughs — startup must never block on them. +// +// Two split contexts: +// - check (8s) — quick HEAD-equivalent against the releases API +// - apply (60s) — actual download + sha256 + atomic replace 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) + + checkCtx, cancelCheck := context.WithTimeout(context.Background(), 8*time.Second) + rel, hasUpdate, err := updater.CheckForUpdate(checkCtx, src, Version) + cancelCheck() 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)) + applyCtx, cancelApply := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelApply() + if err := updater.ApplyUpdate(applyCtx, rel, nil); err != nil { + // Apply failed — surface this one (sha mismatch, write error, + // disk full are not silent-fail cases). The user can keep using + // the current version after dismissing the dialog. + errorDialog(fmt.Sprintf("Update to %s failed: %v\n\nContinuing on current version.", rel.TagName, err)) return } @@ -48,36 +51,10 @@ func autoUpdateOnStartup() { return } - // Successfully spawned the new version — exit cleanly so it can take over. + // Successfully spawned the new version — exit cleanly so it takes 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")