fix(bridge): prefer bonjour TXT displayName
This commit is contained in:
@@ -5,6 +5,7 @@ import Network
|
|||||||
actor BridgeClient {
|
actor BridgeClient {
|
||||||
private let encoder = JSONEncoder()
|
private let encoder = JSONEncoder()
|
||||||
private let decoder = JSONDecoder()
|
private let decoder = JSONDecoder()
|
||||||
|
private var lineBuffer = Data()
|
||||||
|
|
||||||
func pairAndHello(
|
func pairAndHello(
|
||||||
endpoint: NWEndpoint,
|
endpoint: NWEndpoint,
|
||||||
@@ -15,6 +16,7 @@ actor BridgeClient {
|
|||||||
existingToken: String?,
|
existingToken: String?,
|
||||||
onStatus: (@Sendable (String) -> Void)? = nil) async throws -> String
|
onStatus: (@Sendable (String) -> Void)? = nil) async throws -> String
|
||||||
{
|
{
|
||||||
|
self.lineBuffer = Data()
|
||||||
let connection = NWConnection(to: endpoint, using: .tcp)
|
let connection = NWConnection(to: endpoint, using: .tcp)
|
||||||
let queue = DispatchQueue(label: "com.steipete.clawdis.ios.bridge-client")
|
let queue = DispatchQueue(label: "com.steipete.clawdis.ios.bridge-client")
|
||||||
defer { connection.cancel() }
|
defer { connection.cancel() }
|
||||||
@@ -32,9 +34,8 @@ actor BridgeClient {
|
|||||||
version: version),
|
version: version),
|
||||||
over: connection)
|
over: connection)
|
||||||
|
|
||||||
var buffer = Data()
|
|
||||||
let first = try await self.withTimeout(seconds: 10, purpose: "hello") { () -> ReceivedFrame in
|
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: [
|
throw NSError(domain: "Bridge", code: 0, userInfo: [
|
||||||
NSLocalizedDescriptionKey: "Bridge closed connection during hello",
|
NSLocalizedDescriptionKey: "Bridge closed connection during hello",
|
||||||
])
|
])
|
||||||
@@ -66,7 +67,7 @@ actor BridgeClient {
|
|||||||
|
|
||||||
onStatus?("Waiting for approval…")
|
onStatus?("Waiting for approval…")
|
||||||
let ok = try await self.withTimeout(seconds: 60, purpose: "pairing 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 {
|
switch next.base.type {
|
||||||
case "pair-ok":
|
case "pair-ok":
|
||||||
return try self.decoder.decode(BridgePairOk.self, from: next.data)
|
return try self.decoder.decode(BridgePairOk.self, from: next.data)
|
||||||
@@ -110,8 +111,8 @@ actor BridgeClient {
|
|||||||
var data: Data
|
var data: Data
|
||||||
}
|
}
|
||||||
|
|
||||||
private func receiveFrame(over connection: NWConnection, buffer: inout Data) async throws -> ReceivedFrame? {
|
private func receiveFrame(over connection: NWConnection) async throws -> ReceivedFrame? {
|
||||||
guard let lineData = try await self.receiveLineData(over: connection, buffer: &buffer) else {
|
guard let lineData = try await self.receiveLineData(over: connection) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let base = try self.decoder.decode(BridgeBaseFrame.self, from: lineData)
|
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 {
|
while true {
|
||||||
if let idx = buffer.firstIndex(of: 0x0A) {
|
if let idx = self.lineBuffer.firstIndex(of: 0x0A) {
|
||||||
let line = buffer.prefix(upTo: idx)
|
let line = self.lineBuffer.prefix(upTo: idx)
|
||||||
buffer.removeSubrange(...idx)
|
self.lineBuffer.removeSubrange(...idx)
|
||||||
return Data(line)
|
return Data(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
let chunk = try await self.receiveChunk(over: connection)
|
let chunk = try await self.receiveChunk(over: connection)
|
||||||
if chunk.isEmpty { return nil }
|
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,
|
seconds: Int,
|
||||||
purpose: String,
|
purpose: String,
|
||||||
_ op: @escaping @Sendable () async throws -> T) async throws -> T
|
_ op: @escaping @Sendable () async throws -> T) async throws -> T
|
||||||
@@ -181,24 +182,33 @@ actor BridgeClient {
|
|||||||
|
|
||||||
private func startAndWaitForReady(_ connection: NWConnection, queue: DispatchQueue) async throws {
|
private func startAndWaitForReady(_ connection: NWConnection, queue: DispatchQueue) async throws {
|
||||||
try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation<Void, Error>) in
|
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
|
connection.stateUpdateHandler = { state in
|
||||||
if didResume { return }
|
|
||||||
switch state {
|
switch state {
|
||||||
case .ready:
|
case .ready:
|
||||||
didResume = true
|
if didResume.trySet() { cont.resume(returning: ()) }
|
||||||
cont.resume(returning: ())
|
|
||||||
case let .failed(err):
|
case let .failed(err):
|
||||||
didResume = true
|
if didResume.trySet() { cont.resume(throwing: err) }
|
||||||
cont.resume(throwing: err)
|
|
||||||
case let .waiting(err):
|
case let .waiting(err):
|
||||||
didResume = true
|
if didResume.trySet() { cont.resume(throwing: err) }
|
||||||
cont.resume(throwing: err)
|
|
||||||
case .cancelled:
|
case .cancelled:
|
||||||
didResume = true
|
if didResume.trySet() {
|
||||||
cont.resume(throwing: NSError(domain: "Bridge", code: 50, userInfo: [
|
cont.resume(throwing: NSError(domain: "Bridge", code: 50, userInfo: [
|
||||||
NSLocalizedDescriptionKey: "Connection cancelled",
|
NSLocalizedDescriptionKey: "Connection cancelled",
|
||||||
]))
|
]))
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ final class BridgeDiscoveryModel: ObservableObject {
|
|||||||
switch result.endpoint {
|
switch result.endpoint {
|
||||||
case let .service(name, _, _, _):
|
case let .service(name, _, _, _):
|
||||||
let decodedName = BonjourEscapes.decode(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(
|
return DiscoveredBridge(
|
||||||
name: prettyName,
|
name: prettyName,
|
||||||
endpoint: result.endpoint,
|
endpoint: result.endpoint,
|
||||||
@@ -80,6 +84,7 @@ final class BridgeDiscoveryModel: ObservableObject {
|
|||||||
private static func prettifyInstanceName(_ decodedName: String) -> String {
|
private static func prettifyInstanceName(_ decodedName: String) -> String {
|
||||||
let normalized = decodedName.split(whereSeparator: \.isWhitespace).joined(separator: " ")
|
let normalized = decodedName.split(whereSeparator: \.isWhitespace).joined(separator: " ")
|
||||||
let stripped = normalized.replacingOccurrences(of: " (Clawdis)", with: "")
|
let stripped = normalized.replacingOccurrences(of: " (Clawdis)", with: "")
|
||||||
|
.replacingOccurrences(of: #"\s+\(\d+\)$"#, with: "", options: .regularExpression)
|
||||||
return stripped.trimmingCharacters(in: .whitespacesAndNewlines)
|
return stripped.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ enum BridgeEndpointID {
|
|||||||
let normalizedName = Self.normalizeServiceNameForID(name)
|
let normalizedName = Self.normalizeServiceNameForID(name)
|
||||||
return "\(type)|\(domain)|\(normalizedName)"
|
return "\(type)|\(domain)|\(normalizedName)"
|
||||||
default:
|
default:
|
||||||
String(describing: endpoint)
|
return String(describing: endpoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ function safeServiceName(name: string) {
|
|||||||
return trimmed.length > 0 ? trimmed : "Clawdis";
|
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 = {
|
type BonjourService = {
|
||||||
advertise: () => Promise<void>;
|
advertise: () => Promise<void>;
|
||||||
destroy: () => Promise<void>;
|
destroy: () => Promise<void>;
|
||||||
@@ -52,11 +57,13 @@ export async function startGatewayBonjourAdvertiser(
|
|||||||
typeof opts.instanceName === "string" && opts.instanceName.trim()
|
typeof opts.instanceName === "string" && opts.instanceName.trim()
|
||||||
? opts.instanceName.trim()
|
? opts.instanceName.trim()
|
||||||
: `${hostname} (Clawdis)`;
|
: `${hostname} (Clawdis)`;
|
||||||
|
const displayName = prettifyInstanceName(instanceName);
|
||||||
|
|
||||||
const txtBase: Record<string, string> = {
|
const txtBase: Record<string, string> = {
|
||||||
role: "master",
|
role: "master",
|
||||||
gatewayPort: String(opts.gatewayPort),
|
gatewayPort: String(opts.gatewayPort),
|
||||||
lanHost: `${hostname}.local`,
|
lanHost: `${hostname}.local`,
|
||||||
|
displayName,
|
||||||
};
|
};
|
||||||
if (typeof opts.bridgePort === "number" && opts.bridgePort > 0) {
|
if (typeof opts.bridgePort === "number" && opts.bridgePort > 0) {
|
||||||
txtBase.bridgePort = String(opts.bridgePort);
|
txtBase.bridgePort = String(opts.bridgePort);
|
||||||
|
|||||||
Reference in New Issue
Block a user