fix(mac): recover control tunnel after restart

# Conflicts:
#	apps/macos/Sources/Clawdis/GatewayConnection.swift
This commit is contained in:
Peter Steinberger
2025-12-12 22:45:18 +00:00
parent 0484aba892
commit 952d924581
2 changed files with 50 additions and 8 deletions

View File

@@ -75,9 +75,32 @@ enum DebugActions {
static func restartGateway() {
Task { @MainActor in
GatewayProcessManager.shared.stop()
try? await Task.sleep(nanoseconds: 300_000_000)
GatewayProcessManager.shared.setActive(true)
switch AppStateStore.shared.connectionMode {
case .local:
GatewayProcessManager.shared.stop()
// Kick the control channel + health check so the UI recovers immediately.
await GatewayConnection.shared.shutdown()
try? await Task.sleep(nanoseconds: 300_000_000)
GatewayProcessManager.shared.setActive(true)
Task { try? await ControlChannel.shared.configure(mode: .local) }
Task { await HealthStore.shared.refresh(onDemand: true) }
case .remote:
// In remote mode, there is no local gateway to restart. "Restart Gateway" should
// reset the SSH control tunnel + reconnect so the menu recovers.
await RemoteTunnelManager.shared.stopAll()
await GatewayConnection.shared.shutdown()
do {
_ = try await RemoteTunnelManager.shared.ensureControlTunnel()
let settings = CommandResolver.connectionSettings()
try await ControlChannel.shared.configure(mode: .remote(
target: settings.target,
identity: settings.identity))
} catch {
// ControlChannel will surface a degraded state; also refresh health to update the menu text.
Task { await HealthStore.shared.refresh(onDemand: true) }
}
}
}
}

View File

@@ -94,8 +94,24 @@ actor GatewayEndpointStore {
switch self.state {
case let .ready(_, url, token):
return (url, token)
case let .unavailable(_, reason):
throw NSError(domain: "GatewayEndpoint", code: 1, userInfo: [NSLocalizedDescriptionKey: reason])
case let .unavailable(mode, reason):
guard mode == .remote else {
throw NSError(domain: "GatewayEndpoint", code: 1, userInfo: [NSLocalizedDescriptionKey: reason])
}
// Auto-recover for remote mode: if the SSH control tunnel died (or hasn't been created yet),
// recreate it on demand so callers can recover without a manual reconnect.
do {
let forwarded = try await self.deps.ensureRemoteTunnel()
let token = self.deps.token()
let url = URL(string: "ws://127.0.0.1:\(Int(forwarded))")!
self.setState(.ready(mode: .remote, url: url, token: token))
return (url, token)
} catch {
let msg = "\(reason) (\(error.localizedDescription))"
self.setState(.unavailable(mode: .remote, reason: msg))
throw NSError(domain: "GatewayEndpoint", code: 1, userInfo: [NSLocalizedDescriptionKey: msg])
}
}
}
@@ -111,10 +127,13 @@ actor GatewayEndpointStore {
}
switch next {
case let .ready(mode, url, _):
self.logger.debug("resolved endpoint mode=\(String(describing: mode), privacy: .public) url=\(url.absoluteString, privacy: .public)")
self.logger
.debug(
"resolved endpoint mode=\(String(describing: mode), privacy: .public) url=\(url.absoluteString, privacy: .public)")
case let .unavailable(mode, reason):
self.logger.debug("endpoint unavailable mode=\(String(describing: mode), privacy: .public) reason=\(reason, privacy: .public)")
self.logger
.debug(
"endpoint unavailable mode=\(String(describing: mode), privacy: .public) reason=\(reason, privacy: .public)")
}
}
}