fix(mac): recover control tunnel after restart
# Conflicts: # apps/macos/Sources/Clawdis/GatewayConnection.swiftmain
parent
0484aba892
commit
952d924581
|
|
@ -75,9 +75,32 @@ enum DebugActions {
|
||||||
|
|
||||||
static func restartGateway() {
|
static func restartGateway() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
GatewayProcessManager.shared.stop()
|
switch AppStateStore.shared.connectionMode {
|
||||||
try? await Task.sleep(nanoseconds: 300_000_000)
|
case .local:
|
||||||
GatewayProcessManager.shared.setActive(true)
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,24 @@ actor GatewayEndpointStore {
|
||||||
switch self.state {
|
switch self.state {
|
||||||
case let .ready(_, url, token):
|
case let .ready(_, url, token):
|
||||||
return (url, token)
|
return (url, token)
|
||||||
case let .unavailable(_, reason):
|
case let .unavailable(mode, reason):
|
||||||
throw NSError(domain: "GatewayEndpoint", code: 1, userInfo: [NSLocalizedDescriptionKey: 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 {
|
switch next {
|
||||||
case let .ready(mode, url, _):
|
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):
|
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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue