mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(macos): report named-profile startup failures without a listener (#156441)
A named profile's readiness failure required port-ownership proof, which always fails without a listener PID, so the real startup error was replaced by a phantom port conflict. Check ownership only when something is listening; ready results keep strict ownership proof. Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
parent
c1ead4b71e
commit
31dd34ff19
2 changed files with 59 additions and 1 deletions
|
|
@ -1220,7 +1220,14 @@ extension GatewayProcessManager {
|
|||
|
||||
case let .failed(terminalFailure):
|
||||
let instance = await PortGuardian.shared.describe(port: context.port)
|
||||
guard await self.canPublishGatewayReadiness(instance: instance, context: context) else {
|
||||
// Ownership only matters when something is listening. With no listener, a named
|
||||
// profile's startup failure is its own; reporting a port conflict would hide it.
|
||||
let publishable = if instance == nil {
|
||||
self.isCurrentGatewayReadiness(context)
|
||||
} else {
|
||||
await self.canPublishGatewayReadiness(instance: instance, context: context)
|
||||
}
|
||||
guard publishable else {
|
||||
return false
|
||||
}
|
||||
let retainedFailure: LaunchAgentReadinessFailure? = switch terminalFailure {
|
||||
|
|
|
|||
|
|
@ -81,6 +81,57 @@ struct AppStateIsolationTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `named profile startup failure without a listener keeps its own reason`() async throws {
|
||||
try #require(AppProfile.current.isActive)
|
||||
let configPath = TestIsolation.tempConfigPath()
|
||||
try Data(#"{"gateway":{"mode":"local"}}"#.utf8).write(to: URL(fileURLWithPath: configPath))
|
||||
defer { try? FileManager.default.removeItem(atPath: configPath) }
|
||||
let inspectionError = "launchctl inspection failed"
|
||||
await TestIsolation.withIsolatedState(
|
||||
env: ["OPENCLAW_CONFIG_PATH": configPath, "OPENCLAW_GATEWAY_PORT": nil],
|
||||
defaults: [connectionModeKey: "local"])
|
||||
{
|
||||
let state = AppStateStore.shared
|
||||
let previousMode = state.connectionMode
|
||||
state.connectionMode = .local
|
||||
let manager = GatewayProcessManager()
|
||||
let connection = GatewayConnection(testEndpointProvider: { throw CancellationError() })
|
||||
manager.setTestingConnection(connection)
|
||||
manager.setTestingSkipControlChannelRefresh(true)
|
||||
GatewayLaunchAgentManager.setTestingInterceptDaemonCommands(true)
|
||||
GatewayLaunchAgentManager.setTestingDaemonStatusPayload(
|
||||
#"{"ok":false,"error":"\#(inspectionError)"}"#)
|
||||
GatewayLaunchAgentManager.clearTestingDaemonCommandCalls()
|
||||
let port = GatewayEnvironment.gatewayPort()
|
||||
await PortGuardian.shared.setTestingDescriptor(nil, forPort: port)
|
||||
defer {
|
||||
manager.setTestingDesiredActive(false)
|
||||
state.connectionMode = previousMode
|
||||
GatewayLaunchAgentManager.setTestingInterceptDaemonCommands(false)
|
||||
GatewayLaunchAgentManager.setTestingDaemonStatusPayload(nil)
|
||||
GatewayLaunchAgentManager.clearTestingDaemonCommandCalls()
|
||||
}
|
||||
|
||||
// An inconclusive inspection defers installation, so readiness fails with nothing
|
||||
// listening. No other process owns the port; the operator must see the real cause.
|
||||
manager.setActive(true)
|
||||
await manager.waitForStartupAttempt()
|
||||
guard case let .failed(reason) = manager.status else {
|
||||
Issue.record("expected a terminal startup failure")
|
||||
return
|
||||
}
|
||||
#expect(reason.contains(inspectionError))
|
||||
#expect(!reason.contains("already owned by another process"))
|
||||
#expect(manager.lastFailureReason == reason)
|
||||
#expect(!GatewayLaunchAgentManager.testingDaemonCommandCallsSnapshot().contains { $0.first == "install" })
|
||||
|
||||
manager.setTestingDesiredActive(false)
|
||||
await connection.shutdown()
|
||||
await GatewayEndpointStore.shared.setLocalUnavailableReason(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `named profile hosting repair requires restart before activation`() async throws {
|
||||
try #require(AppProfile.current.isActive)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue