19f851afb0
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) <noreply@anthropic.com>
107 lines
3.3 KiB
Go
107 lines
3.3 KiB
Go
//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()
|
|
}
|