macOS: fix gateway strict-concurrency issues

main
Peter Steinberger 2025-12-17 17:22:44 +01:00
parent 17a27fd312
commit c1985443fd
7 changed files with 61 additions and 62 deletions

View File

@ -328,7 +328,6 @@ actor BridgeServer {
} }
private func beaconPresence(nodeId: String, reason: String) async { private func beaconPresence(nodeId: String, reason: String) async {
do {
let paired = await self.store?.find(nodeId: nodeId) let paired = await self.store?.find(nodeId: nodeId)
let host = paired?.displayName?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty let host = paired?.displayName?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
?? nodeId ?? nodeId
@ -358,9 +357,6 @@ actor BridgeServer {
if let ip { params["ip"] = AnyCodable(ip) } if let ip { params["ip"] = AnyCodable(ip) }
if let version { params["version"] = AnyCodable(version) } if let version { params["version"] = AnyCodable(version) }
await GatewayConnection.shared.sendSystemEvent(params) await GatewayConnection.shared.sendSystemEvent(params)
} catch {
// Best-effort only.
}
} }
private func startPresenceTask(nodeId: String) { 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
}
}

View File

@ -215,9 +215,3 @@ final class CanvasManager {
return FileManager.default.fileExists(atPath: index.path) return FileManager.default.fileExists(atPath: index.path)
} }
} }
private extension String {
var nonEmpty: String? {
isEmpty ? nil : self
}
}

View File

@ -118,7 +118,9 @@ final class CronJobsStore {
func setJobEnabled(id: String, enabled: Bool) async { func setJobEnabled(id: String, enabled: Bool) async {
do { 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() await self.refreshJobs()
} catch { } catch {
self.lastError = error.localizedDescription self.lastError = error.localizedDescription
@ -127,7 +129,7 @@ final class CronJobsStore {
func upsertJob( func upsertJob(
id: String?, id: String?,
payload: [String: Any]) async throws payload: [String: AnyCodable]) async throws
{ {
if let id { if let id {
try await GatewayConnection.shared.cronUpdate(jobId: id, patch: payload) try await GatewayConnection.shared.cronUpdate(jobId: id, patch: payload)

View File

@ -454,7 +454,7 @@ struct CronSettings: View {
return "in \(days)d" return "in \(days)d"
} }
private func save(payload: [String: Any]) async { private func save(payload: [String: AnyCodable]) async {
guard !self.isSaving else { return } guard !self.isSaving else { return }
self.isSaving = true self.isSaving = true
self.editorError = nil self.editorError = nil
@ -494,7 +494,7 @@ struct CronJobEditor: View {
@Binding var isSaving: Bool @Binding var isSaving: Bool
@Binding var error: String? @Binding var error: String?
let onCancel: () -> Void let onCancel: () -> Void
let onSave: ([String: Any]) -> Void let onSave: ([String: AnyCodable]) -> Void
private let labelColumnWidth: CGFloat = 160 private let labelColumnWidth: CGFloat = 160
private static let introText = 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 name = self.name.trimmingCharacters(in: .whitespacesAndNewlines)
let schedule: [String: Any] let schedule: [String: Any]
switch self.scheduleKind { switch self.scheduleKind {
@ -969,7 +969,7 @@ struct CronJobEditor: View {
] ]
} }
return root return root.mapValues { AnyCodable($0) }
} }
private func buildAgentTurnPayload() -> [String: Any] { private func buildAgentTurnPayload() -> [String: Any] {

View File

@ -418,13 +418,13 @@ extension GatewayConnection {
try await self.requestVoid(method: .cronRemove, params: ["id": AnyCodable(jobId)]) 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( try await self.requestVoid(
method: .cronUpdate, method: .cronUpdate,
params: ["id": AnyCodable(jobId), "patch": AnyCodable(patch)]) params: ["id": AnyCodable(jobId), "patch": AnyCodable(patch)])
} }
func cronAdd(payload: [String: Any]) async throws { func cronAdd(payload: [String: AnyCodable]) async throws {
try await self.requestVoid(method: .cronAdd, params: payload.mapValues { AnyCodable($0) }) try await self.requestVoid(method: .cronAdd, params: payload)
} }
} }

View File

@ -152,10 +152,9 @@ final class GatewayProcessManager {
private func attachExistingGatewayIfAvailable() async -> Bool { private func attachExistingGatewayIfAvailable() async -> Bool {
let port = GatewayEnvironment.gatewayPort() let port = GatewayEnvironment.gatewayPort()
do { do {
let details: String let data = try await GatewayConnection.shared.requestRaw(method: .health, timeoutMs: 2000)
if let snap = try? await GatewayConnection.shared.healthSnapshot() { let snap = decodeHealthSnapshot(from: data)
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 instance = await PortGuardian.shared.describe(port: port)
let instanceText: String let instanceText: String
if let instance { if let instance {
@ -164,10 +163,16 @@ final class GatewayProcessManager {
} else { } else {
instanceText = "pid unknown" instanceText = "pid unknown"
} }
let details: String
if let snap {
let linked = snap.web.linked ? "linked" : "not linked"
let authAge = snap.web.authAgeMs.flatMap(msToAge) ?? "unknown age"
details = "port \(port), \(linked), auth \(authAge), \(instanceText)" details = "port \(port), \(linked), auth \(authAge), \(instanceText)"
} else { } else {
details = "port \(port), health probe succeeded" details = "port \(port), health probe succeeded, \(instanceText)"
} }
self.existingGatewayDetails = details self.existingGatewayDetails = details
self.status = .attachedExisting(details: details) self.status = .attachedExisting(details: details)
self.appendLog("[gateway] using existing instance: \(details)\n") self.appendLog("[gateway] using existing instance: \(details)\n")

View File

@ -0,0 +1,9 @@
import Foundation
extension String {
var nonEmpty: String? {
let trimmed = self.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
}