//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() }