Peekaboo/docs/swift-subprocess.md
2025-12-19 02:35:28 +00:00

5.6 KiB
Raw Permalink Blame History

summary read_when
Review swift-subprocess Adoption Guide guidance
planning work related to swift-subprocess adoption guide
debugging or extending features described here

swift-subprocess Adoption Guide

Why We Care

  • Our test suites launch dozens of child processes (swift run peekaboo, axorc, shell utilities) and each file hand-rolls Process, Pipe, and blocking drain logic. This duplication is fragile and contributes to flakiness when stdout/stderr buffers fill.
  • The swift-subprocess package (latest tag 0.2.1, Swift 6.1+/macOS 13+) ships an async/await-native wrapper around posix_spawn, providing streaming output via AsyncSequence, structured configuration, and built-in cancellation. It eliminates the classic deadlock that occurs when Process pipes arent drained quickly enough.citeturn1open0turn1open1turn1open2
  • Package status: beta, owned by the Swift project, with the first stable release targeted for early 2026. Expect API polishing; keep adoption behind our own façade so we can react to breaking changes quickly.citeturn1open0turn1open1

Pilot Scope (Tests First)

  • Focus the first integration on the now-retired CLI runner (Apps/CLI/Tests/CLIAutomationTests/Support/CommandRunner.swift). All “safe” suites run via InProcessCommandRunner, and historical references to PeekabooCLITestRunner have been removed.
  • Audit additional hot spots once the pilot lands:
    • AXorcist test helpers (AXorcist/Tests/AXorcistTests/CommonTestHelpers.swift) when invoking the axorc binary.
    • CLI automation tests that manually stand up Process instances for menu/window focus helpers (Apps/CLI/Tests/CLIAutomationTests/*.swift, see rg "Pipe()" output). These can eventually share a common helper that wraps Subprocess.
  • Production code paths (e.g. ShellTool, DockService) remain untouched until the test pilot proves stable and we design a broader façade for long-lived services.

Integration Plan

  1. Add the dependency
  • Declare swift-subprocess in the relevant package manifests: start with Apps/CLI/Package.swift and AXorcist/Package.swift test targets only. Keep it test-only until we validate behavior.
  • Pin to an explicit minor version (from: "0.2.1") and enable exact revisions in Package.resolved to avoid silent API shifts.
  1. Wrap Subprocess behind a helper
    • Introduce a small internal type (e.g. TestChildProcess) that mirrors the subset of features we rely on (arguments, environment, streaming stdout/stderr, timeout). This wrapper will call into Subprocess ChildProcess.spawn(...), surface async iteration of process.stdout.lines, and return collected output on success/failure.
    • Preserve our existing error surface (CommandError(status:output:)) by translating SubprocessError into our domain model. Include the captured stderr text in thrown errors.
  2. Retire PeekabooCLITestRunner
    • Historical note: the runner has been removed now that every automation suite runs via the harness.
  3. Roll out to other helpers
    • Migrate AXorcists runAXORCCommand and similar utilities once the CLI pilot is stable for a week of CI runs.
    • Document any platform-specific observations (e.g. sandbox quirks, resource cleanup) in this file as we go.
  4. Evaluate production adoption
    • After tests prove reliable, design a PeekabooCore abstraction (ChildProcessService) that can swap Process vs. Subprocess internally. Production code often needs cancellation, long-running streaming, and the occasional pseudo-terminal; confirm Subprocess PTY story before we rely on it inside the MCP transports.

Usage Cheatsheet

import Subprocess

struct TestChildProcess {
    static func runPeekaboo(_ args: [String]) async throws -> String {
        var options = ChildProcessOptions()
        options.currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
        options.environment = ProcessInfo.processInfo.environment

        let process = try await ChildProcess.spawn(
            command: "/usr/bin/env",
            arguments: ["swift", "run", "peekaboo"] + args,
            options: options
        )

        var output = ""
        for try await line in process.stdout.lines {
            output.append(line)
            output.append("\n")
        }

        let exitStatus = await process.waitForExit()
        guard exitStatus == .code(0) else {
            throw CommandError(status: exitStatus.exitCode, output: output)
        }
        return output
    }
}
  • ChildProcess.spawn returns immediately; consumers iterate its AsyncThrowingStream properties (stdout.bytes, stdout.lines, stderr.lines) without extra pipes or threads.
  • waitForExit() yields a ChildProcess.Termination enum. Use .code(Int32) for numeric exit codes, .signal(Int32) for signal terminations.
  • Cancellation: wrapping the spawn in withTimeout or explicitly calling process.terminate() cooperates with async tasks. This will help us enforce per-test timeouts instead of blocking on waitUntilExit().

Open Questions

  • PTY support is currently experimental. Even though our MCP server relies on stdio pipes, confirm Subprocess pseudo-terminal roadmap before depending on it for future CLI integrations.
  • Some of our tests rely on combined stdout/stderr ordering. Subprocess exposes them separately; we need to decide whether to merge streams manually or only capture stderr when non-empty.
  • Monitor the upstream issue tracker for breaking changes ahead of 1.0.0; update this doc with any migration notes after each dependency bump.