fix(bridge): prefer bonjour TXT displayName

This commit is contained in:
Peter Steinberger
2025-12-13 18:31:06 +00:00
parent 537c515dde
commit 3b853b329f
4 changed files with 47 additions and 25 deletions

View File

@@ -5,6 +5,7 @@ import Network
actor BridgeClient {
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
private var lineBuffer = Data()
func pairAndHello(
endpoint: NWEndpoint,
@@ -15,6 +16,7 @@ actor BridgeClient {
existingToken: String?,
onStatus: (@Sendable (String) -> Void)? = nil) async throws -> String
{
self.lineBuffer = Data()
let connection = NWConnection(to: endpoint, using: .tcp)
let queue = DispatchQueue(label: "com.steipete.clawdis.ios.bridge-client")
defer { connection.cancel() }
@@ -32,9 +34,8 @@ actor BridgeClient {
version: version),
over: connection)
var buffer = Data()
let first = try await self.withTimeout(seconds: 10, purpose: "hello") { () -> ReceivedFrame in
guard let frame = try await self.receiveFrame(over: connection, buffer: &buffer) else {
guard let frame = try await self.receiveFrame(over: connection) else {
throw NSError(domain: "Bridge", code: 0, userInfo: [
NSLocalizedDescriptionKey: "Bridge closed connection during hello",
])
@@ -66,7 +67,7 @@ actor BridgeClient {
onStatus?("Waiting for approval…")
let ok = try await self.withTimeout(seconds: 60, purpose: "pairing approval") {
while let next = try await self.receiveFrame(over: connection, buffer: &buffer) {
while let next = try await self.receiveFrame(over: connection) {
switch next.base.type {
case "pair-ok":
return try self.decoder.decode(BridgePairOk.self, from: next.data)
@@ -110,8 +111,8 @@ actor BridgeClient {
var data: Data
}
private func receiveFrame(over connection: NWConnection, buffer: inout Data) async throws -> ReceivedFrame? {
guard let lineData = try await self.receiveLineData(over: connection, buffer: &buffer) else {
private func receiveFrame(over connection: NWConnection) async throws -> ReceivedFrame? {
guard let lineData = try await self.receiveLineData(over: connection) else {
return nil
}
let base = try self.decoder.decode(BridgeBaseFrame.self, from: lineData)
@@ -134,17 +135,17 @@ actor BridgeClient {
}
}
private func receiveLineData(over connection: NWConnection, buffer: inout Data) async throws -> Data? {
private func receiveLineData(over connection: NWConnection) async throws -> Data? {
while true {
if let idx = buffer.firstIndex(of: 0x0A) {
let line = buffer.prefix(upTo: idx)
buffer.removeSubrange(...idx)
if let idx = self.lineBuffer.firstIndex(of: 0x0A) {
let line = self.lineBuffer.prefix(upTo: idx)
self.lineBuffer.removeSubrange(...idx)
return Data(line)
}
let chunk = try await self.receiveChunk(over: connection)
if chunk.isEmpty { return nil }
buffer.append(chunk)
self.lineBuffer.append(chunk)
}
}
@@ -160,7 +161,7 @@ actor BridgeClient {
}
}
private func withTimeout<T>(
private func withTimeout<T: Sendable>(
seconds: Int,
purpose: String,
_ op: @escaping @Sendable () async throws -> T) async throws -> T
@@ -181,24 +182,33 @@ actor BridgeClient {
private func startAndWaitForReady(_ connection: NWConnection, queue: DispatchQueue) async throws {
try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation<Void, Error>) in
var didResume = false
final class ResumeFlag: @unchecked Sendable {
private let lock = NSLock()
private var value = false
func trySet() -> Bool {
self.lock.lock()
defer { self.lock.unlock() }
if self.value { return false }
self.value = true
return true
}
}
let didResume = ResumeFlag()
connection.stateUpdateHandler = { state in
if didResume { return }
switch state {
case .ready:
didResume = true
cont.resume(returning: ())
if didResume.trySet() { cont.resume(returning: ()) }
case let .failed(err):
didResume = true
cont.resume(throwing: err)
if didResume.trySet() { cont.resume(throwing: err) }
case let .waiting(err):
didResume = true
cont.resume(throwing: err)
if didResume.trySet() { cont.resume(throwing: err) }
case .cancelled:
didResume = true
cont.resume(throwing: NSError(domain: "Bridge", code: 50, userInfo: [
NSLocalizedDescriptionKey: "Connection cancelled",
]))
if didResume.trySet() {
cont.resume(throwing: NSError(domain: "Bridge", code: 50, userInfo: [
NSLocalizedDescriptionKey: "Connection cancelled",
]))
}
default:
break
}

View File

@@ -52,7 +52,11 @@ final class BridgeDiscoveryModel: ObservableObject {
switch result.endpoint {
case let .service(name, _, _, _):
let decodedName = BonjourEscapes.decode(name)
let prettyName = Self.prettifyInstanceName(decodedName)
let advertisedName = result.endpoint.txtRecord?.dictionary["displayName"]
let prettyAdvertised = advertisedName
.map(Self.prettifyInstanceName)
.flatMap { $0.isEmpty ? nil : $0 }
let prettyName = prettyAdvertised ?? Self.prettifyInstanceName(decodedName)
return DiscoveredBridge(
name: prettyName,
endpoint: result.endpoint,
@@ -80,6 +84,7 @@ final class BridgeDiscoveryModel: ObservableObject {
private static func prettifyInstanceName(_ decodedName: String) -> String {
let normalized = decodedName.split(whereSeparator: \.isWhitespace).joined(separator: " ")
let stripped = normalized.replacingOccurrences(of: " (Clawdis)", with: "")
.replacingOccurrences(of: #"\s+\(\d+\)$"#, with: "", options: .regularExpression)
return stripped.trimmingCharacters(in: .whitespacesAndNewlines)
}
}

View File

@@ -10,7 +10,7 @@ enum BridgeEndpointID {
let normalizedName = Self.normalizeServiceNameForID(name)
return "\(type)|\(domain)|\(normalizedName)"
default:
String(describing: endpoint)
return String(describing: endpoint)
}
}

View File

@@ -24,6 +24,11 @@ function safeServiceName(name: string) {
return trimmed.length > 0 ? trimmed : "Clawdis";
}
function prettifyInstanceName(name: string) {
const normalized = name.trim().replace(/\s+/g, " ");
return normalized.replace(/\s+\(Clawdis\)\s*$/i, "").trim() || normalized;
}
type BonjourService = {
advertise: () => Promise<void>;
destroy: () => Promise<void>;
@@ -52,11 +57,13 @@ export async function startGatewayBonjourAdvertiser(
typeof opts.instanceName === "string" && opts.instanceName.trim()
? opts.instanceName.trim()
: `${hostname} (Clawdis)`;
const displayName = prettifyInstanceName(instanceName);
const txtBase: Record<string, string> = {
role: "master",
gatewayPort: String(opts.gatewayPort),
lanHost: `${hostname}.local`,
displayName,
};
if (typeof opts.bridgePort === "number" && opts.bridgePort > 0) {
txtBase.bridgePort = String(opts.bridgePort);