updater.go
The Wails binding + binary swap.
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)
The Updater struct
type Updater struct {ctx context.ContextGithubOwner stringGithubRepo stringmu sync.RWMutexstatus string // idle | downloading | downloaded | installing | errortarget string // path to the downloaded .exeversion stringerrMsg stringbytesDownloaded atomic.Int64bytesTotal atomic.Int64}// Bound to React — kicks off the downloadfunc (u *Updater) StartDownload(url, version string) error { ... }// Polled by React for the progress barfunc (u *Updater) GetProgress() UpdaterState { ... }// Final user action — swap + relaunchfunc (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 notesasset := 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 asideif err := os.Rename(currentExe, oldPath); err != nil {return err}// Move the downloaded .exe into placeif err := copyFile(u.target, currentExe); err != nil {_ = os.Rename(oldPath, currentExe) // rollbackreturn err}_ = os.Remove(u.target)// Launch the new version + exit the oldexec.Command(currentExe).Start()go func() {time.Sleep(250 * time.Millisecond)runtime.Quit(u.ctx)}()return nil}
%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
func main() {app := NewApp()updater := NewUpdater()go updater.CleanupOldOnStartup() // delete leftover .exe.old, retry 25xwails.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
Try it
Inspect the scaffolded updater.go in your desktop project:
- Find the
swapWindowsfunction. List the 4 steps it takes. - Find the rollback line — what conditions trigger it?
- In
notes.md, write one paragraph explaining whyos.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