Show connection progress at app startup instead of flashing screens
This commit is contained in:
@@ -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 = ""
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user