Show both sign-ins on the first screen and find servers automatically

This commit is contained in:
Ian Gulliver
2026-10-20 14:08:35 -07:00
parent a728cb2c96
commit da7f952855
7 changed files with 130 additions and 48 deletions
+104 -11
View File
@@ -72,7 +72,7 @@ final class TailnetNode: Sendable {
return proxy
}
func status() -> (state: String, authURL: URL?)? {
func status() -> (state: String, authURL: URL?, servers: [URL])? {
var out: UnsafeMutablePointer<CChar>?
guard unsafe tailscale_status_json(sd, &out) == 0, let out = unsafe out else {
return nil
@@ -80,14 +80,34 @@ final class TailnetNode: Sendable {
defer {
unsafe free(out)
}
struct Node: Decodable {
let DNSName: String
let UserID: Int64
let Online: Bool
}
struct Status: Decodable {
let BackendState: String
let AuthURL: String
let me: Node?
let Peer: [String: Node]?
enum CodingKeys: String, CodingKey {
case BackendState, AuthURL, Peer
case me = "Self"
}
}
guard let status = try? JSONDecoder().decode(Status.self, from: unsafe Data(bytes: out, count: strlen(out))) else {
return nil
}
return (status.BackendState, URL(string: status.AuthURL))
let servers = (status.Peer ?? [:]).values.compactMap { peer -> URL? in
let host = peer.DNSName.hasSuffix(".") ? String(peer.DNSName.dropLast()) : peer.DNSName
let name = host.split(separator: ".").first ?? ""
guard peer.Online, peer.UserID == status.me?.UserID, name.hasPrefix("byre-"), !name.hasPrefix("byre-app-") else {
return nil
}
return parseOrigin("https://\(host)")
}
return (status.BackendState, URL(string: status.AuthURL), servers)
}
// Blocks until the connection is made or fails.
@@ -109,41 +129,114 @@ final class Tailnet {
private(set) var state = "Starting"
private(set) var authURL: URL?
private(set) var service: TailnetInfo?
@ObservationIgnored private var servers: [URL] = []
var running: Bool {
state == "Running"
}
var signedIn: Bool {
running && service?.state != "NeedsLogin"
}
private init() {
Task {
while true {
if let status = tailnetNode.status() {
state = status.state
authURL = status.authURL
servers = status.servers
}
try? await Task.sleep(for: .seconds(1))
}
}
Task {
await watchService()
}
Task {
await discover()
}
}
private func watchService() async {
var loopback: URL?
while true {
if loopback == nil {
do {
loopback = URL(string: try await Server.checks().loopback)
} catch {
log.error("find the local service: \(errorText(error), privacy: .public)")
}
}
if let loopback {
service = try? await Server.tailnet(loopback)
if let service, service.state == "Running", let origin = parseOrigin(service.origin), !Servers.shared.origins.contains(origin.absoluteString) {
Servers.shared.add(origin)
}
}
try? await Task.sleep(for: .seconds(2))
}
}
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()
}
}
try? await Task.sleep(for: .seconds(10))
}
}
}
struct TailnetLoginView: View {
var body: some View {
let tailnet = Tailnet.shared
VStack(alignment: .leading, spacing: 10) {
Text("byre reaches its servers through its own Tailscale node, \(tailnetNode.hostname). Sign it in to the tailnet your byre servers are on.")
VStack(alignment: .leading, spacing: 16) {
Text("byre reaches its servers through Tailscale. Sign each of these nodes in to the tailnet your byre servers are on.")
.fixedSize(horizontal: false, vertical: true)
Button("Sign in to Tailscale") {
if let url = tailnet.authURL {
NSWorkspace.shared.open(url)
}
NodeLoginRow(hostname: tailnetNode.hostname, role: "this app", state: tailnet.state, authURL: tailnet.authURL)
if let service = tailnet.service {
NodeLoginRow(hostname: service.hostname, role: "this Mac's server", state: service.state, authURL: URL(string: service.authUrl))
}
.disabled(tailnet.authURL == nil)
Text(tailnet.authURL == nil ? "Waiting for Tailscale (\(tailnet.state))…" : "Waiting for the sign-in (\(tailnet.state))…")
.foregroundStyle(Palette.muted)
}
.frame(width: 460)
.padding(.vertical, 24)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private struct NodeLoginRow: View {
let hostname: String
let role: String
let state: String
let authURL: URL?
var body: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
Text(hostname).font(Fonts.font(13, weight: 700))
Text(role).foregroundStyle(Palette.muted)
}
if state == "Running" {
Text("Signed in").foregroundStyle(Palette.ok)
} else {
Button("Sign in to Tailscale") {
if let authURL {
NSWorkspace.shared.open(authURL)
}
}
.disabled(authURL == nil)
Text(authURL == nil ? "Waiting for Tailscale (\(state))…" : "Waiting for the sign-in (\(state))…")
.foregroundStyle(Palette.muted)
}
}
}
}