auto restart server

This commit is contained in:
Ian Gulliver
2026-10-18 23:07:11 -07:00
parent 374ccf8b5e
commit c64987f2eb
7 changed files with 40 additions and 44 deletions
+2 -2
View File
@@ -74,7 +74,7 @@ API calls go through the generated `Client` over `URLSessionTransport`. A middle
`EventStream` is a hand-written SSE client over `URLSession.bytes(for:)`. It splits lines itself, because `AsyncBytes.lines` drops the blank lines that end SSE events, and parses off the main actor, handing each frame to it. It handles `id`, `event: ping`, `event: ready` and `data`: it tracks the last seq, reconnects from it after a stale threshold (40 s, checked every 5 s) and a retry delay (2 s), when the network comes back (`NWPathMonitor`), and when the app becomes active with a stale stream. An event whose data doesn't decode is logged and skipped. There are two streams, global (after the bootstrap `last_event_seq`) and lead (`?lead=true`, after the last event of the lead's first page), and only the global one drives the connection dot.
Startup: `GET /api/admin/status`, then me, projects, inbox, lead sessions and summary reads in parallel, then drafts, focus, the lead stream, the window, the global stream. A failure before the window shows replaces it with `byre failed to start: <error>` and the whole startup is tried again every 2 s until it succeeds, so a server restarting (the app's own kickstart after an update, for one) heals by itself; Forget Server leaves that screen. `handleEvent` calls `notifyInbox` on `inbox.created` and `notifyTurn` on `turn.ended`.
Startup: `GET /api/admin/status`, then me, projects, inbox, lead sessions and summary reads in parallel, then drafts, focus, the lead stream, the window, the global stream. A failure before the window shows replaces it with `byre failed to start: <error>` and the whole startup is tried again every 2 s until it succeeds, so a server restarting (after an update replaces its binary, for one) heals by itself; Forget Server leaves that screen. `handleEvent` calls `notifyInbox` on `inbox.created` and `notifyTurn` on `turn.ended`.
The app logs through `os.Logger` with subsystem `run.fc.byre.mac`: `log show --last 10m --predicate 'subsystem == "run.fc.byre.mac"'` prints it.
@@ -157,7 +157,7 @@ On first launch, and in a window from New Server Window, the window shows one fi
## Updates
`Updater.swift` runs Sparkle's standard updater (`SPUStandardUpdaterController`), started at launch. It reads `SUFeedURL` from `mac/Info.plist`, the appcast of the newest release (see "Releases" in `docs/operations.md`), checks it daily (`SUEnableAutomaticChecks`, so Sparkle doesn't first ask whether to), compares the appcast's `sparkle:version` with `CFBundleVersion`, and verifies the download's EdDSA signature against `SUPublicEDKey` and its Developer ID signature against the running app's. Check for Updates… in the app menu checks at once. Sparkle's own windows show the offer and the progress; the appcast carries no release notes, so its release-notes view, a web view, is never shown. At launch, the app compares its `CFBundleVersion` with `lastLaunchedVersion` in `UserDefaults` and stores the new one; when a stored version differs, however the bundle was replaced (Sparkle relaunching, installing on quit, or a new copy dragged in), it reads `~/Library/LaunchAgents/run.fc.byre.plist`, and when the service's program is this bundle's `Contents/Helpers/byre`, runs `launchctl kickstart -k gui/<uid>/run.fc.byre`, so the server updates with the app. A server installed from a checkout is left alone.
`Updater.swift` runs Sparkle's standard updater (`SPUStandardUpdaterController`), started at launch. It reads `SUFeedURL` from `mac/Info.plist`, the appcast of the newest release (see "Releases" in `docs/operations.md`), checks it daily (`SUEnableAutomaticChecks`, so Sparkle doesn't first ask whether to), compares the appcast's `sparkle:version` with `CFBundleVersion`, and verifies the download's EdDSA signature against `SUPublicEDKey` and its Developer ID signature against the running app's. Check for Updates… in the app menu checks at once. Sparkle's own windows show the offer and the progress; the appcast carries no release notes, so its release-notes view, a web view, is never shown. When the bundle is replaced, a server running from its `Contents/Helpers/byre` restarts itself onto the new binary (see "Running as a service" in `docs/operations.md`).
## Persistence
+2 -2
View File
@@ -119,7 +119,7 @@ The listen address is `byre serve --listen`, whose default is `defaultListen` in
byre is served at the root of the machine's `ts.net` name on 443 (`https://<machine>.<tailnet>.ts.net`), not under a path: the API uses absolute paths, and the macOS app takes an origin with no path. `tailscale serve status` shows the URL and what else the machine serves. `launchctl print gui/$(id -u)/run.fc.byre` shows the service and the program it runs.
A new build reaches the service by replacing `/Applications/Byre.app` and restarting the service: `mac/deploy.sh` (see "Releases"), or the app's updater, which restarts the service when it launches as a new version. Logs go to `~/Library/Logs/byre.log`.
A new build reaches the service by replacing `/Applications/Byre.app`: `mac/deploy.sh` (see "Releases"), or the app's updater. `byre serve` stats its own executable (`os.Executable`) every 5 seconds (`watchExecutable` in `cmd/byre/serve.go`); once that path holds a different file, it logs `executable replaced, exiting` and shuts down as on SIGTERM, and launchd (`KeepAlive`) starts the new binary. A missing path counts as not yet replaced, so the moment in `install.sh` between removing the old bundle and moving the new one in doesn't trigger it. Logs go to `~/Library/Logs/byre.log`.
## Releases
@@ -127,7 +127,7 @@ A release is one notarized disk image, `byre-<version>.dmg`, holding `Byre.app`:
`mac/package.sh` builds one for whatever the checkout holds, into `mac/.build/release/<version>/`, `<version>` being the bundle's `CFBundleShortVersionString`, and prints the image's path. It builds with `mac/bundle.sh` (signed with Developer ID, see `docs/macos.md`), zips the app and notarizes it with `xcrun notarytool submit --keychain-profile byre-notary --wait`, staples the ticket, builds the disk image with `hdiutil` (the app and an `/Applications` link), signs, notarizes and staples that too, and writes its SHA-256 beside it. It then signs the image for Sparkle with `sign_update --account byre` and writes `appcast.xml`, a feed with that one release.
A deploy is that image installed rather than published: `mac/deploy.sh` runs `package.sh`, installs the image's app with `mac/install.sh <dmg>` (mounts the image, copies `Byre.app` to `/Applications/Byre.app.new`, removes the old bundle and moves the new one into place, so nothing running from the old bundle has its files written over; it prints the installed `ByreCommit`), and restarts the service with `launchctl kickstart -k`. Client Macs get the same image through the client tools and `install.sh` (`AGENTS.md`, "Deploying"). A deployed build's `CFBundleVersion` is at least the last release's, so the updater never offers to go back to it.
A deploy is that image installed rather than published: `mac/deploy.sh` runs `package.sh`, installs the image's app with `mac/install.sh <dmg>` (mounts the image, copies `Byre.app` to `/Applications/Byre.app.new`, removes the old bundle and moves the new one into place, so nothing running from the old bundle has its files written over; it prints the installed `ByreCommit`); the service sees its executable replaced and restarts itself (see "Running as a service"). Client Macs get the same image through the client tools and `install.sh` (`AGENTS.md`, "Deploying"). A deployed build's `CFBundleVersion` is at least the last release's, so the updater never offers to go back to it.
`mac/release.sh <notes>` publishes one, run on byre's host from a checkout whose `HEAD` carries a `v<version>` tag already pushed to `origin`, with a clean tree; it fetches the tag from `origin` and stops unless it names `HEAD`, then runs `package.sh`. Last, it creates the release as a draft through the Gitea API (`POST /api/v1/repos/ai/byre/releases`, its notes the Markdown file `<notes>`, which the releasing agent writes from the diffs since the previous tag, as `AGENTS.md`, "Releasing", describes), uploads the image, its `.sha256` and `appcast.xml` as assets, and only then publishes it (`PATCH` with `draft: false`), so the `latest` release always has its appcast. A run that fails part way leaves the draft on the releases page; delete it there before running again. Everything lands in `mac/.build/release/<version>/`, and the script prints the release page.