Subprocess

Several Jorges running in a relay race, each one passing a baton shaped like the Swift logo to the next, representing the migration from Process to Subprocess.

Table of contents


Problem

When working with external processes in Swift applications, the traditional Process class presents several significant limitations:

  • Manual resource management: Requires explicit configuration of executable URLs, arguments, and output handling
  • Lack of async/await support: Uses synchronous methods that block the execution thread
  • Complex error handling: Makes it difficult to capture and process errors from child processes
  • Verbose configuration: Each execution requires multiple lines of repetitive configuration

In the previous code, I needed to execute Ghostscript to convert PDF files to PNG images, but the implementation with Process was extensive and inelegant.

func _PDFToImages(
    _ fileName: PathEnum
) async throws {
    let process = Process()

    process.executableURL = URL(
        fileURLWithPath: "/opt/homebrew/bin/gs"
    )

    process.arguments = [
        "-dNOPAUSE", "-dBATCH",
        "-dQUIET", "-sDEVICE=png16m",
        "-r300",
        "-sOutputFile=\(fileName.rawValue)-%d.png",
        fileName.rawValue.appending(".pdf"),
    ]

    try process.run()
    process.waitUntilExit()
}

Solution

Apple’s new Subprocess library provides a modern and robust API for process execution in Swift. This cross-platform library offers native support for async/await and automatic resource management.

Key features:

Native async/await API for non-blocking operations.

Automatic resource management and process cleanup.

Granular output control (stdout, stderr).

Integrated termination status verification.

Concise and expressive syntax.

The modernized implementation uses the run function from Subprocess:

func _PDFToImages(
    _ fileName: PathEnum
) async throws {
    let result = try await run(
        .path(.init("/opt/homebrew/bin/gs")),
        arguments: [
            "-dNOPAUSE", "-dBATCH",
            "-dQUIET", "-sDEVICE=png16m",
            "-r300",
            "-sOutputFile=\(fileName.rawValue)-%d.png",
            fileName.rawValue.appending(".pdf"),
        ],
        output: .discarded,
        error: .string(limit: .max)
    )

    try guardAndLogError(
        result.terminationStatus == .exited(0),
        message: result.standardError,
        status: .internalServerError
    )
}

Key parameters:

  • .path: Specifies the executable directly
  • output: .discarded: Discards standard output since we don’t need to process it
  • error: .string(limit: .max): Captures errors as a string for logging
  • result.terminationStatus: Verifies that the process terminated successfully

Result

The migration to Subprocess transforms process management code into a cleaner, safer, and more efficient solution:

Benefits achieved:

🚀 Improved performance with true asynchronous operations.

🔒 Better error handling with integrated stderr capture.

📝 More readable code with less manual configuration.

Seamless integration with the modern Swift concurrency ecosystem.

The new implementation not only reduces complexity but also improves system robustness by providing better error visibility and more elegant asynchronous execution handling in Vapor applications.


Note on DYLD_LIBRARY_PATH

In earlier versions of Swift, there was a known issue with the DYLD_LIBRARY_PATH environment variable when executing external processes. Due to macOS’s System Integrity Protection (SIP) restrictions, this variable was automatically removed when launching subprocesses, causing “Library not loaded” errors in certain cases.

The temporary solution required manually configuring library paths using install_name_tool with @rpath, or alternatively setting the DYLD_LIBRARY_PATH variable instead.

Good news! 🎉 This issue has been resolved in recent versions of Swift. The Subprocess library correctly handles system environment variables, including DYLD_LIBRARY_PATH, without requiring additional configuration.

Keep coding, keep running 🏃‍♂️