macOS: fix gateway strict-concurrency issues

This commit is contained in:
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,39 +328,35 @@ 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 let version = paired?.version?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
let version = paired?.version?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty let platform = paired?.platform?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
let platform = paired?.platform?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty let ip = await self.connections[nodeId]?.remoteAddress()
let ip = await self.connections[nodeId]?.remoteAddress()
var tags: [String] = ["node", "ios"] var tags: [String] = ["node", "ios"]
if let platform { tags.append(platform) } if let platform { tags.append(platform) }
let summary = [ let summary = [
"Node: \(host)\(ip.map { " (\($0))" } ?? "")", "Node: \(host)\(ip.map { " (\($0))" } ?? "")",
platform.map { "platform \($0)" }, platform.map { "platform \($0)" },
version.map { "app \($0)" }, version.map { "app \($0)" },
"mode node", "mode node",
"reason \(reason)", "reason \(reason)",
].compactMap(\.self).joined(separator: " · ") ].compactMap(\.self).joined(separator: " · ")
var params: [String: AnyCodable] = [ var params: [String: AnyCodable] = [
"text": AnyCodable(summary), "text": AnyCodable(summary),
"instanceId": AnyCodable(nodeId), "instanceId": AnyCodable(nodeId),
"host": AnyCodable(host), "host": AnyCodable(host),
"mode": AnyCodable("node"), "mode": AnyCodable("node"),
"reason": AnyCodable(reason), "reason": AnyCodable(reason),
"tags": AnyCodable(tags), "tags": AnyCodable(tags),
] ]
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,22 +152,27 @@ final class GatewayProcessManager {
private func attachExistingGatewayIfAvailable() async -> Bool { private func attachExistingGatewayIfAvailable() async -> Bool {
let port = GatewayEnvironment.gatewayPort() let port = GatewayEnvironment.gatewayPort()
do { 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 let details: String
if let snap = try? await GatewayConnection.shared.healthSnapshot() { if let snap {
let linked = snap.web.linked ? "linked" : "not linked" let linked = snap.web.linked ? "linked" : "not linked"
let authAge = snap.web.authAgeMs.flatMap(msToAge) ?? "unknown age" 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)" 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
}
}