15495d41ea
Chrome-style silent updater: no prompt, no progress UI, no buttons. On startup we check for updates (8s timeout), and if one is found we download + verify + apply + relaunch — total ~3-5s on a fast connection. The user sees the new version's window instead of the old one, period. Errors that do warrant a dialog (apply failed mid-write, sha256 mismatch, can't relaunch) still surface as a message box so the user knows their copy is on the previous version. 'No update' and network timeouts stay silent. Split timeouts so a 60s download doesn't get killed by the 8s check budget. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
84 lines
2.8 KiB
Go
84 lines
2.8 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 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()
|
|
}
|