Show connection progress at app startup instead of flashing screens

This commit is contained in:
Ian Gulliver
2026-10-20 14:20:35 -07:00
parent da7f952855
commit a2571763b4
3 changed files with 80 additions and 26 deletions
+32 -12
View File
@@ -198,6 +198,9 @@ final class Connection {
private func start() async {
let old = store
while !stopped, !Tailnet.shared.running {
try? await Task.sleep(for: .milliseconds(500))
}
while !stopped, !(await attempt(old)) {
try? await Task.sleep(for: .seconds(2))
}
@@ -251,7 +254,7 @@ final class Connection {
} catch {
await store.stop()
if !stopped {
phase = .failed("byre failed to start: \(errorText(error))")
phase = .failed(explain(error, origin))
}
return false
}
@@ -437,15 +440,27 @@ struct ConnectView: View {
}
}
struct FatalView: View {
let message: String
struct ConnectingView: View {
let step: String
var problem: String?
var body: some View {
Text(message)
.foregroundStyle(Palette.danger)
.textSelection(.enabled)
.padding(24)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 10) {
ProgressView()
.controlSize(.small)
Text(step)
}
if let problem {
Text("Retrying: \(problem)")
.foregroundStyle(Palette.muted)
.textSelection(.enabled)
.fixedSize(horizontal: false, vertical: true)
}
}
.frame(width: 460, alignment: .leading)
.padding(.vertical, 24)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
@@ -477,20 +492,25 @@ struct ServerWindow: View {
@ViewBuilder
private var content: some View {
if !Tailnet.shared.signedIn {
if Tailnet.shared.needsLogin {
TailnetLoginView()
} else if !Tailnet.shared.running {
ConnectingView(step: "Starting Tailscale…")
} else if let connection {
let step = "Connecting to \(serverName(connection.origin.absoluteString))…"
switch connection.phase {
case .starting:
Color.clear
case .failed(let message):
FatalView(message: message)
ConnectingView(step: step)
case .failed(let problem):
ConnectingView(step: step, problem: problem)
case .running(let store):
MainView()
.environment(store)
.environment(connection.overlays)
.id(ObjectIdentifier(store))
}
} else if !Tailnet.shared.searched {
ConnectingView(step: "Looking for byre servers on the tailnet…")
} else {
ConnectView(form: form) { connected in
form.serverText = ""
+45 -11
View File
@@ -130,14 +130,15 @@ final class Tailnet {
private(set) var state = "Starting"
private(set) var authURL: URL?
private(set) var service: TailnetInfo?
private(set) var searched = false
@ObservationIgnored private var servers: [URL] = []
var running: Bool {
state == "Running"
}
var signedIn: Bool {
running && service?.state != "NeedsLogin"
var needsLogin: Bool {
state == "NeedsLogin" || state == "NeedsMachineAuth" || service?.state == "NeedsLogin"
}
private init() {
@@ -181,19 +182,52 @@ final class Tailnet {
private func discover() async {
while true {
if running {
for origin in servers where !Servers.shared.origins.contains(origin.absoluteString) {
let api = API(origin: origin)
if (try? await api.client.getStatus().ok) != nil {
log.notice("found \(origin.absoluteString, privacy: .public) on the tailnet")
Servers.shared.add(origin)
}
api.session.finishTasksAndInvalidate()
}
guard running else {
try? await Task.sleep(for: .milliseconds(500))
continue
}
let candidates = servers.filter { !Servers.shared.origins.contains($0.absoluteString) }
let found = await withTaskGroup(of: URL?.self) { group in
for origin in candidates {
group.addTask {
await Self.probe(origin) ? origin : nil
}
}
var found: [URL] = []
for await origin in group {
if let origin {
found.append(origin)
}
}
return found
}
for origin in found {
log.notice("found \(origin.absoluteString, privacy: .public) on the tailnet")
Servers.shared.add(origin)
}
searched = true
try? await Task.sleep(for: .seconds(10))
}
}
private nonisolated static func probe(_ origin: URL) async -> Bool {
let api = API(origin: origin)
defer {
api.session.invalidateAndCancel()
}
return await withTaskGroup(of: Bool.self) { group in
group.addTask {
(try? await api.client.getStatus().ok) != nil
}
group.addTask {
try? await Task.sleep(for: .seconds(5))
return false
}
let ok = await group.next() ?? false
group.cancelAll()
return ok
}
}
}
struct TailnetLoginView: View {