Skip to content

razvandimescu/go-launcher

Repository files navigation

go-launcher

Go Reference CI Go Report Card License: MIT

The Squirrel.Windows pattern for Go. External supervisor with versioned directories, crash-based rollback, and zero-dependency child integration.

Website | Go Docs

Native macOS splash screen with animated spinner, progress bar, and status text
Built-in native splash screen (macOS Cocoa/AppKit) -- no competing Go update library offers this.

The Problem

Every Go auto-update library uses the same approach: self-surgery -- the running binary replaces itself on disk, then restarts. If the new version crashes at startup, recovery logic never executes. If the replacement is interrupted (power loss, OOM kill), the binary is corrupted with no rollback path.

Discord, Slack, and VS Code solved this years ago: a thin launcher manages versioned directories side-by-side. The old version stays intact until the new one proves stable.

We could not find a Go library that implements this pattern. go-launcher does.

What It Does

go-launcher is a library you embed in a small launcher binary that supervises your actual application:

your-launcher          (thin binary, ~40 lines of your code + go-launcher)
  └── your-app         (your actual application, spawned as a child process)

The launcher handles:

  • Crash detection + automatic rollback -- if the new version crash-loops, the previous version comes back automatically
  • Versioned directories -- versions/current/ and versions/previous/ side-by-side
  • Update orchestration -- download to staging, verify SHA-256 checksum, atomic rotation
  • Probation period -- new versions must survive a configurable window before being marked stable
  • Process supervision -- spawn, monitor, restart with configurable backoff
  • Anti-oscillation -- prevents infinite swapping between two broken versions
  • Bootstrap download -- if no child binary exists, download the latest version on first launch
  • Self-relocation -- launcher copies itself from Downloads to a permanent install location on first run
  • Singleton enforcement -- PID lockfile prevents duplicate instances

Single dependency (golang.org/x/sys). The child package imported by your application has zero transitive dependencies -- standard library only.

Quick Start

For a runnable end-to-end demo, see the _example/ directory.

Launcher side (your thin launcher binary)

package main

import (
    "context"
    "os"

    "github.com/razvandimescu/go-launcher"
    "github.com/razvandimescu/go-launcher/fetch"
    "github.com/razvandimescu/go-launcher/ui/splash"
)

func main() {
    l := launcher.New(launcher.Config{
        AppName:         "My App",
        ChildBinaryName: "my-app",
        DataDir:         launcher.DefaultDataDir("MyApp"),
        InstallDir:      launcher.DefaultInstallDir("MyApp"),
        EnvVarName:      "MYAPP_LAUNCHER_STATE_DIR",
        Fetcher:         fetch.GitHubRelease("myorg", "myapp", fetch.AssetPattern("my-app-*")),
        UI:              splash.New(splash.Config{AppName: "My App"}),
    })

    os.Exit(l.Run(context.Background()))
}

Child side (your actual application)

Version discovery is your application's concern -- poll your own API, check GitHub, read a config file. The child tells the launcher what to download:

package main

import (
    "os"

    "github.com/razvandimescu/go-launcher/child"
)

func init() {
    child.SetEnvVar("MYAPP_LAUNCHER_STATE_DIR")
}

func main() {
    // ... application init ...

    // Signal healthy startup
    if child.IsManaged() {
        child.TouchHeartbeat()
    }

    // ... application runs ...

    // When you detect a new version is available:
    if child.IsManaged() {
        child.RequestUpdate("1.2.0", "https://example.com/my-app-1.2.0", "sha256:abc123...")
        os.Exit(0) // launcher handles download, rotation, and restart
    }
}

How Existing Libraries Compare

Library Approach Rollback Supervisor Versioned dirs Built-in UI Windows
creativeprojects/go-selfupdate Self-surgery Apply-time only No No No Yes
minio/selfupdate Self-surgery No No No No Yes
sanbornm/go-selfupdate Self-surgery No No No No Yes
rhysd/go-github-selfupdate Self-surgery Apply-time only No No No Yes
jpillora/overseer Master/child No Yes No No No
fynelabs/selfupdate Self-surgery Apply-time only No No No Yes
go-launcher External supervisor Crash-based Yes Yes Yes Yes

Apply-time rollback means the .old file is restored if the rename/copy fails during the swap. It does not help if the new version starts successfully but crashes 30 seconds later.

Crash-based rollback means the launcher detects that the new version is crash-looping and automatically reverts to the previous known-good version -- even if the new version ran briefly before crashing.

This table compares deployment architecture. Some of these libraries have strengths in other dimensions -- multi-backend support (GitHub/GitLab/S3), code signing verification, GOOS/GOARCH detection -- see each library's documentation for full feature sets.

Architecture

