From c1985443fda616ad412dd0a2717077e5d8fb61dd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 17 Dec 2025 17:22:44 +0100 Subject: [PATCH] macOS: fix gateway strict-concurrency issues --- .../Sources/Clawdis/Bridge/BridgeServer.swift | 63 ++++++++----------- .../macos/Sources/Clawdis/CanvasManager.swift | 6 -- .../macos/Sources/Clawdis/CronJobsStore.swift | 6 +- apps/macos/Sources/Clawdis/CronSettings.swift | 8 +-- .../Sources/Clawdis/GatewayConnection.swift | 6 +- .../Clawdis/GatewayProcessManager.swift | 25 +++++--- .../Sources/Clawdis/String+NonEmpty.swift | 9 +++ 7 files changed, 61 insertions(+), 62 deletions(-) create mode 100644 apps/macos/Sources/Clawdis/String+NonEmpty.swift diff --git a/apps/macos/Sources/Clawdis/Bridge/BridgeServer.swift b/apps/macos/Sources/Clawdis/Bridge/BridgeServer.swift index a874c7bb4..897684b50 100644 --- a/apps/macos/Sources/Clawdis/Bridge/BridgeServer.swift +++ b/apps/macos/Sources/Clawdis/Bridge/BridgeServer.swift @@ -328,39 +328,35 @@ actor BridgeServer { } private func beaconPresence(nodeId: String, reason: String) async { - do { - let paired = await self.store?.find(nodeId: nodeId) - let host = paired?.displayName?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - ?? nodeId - let version = paired?.version?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - let platform = paired?.platform?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - let ip = await self.connections[nodeId]?.remoteAddress() + let paired = await self.store?.find(nodeId: nodeId) + let host = paired?.displayName?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty + ?? nodeId + let version = paired?.version?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty + let platform = paired?.platform?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty + let ip = await self.connections[nodeId]?.remoteAddress() - var tags: [String] = ["node", "ios"] - if let platform { tags.append(platform) } + var tags: [String] = ["node", "ios"] + if let platform { tags.append(platform) } - let summary = [ - "Node: \(host)\(ip.map { " (\($0))" } ?? "")", - platform.map { "platform \($0)" }, - version.map { "app \($0)" }, - "mode node", - "reason \(reason)", - ].compactMap(\.self).joined(separator: " · ") + let summary = [ + "Node: \(host)\(ip.map { " (\($0))" } ?? "")", + platform.map { "platform \($0)" }, + version.map { "app \($0)" }, + "mode node", + "reason \(reason)", + ].compactMap(\.self).joined(separator: " · ") - var params: [String: AnyCodable] = [ - "text": AnyCodable(summary), - "instanceId": AnyCodable(nodeId), - "host": AnyCodable(host), - "mode": AnyCodable("node"), - "reason": AnyCodable(reason), - "tags": AnyCodable(tags), - ] - if let ip { params["ip"] = AnyCodable(ip) } - if let version { params["version"] = AnyCodable(version) } - await GatewayConnection.shared.sendSystemEvent(params) - } catch { - // Best-effort only. - } + var params: [String: AnyCodable] = [ + "text": AnyCodable(summary), + "instanceId": AnyCodable(nodeId), + "host": AnyCodable(host), + "mode": AnyCodable("node"), + "reason": AnyCodable(reason), + "tags": AnyCodable(tags), + ] + if let ip { params["ip"] = AnyCodable(ip) } + if let version { params["version"] = AnyCodable(version) } + await GatewayConnection.shared.sendSystemEvent(params) } private func startPresenceTask(nodeId: String) { @@ -469,10 +465,3 @@ enum BridgePairingApprover { } } } - -extension String { - fileprivate var nonEmpty: String? { - let trimmed = trimmingCharacters(in: .whitespacesAndNewlines) - return trimmed.isEmpty ? nil : trimmed - } -} diff --git a/apps/macos/Sources/Clawdis/CanvasManager.swift b/apps/macos/Sources/Clawdis/CanvasManager.swift index 0fe061e11..c5b60a0d2 100644 --- a/apps/macos/Sources/Clawdis/CanvasManager.swift +++ b/apps/macos/Sources/Clawdis/CanvasManager.swift @@ -215,9 +215,3 @@ final class CanvasManager { return FileManager.default.fileExists(atPath: index.path) } } - -private extension String { - var nonEmpty: String? { - isEmpty ? nil : self - } -} diff --git a/apps/macos/Sources/Clawdis/CronJobsStore.swift b/apps/macos/Sources/Clawdis/CronJobsStore.swift index 158d05cce..e48cd6c55 100644 --- a/apps/macos/Sources/Clawdis/CronJobsStore.swift +++ b/apps/macos/Sources/Clawdis/CronJobsStore.swift @@ -118,7 +118,9 @@ final class CronJobsStore { func setJobEnabled(id: String, enabled: Bool) async { do { - try await GatewayConnection.shared.cronUpdate(jobId: id, patch: ["enabled": enabled]) + try await GatewayConnection.shared.cronUpdate( + jobId: id, + patch: ["enabled": AnyCodable(enabled)]) await self.refreshJobs() } catch { self.lastError = error.localizedDescription @@ -127,7 +129,7 @@ final class CronJobsStore { func upsertJob( id: String?, - payload: [String: Any]) async throws + payload: [String: AnyCodable]) async throws { if let id { try await GatewayConnection.shared.cronUpdate(jobId: id, patch: payload) diff --git a/apps/macos/Sources/Clawdis/CronSettings.swift b/apps/macos/Sources/Clawdis/CronSettings.swift index dd913131b..3d8875623 100644 --- a/apps/macos/Sources/Clawdis/CronSettings.swift +++ b/apps/macos/Sources/Clawdis/CronSettings.swift @@ -454,7 +454,7 @@ struct CronSettings: View { return "in \(days)d" } - private func save(payload: [String: Any]) async { + private func save(payload: [String: AnyCodable]) async { guard !self.isSaving else { return } self.isSaving = true self.editorError = nil @@ -494,7 +494,7 @@ struct CronJobEditor: View { @Binding var isSaving: Bool @Binding var error: String? let onCancel: () -> Void - let onSave: ([String: Any]) -> Void + let onSave: ([String: AnyCodable]) -> Void private let labelColumnWidth: CGFloat = 160 private static let introText = @@ -879,7 +879,7 @@ struct CronJobEditor: View { } } - private func buildPayload() throws -> [String: Any] { + private func buildPayload() throws -> [String: AnyCodable] { let name = self.name.trimmingCharacters(in: .whitespacesAndNewlines) let schedule: [String: Any] switch self.scheduleKind { @@ -969,7 +969,7 @@ struct CronJobEditor: View { ] } - return root + return root.mapValues { AnyCodable($0) } } private func buildAgentTurnPayload() -> [String: Any] { diff --git a/apps/macos/Sources/Clawdis/GatewayConnection.swift b/apps/macos/Sources/Clawdis/GatewayConnection.swift index 1871d35a5..2b7abb4cc 100644 --- a/apps/macos/Sources/Clawdis/GatewayConnection.swift +++ b/apps/macos/Sources/Clawdis/GatewayConnection.swift @@ -418,13 +418,13 @@ extension GatewayConnection { try await self.requestVoid(method: .cronRemove, params: ["id": AnyCodable(jobId)]) } - func cronUpdate(jobId: String, patch: [String: Any]) async throws { + func cronUpdate(jobId: String, patch: [String: AnyCodable]) async throws { try await self.requestVoid( method: .cronUpdate, params: ["id": AnyCodable(jobId), "patch": AnyCodable(patch)]) } - func cronAdd(payload: [String: Any]) async throws { - try await self.requestVoid(method: .cronAdd, params: payload.mapValues { AnyCodable($0) }) + func cronAdd(payload: [String: AnyCodable]) async throws { + try await self.requestVoid(method: .cronAdd, params: payload) } } diff --git a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift index ce072c11e..441b84be6 100644 --- a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift +++ b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift @@ -152,22 +152,27 @@ final class GatewayProcessManager { private func attachExistingGatewayIfAvailable() async -> Bool { let port = GatewayEnvironment.gatewayPort() do { + let data = try await GatewayConnection.shared.requestRaw(method: .health, timeoutMs: 2000) + let snap = decodeHealthSnapshot(from: data) + + let instance = await PortGuardian.shared.describe(port: port) + let instanceText: String + if let instance { + let path = instance.executablePath ?? "path unknown" + instanceText = "pid \(instance.pid) \(instance.command) @ \(path)" + } else { + instanceText = "pid unknown" + } + let details: String - if let snap = try? await GatewayConnection.shared.healthSnapshot() { + if let snap { let linked = snap.web.linked ? "linked" : "not linked" let authAge = snap.web.authAgeMs.flatMap(msToAge) ?? "unknown age" - let instance = await PortGuardian.shared.describe(port: port) - let instanceText: String - if let instance { - let path = instance.executablePath ?? "path unknown" - instanceText = "pid \(instance.pid) \(instance.command) @ \(path)" - } else { - instanceText = "pid unknown" - } details = "port \(port), \(linked), auth \(authAge), \(instanceText)" } else { - details = "port \(port), health probe succeeded" + details = "port \(port), health probe succeeded, \(instanceText)" } + self.existingGatewayDetails = details self.status = .attachedExisting(details: details) self.appendLog("[gateway] using existing instance: \(details)\n") diff --git a/apps/macos/Sources/Clawdis/String+NonEmpty.swift b/apps/macos/Sources/Clawdis/String+NonEmpty.swift new file mode 100644 index 000000000..a5d208e2e --- /dev/null +++ b/apps/macos/Sources/Clawdis/String+NonEmpty.swift @@ -0,0 +1,9 @@ +import Foundation + +extension String { + var nonEmpty: String? { + let trimmed = self.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } +} +