From 4c414d9838da0dfdcd5da066a21fdf5e48989c54 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Tue, 20 Oct 2026 16:08:38 -0700 Subject: [PATCH] murky 8270 retries --- docs/macos.md | 2 +- mac/Sources/Byre/Server.swift | 6 ++++++ mac/Sources/Byre/Tailnet.swift | 13 +++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/macos.md b/docs/macos.md index 920d773..2fa651e 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -10,7 +10,7 @@ The app joins the tailnet as its own node and reaches byre's HTTPS origin on byr ## Tailnet -`Tailnet.swift` runs one node per app install with libtailscale, through the C API of TailscaleKit (`tailscale_new` and the rest, from the umbrella header): hostname `byre-app-` (the slug as the server's, see "Tailnet" in `docs/operations.md`), not ephemeral, its state in `tsnet` in `run.fc.byre.mac` under Application Support, its log (`tailscale_set_logfd`, a pipe) in the app's log as `tsnet:` lines. It starts at launch (`tailnetNode`, which exits the app if libtailscale can't start) and opens the node's loopback SOCKS5 proxy (`tailscale_loopback`). `API` sets that proxy, with user `tsnet` and the proxy credential, as the `proxyConfigurations` of its `URLSession`, so REST, the SSE streams (`EventStream`), the command channel (`Remote`) and terminals (`Terminal`) all go over the tailnet; only `GET /api/tailnet` to the local service's loopback address uses `URLSession.shared`. `Tailnet` polls the node's status (`tailscale_status_json`) every second, and every 2 s reads `GET /api/tailnet` at the local service's loopback address (the `loopback` `byre check` prints, found once at launch). Until the app's node is `Running`, the window shows a spinner with "Starting Tailscale…"; while the app's node is `NeedsLogin` or `NeedsMachineAuth`, or the local service's node is `NeedsLogin`, it shows only the sign-in screen instead: a row per node (the app's, and the local service's when one answers), each **Signed in**, or **Sign in to Tailscale**, which opens that node's `AuthURL` in the browser. Servers are added without typing in two ways. When the local service's node runs with an `origin`, that origin is saved as a server (a `ServerRecord`, so iCloud carries it to every Mac on the account). Once the app's node runs, and every 10 s after, the app also takes each online tailnet peer owned by its node's user whose MagicDNS name starts with `byre-` but not `byre-app-`, and saves it when `GET /api/admin/status` answers there within 5 s, probing all of them at once. Either way a forgotten server comes back the next time it is found. Preview forwards dial through the node too (see "Previews" in `docs/operations.md`). +`Tailnet.swift` runs one node per app install with libtailscale, through the C API of TailscaleKit (`tailscale_new` and the rest, from the umbrella header): hostname `byre-app-` (the slug as the server's, see "Tailnet" in `docs/operations.md`), not ephemeral, its state in `tsnet` in `run.fc.byre.mac` under Application Support, its log (`tailscale_set_logfd`, a pipe) in the app's log as `tsnet:` lines. It starts at launch (`tailnetNode`, which exits the app if libtailscale can't start) and opens the node's loopback SOCKS5 proxy (`tailscale_loopback`). `API` sets that proxy, with user `tsnet` and the proxy credential, as the `proxyConfigurations` of its `URLSession`, so REST, the SSE streams (`EventStream`), the command channel (`Remote`) and terminals (`Terminal`) all go over the tailnet; only `GET /api/tailnet` to the local service's loopback address uses `URLSession.shared`. `Tailnet` polls the node's status (`tailscale_status_json`) every second, and reads `GET /api/tailnet` at the local service's loopback address (the `loopback` `byre check` prints, found once at launch) every 30 s, or every 2 s while the Server tab or the sign-in screen shows (`serviceWatchers`), since a Mac with no service of its own gets a refused connection each time. Until the app's node is `Running`, the window shows a spinner with "Starting Tailscale…"; while the app's node is `NeedsLogin` or `NeedsMachineAuth`, or the local service's node is `NeedsLogin`, it shows only the sign-in screen instead: a row per node (the app's, and the local service's when one answers), each **Signed in**, or **Sign in to Tailscale**, which opens that node's `AuthURL` in the browser. Servers are added without typing in two ways. When the local service's node runs with an `origin`, that origin is saved as a server (a `ServerRecord`, so iCloud carries it to every Mac on the account). Once the app's node runs, and every 10 s after, the app also takes each online tailnet peer owned by its node's user whose MagicDNS name starts with `byre-` but not `byre-app-`, and saves it when `GET /api/admin/status` answers there within 5 s, probing all of them at once. Either way a forgotten server comes back the next time it is found. Preview forwards dial through the node too (see "Previews" in `docs/operations.md`). `mac/bundle.sh` builds TailscaleKit from `github.com/tailscale/libtailscale` at the commit pinned in the script (`libtailscale=`): it clones it into `~/Library/Caches/byre/build/libtailscale-`, runs its `make -C swift macos` (Go's `c-archive` build, then `xcodebuild` of the framework, unsigned) and wraps the framework with `xcodebuild -create-xcframework`, once per commit, under the build lock. It copies that into the shared build directory's `mac/Frameworks/TailscaleKit.xcframework`, which `mac/Package.swift` names as a binary target, and bundles and signs the framework in `Contents/Frameworks` like Sparkle's. To move to a newer libtailscale, change the pinned commit. diff --git a/mac/Sources/Byre/Server.swift b/mac/Sources/Byre/Server.swift index c0becf8..940743f 100644 --- a/mac/Sources/Byre/Server.swift +++ b/mac/Sources/Byre/Server.swift @@ -426,6 +426,12 @@ struct ServerView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Palette.bg) + .onAppear { + Tailnet.shared.serviceWatchers += 1 + } + .onDisappear { + Tailnet.shared.serviceWatchers -= 1 + } .task { await refresh() while !Task.isCancelled { diff --git a/mac/Sources/Byre/Tailnet.swift b/mac/Sources/Byre/Tailnet.swift index 9b7169f..160cecf 100644 --- a/mac/Sources/Byre/Tailnet.swift +++ b/mac/Sources/Byre/Tailnet.swift @@ -194,6 +194,7 @@ final class Tailnet { private(set) var searched = false private(set) var peers: Set = [] @ObservationIgnored private var servers: [URL] = [] + @ObservationIgnored var serviceWatchers = 0 var running: Bool { state == "Running" @@ -232,7 +233,13 @@ final class Tailnet { private func watchService() async { var loopback: URL? + var asked: ContinuousClock.Instant? while true { + if serviceWatchers == 0, let asked, asked.duration(to: .now) < .seconds(30) { + try? await Task.sleep(for: .seconds(2)) + continue + } + asked = .now if loopback == nil { do { loopback = URL(string: try await Server.checks().loopback) @@ -314,6 +321,12 @@ struct TailnetLoginView: View { .frame(width: 460) .padding(.vertical, 24) .frame(maxWidth: .infinity, maxHeight: .infinity) + .onAppear { + Tailnet.shared.serviceWatchers += 1 + } + .onDisappear { + Tailnet.shared.serviceWatchers -= 1 + } } }