Show both sign-ins on the first screen and find servers automatically
This commit is contained in:
+104
-11
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user