$DATA_DIR/
  launcher.json                       # persistent state (7 flat JSON fields)
  launcher.lock                       # PID lockfile
  heartbeat                           # touched by child after healthy init
  pending_update.json                 # written by child when update is available
  shutdown_requested                  # flag file for clean exit
  versions/
    current/                          # active version (opaque directory)
    previous/                         # rollback target
    staging/                          # download in progress

Communication uses file-based IPC -- no sockets, no named pipes. The launcher sets an environment variable pointing to the data directory. The child writes files to signal state changes:

Direction Signal Mechanism
Launcher → Child "You are managed" Environment variable
Child → Launcher "I'm healthy" Touch heartbeat file
Child → Launcher "Update available" Write pending_update.json + exit 0
Child → Launcher "Shut down" Write shutdown_requested + exit 0

The launcher always restarts the child unless shutdown_requested exists with exit code 0. An unexpected exit 0 (without the file) is treated as a crash -- this avoids ambiguity from stray os.Exit(0) calls.

For the full supervisor loop, update flow, and rollback mechanics, see docs/architecture.md.

Interfaces

go-launcher is interface-driven. Provide your own implementations or use the built-in ones.

Fetcher (required for updates/bootstrap)

type Fetcher interface {
    LatestVersion(ctx context.Context) (*Release, error)
    Download(ctx context.Context, release *Release, dst io.Writer, progress func(float64)) error
}

Built-in: fetch.GitHubRelease(), fetch.HTTP().

UI (optional)

type UI interface {
    ShowSplash(status string)
    UpdateProgress(percent float64, status string)
    HideSplash()
    ShowError(msg string)
}

Built-in: ui/splash provides native splash screens for macOS (Cocoa/AppKit) and Windows (GDI+) with animated spinner, progress bar, and configurable branding:

import "github.com/razvandimescu/go-launcher/ui/splash"

UI: splash.New(splash.Config{
    AppName:   "My App",
    Logo:      logoBytes,   // PNG, or nil for text-only
    AccentHex: "#2E67B2",   // spinner + progress bar color
})

Returns a silent no-op on Linux or when CGo is unavailable on macOS. Pass nil for fully headless operation.

Registrar (optional)

type Registrar interface {
    RegisterLoginItem(binaryPath string) error
    UnregisterLoginItem() error
    RegisterService(binaryPath string, args []string) error
    UnregisterService() error
}

Handles OS-level registration (login items, system services). No built-in implementations yet — provide your own or pass nil to skip.

Configuration

launcher.Config{
    // Required
    AppName         string          // display name
    ChildBinaryName string          // binary filename in versions/current/
    DataDir         string          // state, versions, IPC files
    InstallDir      string          // where the launcher lives permanently
    EnvVarName      string          // env var set on child process

    // Optional (sensible defaults)
    ChildArgs         []string        // args forwarded to child (default: none)
    Backoff           []time.Duration // restart delays (default: [2s, 5s, 15s])
    CrashThreshold    int             // crashes before rollback (default: 3)
    CrashWindow       time.Duration   // crash count resets after this (default: 5min)
    ProbationDuration time.Duration   // new version probation (default: 10min)
    KillTimeout       time.Duration   // SIGTERM -> SIGKILL escalation (default: 30s)

    // Pluggable (all optional except Fetcher if you want updates)
    UI        UI          // nil = headless
    Fetcher   Fetcher     // nil = no bootstrap/updates
    Registrar Registrar   // nil = skip OS registration
}
Platform DefaultDataDir DefaultInstallDir
macOS ~/Library/Application Support/{appName}/ /Applications/
Windows %LOCALAPPDATA%\{appName}\ %LOCALAPPDATA%\{appName}\
Linux ~/.local/share/{appName}/ ~/.local/bin/

go-launcher logs via log/slog. Configure slog.SetDefault() before calling Run().

Security

go-launcher downloads binaries from the internet and executes them. The built-in fetchers enforce HTTPS. Downloaded artifacts are verified against SHA-256 checksums provided in the Release.Checksum field.

Code signing verification is not currently built in. If your threat model requires it, implement a custom Fetcher that verifies signatures before writing to the dst writer.

Limitations

  • Single-unit child. The child must be a single binary or a directory managed as an opaque unit.
  • No self-update. The launcher does not update itself. This is deliberate -- the launcher is a thin, stable binary that changes rarely. Update it via your installer or a manual download.
  • Full downloads only. No delta/incremental updates. For most Go binaries (5-30MB), full downloads complete in seconds.
  • No download resumption. Interrupted downloads restart from the beginning.

License

MIT

Contributing

Issues and pull requests are welcome. See the _example/ directory for a working launcher + child pair you can use for testing.

About

Crash-safe auto-updates for Go applications — versioned deployments with automatic rollback when the new version fails.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors