feat(ios): unify manual bridge config and auto-reconnect
This commit is contained in:
@@ -52,9 +52,7 @@ final class BridgeConnectionController: ObservableObject {
|
||||
guard appModel.bridgeServerName == nil else { return }
|
||||
|
||||
let defaults = UserDefaults.standard
|
||||
let targetStableID = defaults.string(forKey: "bridge.lastDiscoveredStableID")?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !targetStableID.isEmpty else { return }
|
||||
let manualEnabled = defaults.bool(forKey: "bridge.manual.enabled")
|
||||
|
||||
let instanceId = defaults.string(forKey: "node.instanceId")?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
@@ -66,6 +64,26 @@ final class BridgeConnectionController: ObservableObject {
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !token.isEmpty else { return }
|
||||
|
||||
if manualEnabled {
|
||||
let manualHost = defaults.string(forKey: "bridge.manual.host")?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !manualHost.isEmpty else { return }
|
||||
|
||||
let manualPort = defaults.integer(forKey: "bridge.manual.port")
|
||||
let resolvedPort = manualPort > 0 ? manualPort : 18790
|
||||
guard let port = NWEndpoint.Port(rawValue: UInt16(resolvedPort)) else { return }
|
||||
|
||||
self.didAutoConnect = true
|
||||
appModel.connectToBridge(
|
||||
endpoint: .hostPort(host: NWEndpoint.Host(manualHost), port: port),
|
||||
hello: self.makeHello(token: token))
|
||||
return
|
||||
}
|
||||
|
||||
let targetStableID = defaults.string(forKey: "bridge.lastDiscoveredStableID")?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !targetStableID.isEmpty else { return }
|
||||
|
||||
guard let target = self.bridges.first(where: { $0.stableID == targetStableID }) else { return }
|
||||
|
||||
self.didAutoConnect = true
|
||||
|
||||
@@ -54,52 +54,72 @@ final class NodeAppModel: ObservableObject {
|
||||
hello: BridgeHello)
|
||||
{
|
||||
self.bridgeTask?.cancel()
|
||||
self.bridgeStatusText = "Connecting…"
|
||||
self.bridgeServerName = nil
|
||||
self.bridgeRemoteAddress = nil
|
||||
self.connectedBridgeID = BridgeEndpointID.stableID(endpoint)
|
||||
|
||||
self.bridgeTask = Task {
|
||||
do {
|
||||
try await self.bridge.connect(
|
||||
endpoint: endpoint,
|
||||
hello: hello,
|
||||
onConnected: { [weak self] serverName in
|
||||
guard let self else { return }
|
||||
await MainActor.run {
|
||||
self.bridgeStatusText = "Connected"
|
||||
self.bridgeServerName = serverName
|
||||
}
|
||||
if let addr = await self.bridge.currentRemoteAddress() {
|
||||
await MainActor.run {
|
||||
self.bridgeRemoteAddress = addr
|
||||
}
|
||||
}
|
||||
},
|
||||
onInvoke: { [weak self] req in
|
||||
guard let self else {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdisNodeError(code: .unavailable, message: "UNAVAILABLE: node not ready"))
|
||||
}
|
||||
return await self.handleInvoke(req)
|
||||
})
|
||||
var attempt = 0
|
||||
while !Task.isCancelled {
|
||||
await MainActor.run {
|
||||
if attempt == 0 {
|
||||
self.bridgeStatusText = "Connecting…"
|
||||
} else {
|
||||
self.bridgeStatusText = "Reconnecting…"
|
||||
}
|
||||
self.bridgeServerName = nil
|
||||
self.bridgeRemoteAddress = nil
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
self.bridgeStatusText = "Disconnected"
|
||||
self.bridgeServerName = nil
|
||||
self.bridgeRemoteAddress = nil
|
||||
self.connectedBridgeID = nil
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.bridgeStatusText = "Bridge error: \(error.localizedDescription)"
|
||||
self.bridgeServerName = nil
|
||||
self.bridgeRemoteAddress = nil
|
||||
self.connectedBridgeID = nil
|
||||
do {
|
||||
try await self.bridge.connect(
|
||||
endpoint: endpoint,
|
||||
hello: hello,
|
||||
onConnected: { [weak self] serverName in
|
||||
guard let self else { return }
|
||||
await MainActor.run {
|
||||
self.bridgeStatusText = "Connected"
|
||||
self.bridgeServerName = serverName
|
||||
}
|
||||
if let addr = await self.bridge.currentRemoteAddress() {
|
||||
await MainActor.run {
|
||||
self.bridgeRemoteAddress = addr
|
||||
}
|
||||
}
|
||||
},
|
||||
onInvoke: { [weak self] req in
|
||||
guard let self else {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: ClawdisNodeError(code: .unavailable, message: "UNAVAILABLE: node not ready"))
|
||||
}
|
||||
return await self.handleInvoke(req)
|
||||
})
|
||||
|
||||
if Task.isCancelled { break }
|
||||
attempt += 1
|
||||
let sleepSeconds = min(6.0, 0.35 * pow(1.7, Double(attempt)))
|
||||
try? await Task.sleep(nanoseconds: UInt64(sleepSeconds * 1_000_000_000))
|
||||
} catch {
|
||||
if Task.isCancelled { break }
|
||||
attempt += 1
|
||||
await MainActor.run {
|
||||
self.bridgeStatusText = "Bridge error: \(error.localizedDescription)"
|
||||
self.bridgeServerName = nil
|
||||
self.bridgeRemoteAddress = nil
|
||||
}
|
||||
let sleepSeconds = min(8.0, 0.5 * pow(1.7, Double(attempt)))
|
||||
try? await Task.sleep(nanoseconds: UInt64(sleepSeconds * 1_000_000_000))
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
self.bridgeStatusText = "Disconnected"
|
||||
self.bridgeServerName = nil
|
||||
self.bridgeRemoteAddress = nil
|
||||
self.connectedBridgeID = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ClawdisKit
|
||||
import Network
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
@@ -20,6 +21,9 @@ struct SettingsTab: View {
|
||||
@AppStorage("camera.enabled") private var cameraEnabled: Bool = true
|
||||
@AppStorage("bridge.preferredStableID") private var preferredBridgeStableID: String = ""
|
||||
@AppStorage("bridge.lastDiscoveredStableID") private var lastDiscoveredBridgeStableID: String = ""
|
||||
@AppStorage("bridge.manual.enabled") private var manualBridgeEnabled: Bool = false
|
||||
@AppStorage("bridge.manual.host") private var manualBridgeHost: String = ""
|
||||
@AppStorage("bridge.manual.port") private var manualBridgePort: Int = 18790
|
||||
@StateObject private var connectStatus = ConnectStatusStore()
|
||||
@State private var connectingBridgeID: String?
|
||||
@State private var localIPAddress: String?
|
||||
@@ -114,6 +118,40 @@ struct SettingsTab: View {
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
DisclosureGroup("Advanced") {
|
||||
Toggle("Use Manual Bridge", isOn: self.$manualBridgeEnabled)
|
||||
|
||||
TextField("Host", text: self.$manualBridgeHost)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
|
||||
TextField("Port", value: self.$manualBridgePort, format: .number)
|
||||
.keyboardType(.numberPad)
|
||||
|
||||
Button {
|
||||
Task { await self.connectManual() }
|
||||
} label: {
|
||||
if self.connectingBridgeID == "manual" {
|
||||
HStack(spacing: 8) {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
Text("Connecting…")
|
||||
}
|
||||
} else {
|
||||
Text("Connect (Manual)")
|
||||
}
|
||||
}
|
||||
.disabled(self.connectingBridgeID != nil || self.manualBridgeHost
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.isEmpty || self.manualBridgePort <= 0 || self.manualBridgePort > 65535)
|
||||
|
||||
Text(
|
||||
"Use this when mDNS/Bonjour discovery is blocked. "
|
||||
+ "The bridge runs on the gateway (default port 18790).")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
@@ -206,6 +244,7 @@ struct SettingsTab: View {
|
||||
|
||||
private func connect(_ bridge: BridgeDiscoveryModel.DiscoveredBridge) async {
|
||||
self.connectingBridgeID = bridge.id
|
||||
self.manualBridgeEnabled = false
|
||||
self.preferredBridgeStableID = bridge.stableID
|
||||
BridgeSettingsStore.savePreferredBridgeStableID(bridge.stableID)
|
||||
self.lastDiscoveredBridgeStableID = bridge.stableID
|
||||
@@ -257,6 +296,72 @@ struct SettingsTab: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func connectManual() async {
|
||||
let host = self.manualBridgeHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !host.isEmpty else {
|
||||
self.connectStatus.text = "Failed: host required"
|
||||
return
|
||||
}
|
||||
guard self.manualBridgePort > 0, self.manualBridgePort <= 65535 else {
|
||||
self.connectStatus.text = "Failed: invalid port"
|
||||
return
|
||||
}
|
||||
guard let port = NWEndpoint.Port(rawValue: UInt16(self.manualBridgePort)) else {
|
||||
self.connectStatus.text = "Failed: invalid port"
|
||||
return
|
||||
}
|
||||
|
||||
self.connectingBridgeID = "manual"
|
||||
self.manualBridgeEnabled = true
|
||||
defer { self.connectingBridgeID = nil }
|
||||
|
||||
let endpoint: NWEndpoint = .hostPort(host: NWEndpoint.Host(host), port: port)
|
||||
|
||||
do {
|
||||
let existing = KeychainStore.loadString(
|
||||
service: "com.steipete.clawdis.bridge",
|
||||
account: self.keychainAccount())
|
||||
let existingToken = (existing?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ?
|
||||
existing :
|
||||
nil
|
||||
|
||||
let hello = BridgeHello(
|
||||
nodeId: self.instanceId,
|
||||
displayName: self.displayName,
|
||||
token: existingToken,
|
||||
platform: self.platformString(),
|
||||
version: self.appVersion())
|
||||
let token = try await BridgeClient().pairAndHello(
|
||||
endpoint: endpoint,
|
||||
hello: hello,
|
||||
onStatus: { status in
|
||||
let store = self.connectStatus
|
||||
Task { @MainActor in
|
||||
store.text = status
|
||||
}
|
||||
})
|
||||
|
||||
if !token.isEmpty, token != existingToken {
|
||||
_ = KeychainStore.saveString(
|
||||
token,
|
||||
service: "com.steipete.clawdis.bridge",
|
||||
account: self.keychainAccount())
|
||||
}
|
||||
|
||||
self.appModel.connectToBridge(
|
||||
endpoint: endpoint,
|
||||
hello: BridgeHello(
|
||||
nodeId: self.instanceId,
|
||||
displayName: self.displayName,
|
||||
token: token,
|
||||
platform: self.platformString(),
|
||||
version: self.appVersion()))
|
||||
|
||||
} catch {
|
||||
self.connectStatus.text = "Failed: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
private static func primaryIPv4Address() -> String? {
|
||||
var addrList: UnsafeMutablePointer<ifaddrs>?
|
||||
guard getifaddrs(&addrList) == 0, let first = addrList else { return nil }
|
||||
|
||||
Reference in New Issue
Block a user