Files
openclaw/apps/macos/Sources/OpenClaw/ProfileGatewayPortReservation.swift
T
Peter Steinberger 0dbdf994b3 feat(macos): isolate named app profiles (#121136)
* feat(macos): isolate named app profiles

* refactor(macos): isolate profile launch ownership

* fix(macos): avoid overlapping approvals socket access

* fix(macos): declare profile defaults concurrency ownership

* fix(macos): return profiled node launch arguments

* chore(i18n): refresh macOS profile source inventory

* fix(macos): gate profile startup before services

* test(macos): evaluate profile state before assertions

* fix(daemon): skip absent launchd deactivation

* fix(macos): fail closed on profile port conflicts

* chore(i18n): refresh profile conflict inventory

* fix(macos): ignore non-gateway launch agent claims

* test(macos): stabilize profile lifecycle timing

* fix(macos): remove stale dashboard URL

* chore(macos): refresh native source baseline
2026-08-09 14:50:15 -07:00

59 lines
2.2 KiB
Swift

import Darwin
import Foundation
final class ProfileGatewayPortReservation: @unchecked Sendable {
let port: Int
let conflict: String?
private let lock: AppInstanceLock?
private init(port: Int, conflict: String?, lock: AppInstanceLock?) {
self.port = port
self.conflict = conflict
self.lock = lock
}
static func acquire(
profile: AppProfile,
port: Int,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser,
temporaryDirectory: URL = URL(fileURLWithPath: "/tmp", isDirectory: true)) -> Self
{
guard let profileName = profile.name else { return Self(port: port, conflict: nil, lock: nil) }
let lockURL = temporaryDirectory
.appendingPathComponent("openclaw-\(geteuid())-app-profile-ports", isDirectory: true)
.appendingPathComponent("\(port).lock")
let lock: AppInstanceLock
switch AppInstanceLock.acquire(url: lockURL) {
case let .acquired(acquired): lock = acquired
case .busy:
return self.conflict(
profile: profileName,
port: port,
reason: "another running OpenClaw profile already reserves it")
case let .failed(reason):
return self.conflict(
profile: profileName,
port: port,
reason: "the profile reservation could not be verified (\(reason))")
}
if let reason = GatewayLaunchAgentManager.conflictingProfileClaimOwner(
port: port,
excludingLabel: profile.gatewayLaunchAgentLabel,
homeDirectory: homeDirectory)
{
return self.conflict(profile: profileName, port: port, reason: reason)
}
return Self(port: port, conflict: nil, lock: lock)
}
private static func conflict(profile: String, port: Int, reason: String) -> Self {
let message = "Profile \"\(profile)\" cannot use Gateway port \(port) because \(reason). " +
"Set gateway.port to a free port for this profile, or stop/uninstall the other Gateway."
return Self(
port: port,
conflict: message,
lock: nil)
}
}