Skip to main content
Glama
InProcessCommandRunner.swift9.9 kB
import Darwin import Foundation import PeekabooCore import Testing @testable import PeekabooCLI private actor InProcessRunGate { func run<T>(_ operation: @Sendable () async throws -> T) async rethrows -> T { try await operation() } } struct CommandRunResult { let stdout: String let stderr: String let exitStatus: Int32 var combinedOutput: String { self.stdout.isEmpty ? self.stderr : self.stdout } func validateExitStatus(allowedExitCodes: Set<Int32>, arguments: [String]) throws { guard allowedExitCodes.contains(self.exitStatus) else { throw CommandExecutionError( status: self.exitStatus, stdout: self.stdout, stderr: self.stderr, arguments: arguments ) } } } struct CommandExecutionError: Error, CustomStringConvertible { let status: Int32 let stdout: String let stderr: String let arguments: [String] var description: String { "Command \(self.arguments.joined(separator: " ")) failed with exit code \(self.status)." + "\nstdout: \(self.stdout)\nstderr: \(self.stderr)" } } enum InProcessCommandRunner { private static let gate = InProcessRunGate() static func run( _ arguments: [String], services: PeekabooServices, spaceService: (any SpaceCommandSpaceService)? = nil ) async throws -> CommandRunResult { try await self.gate.run { try await CommandRuntime.withInjectedServices(services) { if let spaceService { try await SpaceCommandEnvironment.withSpaceService(spaceService) { try await self.execute(arguments: arguments) } } else { try await self.execute(arguments: arguments) } } } } /// Run the CLI using the default shared services (no overrides). static func runWithSharedServices(_ arguments: [String]) async throws -> CommandRunResult { // Use stubbed services in tests to avoid driving the real UI while still exercising // command wiring and JSON formatting. let services = TestServicesFactory.makePeekabooServices() return try await self.gate.run { try await CommandRuntime.withInjectedServices(services) { try await self.execute(arguments: arguments) } } } /// Convenience helper for tests that rely on the shared service stack and expect specific exit codes. static func runShared( _ arguments: [String], allowedExitCodes: Set<Int32> = [0] ) async throws -> CommandRunResult { let result = try await self.runWithSharedServices(arguments) try result.validateExitStatus(allowedExitCodes: allowedExitCodes, arguments: arguments) return result } private static func execute(arguments: [String]) async throws -> CommandRunResult { try await self.captureOutput { var exitStatus: Int32 = 0 var stdoutData = Data() var stderrData = Data() let result: (Int32, Data, Data) = try await self.redirectOutput { await executePeekabooCLI(arguments: ["peekaboo"] + arguments) } exitStatus = result.0 stdoutData = result.1 stderrData = result.2 let stdout = String(data: stdoutData, encoding: .utf8) ?? "" let stderr = String(data: stderrData, encoding: .utf8) ?? "" return CommandRunResult(stdout: stdout, stderr: stderr, exitStatus: exitStatus) } } private static func captureOutput( _ operation: () async throws -> CommandRunResult ) async throws -> CommandRunResult { try await operation() } private static func redirectOutput( _ body: () async throws -> Int32 ) async throws -> (Int32, Data, Data) { // Prevent writes to closed pipes from crashing the test runner. let previousSigpipeHandler = signal(SIGPIPE, SIG_IGN) defer { _ = signal(SIGPIPE, previousSigpipeHandler) } let stdoutPipe = Pipe() let stderrPipe = Pipe() let originalStdout = dup(STDOUT_FILENO) let originalStderr = dup(STDERR_FILENO) dup2(stdoutPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO) dup2(stderrPipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO) stdoutPipe.fileHandleForWriting.closeFile() stderrPipe.fileHandleForWriting.closeFile() do { let status = try await body() dup2(originalStdout, STDOUT_FILENO) dup2(originalStderr, STDERR_FILENO) close(originalStdout) close(originalStderr) let stdoutData = self.drainNonBlocking(stdoutPipe.fileHandleForReading) let stderrData = self.drainNonBlocking(stderrPipe.fileHandleForReading) stdoutPipe.fileHandleForReading.closeFile() stderrPipe.fileHandleForReading.closeFile() return (status, stdoutData, stderrData) } catch { dup2(originalStdout, STDOUT_FILENO) dup2(originalStderr, STDERR_FILENO) close(originalStdout) close(originalStderr) _ = self.drainNonBlocking(stdoutPipe.fileHandleForReading) _ = self.drainNonBlocking(stderrPipe.fileHandleForReading) stdoutPipe.fileHandleForReading.closeFile() stderrPipe.fileHandleForReading.closeFile() throw error } } private static func drainNonBlocking(_ handle: FileHandle) -> Data { let fd = handle.fileDescriptor let flags = fcntl(fd, F_GETFL) if flags != -1 { _ = fcntl(fd, F_SETFL, flags | O_NONBLOCK) } var buffer = [UInt8](repeating: 0, count: 4096) var data = Data() while true { let bytesRead = read(fd, &buffer, buffer.count) if bytesRead > 0 { data.append(buffer, count: bytesRead) continue } if bytesRead == 0 { break // EOF } if errno == EAGAIN || errno == EWOULDBLOCK { break // no more data right now } break // other error; bail out } return data } } enum ExternalCommandRunner { enum Error: Swift.Error, LocalizedError { case executableNotFound(String) case jsonPayloadMissing(output: String) var errorDescription: String? { switch self { case let .executableNotFound(path): "Unable to find executable at \(path)" case let .jsonPayloadMissing(output): "Expected JSON payload was not found in command output:\n\(output)" } } } @discardableResult static func runPolterPeekaboo( _ arguments: [String], allowedExitCodes: Set<Int32> = [0], environment: [String: String] = ProcessInfo.processInfo.environment ) throws -> CommandRunResult { let runnerPath = "./runner" guard FileManager.default.isExecutableFile(atPath: runnerPath) else { throw Error.executableNotFound(runnerPath) } let process = Process() process.currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) process.executableURL = URL(fileURLWithPath: runnerPath) process.arguments = ["polter", "peekaboo", "--"] + arguments process.environment = environment let stdoutPipe = Pipe() let stderrPipe = Pipe() process.standardOutput = stdoutPipe process.standardError = stderrPipe try process.run() process.waitUntilExit() let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() let stdout = String(data: stdoutData, encoding: .utf8) ?? "" let stderr = String(data: stderrData, encoding: .utf8) ?? "" let result = CommandRunResult( stdout: stdout, stderr: stderr, exitStatus: process.terminationStatus ) try result.validateExitStatus( allowedExitCodes: allowedExitCodes, arguments: ["polter", "peekaboo"] + arguments ) return result } static func decodeJSONResponse<T: Decodable>( from result: CommandRunResult, as type: T.Type ) throws -> T { let combinedOutput: String = if result.stdout.isEmpty { result.stderr } else if result.stderr.isEmpty { result.stdout } else { result.stdout + "\n" + result.stderr } guard let jsonString = Self.extractFirstJSONObject(from: combinedOutput), let data = jsonString.data(using: .utf8) else { throw Error.jsonPayloadMissing(output: combinedOutput) } let decoder = JSONDecoder() return try decoder.decode(T.self, from: data) } private static func extractFirstJSONObject(from output: String) -> String? { guard let firstBraceIndex = output.firstIndex(of: "{") else { return nil } var depth = 0 var currentIndex = firstBraceIndex while currentIndex < output.endIndex { let character = output[currentIndex] if character == "{" { depth += 1 } else if character == "}" { depth -= 1 if depth == 0 { return String(output[firstBraceIndex...currentIndex]) } } output.formIndex(after: &currentIndex) } return nil } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/steipete/Peekaboo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server