Mac: surface gateway errors in remote test
This commit is contained in:
@@ -99,8 +99,9 @@ final class ControlChannel: ObservableObject {
|
|||||||
self.state = .connected
|
self.state = .connected
|
||||||
return payload
|
return payload
|
||||||
} catch {
|
} catch {
|
||||||
self.state = .degraded(error.localizedDescription)
|
let message = self.friendlyGatewayMessage(error)
|
||||||
throw error
|
self.state = .degraded(message)
|
||||||
|
throw ControlChannelError.badResponse(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,11 +117,41 @@ final class ControlChannel: ObservableObject {
|
|||||||
self.state = .connected
|
self.state = .connected
|
||||||
return data
|
return data
|
||||||
} catch {
|
} catch {
|
||||||
self.state = .degraded(error.localizedDescription)
|
let message = self.friendlyGatewayMessage(error)
|
||||||
throw error
|
self.state = .degraded(message)
|
||||||
|
throw ControlChannelError.badResponse(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func friendlyGatewayMessage(_ error: Error) -> String {
|
||||||
|
// Map URLSession/WS errors into user-facing, actionable text.
|
||||||
|
if let ctrlErr = error as? ControlChannelError, let desc = ctrlErr.errorDescription {
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
|
||||||
|
if let urlError = error as? URLError {
|
||||||
|
let port = RelayEnvironment.gatewayPort()
|
||||||
|
switch urlError.code {
|
||||||
|
case .cancelled:
|
||||||
|
return "Gateway connection was closed; start the relay (localhost:\(port)) and retry."
|
||||||
|
case .cannotFindHost, .cannotConnectToHost:
|
||||||
|
return "Cannot reach gateway at localhost:\(port); ensure the relay is running."
|
||||||
|
case .networkConnectionLost:
|
||||||
|
return "Gateway connection dropped; relay likely restarted—retry."
|
||||||
|
case .timedOut:
|
||||||
|
return "Gateway request timed out; check relay on localhost:\(port)."
|
||||||
|
case .notConnectedToInternet:
|
||||||
|
return "No network connectivity; cannot reach gateway."
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nsError = error as NSError
|
||||||
|
let detail = nsError.localizedDescription.isEmpty ? "unknown gateway error" : nsError.localizedDescription
|
||||||
|
return "Gateway error: \(detail)"
|
||||||
|
}
|
||||||
|
|
||||||
func sendSystemEvent(_ text: String) async throws {
|
func sendSystemEvent(_ text: String) async throws {
|
||||||
_ = try await self.request(method: "system-event", params: ["text": AnyHashable(text)])
|
_ = try await self.request(method: "system-event", params: ["text": AnyHashable(text)])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,12 @@ private actor GatewayChannelActor {
|
|||||||
self.task?.cancel(with: .goingAway, reason: nil)
|
self.task?.cancel(with: .goingAway, reason: nil)
|
||||||
self.task = self.session.webSocketTask(with: self.url)
|
self.task = self.session.webSocketTask(with: self.url)
|
||||||
self.task?.resume()
|
self.task?.resume()
|
||||||
try await self.sendHello()
|
do {
|
||||||
|
try await self.sendHello()
|
||||||
|
} catch {
|
||||||
|
let wrapped = self.wrap(error, context: "connect to gateway @ \(self.url.absoluteString)")
|
||||||
|
throw wrapped
|
||||||
|
}
|
||||||
self.listen()
|
self.listen()
|
||||||
self.connected = true
|
self.connected = true
|
||||||
self.backoffMs = 500
|
self.backoffMs = 500
|
||||||
@@ -110,7 +115,8 @@ private actor GatewayChannelActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleReceiveFailure(_ err: Error) async {
|
private func handleReceiveFailure(_ err: Error) async {
|
||||||
self.logger.error("gateway ws receive failed \(err.localizedDescription, privacy: .public)")
|
let wrapped = self.wrap(err, context: "gateway receive")
|
||||||
|
self.logger.error("gateway ws receive failed \(wrapped.localizedDescription, privacy: .public)")
|
||||||
self.connected = false
|
self.connected = false
|
||||||
await self.scheduleReconnect()
|
await self.scheduleReconnect()
|
||||||
}
|
}
|
||||||
@@ -177,13 +183,18 @@ private actor GatewayChannelActor {
|
|||||||
do {
|
do {
|
||||||
try await self.connect()
|
try await self.connect()
|
||||||
} catch {
|
} catch {
|
||||||
self.logger.error("gateway reconnect failed \(error.localizedDescription, privacy: .public)")
|
let wrapped = self.wrap(error, context: "gateway reconnect")
|
||||||
|
self.logger.error("gateway reconnect failed \(wrapped.localizedDescription, privacy: .public)")
|
||||||
await self.scheduleReconnect()
|
await self.scheduleReconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func request(method: String, params: [String: AnyCodable]?) async throws -> Data {
|
func request(method: String, params: [String: AnyCodable]?) async throws -> Data {
|
||||||
try await self.connect()
|
do {
|
||||||
|
try await self.connect()
|
||||||
|
} catch {
|
||||||
|
throw self.wrap(error, context: "gateway connect")
|
||||||
|
}
|
||||||
let id = UUID().uuidString
|
let id = UUID().uuidString
|
||||||
let paramsObject = params?.reduce(into: [String: Any]()) { dict, entry in
|
let paramsObject = params?.reduce(into: [String: Any]()) { dict, entry in
|
||||||
dict[entry.key] = entry.value.value
|
dict[entry.key] = entry.value.value
|
||||||
@@ -202,7 +213,7 @@ private actor GatewayChannelActor {
|
|||||||
try await self.task?.send(.data(data))
|
try await self.task?.send(.data(data))
|
||||||
} catch {
|
} catch {
|
||||||
self.pending.removeValue(forKey: id)
|
self.pending.removeValue(forKey: id)
|
||||||
cont.resume(throwing: error)
|
cont.resume(throwing: self.wrap(error, context: "gateway send \(method)"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,6 +232,20 @@ private actor GatewayChannelActor {
|
|||||||
}
|
}
|
||||||
return Data()
|
return Data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap low-level URLSession/WebSocket errors with context so UI can surface them.
|
||||||
|
private func wrap(_ error: Error, context: String) -> Error {
|
||||||
|
if let urlError = error as? URLError {
|
||||||
|
let desc = urlError.localizedDescription.isEmpty ? "cancelled" : urlError.localizedDescription
|
||||||
|
return NSError(
|
||||||
|
domain: urlError.errorDomain,
|
||||||
|
code: urlError.errorCode,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: "\(context): \(desc)"])
|
||||||
|
}
|
||||||
|
let ns = error as NSError
|
||||||
|
let desc = ns.localizedDescription.isEmpty ? "unknown" : ns.localizedDescription
|
||||||
|
return NSError(domain: ns.domain, code: ns.code, userInfo: [NSLocalizedDescriptionKey: "\(context): \(desc)"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actor GatewayChannel {
|
actor GatewayChannel {
|
||||||
|
|||||||
Reference in New Issue
Block a user