Show both sign-ins on the first screen and find servers automatically
This commit is contained in:
@@ -23,26 +23,6 @@ struct ServerState {
|
||||
|
||||
let noCertificates = "byre's Tailscale node is signed in, but the tailnet has no HTTPS certificates, so byre can't serve its API. Turn on HTTPS Certificates on the DNS page of Tailscale's admin console, https://login.tailscale.com/admin/dns, then turn the server off and on in the Server tab."
|
||||
|
||||
struct ServiceLoginView: View {
|
||||
let info: TailnetInfo
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("byre's server on this Mac has its own Tailscale node, \(info.hostname). Sign it in to the same tailnet.")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Button("Sign in to Tailscale") {
|
||||
if let url = URL(string: info.authUrl) {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
.buttonStyle(WebButtonStyle())
|
||||
.disabled(URL(string: info.authUrl) == nil)
|
||||
Text("Waiting for the sign-in (\(info.state))…")
|
||||
.foregroundStyle(Palette.muted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Check: Decodable {
|
||||
let name: String
|
||||
let need: String
|
||||
@@ -478,7 +458,8 @@ struct ServerView: View {
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} else if let tailnet = state.tailnet {
|
||||
ServiceLoginView(info: tailnet)
|
||||
Text("Tailscale node \(tailnet.hostname): \(tailnet.state)")
|
||||
.foregroundStyle(Palette.muted)
|
||||
} else {
|
||||
Text("Not answering at \(loopback?.absoluteString ?? "its loopback address") yet.")
|
||||
.foregroundStyle(Palette.muted)
|
||||
|
||||
@@ -265,7 +265,6 @@ final class ConnectForm {
|
||||
var problem: String?
|
||||
var missing: [Check] = []
|
||||
var connecting = false
|
||||
var serviceLogin: TailnetInfo?
|
||||
|
||||
func connect() async -> URL? {
|
||||
guard let origin = parseOrigin(serverText) else {
|
||||
@@ -303,7 +302,6 @@ final class ConnectForm {
|
||||
try await waitForServer(origin)
|
||||
} catch {
|
||||
connecting = false
|
||||
serviceLogin = nil
|
||||
problem = errorText(error)
|
||||
return nil
|
||||
}
|
||||
@@ -315,13 +313,11 @@ final class ConnectForm {
|
||||
while true {
|
||||
let info = try? await Server.tailnet(loopback)
|
||||
if let info, info.state == "Running" {
|
||||
serviceLogin = nil
|
||||
guard let origin = URL(string: info.origin), !info.origin.isEmpty else {
|
||||
throw ProcessFailure(errorDescription: noCertificates)
|
||||
}
|
||||
return origin
|
||||
}
|
||||
serviceLogin = info
|
||||
if info == nil, Date().timeIntervalSince(started) > 10 {
|
||||
throw ProcessFailure(errorDescription: "byre was installed but \(loopback.absoluteString) didn't answer within 10 s.")
|
||||
}
|
||||
@@ -408,9 +404,6 @@ struct ConnectView: View {
|
||||
Button("Run byre on this Mac", action: installHere)
|
||||
}
|
||||
.disabled(form.connecting)
|
||||
if let login = form.serviceLogin {
|
||||
ServiceLoginView(info: login)
|
||||
}
|
||||
if let problem = form.problem {
|
||||
Text(problem)
|
||||
.foregroundStyle(Palette.danger)
|
||||
@@ -484,7 +477,7 @@ struct ServerWindow: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
if !Tailnet.shared.running {
|
||||
if !Tailnet.shared.signedIn {
|
||||
TailnetLoginView()
|
||||
} else if let connection {
|
||||
switch connection.phase {
|
||||
|
||||
+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