Files
openclaw/apps/macos/Sources/OpenClaw/ConnectionModeCoordinator.swift
Peter Steinberger 167a8ef20a fix(onboarding): harden fresh local startup and activation (#108764)
* fix(macos): preserve gateway during fresh setup

* fix(macos): validate reusable gateway service

* chore(macos): sync native i18n inventory

* fix(macos): preserve latest gateway service request

* test(macos): isolate gateway port concurrency coverage

* fix(macos): recover gateways after failed readiness

* fix(macos): repair wedged gateway listeners

* fix(macos): bound gateway startup probes

* chore(macos): sync native i18n inventory

* fix(onboarding): activate verified inference immediately

* fix(macos): isolate readiness failure generations

* chore(macos): refresh native i18n inventory

* fix(macos): serialize gateway service lifecycle

* fix(macos): sequence gateway startup persistence

* fix(macos): close gateway readiness races

* fix(macos): publish recovered gateway state

* fix(macos): make gateway readiness generation-safe

* chore(macos): refresh native i18n inventory

* fix(macos): preserve gateway endpoint identity

* fix(macos): refresh gateway ownership after attach

* fix(macos): distinguish transient gateway readiness

* refactor(macos): split gateway readiness lifecycle

* chore(macos): refresh native i18n inventory

* chore(release): keep changelog release-owned
2026-07-16 17:24:53 -07:00

93 lines
4.3 KiB
Swift

import Foundation
import OSLog
@MainActor
final class ConnectionModeCoordinator {
static let shared = ConnectionModeCoordinator()
private let logger = Logger(subsystem: "ai.openclaw", category: "connection")
private var lastMode: AppState.ConnectionMode?
private var applyGeneration: UInt64 = 0
/// Apply the requested connection mode by starting/stopping local gateway,
/// managing the control-channel SSH tunnel, and cleaning up chat windows/panels.
func apply(mode: AppState.ConnectionMode, paused: Bool) async {
self.applyGeneration &+= 1
let applyGeneration = self.applyGeneration
if let lastMode = self.lastMode, lastMode != mode {
GatewayProcessManager.shared.clearLastFailure()
NodesStore.shared.lastError = nil
}
self.lastMode = mode
switch mode {
case .unconfigured:
_ = await NodeServiceManager.stop()
NodesStore.shared.lastError = nil
await RemoteTunnelManager.shared.stopAll()
WebChatManager.shared.resetTunnels()
GatewayProcessManager.shared.stop()
await GatewayConnection.shared.shutdown()
await ControlChannel.shared.disconnect()
Task.detached { await PortGuardian.shared.sweep(mode: .unconfigured) }
case .local:
_ = await NodeServiceManager.stop()
guard self.applyGeneration == applyGeneration else { return }
NodesStore.shared.lastError = nil
await RemoteTunnelManager.shared.stopAll()
guard self.applyGeneration == applyGeneration else { return }
WebChatManager.shared.resetTunnels()
let shouldStart = GatewayAutostartPolicy.shouldStartGateway(mode: .local, paused: paused)
if shouldStart {
GatewayProcessManager.shared.setActive(true)
await GatewayProcessManager.shared.waitForStartupAttempt()
guard self.applyGeneration == applyGeneration else { return }
var launchAgentInstalled = false
if GatewayAutostartPolicy.shouldEnsureLaunchAgent(
mode: .local,
paused: paused)
{
launchAgentInstalled = await GatewayProcessManager.shared.ensureLaunchAgentEnabledIfNeeded()
}
guard self.applyGeneration == applyGeneration else { return }
// Always finish the generation-aware health audit after persistence work. A newer
// inactive lifecycle makes this return false without touching its repair marker.
_ = await GatewayProcessManager.shared.waitForGatewayReady(
launchAgentInstalled: launchAgentInstalled)
guard self.applyGeneration == applyGeneration else { return }
} else {
GatewayProcessManager.shared.stop()
}
do {
try await ControlChannel.shared.configure(mode: .local)
} catch {
// Control channel will mark itself degraded; nothing else to do here.
self.logger.error(
"control channel local configure failed: \(error.localizedDescription, privacy: .public)")
}
Task.detached { await PortGuardian.shared.sweep(mode: .local) }
case .remote:
// Never run a local gateway in remote mode.
GatewayProcessManager.shared.stop()
WebChatManager.shared.resetTunnels()
do {
NodesStore.shared.lastError = nil
if let error = await NodeServiceManager.start() {
NodesStore.shared.lastError = "Node service start failed: \(error)"
}
_ = try await GatewayEndpointStore.shared.ensureRemoteControlTunnel()
let settings = CommandResolver.connectionSettings()
try await ControlChannel.shared.configure(mode: .remote(
target: settings.target,
identity: settings.identity))
} catch {
self.logger.error("remote tunnel/configure failed: \(error.localizedDescription, privacy: .public)")
}
Task.detached { await PortGuardian.shared.sweep(mode: .remote) }
}
}
}