//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 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, 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() { src := updater.NewForgejoSource("git.okcu.io", "root", "drover-go", "windows-amd64.exe") checkCtx, cancelCheck := context.WithTimeout(context.Background(), 8*time.Second) rel, hasUpdate, err := updater.CheckForUpdate(checkCtx, src, Version) cancelCheck() if err != nil || !hasUpdate { return } 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 } 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 takes over. os.Exit(0) } 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() }