<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** N/A — internal developer tooling (`tools/hangar`). ## Summary Fleet Hangar is the local dev-environment control panel (`tools/hangar`, Go + Wails). This PR expands it into a broader **local dev-services** toolkit for contributors/QA: - **Multi-server support** — run up to 3 independent local Fleet servers in parallel, each on its own git worktree, offset ports, and docker compose project (server switcher + server-scoped Server/Logs/Database/Git tabs). - **SCEP tab** — run local SCEP CA servers using the in-repo `server/mdm/scep/cmd/scepserver` (built once to a cached binary). Per-depot profiles, `ca -init`, concurrent start/stop with live logs, and one-click copy for the SCEP URL / challenge / thumbprint (parsed from `ca.pem`). - **MDM assets tab** — run `tools/mdm/assets export` from saved configs; results list each written file with copy-contents/path + size + timestamp, plus the `FLEET_MDM_APPLE_*` env block. - **TUF tab** — drive `tools/tuf/test/main.sh` from platform checkboxes. Hangar runs the file-server itself (`SKIP_SERVER=1`) so `fleetctl package` can reach the TUF URL during packaging; streams live build output; shows ngrok tunnel + TUF-server prerequisites; and offers kill-server + delete-assets. - **Supporting work** — DB backups in app-data + cross-server restore; ngrok live public-URL links + stale-tunnel heal; per-server open-in-browser; Settings → Troubleshoot cards to reap stray `scepserver`/TUF-server processes and delete `test_tuf`. Opening as a **draft for transparency**. All changes are confined to `tools/hangar/`; nothing touches the Fleet server, agent, or any shipped code. **Architecture:** each tab is an `internal/<feature>` package (pure, unit-tested logic) behind a thin `services/<feature>_service.go` Wails adapter, reached from the UI as `api.*`. Long-running processes go through the shared process engine; everything builds from / runs against the primary repo (Server 1). # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes — N/A: `tools/hangar` is a developer tool and is not part of a Fleet release. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented, JS inline code is prevented, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - External commands (`scepserver`, `go run ./tools/mdm/assets`, `bash main.sh`, the backup/restore `docker` invocation) are spawned with discrete argv slices via the process engine — no shell string interpolation — so user-supplied values (challenge, enroll secret, depot/dir paths) can't inject. Backup names are validated to `[A-Za-z0-9._-]`; server-id path segments are sanitized to `[A-Za-z0-9_-]` (no traversal); TUF asset deletion is scoped to `<repo>/test_tuf`. - [x] Timeouts are implemented and retries are limited to avoid infinite loops - Binary builds / one-shot commands run under bounded `context.WithTimeout`; the TUF-server readiness and ngrok local-API fetches use short HTTP timeouts; no unbounded loops or retries were added. - [ ] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI — N/A: no Fleet server API changes. ## Testing - [x] Added/updated automated tests - Go unit tests across the new packages: `settings` (SCEP profiles, TUF config, `migrate` incl. the empty-`servers` case), `scep` (depot/CA parsing, arg builders), `mdmassets` (export args, `wrote … in …` parsing, config persistence), `tuf` (env building, file-server args, asset delete), and `troubleshoot` (live-PID filtering) — plus the existing backups logic. - `tsc --noEmit` clean and `task build` green. - [ ] Where appropriate, automated tests simulate multiple hosts and test for host isolation — N/A. - [x] QA'd all new/changed functionality manually (ongoing local testing of all three tabs). ## Database migrations N/A — no database migrations. ## New Fleet configuration settings N/A — no Fleet server configuration settings (Hangar stores its own settings in app-data). ## fleetd/orbit/Fleet Desktop N/A — no fleetd/orbit/Fleet Desktop changes. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Multi-server support (up to three) with server switcher, server-scoped health/logs, and server-scoped Docker Compose controls. * New **Servers** settings section plus per-server configuration (including ports/compose project) and server-aware start/stop/quit flows. * New **SCEP**, **MDM Assets**, and **TUF** tabs for managing profiles/assets and discovering ngrok URLs. * Git worktree listing/creation/removal. * Centralized, server-scoped database backup management. * **Bug Fixes** * Improved process discovery to skip dead or racing entries and avoid duplicate docker-compose-up display. * Self-healing pruning of stale ngrok tunnel selections. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Fleet Hangar
A desktop control panel for Fleet contributors, built
with Go and Wails 3. Bundles the daily tasks of working on a
Fleet clone (checking out branches, building, running fleet serve, tailing logs, managing
the dev MySQL, driving fleetctl, applying GitOps repos, spinning up osquery-perf) into
one app. macOS-first.
Why Go? To match the rest of the repo so Fleet engineers can contribute to it. The
backend is plain Go (os/exec, syscall, goroutines); only the desktop shell is Wails.
Hangar began as a Rust/Tauri app; it was ported to Go and that port is now the canonical
implementation.
Architecture
internal/— all the logic, pure and unit-tested (each package takes explicit paths/timestamps so tests are hermetic):processes— spawn/log/lifecycle engine: child-process management, streamed log readers (level detection, secret scrubbing, on-disk rotation, in-memory ring),running.jsoncrash-recovery, SIGTERM→SIGKILL on process groups, docker-compose orchestration, TLS probesettings,gitrepo,db,gitops,fleetctl,deps,troubleshoot,perf,perfconfig— one per formersrc-tauri/src/*.rsmodulepaths(macOS dirs + path safety),shellpath(login-shell PATH warming),traymenu(tray menu model)
services/— thin Wails-bound service structs; each exported method is callable from the frontend. They resolve real paths and delegate tointernal/.main.go/tray.go/emitter.go— the native shell: app bootstrap, system tray, and window lifecycle (hide-to-tray, dock reopen, Cmd+Q→confirm).frontend/— the React + TypeScript UI (shared with the Rust app). The only Wails-specific glue issrc/lib/tauri.ts(theapi.*IPC layer over the generated bindings) andsrc/lib/events.ts(thelisten()adapter over Wails events).
Development
Requirements: Go (see go.mod), Node 24+, and the
Wails 3 prerequisites. Install the
CLIs once:
go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-alpha.98
go install github.com/go-task/task/v3/cmd/task@latest
Then, from this directory:
task dev # live-reload dev mode (Vite + Go)
task build # type-check + production build -> bin/fleet-hangar
task package # build + bundle + ad-hoc sign -> "bin/Fleet Hangar.app"
task dist # zip the existing .app into a shareable "bin/Fleet Hangar.zip"
task pkg # wrap the existing .app into a Fleet-installable "bin/Fleet Hangar.pkg"
go test ./... # backend unit tests
After changing any Go service signature, regenerate the TypeScript bindings (also run by
task build):
wails3 generate bindings -clean=true -ts
Notes
-
Names. The bundle is
Fleet Hangar.app(thePRODUCT_NAMETaskfile var) so Finder, Launchpad, and Spotlight show "Fleet Hangar". The executable inside staysfleet-hangar(theAPP_NAMEvar) — it's also reused for the-serverbinary and Docker image tags, where a space would break things. The bundle identifier iscom.fleetdm.fleet-hangar— the same ID the original Rust app used, so settings written by it carry over untouched. Settings live under~/Library/Application Support/<id>/and logs under~/Library/Logs/<id>/; DB backups live in<repo>/db-backups/. -
Distribution.
task packageproduces only an ad-hoc-signed.app;task distzips whatever bundle is inbin/intobin/Fleet Hangar.zip(viaditto, so the signature survives) for handoff. Two paths:- Quick / trusted teammate:
task package→task dist, then the recipient clears quarantine after unzipping:xattr -dr com.apple.quarantine "/path/to/Fleet Hangar.app"(an ad-hoc-signed app from another machine is otherwise blocked by Gatekeeper). - Clean install anywhere: configure
SIGN_IDENTITY+KEYCHAIN_PROFILEinbuild/darwin/Taskfile.yml, runtask darwin:sign:notarize(Developer ID sign + Apple notarization), thentask dist. No quarantine step needed.
distnever rebuilds, so running it aftersign:notarizepreserves the notarized signature. - Quick / trusted teammate:
-
Install onto hosts via Fleet.
task pkgwraps the existingbin/Fleet Hangar.appintobin/Fleet Hangar.pkg— a component installer that drops the app into/Applications. Upload it under Fleet > Software > Add software > Custom package; Fleet reads the identifier and version from the app'sInfo.plistto track install status. Likedist, it never rebuilds, so notarize first (task darwin:sign:notarize) for real hosts — an ad-hoc build installs cleanly only because a pkg install skips the quarantine flag.
Known issues
- Rare crash on display sleep/wake or monitor changes (upstream Wails v3 alpha bug, not
ours). Wails'
screen_darwin.gostores the autoreleased[NSString UTF8String]buffers for each screen'sid/namein a C struct and reads them from Go later, after the autorelease pool has drained — a use-after-free. On anApplicationDidChangeScreenParametersevent the dangling pointer tripsfatal error: invalid pointer found on stack. It's afatal error, not a panic, so it can't be recovered, and the handler is registered inside Wails so we can't intercept it. It's infrequent (one occurrence observed over an ~11h session). Until a fixed Wails alpha ships, relaunch the app if it happens. Reported upstream: wailsapp/wails#5556.