Files
drover-go/cmd/drover/autoupdate_windows.go
T
root 15495d41ea
Build / test (push) Successful in 1m14s
Build / build-windows (push) Successful in 57s
Release / release (push) Successful in 2m34s
auto-update: drop confirmation dialog, do it silently
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>
2026-05-01 03:58:49 +03:00

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