#if canImport(Darwin) import Darwin #elseif canImport(Glibc) import Glibc #elseif canImport(Musl) import Musl #endif import Foundation // MARK: - Antigravity CLI Process Abstractions protocol AntigravityCLIProcessHandle: AnyObject, Sendable { var pid: pid_t { get } var isRunning: Bool { get } var processGroup: pid_t? { get } func assignProcessGroup() -> pid_t? func sendExit() throws func closePTY() func terminateRoot() func killRoot() func descendantPIDs() -> [pid_t] func terminateTree(signal: Int32, knownDescendants: [pid_t]) func killDescendants(_ descendants: [pid_t]) func drainOutput() -> Data } protocol AntigravityCLIProcessLaunching: Sendable { func launch(binary: String) throws -> any AntigravityCLIProcessHandle } enum AntigravityCLIAuthenticationPrompt { static let evidence = Data("Select login method:".utf8) private static let promptPattern = #"select\s+login\s+method\s*:?"# static func contains(_ output: Data) -> Bool { // `agy` briefly prints "You are currently not signed in" before it // auto-refreshes an existing login. The actual blocking state is the // interactive login-method prompt. The exact prompt is a CLI-owned TUI // string, so keep matching tolerant to casing and whitespace changes. if output.range(of: self.evidence) != nil { return true } let asciiBytes = output.map { $0 < 0x80 ? $0 : 0x20 } let text = String(bytes: asciiBytes, encoding: .utf8) ?? "" return text.range( of: self.promptPattern, options: [.regularExpression, .caseInsensitive]) != nil } } struct AntigravityCLIProcessIdentity: Equatable { let executablePath: String let startEpoch: TimeInterval } protocol AntigravityCLIProcessIdentityProviding: Sendable { func identity(for pid: pid_t) -> AntigravityCLIProcessIdentity? } struct AntigravityCLISessionRecord: Codable, Equatable { let pid: pid_t let requestedBinaryPath: String let executablePath: String let startEpoch: TimeInterval let processGroup: pid_t? let ownerPID: pid_t? let ownerExecutablePath: String? let ownerStartEpoch: TimeInterval? init( pid: pid_t, requestedBinaryPath: String, executablePath: String, startEpoch: TimeInterval, processGroup: pid_t?, ownerPID: pid_t? = nil, ownerExecutablePath: String? = nil, ownerStartEpoch: TimeInterval? = nil) { self.pid = pid self.requestedBinaryPath = requestedBinaryPath self.executablePath = executablePath self.startEpoch = startEpoch self.processGroup = processGroup self.ownerPID = ownerPID self.ownerExecutablePath = ownerExecutablePath self.ownerStartEpoch = ownerStartEpoch } } protocol AntigravityCLISessionRecordStoring: Sendable { func load() throws -> [AntigravityCLISessionRecord] func save(_ record: AntigravityCLISessionRecord) throws func remove(_ record: AntigravityCLISessionRecord) throws } protocol AntigravityCLISessionLaunchLocking: Sendable { func withLock(_ operation: () throws -> T) throws -> T } // MARK: - AntigravityCLISession /// Manages a bounded background ``agy`` process whose embedded localhost server /// provides the same ``GetUserStatus`` endpoint as the desktop Antigravity app's /// ``language_server``. The CLI is kept alive in a PTY so its daemon stays bound /// to a local port - this lets CodexBar read Claude + Gemini quotas even when /// the desktop Antigravity app is closed. /// /// The session intentionally does not scrape TUI output. It only launches and /// keeps the process reachable for HTTPS probing, drains discarded PTY output so /// the CLI cannot block on a full terminal buffer, and bounds the warm lifetime /// with an idle timer so CodexBar does not run an IDE backend forever. actor AntigravityCLISession { static let shared = AntigravityCLISession() private static let log = CodexBarLog.logger(LogCategories.antigravity) enum ResetCause: Int { case deferred case oneShotCLI case unhealthy case authentication var message: String { switch self { case .deferred: "deferred reset" case .oneShotCLI: "one-shot CLI fetch" case .unhealthy: "unhealthy CLI HTTPS session" case .authentication: "authentication required" } } } private struct LaunchOutcome { let pid: pid_t let rejectedProcess: (any AntigravityCLIProcessHandle)? let rejectionMessage: String? let holdsLaunchReservation: Bool } struct Dependencies { var launcher: any AntigravityCLIProcessLaunching var identityProvider: any AntigravityCLIProcessIdentityProviding var recordStore: any AntigravityCLISessionRecordStoring var launchLock: any AntigravityCLISessionLaunchLocking var beginAppShutdownTrackedLaunch: @Sendable () -> Bool var endAppShutdownTrackedLaunch: @Sendable () -> Void var registerForAppShutdown: @Sendable (pid_t, String) -> Bool var updateAppShutdownProcessGroup: @Sendable (pid_t, pid_t?) -> Void var unregisterForAppShutdown: @Sendable (pid_t) -> Void var descendantPIDs: @Sendable (pid_t) -> [pid_t] var terminateProcessTree: @Sendable (pid_t, pid_t?, Int32, [pid_t]) -> Void var currentProcessID: @Sendable () -> pid_t var now: @Sendable () -> Date var sleep: @Sendable (UInt64) async throws -> Void var idleWindow: TimeInterval var failureRelaunchThreshold: Int var terminationGracePeriod: TimeInterval static func live() -> Self { Self( launcher: AntigravityPTYProcessLauncher(), identityProvider: AntigravityProcessIdentityProvider(), recordStore: AntigravityFileCLISessionRecordStore(), launchLock: AntigravityFileCLISessionLaunchLock(), beginAppShutdownTrackedLaunch: { TTYCommandRunner.beginActiveProcessLaunchForAppShutdown() }, endAppShutdownTrackedLaunch: { TTYCommandRunner.endActiveProcessLaunchForAppShutdown() }, registerForAppShutdown: { pid, binary in TTYCommandRunner.registerActiveProcessForAppShutdown(pid: pid, binary: binary) }, updateAppShutdownProcessGroup: { pid, group in TTYCommandRunner.updateActiveProcessGroupForAppShutdown(pid: pid, processGroup: group) }, unregisterForAppShutdown: { pid in TTYCommandRunner.unregisterActiveProcessForAppShutdown(pid: pid) }, descendantPIDs: { pid in TTYProcessTreeTerminator.descendantPIDs(of: pid) }, terminateProcessTree: { pid, group, signal, knownDescendants in TTYProcessTreeTerminator.terminateProcessTree( rootPID: pid, processGroup: group, signal: signal, knownDescendants: knownDescendants) }, currentProcessID: getpid, now: Date.init, sleep: { nanoseconds in try await Task.sleep(nanoseconds: nanoseconds) }, idleWindow: 180, failureRelaunchThreshold: 2, terminationGracePeriod: 1) } } // MARK: State private let dependencies: Dependencies private var process: (any AntigravityCLIProcessHandle)? private var binaryPath: String? private var sessionIdleWindow: TimeInterval private var activeProbeCount = 0 private var activeSessionProbeCount = 0 private var resetRequestedWhenIdle = false private var hardResetRequestedWhenIdle = false private var pendingResetCause: ResetCause? private var idleTask: Task? private var sessionGeneration: UInt64 = 0 private var consecutiveProbeFailures = 0 private var persistedProcessIdentity: AntigravityCLIProcessIdentity? private var recentOutput = Data() private var authenticationPromptObserved = false private var lastStopReason: String? private var lifecycleOperationInProgress = false private var lifecycleWaiters: [CheckedContinuation] = [] private var exclusiveProbeWaiters: [CheckedContinuation] = [] init(dependencies: Dependencies = .live()) { self.dependencies = dependencies self.sessionIdleWindow = dependencies.idleWindow } /// The pid of the running ``agy`` process, exposed so callers can discover /// its listening ports via `lsof`. var pid: pid_t? { guard let proc = self.process, proc.isRunning else { return nil } return proc.pid } /// Whether the managed process is alive and matches ``binaryPath``. var isRunning: Bool { guard let proc = self.process, proc.isRunning, self.binaryPath != nil else { return false } return true } var failureCountForTesting: Int { self.consecutiveProbeFailures } var idleWindowForTesting: TimeInterval { self.sessionIdleWindow } var activeProbeCountForTesting: Int { self.activeProbeCount } var lastStopReasonForTesting: String? { self.lastStopReason } // MARK: Lifecycle /// Mark a probe as active and ensure a warm ``agy`` is running on the given binary path. /// /// Callers must balance this with ``finishProbe(success:resetAfterFetch:)`` so /// idle/reset cleanup cannot kill the process while its ports are being probed. /// If previous probes repeatedly failed while the process stayed alive, this /// force-relaunches instead of reusing a wedged HTTPS server forever. func beginProbe(binary: String, idleWindow: TimeInterval? = nil) async throws -> pid_t { self.activeProbeCount += 1 if let idleWindow, idleWindow > 0 { self.sessionIdleWindow = max(self.dependencies.idleWindow, idleWindow) } self.cancelIdleTimer() do { return try await self.withLifecycleOperation { let pid = try await self.ensureStartedLocked(binary: binary) self.activeSessionProbeCount += 1 return pid } } catch { self.activeProbeCount = max(0, self.activeProbeCount - 1) self.notifyExclusiveProbeWaitersIfNeeded() if self.activeProbeCount == 0, self.resetRequestedWhenIdle { await self.withLifecycleOperation { guard self.activeProbeCount == 0 else { self.resetRequestedWhenIdle = true return } let forceTerminate = self.hardResetRequestedWhenIdle || self.consecutiveProbeFailures > 0 self.resetRequestedWhenIdle = false self.hardResetRequestedWhenIdle = false await self.stopCurrentSessionLocked( reason: "deferred reset after failed begin", clearRecord: true, graceful: !forceTerminate) } } throw error } } /// Record probe completion and either keep the session warm for the bounded /// idle window or tear it down immediately for one-shot CLI invocations. func finishProbe(success: Bool, resetAfterFetch: Bool, forceTerminate: Bool = false) async { if success { self.consecutiveProbeFailures = 0 } else { self.consecutiveProbeFailures += 1 } self.activeProbeCount = max(0, self.activeProbeCount - 1) self.activeSessionProbeCount = max(0, self.activeSessionProbeCount - 1) self.notifyExclusiveProbeWaitersIfNeeded() let shouldForceStopUnhealthy = !success && self.consecutiveProbeFailures >= max(1, self.dependencies.failureRelaunchThreshold) let shouldReset = forceTerminate || resetAfterFetch || self.resetRequestedWhenIdle || shouldForceStopUnhealthy let currentResetCause = Self.resetCause( authenticationRequired: forceTerminate, resetAfterFetch: resetAfterFetch, shouldForceStopUnhealthy: shouldForceStopUnhealthy) guard self.activeProbeCount == 0 else { if shouldReset { self.resetRequestedWhenIdle = true self.pendingResetCause = Self.strongestResetCause(self.pendingResetCause, currentResetCause) } if shouldReset, !success || forceTerminate { self.hardResetRequestedWhenIdle = true } return } if shouldReset { let shouldForceTerminate = !success || forceTerminate || self.hardResetRequestedWhenIdle let resetCause = Self.strongestResetCause(self.pendingResetCause, currentResetCause) await self.withLifecycleOperation { guard self.activeProbeCount == 0 else { self.resetRequestedWhenIdle = true self.pendingResetCause = Self.strongestResetCause(self.pendingResetCause, resetCause) if shouldForceTerminate { self.hardResetRequestedWhenIdle = true } return } self.resetRequestedWhenIdle = false self.hardResetRequestedWhenIdle = false self.pendingResetCause = nil await self.stopCurrentSessionLocked( reason: resetCause.message, clearRecord: true, graceful: !shouldForceTerminate) } } else { self.armIdleTimer() } } static func resetCause( authenticationRequired: Bool, resetAfterFetch: Bool, shouldForceStopUnhealthy: Bool) -> ResetCause { if authenticationRequired { .authentication } else if shouldForceStopUnhealthy { .unhealthy } else if resetAfterFetch { .oneShotCLI } else { .deferred } } static func strongestResetCause(_ existing: ResetCause?, _ incoming: ResetCause) -> ResetCause { guard let existing else { return incoming } return existing.rawValue >= incoming.rawValue ? existing : incoming } /// Ensure a warm ``agy`` is running on the given binary path. /// /// - If the process is already alive with the same binary, this returns immediately. /// - If the process died, the binary changed, or repeated probes failed, it tears down the old one first. /// - Returns the process identifier for port discovery. func ensureStarted(binary: String) async throws -> pid_t { try await self.withLifecycleOperation { try await self.ensureStartedLocked(binary: binary) } } /// Request teardown. If a probe is in flight, cleanup is deferred until the /// matching ``finishProbe(success:resetAfterFetch:)`` call. func reset() async { self.cancelIdleTimer() guard self.activeProbeCount == 0 else { self.resetRequestedWhenIdle = true return } await self.withLifecycleOperation { guard self.activeProbeCount == 0 else { self.resetRequestedWhenIdle = true return } self.resetRequestedWhenIdle = false let forceTerminate = self.hardResetRequestedWhenIdle || self.consecutiveProbeFailures > 0 self.hardResetRequestedWhenIdle = false await self.stopCurrentSessionLocked( reason: "manual reset", clearRecord: true, graceful: !forceTerminate) } } /// Drain PTY output into one rolling buffer shared by concurrent probes. func drainOutput() -> Data { var searchableOutput = self.recentOutput if let output = self.process?.drainOutput(), !output.isEmpty { searchableOutput.append(output) self.recentOutput = Data(searchableOutput.suffix(4096)) } if !self.authenticationPromptObserved, AntigravityCLIAuthenticationPrompt.contains(searchableOutput) { self.authenticationPromptObserved = true } if self.authenticationPromptObserved, !AntigravityCLIAuthenticationPrompt.contains(searchableOutput) { searchableOutput.append(AntigravityCLIAuthenticationPrompt.evidence) } return searchableOutput } // MARK: Errors enum SessionError: LocalizedError { case launchFailed(String) var errorDescription: String? { switch self { case let .launchFailed(msg): "Failed to launch Antigravity CLI session: \(msg)" } } } // MARK: Private private func withLifecycleOperation(_ operation: () async throws -> T) async throws -> T { await self.acquireLifecycleOperation() defer { self.releaseLifecycleOperation() } return try await operation() } private func withLifecycleOperation(_ operation: () async -> Void) async { await self.acquireLifecycleOperation() defer { self.releaseLifecycleOperation() } await operation() } private func acquireLifecycleOperation() async { guard self.lifecycleOperationInProgress else { self.lifecycleOperationInProgress = true return } await withCheckedContinuation { continuation in self.lifecycleWaiters.append(continuation) } } private func releaseLifecycleOperation() { guard !self.lifecycleWaiters.isEmpty else { self.lifecycleOperationInProgress = false return } let next = self.lifecycleWaiters.removeFirst() next.resume() } private func waitForExclusiveProbeIfNeeded() async { while self.activeSessionProbeCount > 0 { await withCheckedContinuation { continuation in self.exclusiveProbeWaiters.append(continuation) } } } private func notifyExclusiveProbeWaitersIfNeeded() { guard self.activeSessionProbeCount == 0, !self.exclusiveProbeWaiters.isEmpty else { return } let waiters = self.exclusiveProbeWaiters self.exclusiveProbeWaiters.removeAll() for waiter in waiters { waiter.resume() } } private func ensureStartedLocked(binary: String) async throws -> pid_t { while true { if let proc = self.process, proc.isRunning, self.binaryPath == binary, !self.resetRequestedWhenIdle, !self.hardResetRequestedWhenIdle, self.consecutiveProbeFailures < max(1, self.dependencies.failureRelaunchThreshold) { try? self.dependencies.launchLock.withLock { self.reapRecordedSessionsIfNeeded() } self.persistCurrentRecordIfNeeded(proc, binary: binary) Self.log.debug("Antigravity CLI session reused", metadata: ["pid": "\(proc.pid)"]) return proc.pid } if self.process != nil { if self.activeSessionProbeCount > 0 { await self.waitForExclusiveProbeIfNeeded() continue } let reason = self.consecutiveProbeFailures >= max(1, self.dependencies.failureRelaunchThreshold) ? "relaunching unhealthy session" : "replacing stale session" let forceTerminate = self.hardResetRequestedWhenIdle || self.consecutiveProbeFailures > 0 self.resetRequestedWhenIdle = false self.hardResetRequestedWhenIdle = false await self.stopCurrentSessionLocked( reason: reason, clearRecord: true, graceful: !forceTerminate) } let binaryName = URL(fileURLWithPath: binary).lastPathComponent let launch: (Bool) throws -> LaunchOutcome = { canPersistRecord in if canPersistRecord { self.prepareRecordStoreForLaunch() } guard self.dependencies.beginAppShutdownTrackedLaunch() else { throw SessionError.launchFailed("App shutdown in progress") } let launched: any AntigravityCLIProcessHandle do { launched = try self.dependencies.launcher.launch(binary: binary) } catch { self.dependencies.endAppShutdownTrackedLaunch() throw error } let launchedPID = launched.pid guard self.dependencies.registerForAppShutdown(launchedPID, binaryName) else { return LaunchOutcome( pid: launchedPID, rejectedProcess: launched, rejectionMessage: "App shutdown in progress", holdsLaunchReservation: true) } let processGroup = launched.processGroup ?? launched.assignProcessGroup() self.dependencies.updateAppShutdownProcessGroup(launchedPID, processGroup) self.process = launched self.binaryPath = binary self.recentOutput.removeAll(keepingCapacity: true) self.authenticationPromptObserved = false self.consecutiveProbeFailures = 0 self.sessionGeneration &+= 1 if canPersistRecord { _ = self.persistRecord(pid: launchedPID, binary: binary, processGroup: processGroup) } else { self.persistedProcessIdentity = nil } self.dependencies.endAppShutdownTrackedLaunch() return LaunchOutcome( pid: launchedPID, rejectedProcess: nil, rejectionMessage: nil, holdsLaunchReservation: false) } var lockedLaunch: Result? var lockFailure: Error? do { try self.dependencies.launchLock.withLock { lockedLaunch = Result { try launch(true) } } } catch { lockFailure = error } let outcome: LaunchOutcome if let lockFailure { Self.log.warning( "Antigravity CLI session coordination unavailable", metadata: ["error": lockFailure.localizedDescription]) outcome = try launch(false) } else if let lockedLaunch { outcome = try lockedLaunch.get() } else { throw SessionError.launchFailed("CLI session launch did not complete") } if let rejectedProcess = outcome.rejectedProcess { await self.terminateLaunchedProcess(rejectedProcess, graceful: false) if outcome.holdsLaunchReservation { self.dependencies.endAppShutdownTrackedLaunch() } throw SessionError.launchFailed(outcome.rejectionMessage ?? "App shutdown in progress") } Self.log.debug( "Antigravity CLI session started", metadata: [ "binary": binaryName, "pid": "\(outcome.pid)", ]) return outcome.pid } } private func prepareRecordStoreForLaunch() { guard self.process == nil || self.persistedProcessIdentity == nil else { return } self.reapRecordedSessionsIfNeeded() } private func persistCurrentRecordIfNeeded(_ proc: any AntigravityCLIProcessHandle, binary: String) { guard self.persistedProcessIdentity == nil else { return } try? self.dependencies.launchLock.withLock { self.prepareRecordStoreForLaunch() _ = self.persistRecord(pid: proc.pid, binary: binary, processGroup: proc.processGroup) } } private func cancelIdleTimer() { self.idleTask?.cancel() self.idleTask = nil } private func armIdleTimer() { guard self.process != nil, self.sessionIdleWindow > 0 else { return } self.cancelIdleTimer() let generation = self.sessionGeneration let nanoseconds = Self.nanoseconds(from: self.sessionIdleWindow) let sleep = self.dependencies.sleep self.idleTask = Task { [weak self] in do { try await sleep(nanoseconds) await self?.stopIfIdle(generation: generation) } catch { // Cancellation is the normal path when a refresh reuses the warm session. } } } private func stopIfIdle(generation: UInt64) async { await self.withLifecycleOperation { guard generation == self.sessionGeneration else { return } guard self.activeProbeCount == 0 else { self.armIdleTimer() return } let forceTerminate = self.hardResetRequestedWhenIdle || self.consecutiveProbeFailures > 0 self.resetRequestedWhenIdle = false self.hardResetRequestedWhenIdle = false await self.stopCurrentSessionLocked( reason: "idle timeout", clearRecord: true, graceful: !forceTerminate) } } private func stopCurrentSessionLocked(reason: String, clearRecord: Bool, graceful: Bool = true) async { self.cancelIdleTimer() let effectiveReason = self.pendingResetCause?.message ?? reason self.pendingResetCause = nil self.lastStopReason = effectiveReason guard let proc = self.process else { if clearRecord { try? self.dependencies.launchLock.withLock { self.reapRecordedSessionsIfNeeded() } } self.sessionIdleWindow = self.dependencies.idleWindow self.recentOutput.removeAll(keepingCapacity: true) self.authenticationPromptObserved = false return } let pid = proc.pid let identity = self.persistedProcessIdentity Self.log.debug("Antigravity CLI session stopping", metadata: ["pid": "\(pid)", "reason": "\(effectiveReason)"]) self.process = nil self.binaryPath = nil self.persistedProcessIdentity = nil self.sessionIdleWindow = self.dependencies.idleWindow self.recentOutput.removeAll(keepingCapacity: true) self.authenticationPromptObserved = false self.sessionGeneration &+= 1 await self.terminateLaunchedProcess(proc, graceful: graceful) self.dependencies.unregisterForAppShutdown(pid) if clearRecord { self.removeRecordIfMatches(pid: pid, identity: identity) } } private func terminateLaunchedProcess( _ proc: any AntigravityCLIProcessHandle, graceful: Bool = true) async { if graceful { try? proc.sendExit() } proc.closePTY() let descendants = proc.descendantPIDs() if proc.isRunning { proc.terminateRoot() } proc.terminateTree(signal: SIGTERM, knownDescendants: descendants) let gracePeriod = self.dependencies.terminationGracePeriod if gracePeriod > 0 { let deadline = self.dependencies.now().addingTimeInterval(gracePeriod) while proc.isRunning, self.dependencies.now() < deadline { try? await self.dependencies.sleep(100_000_000) } } if proc.isRunning { proc.terminateTree(signal: SIGKILL, knownDescendants: descendants) await self.waitUntilProcessExits(proc, timeout: 1) } else { proc.killDescendants(descendants) } } private func waitUntilProcessExits(_ proc: any AntigravityCLIProcessHandle, timeout: TimeInterval) async { let deadline = self.dependencies.now().addingTimeInterval(timeout) while proc.isRunning, self.dependencies.now() < deadline { try? await self.dependencies.sleep(50_000_000) } _ = proc.isRunning } @discardableResult private func persistRecord(pid: pid_t, binary: String, processGroup: pid_t?) -> Bool { guard let identity = self.dependencies.identityProvider.identity(for: pid) else { self.persistedProcessIdentity = nil return false } let ownerPID = self.dependencies.currentProcessID() let ownerIdentity = self.dependencies.identityProvider.identity(for: ownerPID) let record = AntigravityCLISessionRecord( pid: pid, requestedBinaryPath: binary, executablePath: identity.executablePath, startEpoch: identity.startEpoch, processGroup: processGroup, ownerPID: ownerPID, ownerExecutablePath: ownerIdentity?.executablePath, ownerStartEpoch: ownerIdentity?.startEpoch) do { try self.dependencies.recordStore.save(record) self.persistedProcessIdentity = identity return true } catch { self.persistedProcessIdentity = nil return false } } private func reapRecordedSessionsIfNeeded() { guard let records = try? self.dependencies.recordStore.load() else { return } for record in records { guard let liveIdentity = self.dependencies.identityProvider.identity(for: record.pid), liveIdentity.executablePath == record.executablePath, abs(liveIdentity.startEpoch - record.startEpoch) < 0.001 else { try? self.dependencies.recordStore.remove(record) continue } if let proc = self.process, proc.pid == record.pid { self.persistedProcessIdentity = liveIdentity continue } if let ownerPID = record.ownerPID, ownerPID != self.dependencies.currentProcessID(), let ownerExecutablePath = record.ownerExecutablePath, let ownerStartEpoch = record.ownerStartEpoch, let liveOwnerIdentity = self.dependencies.identityProvider.identity(for: ownerPID), liveOwnerIdentity.executablePath == ownerExecutablePath, abs(liveOwnerIdentity.startEpoch - ownerStartEpoch) < 0.001 { continue } let knownDescendants = self.dependencies.descendantPIDs(record.pid) Self.log.debug("Reaping stale Antigravity CLI session", metadata: ["pid": "\(record.pid)"]) self.dependencies.terminateProcessTree(record.pid, record.processGroup, SIGTERM, knownDescendants) self.dependencies.terminateProcessTree(record.pid, record.processGroup, SIGKILL, knownDescendants) try? self.dependencies.recordStore.remove(record) } } private func removeRecordIfMatches(pid: pid_t, identity: AntigravityCLIProcessIdentity?) { try? self.dependencies.launchLock.withLock { guard let identity, let records = try? self.dependencies.recordStore.load() else { return } for record in records where record.pid == pid && record.executablePath == identity.executablePath && abs(record.startEpoch - identity.startEpoch) < 0.001 { try? self.dependencies.recordStore.remove(record) } } } private static func nanoseconds(from interval: TimeInterval) -> UInt64 { guard interval > 0 else { return 0 } guard interval.isFinite else { return UInt64.max } let nanoseconds = interval * 1_000_000_000 guard nanoseconds < TimeInterval(UInt64.max) else { return UInt64.max } return UInt64(nanoseconds) } } // MARK: - Production Process Implementation struct AntigravityPTYProcessLauncher: AntigravityCLIProcessLaunching { static func defaultSignalsForSpawn() -> sigset_t { var signals = sigset_t() sigemptyset(&signals) sigaddset(&signals, SIGINT) sigaddset(&signals, SIGTERM) sigaddset(&signals, SIGHUP) return signals } static func spawnWithTextBusyRetry( maxAttempts: Int = 3, retryDelay: TimeInterval = 0.01, spawn: () -> Int32) -> Int32 { var result = spawn() guard maxAttempts > 1 else { return result } for _ in 1.. 0 { Thread.sleep(forTimeInterval: retryDelay) } result = spawn() } return result } func launch(binary: String) throws -> any AntigravityCLIProcessHandle { try self.launch(binary: binary, arguments: []) } func launch(binary: String, arguments: [String]) throws -> any AntigravityCLIProcessHandle { var primaryFD: Int32 = -1 var secondaryFD: Int32 = -1 var win = winsize(ws_row: 50, ws_col: 160, ws_xpixel: 0, ws_ypixel: 0) guard openpty(&primaryFD, &secondaryFD, nil, nil, &win) == 0 else { throw AntigravityCLISession.SessionError.launchFailed("openpty failed") } _ = fcntl(primaryFD, F_SETFL, O_NONBLOCK) let primaryHandle = FileHandle(fileDescriptor: primaryFD, closeOnDealloc: true) let secondaryHandle = FileHandle(fileDescriptor: secondaryFD, closeOnDealloc: true) #if canImport(Darwin) var fileActions: posix_spawn_file_actions_t? #else var fileActions = posix_spawn_file_actions_t() #endif guard posix_spawn_file_actions_init(&fileActions) == 0 else { try? primaryHandle.close() try? secondaryHandle.close() throw AntigravityCLISession.SessionError.launchFailed("posix_spawn_file_actions_init failed") } defer { posix_spawn_file_actions_destroy(&fileActions) } posix_spawn_file_actions_adddup2(&fileActions, secondaryFD, 0) posix_spawn_file_actions_adddup2(&fileActions, secondaryFD, 1) posix_spawn_file_actions_adddup2(&fileActions, secondaryFD, 2) posix_spawn_file_actions_addclose(&fileActions, primaryFD) posix_spawn_file_actions_addclose(&fileActions, secondaryFD) let homeDirectory = NSHomeDirectory() _ = homeDirectory.withCString { path in posix_spawn_file_actions_addchdir_np(&fileActions, path) } #if canImport(Glibc) || canImport(Musl) do { try PosixSpawnFileActionsCloseFrom.addCloseFrom(&fileActions, startingAt: 3) } catch { try? primaryHandle.close() try? secondaryHandle.close() throw AntigravityCLISession.SessionError.launchFailed(error.localizedDescription) } #endif #if canImport(Darwin) var attr: posix_spawnattr_t? #else var attr = posix_spawnattr_t() #endif guard posix_spawnattr_init(&attr) == 0 else { try? primaryHandle.close() try? secondaryHandle.close() throw AntigravityCLISession.SessionError.launchFailed("posix_spawnattr_init failed") } defer { posix_spawnattr_destroy(&attr) } var defaultSignals = Self.defaultSignalsForSpawn() posix_spawnattr_setsigdefault(&attr, &defaultSignals) #if canImport(Darwin) let spawnFlags = POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_CLOEXEC_DEFAULT #else let spawnFlags = POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGDEF #endif posix_spawnattr_setflags(&attr, Int16(spawnFlags)) posix_spawnattr_setpgroup(&attr, 0) var env = TTYCommandRunner.enrichedEnvironment() env["PWD"] = NSHomeDirectory() env["TERM"] = "xterm-256color" var cArgs = ([binary] + arguments).map { strdup($0) as UnsafeMutablePointer? } cArgs.append(nil) defer { for arg in cArgs { if let arg { free(arg) } } } var cEnv: [UnsafeMutablePointer?] = env.map { key, value in strdup("\(key)=\(value)") } cEnv.append(nil) defer { for entry in cEnv { if let entry { free(entry) } } } var pid: pid_t = 0 let spawnResult = Self.spawnWithTextBusyRetry { binary.withCString { execPath in posix_spawn(&pid, execPath, &fileActions, &attr, cArgs, cEnv) } } guard spawnResult == 0 else { try? primaryHandle.close() try? secondaryHandle.close() throw AntigravityCLISession.SessionError.launchFailed(String(cString: strerror(spawnResult))) } return AntigravitySpawnedPTYProcessHandle( pid: pid, processGroup: pid, primaryFD: primaryFD, primaryHandle: primaryHandle, secondaryHandle: secondaryHandle) } } final class AntigravitySpawnedPTYProcessHandle: AntigravityCLIProcessHandle, @unchecked Sendable { private let lock = NSLock() private let processPID: pid_t private let processGroupID: pid_t private let primaryFD: Int32 private let primaryHandle: FileHandle private let secondaryHandle: FileHandle private var reaped = false init( pid: pid_t, processGroup: pid_t, primaryFD: Int32, primaryHandle: FileHandle, secondaryHandle: FileHandle) { self.processPID = pid self.processGroupID = processGroup self.primaryFD = primaryFD self.primaryHandle = primaryHandle self.secondaryHandle = secondaryHandle } var pid: pid_t { self.processPID } var isRunning: Bool { self.lock.lock() if self.reaped { self.lock.unlock() return false } self.lock.unlock() var status: Int32 = 0 let result = waitpid(self.processPID, &status, WNOHANG) self.lock.lock() defer { self.lock.unlock() } switch result { case 0: return true case self.processPID: self.reaped = true return false case -1 where errno == ECHILD: self.reaped = true return false default: return kill(self.processPID, 0) == 0 || errno == EPERM } } var processGroup: pid_t? { self.processGroupID } func assignProcessGroup() -> pid_t? { self.processGroupID } func sendExit() throws { try self.writeAllToPrimary(Data("/exit\r".utf8)) } func closePTY() { try? self.primaryHandle.close() try? self.secondaryHandle.close() } func terminateRoot() { kill(self.processPID, SIGTERM) } func killRoot() { kill(self.processPID, SIGKILL) } func descendantPIDs() -> [pid_t] { TTYProcessTreeTerminator.descendantPIDs(of: self.processPID) } func terminateTree(signal: Int32, knownDescendants: [pid_t]) { TTYProcessTreeTerminator.terminateProcessTree( rootPID: self.processPID, processGroup: self.processGroupID, signal: signal, knownDescendants: knownDescendants) } func killDescendants(_ descendants: [pid_t]) { for pid in descendants where pid > 0 { kill(pid, SIGKILL) } } func drainOutput() -> Data { var tmp = [UInt8](repeating: 0, count: 8192) var output: [UInt8] = [] for _ in 0..<64 { let n = read(self.primaryFD, &tmp, tmp.count) if n > 0 { output.append(contentsOf: tmp.prefix(n)) continue } break } return Data(output) } private func writeAllToPrimary(_ data: Data) throws { data.withUnsafeBytes { rawBytes in guard let baseAddress = rawBytes.baseAddress else { return } var offset = 0 var retries = 0 while offset < rawBytes.count { let written = write(self.primaryFD, baseAddress.advanced(by: offset), rawBytes.count - offset) if written > 0 { offset += written retries = 0 continue } if written == 0 { break } let err = errno if err == EINTR || err == EAGAIN || err == EWOULDBLOCK { retries += 1 if retries > 200 { return } usleep(5000) continue } return } } } } // MARK: - Production Stale Session Identity + Storage struct AntigravityProcessIdentityProvider: AntigravityCLIProcessIdentityProviding { func identity(for pid: pid_t) -> AntigravityCLIProcessIdentity? { #if canImport(Darwin) var pathBuffer = [CChar](repeating: 0, count: 4096) let pathLength = proc_pidpath(pid, &pathBuffer, UInt32(pathBuffer.count)) guard pathLength > 0 else { return nil } let executablePath = pathBuffer.withUnsafeBufferPointer { buffer -> String? in let rawBytes = UnsafeRawBufferPointer(start: buffer.baseAddress, count: Int(pathLength)) return String(bytes: rawBytes.prefix { $0 != 0 }, encoding: .utf8) } guard let executablePath, !executablePath.isEmpty else { return nil } var info = proc_bsdinfo() let size = proc_pidinfo( pid, PROC_PIDTBSDINFO, 0, &info, Int32(MemoryLayout.stride)) guard size == Int32(MemoryLayout.stride) else { return nil } let startEpoch = TimeInterval(info.pbi_start_tvsec) + (TimeInterval(info.pbi_start_tvusec) / 1_000_000) return AntigravityCLIProcessIdentity(executablePath: executablePath, startEpoch: startEpoch) #else let procDirectory = "/proc/\(pid)" guard let executablePath = try? FileManager.default.destinationOfSymbolicLink( atPath: "\(procDirectory)/exe"), let stat = try? String(contentsOfFile: "\(procDirectory)/stat", encoding: .utf8), let closeParen = stat.lastIndex(of: ")") else { return nil } let fields = stat[stat.index(after: closeParen)...].split(whereSeparator: \.isWhitespace) let clockTicksPerSecond = sysconf(Int32(_SC_CLK_TCK)) let systemStat = try? String(contentsOfFile: "/proc/stat", encoding: .utf8) let bootEpoch = systemStat? .split(separator: "\n") .first { $0.hasPrefix("btime ") }? .split(whereSeparator: \.isWhitespace) .dropFirst() .first .flatMap { TimeInterval($0) } guard fields.count > 19, let startTicks = TimeInterval(fields[19]), clockTicksPerSecond > 0, let bootEpoch else { return nil } let startEpoch = bootEpoch + (startTicks / TimeInterval(clockTicksPerSecond)) return AntigravityCLIProcessIdentity(executablePath: executablePath, startEpoch: startEpoch) #endif } } final class AntigravityFileCLISessionRecordStore: AntigravityCLISessionRecordStoring, @unchecked Sendable { private let fileURL: URL private let fileManager: FileManager init( fileURL: URL = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) .appendingPathComponent(".codexbar", isDirectory: true) .appendingPathComponent("antigravity", isDirectory: true) .appendingPathComponent("agy-session.json"), fileManager: FileManager = .default) { self.fileURL = fileURL self.fileManager = fileManager } func load() throws -> [AntigravityCLISessionRecord] { guard self.fileManager.fileExists(atPath: self.fileURL.path) else { return [] } let data = try Data(contentsOf: self.fileURL) let decoder = JSONDecoder() if let records = try? decoder.decode([AntigravityCLISessionRecord].self, from: data) { return records } return try [decoder.decode(AntigravityCLISessionRecord.self, from: data)] } func save(_ record: AntigravityCLISessionRecord) throws { var records = (try? self.load()) ?? [] records.removeAll { Self.sameOwner($0, record) } records.append(record) try self.write(records) } func remove(_ record: AntigravityCLISessionRecord) throws { var records = try self.load() records.removeAll { $0.pid == record.pid && $0.executablePath == record.executablePath && abs($0.startEpoch - record.startEpoch) < 0.001 } if records.isEmpty { guard self.fileManager.fileExists(atPath: self.fileURL.path) else { return } try self.fileManager.removeItem(at: self.fileURL) } else { try self.write(records) } } private func write(_ records: [AntigravityCLISessionRecord]) throws { let directory = self.fileURL.deletingLastPathComponent() try self.fileManager.createDirectory(at: directory, withIntermediateDirectories: true) let data = try JSONEncoder().encode(records) try data.write(to: self.fileURL, options: [.atomic]) } private static func sameOwner( _ lhs: AntigravityCLISessionRecord, _ rhs: AntigravityCLISessionRecord) -> Bool { if let lhsOwnerPID = lhs.ownerPID, let rhsOwnerPID = rhs.ownerPID, let lhsOwnerPath = lhs.ownerExecutablePath, let rhsOwnerPath = rhs.ownerExecutablePath, let lhsOwnerStart = lhs.ownerStartEpoch, let rhsOwnerStart = rhs.ownerStartEpoch { return lhsOwnerPID == rhsOwnerPID && lhsOwnerPath == rhsOwnerPath && abs(lhsOwnerStart - rhsOwnerStart) < 0.001 } return lhs.pid == rhs.pid && lhs.executablePath == rhs.executablePath && abs(lhs.startEpoch - rhs.startEpoch) < 0.001 } } final class AntigravityFileCLISessionLaunchLock: AntigravityCLISessionLaunchLocking, @unchecked Sendable { private let fileURL: URL private let fileManager: FileManager init( fileURL: URL = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) .appendingPathComponent(".codexbar", isDirectory: true) .appendingPathComponent("antigravity", isDirectory: true) .appendingPathComponent("agy-session.lock"), fileManager: FileManager = .default) { self.fileURL = fileURL self.fileManager = fileManager } func withLock(_ operation: () throws -> T) throws -> T { let directory = self.fileURL.deletingLastPathComponent() try self.fileManager.createDirectory(at: directory, withIntermediateDirectories: true) let fd = open(self.fileURL.path, O_CREAT | O_RDWR | O_CLOEXEC, S_IRUSR | S_IWUSR) guard fd >= 0 else { throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) } defer { _ = flock(fd, LOCK_UN) close(fd) } while flock(fd, LOCK_EX) != 0 { guard errno == EINTR else { throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) } } return try operation() } }