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 {
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
}
}

View File

@@ -215,9 +215,3 @@ final class CanvasManager {
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 {
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)

View File

@@ -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] {

View File

@@ -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)
}
}

View File

@@ -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")

View File

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