{"id":48649746,"url":"https://github.com/razvandimescu/go-launcher","last_synced_at":"2026-04-10T08:31:49.631Z","repository":{"id":345017503,"uuid":"1179325477","full_name":"razvandimescu/go-launcher","owner":"razvandimescu","description":"Crash-safe auto-updates for Go applications — versioned deployments with automatic rollback when the new version fails.","archived":false,"fork":false,"pushed_at":"2026-03-25T04:18:05.000Z","size":253,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-26T01:58:22.348Z","etag":null,"topics":["auto-update","crash-recovery","desktop-application","go","golang","launcher","process-supervisor","rollback","self-update"],"latest_commit_sha":null,"homepage":"https://razvandimescu.github.io/go-launcher/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/razvandimescu.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-11T23:16:42.000Z","updated_at":"2026-03-25T04:18:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/razvandimescu/go-launcher","commit_stats":null,"previous_names":["rinktltd/go-launcher","razvandimescu/go-launcher"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/razvandimescu/go-launcher","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/razvandimescu%2Fgo-launcher","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/razvandimescu%2Fgo-launcher/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/razvandimescu%2Fgo-launcher/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/razvandimescu%2Fgo-launcher/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/razvandimescu","download_url":"https://codeload.github.com/razvandimescu/go-launcher/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/razvandimescu%2Fgo-launcher/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31635117,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-10T07:40:12.752Z","status":"ssl_error","status_checked_at":"2026-04-10T07:40:11.664Z","response_time":98,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["auto-update","crash-recovery","desktop-application","go","golang","launcher","process-supervisor","rollback","self-update"],"created_at":"2026-04-10T08:31:47.565Z","updated_at":"2026-04-10T08:31:49.605Z","avatar_url":"https://github.com/razvandimescu.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# go-launcher\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/razvandimescu/go-launcher.svg)](https://pkg.go.dev/github.com/razvandimescu/go-launcher)\n[![CI](https://github.com/razvandimescu/go-launcher/actions/workflows/ci.yml/badge.svg)](https://github.com/razvandimescu/go-launcher/actions/workflows/ci.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/razvandimescu/go-launcher)](https://goreportcard.com/report/github.com/razvandimescu/go-launcher)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\nThe [Squirrel.Windows](https://github.com/Squirrel/Squirrel.Windows) pattern for Go. External supervisor with versioned directories, crash-based rollback, and zero-dependency child integration.\n\n**[Website](https://razvandimescu.github.io/go-launcher/)** | **[Go Docs](https://pkg.go.dev/github.com/razvandimescu/go-launcher)**\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"splash-demo.gif\" alt=\"Native macOS splash screen with animated spinner, progress bar, and status text\" width=\"340\"\u003e\n  \u003cbr\u003e\n  \u003csub\u003eBuilt-in native splash screen (macOS Cocoa/AppKit) -- no competing Go update library offers this.\u003c/sub\u003e\n\u003c/p\u003e\n\n## The Problem\n\nEvery 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.\n\nDiscord, 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.\n\nWe could not find a Go library that implements this pattern. **go-launcher** does.\n\n## What It Does\n\ngo-launcher is a library you embed in a small launcher binary that supervises your actual application:\n\n```\nyour-launcher          (thin binary, ~40 lines of your code + go-launcher)\n  └── your-app         (your actual application, spawned as a child process)\n```\n\nThe launcher handles:\n\n- **Crash detection + automatic rollback** -- if the new version crash-loops, the previous version comes back automatically\n- **Versioned directories** -- `versions/current/` and `versions/previous/` side-by-side\n- **Update orchestration** -- download to staging, verify SHA-256 checksum, atomic rotation\n- **Probation period** -- new versions must survive a configurable window before being marked stable\n- **Process supervision** -- spawn, monitor, restart with configurable backoff\n- **Anti-oscillation** -- prevents infinite swapping between two broken versions\n- **Bootstrap download** -- if no child binary exists, download the latest version on first launch\n- **Self-relocation** -- launcher copies itself from Downloads to a permanent install location on first run\n- **Singleton enforcement** -- PID lockfile prevents duplicate instances\n\nSingle dependency (`golang.org/x/sys`). The `child` package imported by your application has **zero transitive dependencies** -- standard library only.\n\n## Quick Start\n\n\u003e For a runnable end-to-end demo, see the [`_example/`](./_example/) directory.\n\n### Launcher side (your thin launcher binary)\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"os\"\n\n    \"github.com/razvandimescu/go-launcher\"\n    \"github.com/razvandimescu/go-launcher/fetch\"\n    \"github.com/razvandimescu/go-launcher/ui/splash\"\n)\n\nfunc main() {\n    l := launcher.New(launcher.Config{\n        AppName:         \"My App\",\n        ChildBinaryName: \"my-app\",\n        DataDir:         launcher.DefaultDataDir(\"MyApp\"),\n        InstallDir:      launcher.DefaultInstallDir(\"MyApp\"),\n        EnvVarName:      \"MYAPP_LAUNCHER_STATE_DIR\",\n        Fetcher:         fetch.GitHubRelease(\"myorg\", \"myapp\", fetch.AssetPattern(\"my-app-*\")),\n        UI:              splash.New(splash.Config{AppName: \"My App\"}),\n    })\n\n    os.Exit(l.Run(context.Background()))\n}\n```\n\n### Child side (your actual application)\n\nVersion discovery is your application's concern -- poll your own API, check GitHub, read a config file. The child tells the launcher what to download:\n\n```go\npackage main\n\nimport (\n    \"os\"\n\n    \"github.com/razvandimescu/go-launcher/child\"\n)\n\nfunc init() {\n    child.SetEnvVar(\"MYAPP_LAUNCHER_STATE_DIR\")\n}\n\nfunc main() {\n    // ... application init ...\n\n    // Signal healthy startup\n    if child.IsManaged() {\n        child.TouchHeartbeat()\n    }\n\n    // ... application runs ...\n\n    // When you detect a new version is available:\n    if child.IsManaged() {\n        child.RequestUpdate(\"1.2.0\", \"https://example.com/my-app-1.2.0\", \"sha256:abc123...\")\n        os.Exit(0) // launcher handles download, rotation, and restart\n    }\n}\n```\n\n## How Existing Libraries Compare\n\n| Library | Approach | Rollback | Supervisor | Versioned dirs | Built-in UI | Windows |\n|---|---|---|---|---|---|---|\n| [creativeprojects/go-selfupdate](https://github.com/creativeprojects/go-selfupdate) | Self-surgery | Apply-time only | No | No | No | Yes |\n| [minio/selfupdate](https://github.com/minio/selfupdate) | Self-surgery | No | No | No | No | Yes |\n| [sanbornm/go-selfupdate](https://github.com/sanbornm/go-selfupdate) | Self-surgery | No | No | No | No | Yes |\n| [rhysd/go-github-selfupdate](https://github.com/rhysd/go-github-selfupdate) | Self-surgery | Apply-time only | No | No | No | Yes |\n| [jpillora/overseer](https://github.com/jpillora/overseer) | Master/child | No | Yes | No | No | No |\n| [fynelabs/selfupdate](https://github.com/fynelabs/selfupdate) | Self-surgery | Apply-time only | No | No | No | Yes |\n| **go-launcher** | **External supervisor** | **Crash-based** | **Yes** | **Yes** | **Yes** | **Yes** |\n\n**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.\n\n**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.\n\n\u003e 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.\n\n## Architecture\n\n```\n$DATA_DIR/\n  launcher.json                       # persistent state (7 flat JSON fields)\n  launcher.lock                       # PID lockfile\n  heartbeat                           # touched by child after healthy init\n  pending_update.json                 # written by child when update is available\n  shutdown_requested                  # flag file for clean exit\n  versions/\n    current/                          # active version (opaque directory)\n    previous/                         # rollback target\n    staging/                          # download in progress\n```\n\nCommunication 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:\n\n| Direction | Signal | Mechanism |\n|---|---|---|\n| Launcher → Child | \"You are managed\" | Environment variable |\n| Child → Launcher | \"I'm healthy\" | Touch `heartbeat` file |\n| Child → Launcher | \"Update available\" | Write `pending_update.json` + exit 0 |\n| Child → Launcher | \"Shut down\" | Write `shutdown_requested` + exit 0 |\n\nThe 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.\n\nFor the full supervisor loop, update flow, and rollback mechanics, see [docs/architecture.md](docs/architecture.md).\n\n## Interfaces\n\ngo-launcher is interface-driven. Provide your own implementations or use the built-in ones.\n\n### Fetcher (required for updates/bootstrap)\n\n```go\ntype Fetcher interface {\n    LatestVersion(ctx context.Context) (*Release, error)\n    Download(ctx context.Context, release *Release, dst io.Writer, progress func(float64)) error\n}\n```\n\nBuilt-in: `fetch.GitHubRelease()`, `fetch.HTTP()`.\n\n### UI (optional)\n\n```go\ntype UI interface {\n    ShowSplash(status string)\n    UpdateProgress(percent float64, status string)\n    HideSplash()\n    ShowError(msg string)\n}\n```\n\nBuilt-in: [`ui/splash`](ui/splash/) provides native splash screens for macOS (Cocoa/AppKit) and Windows (GDI+) with animated spinner, progress bar, and configurable branding:\n\n```go\nimport \"github.com/razvandimescu/go-launcher/ui/splash\"\n\nUI: splash.New(splash.Config{\n    AppName:   \"My App\",\n    Logo:      logoBytes,   // PNG, or nil for text-only\n    AccentHex: \"#2E67B2\",   // spinner + progress bar color\n})\n```\n\nReturns a silent no-op on Linux or when CGo is unavailable on macOS. Pass `nil` for fully headless operation.\n\n### Registrar (optional)\n\n```go\ntype Registrar interface {\n    RegisterLoginItem(binaryPath string) error\n    UnregisterLoginItem() error\n    RegisterService(binaryPath string, args []string) error\n    UnregisterService() error\n}\n```\n\nHandles OS-level registration (login items, system services). No built-in implementations yet — provide your own or pass `nil` to skip.\n\n## Configuration\n\n```go\nlauncher.Config{\n    // Required\n    AppName         string          // display name\n    ChildBinaryName string          // binary filename in versions/current/\n    DataDir         string          // state, versions, IPC files\n    InstallDir      string          // where the launcher lives permanently\n    EnvVarName      string          // env var set on child process\n\n    // Optional (sensible defaults)\n    ChildArgs         []string        // args forwarded to child (default: none)\n    Backoff           []time.Duration // restart delays (default: [2s, 5s, 15s])\n    CrashThreshold    int             // crashes before rollback (default: 3)\n    CrashWindow       time.Duration   // crash count resets after this (default: 5min)\n    ProbationDuration time.Duration   // new version probation (default: 10min)\n    KillTimeout       time.Duration   // SIGTERM -\u003e SIGKILL escalation (default: 30s)\n\n    // Pluggable (all optional except Fetcher if you want updates)\n    UI        UI          // nil = headless\n    Fetcher   Fetcher     // nil = no bootstrap/updates\n    Registrar Registrar   // nil = skip OS registration\n}\n```\n\n| Platform | DefaultDataDir | DefaultInstallDir |\n|---|---|---|\n| macOS | `~/Library/Application Support/{appName}/` | `/Applications/` |\n| Windows | `%LOCALAPPDATA%\\{appName}\\` | `%LOCALAPPDATA%\\{appName}\\` |\n| Linux | `~/.local/share/{appName}/` | `~/.local/bin/` |\n\ngo-launcher logs via `log/slog`. Configure `slog.SetDefault()` before calling `Run()`.\n\n## Security\n\ngo-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.\n\nCode 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.\n\n## Limitations\n\n- **Single-unit child.** The child must be a single binary or a directory managed as an opaque unit.\n- **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.\n- **Full downloads only.** No delta/incremental updates. For most Go binaries (5-30MB), full downloads complete in seconds.\n- **No download resumption.** Interrupted downloads restart from the beginning.\n\n## License\n\nMIT\n\n## Contributing\n\nIssues and pull requests are welcome. See the [_example/](./_example/) directory for a working launcher + child pair you can use for testing.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frazvandimescu%2Fgo-launcher","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frazvandimescu%2Fgo-launcher","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frazvandimescu%2Fgo-launcher/lists"}