updater.go

The Wails binding + binary swap.

9 minhard

The Grit scaffold ships an updater that polls GitHub for new releases, downloads the new .exe to %TEMP%, then swaps it with the running binary on user confirmation. This lesson covers the Go side; next lesson covers the React modal.

The swap mechanism — the Windows .exe rename trick

Binary-swap sequence (Windows)

┌─────────────────────────────────────────────────┐ │ │ │ 1. Download new v1.2.1.exe to %TEMP% │ │ │ │ C:/Users/.../%TEMP%/FieldPOS-v1.2.1.exe │ │ │ │ 2. Rename running .exe out of the way │ │ │ │ FieldPOS.exe ──► FieldPOS.exe.old │ │ │ │ (Windows allows this even while running) │ │ │ │ 3. Move new .exe into place │ │ │ │ %TEMP%/FieldPOS-v1.2.1.exe │ │ ──► FieldPOS.exe │ │ │ │ 4. Spawn the new binary, exit the old │ │ │ │ cmd.Start(FieldPOS.exe) │ │ runtime.Quit(ctx) │ │ │ │ 5. Next startup: delete FieldPOS.exe.old │ │ │ │ go updater.CleanupOldOnStartup() │ │ │ └─────────────────────────────────────────────────┘
You can't overwrite a running .exe — but you CAN rename it. Windows keeps the kernel object alive while the directory entry moves.

The Updater struct

updater.go (scaffolded)
type Updater struct {
ctx context.Context
GithubOwner string
GithubRepo string
mu sync.RWMutex
status string // idle | downloading | downloaded | installing | error
target string // path to the downloaded .exe
version string
errMsg string
bytesDownloaded atomic.Int64
bytesTotal atomic.Int64
}
// Bound to React — kicks off the download
func (u *Updater) StartDownload(url, version string) error { ... }
// Polled by React for the progress bar
func (u *Updater) GetProgress() UpdaterState { ... }
// Final user action — swap + relaunch
func (u *Updater) InstallAndRestart() error { ... }

The check + download

func (u *Updater) CheckForUpdates() (*UpdateRelease, error) {
res, err := http.Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest",
u.GithubOwner, u.GithubRepo))
// … decode JSON, extract version, URLs, release notes
asset := pickAsset(release.Assets, runtime.GOOS, runtime.GOARCH)
return &UpdateRelease{
Version: release.TagName,
DownloadURL: asset.URL,
ReleaseNotes: release.Body,
}, nil
}

Polls every 6 hours, caches the result. On a hit, the React side renders the "update available" banner.

The swap call

func (u *Updater) InstallAndRestart() error {
currentExe, _ := os.Executable()
oldPath := currentExe + ".old"
_ = os.Remove(oldPath)
// Move the running .exe aside
if err := os.Rename(currentExe, oldPath); err != nil {
return err
}
// Move the downloaded .exe into place
if err := copyFile(u.target, currentExe); err != nil {
_ = os.Rename(oldPath, currentExe) // rollback
return err
}
_ = os.Remove(u.target)
// Launch the new version + exit the old
exec.Command(currentExe).Start()
go func() {
time.Sleep(250 * time.Millisecond)
runtime.Quit(u.ctx)
}()
return nil
}
Why copy, not rename, for the install step? %TEMP% is often on a different drive (C:) than %LOCALAPPDATA%. os.Rename across drives fails with EXDEV. copyFile works regardless. The rename trick from step 2 works because both paths are on the same drive.

Cleanup on startup

main.go
func main() {
app := NewApp()
updater := NewUpdater()
go updater.CleanupOldOnStartup() // delete leftover .exe.old, retry 25x
wails.Run(...)
}

Background goroutine, fires once at startup. If the old .exe is still locked (antivirus scanning it), retry 25 times at 200ms intervals. Best-effort.

POSIX (macOS/Linux) is simpler

On Mac/Linux, you CAN overwrite a running binary directly. The OS keeps the running process's inode alive while the file at the path is replaced. The Grit scaffold branches on OS:

if runtime.GOOS == "windows" {
u.swapWindows(currentExe, src)
} else {
copyFile(src, currentExe) // straight overwrite — POSIX magic
}

Quick check

You're testing the auto-updater. Step 2 (rename running .exe) succeeds, but step 3 (copy new .exe into place) fails with 'disk full'. What's the safest behaviour?

Try it

Inspect the scaffolded updater.go in your desktop project:

  1. Find the swapWindows function. List the 4 steps it takes.
  2. Find the rollback line — what conditions trigger it?
  3. In notes.md, write one paragraph explaining why os.Rename(currentExe, oldPath) works even though the .exe is running.

What's next

Updater logic done. Next — the React modal + banner that drive it: progress bar, install button, release notes.

Spot a typo? Have an idea?

Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.

Suggest an improvement on GitHub