mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(macos): duplicate launches open SSH tunnels before handoff (#103892)
* fix(macos): gate tunnels behind singleton launch * chore(macos): leave changelog to release workflow * chore(macos): sync native i18n inventory
This commit is contained in:
committed by
GitHub
parent
7af7e41eed
commit
5a48e73973
@@ -18147,7 +18147,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 103,
|
||||
"line": 102,
|
||||
"path": "apps/macos/Sources/OpenClaw/MenuBar.swift",
|
||||
"source": "Settings...",
|
||||
"surface": "apple",
|
||||
@@ -18155,7 +18155,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 122,
|
||||
"line": 121,
|
||||
"path": "apps/macos/Sources/OpenClaw/MenuBar.swift",
|
||||
"source": "OpenClaw",
|
||||
"surface": "apple",
|
||||
@@ -18163,7 +18163,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 122,
|
||||
"line": 121,
|
||||
"path": "apps/macos/Sources/OpenClaw/MenuBar.swift",
|
||||
"source": "OpenClaw - Voice Wake live meter active",
|
||||
"surface": "apple",
|
||||
@@ -18171,7 +18171,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 317,
|
||||
"line": 316,
|
||||
"path": "apps/macos/Sources/OpenClaw/MenuBar.swift",
|
||||
"source": "Close Canvas",
|
||||
"surface": "apple",
|
||||
@@ -18179,7 +18179,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 317,
|
||||
"line": 316,
|
||||
"path": "apps/macos/Sources/OpenClaw/MenuBar.swift",
|
||||
"source": "Open Canvas",
|
||||
"surface": "apple",
|
||||
|
||||
@@ -38,6 +38,7 @@ actor GatewayEndpointStore {
|
||||
let localPort: @Sendable () -> Int
|
||||
let localHost: @Sendable () async -> String
|
||||
let remotePortIfRunning: @Sendable () async -> UInt16?
|
||||
let canStartRemoteTunnel: @Sendable () -> Bool
|
||||
let ensureRemoteTunnel: @Sendable () async throws -> UInt16
|
||||
|
||||
static let live = Deps(
|
||||
@@ -75,9 +76,16 @@ actor GatewayEndpointStore {
|
||||
tailscaleIP: tailscaleIP)
|
||||
},
|
||||
remotePortIfRunning: { await RemoteTunnelManager.shared.controlTunnelPortIfRunning() },
|
||||
canStartRemoteTunnel: { GatewayEndpointStore.primaryAppLaunchAdmitted.withValue { $0 } },
|
||||
ensureRemoteTunnel: { try await RemoteTunnelManager.shared.ensureControlTunnel() })
|
||||
}
|
||||
|
||||
private static let primaryAppLaunchAdmitted = LockIsolated(false)
|
||||
|
||||
static func admitPrimaryAppLaunch() {
|
||||
self.primaryAppLaunchAdmitted.withValue { $0 = true }
|
||||
}
|
||||
|
||||
private static func resolveGatewayPassword(
|
||||
isRemote: Bool,
|
||||
root: [String: Any],
|
||||
@@ -461,10 +469,15 @@ actor GatewayEndpointStore {
|
||||
self.remoteEnsure = nil
|
||||
}
|
||||
|
||||
private func kickRemoteEnsureIfNeeded(detail: String) {
|
||||
@discardableResult
|
||||
private func kickRemoteEnsureIfNeeded(detail: String) -> Bool {
|
||||
guard self.deps.canStartRemoteTunnel() else {
|
||||
self.setState(.connecting(mode: .remote, detail: detail))
|
||||
return false
|
||||
}
|
||||
if self.remoteEnsure != nil {
|
||||
self.setState(.connecting(mode: .remote, detail: detail))
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
let deps = self.deps
|
||||
@@ -472,6 +485,7 @@ actor GatewayEndpointStore {
|
||||
let task = Task.detached(priority: .utility) { try await deps.ensureRemoteTunnel() }
|
||||
self.remoteEnsure = (token: token, task: task)
|
||||
self.setState(.connecting(mode: .remote, detail: detail))
|
||||
return true
|
||||
}
|
||||
|
||||
private func ensureRemoteConfig(detail: String) async throws -> GatewayConnection.Config {
|
||||
@@ -485,7 +499,9 @@ actor GatewayEndpointStore {
|
||||
return (url, token, password)
|
||||
}
|
||||
|
||||
self.kickRemoteEnsureIfNeeded(detail: detail)
|
||||
guard self.kickRemoteEnsureIfNeeded(detail: detail) else {
|
||||
throw CancellationError()
|
||||
}
|
||||
guard let ensure = self.remoteEnsure else {
|
||||
throw NSError(domain: "GatewayEndpoint", code: 1, userInfo: [NSLocalizedDescriptionKey: "Connecting…"])
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ struct OpenClawApp: App {
|
||||
|
||||
init() {
|
||||
OpenClawLogging.bootstrapIfNeeded()
|
||||
GatewayConnectivityCoordinator.shared.start()
|
||||
|
||||
Self.applyAttachOnlyOverrideIfNeeded()
|
||||
_state = State(initialValue: AppStateStore.shared)
|
||||
@@ -379,6 +378,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
NSApp.terminate(nil)
|
||||
return
|
||||
}
|
||||
// Remote startup can spawn an SSH child. Admit tunnel work only after the
|
||||
// singleton check so a short-lived handoff process cannot orphan that child.
|
||||
GatewayEndpointStore.admitPrimaryAppLaunch()
|
||||
GatewayConnectivityCoordinator.shared.start()
|
||||
self.state = AppStateStore.shared
|
||||
if let state {
|
||||
MacNodeModeCoordinator.prepareNodeIdentityProfile(
|
||||
|
||||
@@ -1,8 +1,34 @@
|
||||
import ConcurrencyExtras
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct GatewayEndpointStoreTests {
|
||||
@Test func `remote tunnel waits for primary app launch admission`() async throws {
|
||||
let admitted = LockIsolated(false)
|
||||
let tunnelStarts = LockIsolated(0)
|
||||
let deps = GatewayEndpointStore.Deps(
|
||||
mode: { .remote },
|
||||
token: { nil },
|
||||
password: { nil },
|
||||
localPort: { 18789 },
|
||||
localHost: { "127.0.0.1" },
|
||||
remotePortIfRunning: { nil },
|
||||
canStartRemoteTunnel: { admitted.withValue { $0 } },
|
||||
ensureRemoteTunnel: {
|
||||
tunnelStarts.withValue { $0 += 1 }
|
||||
return 18789
|
||||
})
|
||||
let store = GatewayEndpointStore(deps: deps)
|
||||
|
||||
await store.setMode(.remote)
|
||||
#expect(tunnelStarts.withValue { $0 } == 0)
|
||||
|
||||
admitted.withValue { $0 = true }
|
||||
#expect(try await store.ensureRemoteControlTunnel() == 18789)
|
||||
#expect(tunnelStarts.withValue { $0 } == 1)
|
||||
}
|
||||
|
||||
private func makeLaunchAgentSnapshot(
|
||||
env: [String: String],
|
||||
token: String?,
|
||||
|
||||
Reference in New Issue
Block a user