├── .swift-version ├── .licenseignore ├── .spi.yml ├── Sources ├── _SubprocessCShims │ ├── include │ │ ├── module.modulemap │ │ ├── target_conditionals.h │ │ └── process_shims.h │ └── CMakeLists.txt ├── CMakeLists.txt └── Subprocess │ ├── Execution.swift │ ├── SubprocessFoundation │ ├── Output+Foundation.swift │ ├── Span+SubprocessFoundation.swift │ └── Input+Foundation.swift │ ├── Platforms │ └── Subprocess+BSD.swift │ ├── Span+Subprocess.swift │ ├── CMakeLists.txt │ ├── Result.swift │ ├── Buffer.swift │ ├── IO │ ├── AsyncIO+Dispatch.swift │ ├── Input.swift │ └── Output.swift │ ├── Error.swift │ ├── Teardown.swift │ ├── Thread.swift │ └── AsyncBufferSequence.swift ├── CODE_OF_CONDUCT.md ├── CODEOWNERS ├── .license_header_template ├── CONTRIBUTING.md ├── cmake └── modules │ ├── SwiftSubprocessConfig.cmake.in │ ├── CMakeLists.txt │ ├── InstallSwiftInterface.cmake │ ├── InstallExternalDependencies.cmake │ ├── PlatformInfo.cmake │ └── EmitSwiftInterface.cmake ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── pull_request.yml ├── Tests ├── TestResources │ ├── Resources │ │ ├── getgroups.swift │ │ └── windows-tester.ps1 │ └── TestResources.swift └── SubprocessTests │ ├── PlatformConformance.swift │ ├── TestSupport.swift │ ├── LinterTests.swift │ ├── LinuxTests.swift │ ├── DarwinTests.swift │ ├── AsyncIOTests.swift │ └── ProcessMonitoringTests.swift ├── CMakeLists.txt ├── .gitignore ├── Package.swift ├── .swift-format ├── README.md └── LICENSE /.swift-version: -------------------------------------------------------------------------------- 1 | 6.2.0 -------------------------------------------------------------------------------- /.licenseignore: -------------------------------------------------------------------------------- 1 | Package.swift 2 | Package@swift-6.0.swift 3 | LICENSE 4 | .swift-version 5 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - swift_version: 6.1 5 | documentation_targets: [Subprocess] 6 | scheme: Subprocess 7 | -------------------------------------------------------------------------------- /Sources/_SubprocessCShims/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module _SubprocessCShims { 2 | header "process_shims.h" 3 | header "target_conditionals.h" 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The code of conduct for this project can be found at https://swift.org/code-of-conduct. 4 | 5 | 6 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # 2 | # This source file is part of the Swift.org open source project 3 | # 4 | # Copyright (c) 2023 Apple Inc. and the Swift project authors 5 | # Licensed under Apache License v2.0 with Runtime Library Exception 6 | # 7 | # See https://swift.org/LICENSE.txt for license information 8 | # See https://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | # 10 | 11 | * @iCharlesHu -------------------------------------------------------------------------------- /.license_header_template: -------------------------------------------------------------------------------- 1 | @@===----------------------------------------------------------------------===@@ 2 | @@ 3 | @@ This source file is part of the Swift.org open source project 4 | @@ 5 | @@ Copyright (c) YEARS Apple Inc. and the Swift project authors 6 | @@ Licensed under Apache License v2.0 with Runtime Library Exception 7 | @@ 8 | @@ See https://swift.org/LICENSE.txt for license information 9 | @@ 10 | @@===----------------------------------------------------------------------===@@ 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | By submitting a pull request, you represent that you have the right to license 2 | your contribution to Apple and the community, and agree by submitting the patch 3 | that your contributions are licensed under the [Swift 4 | license](https://swift.org/LICENSE.txt). 5 | 6 | --- 7 | 8 | Before submitting the pull request, please make sure you have tested your 9 | changes and that they follow the Swift project [guidelines for contributing 10 | code](https://swift.org/contributing/#contributing-code). 11 | -------------------------------------------------------------------------------- /Sources/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ##===----------------------------------------------------------------------===## 2 | ## 3 | ## This source file is part of the Swift.org open source project 4 | ## 5 | ## Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | ## Licensed under Apache License v2.0 with Runtime Library Exception 7 | ## 8 | ## See https://swift.org/LICENSE.txt for license information 9 | ## 10 | ##===----------------------------------------------------------------------===## 11 | 12 | add_subdirectory(_SubprocessCShims) 13 | add_subdirectory(Subprocess) 14 | -------------------------------------------------------------------------------- /cmake/modules/SwiftSubprocessConfig.cmake.in: -------------------------------------------------------------------------------- 1 | ##===----------------------------------------------------------------------===## 2 | ## 3 | ## This source file is part of the Swift.org open source project 4 | ## 5 | ## Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | ## Licensed under Apache License v2.0 with Runtime Library Exception 7 | ## 8 | ## See https://swift.org/LICENSE.txt for license information 9 | ## 10 | ##===----------------------------------------------------------------------===## 11 | 12 | include(${CMAKE_CURRENT_BINARY_DIR}/../SwiftSubprocess/SwiftSubprocessTargets.cmake) 13 | -------------------------------------------------------------------------------- /cmake/modules/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ##===----------------------------------------------------------------------===## 2 | ## 3 | ## This source file is part of the Swift.org open source project 4 | ## 5 | ## Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | ## Licensed under Apache License v2.0 with Runtime Library Exception 7 | ## 8 | ## See https://swift.org/LICENSE.txt for license information 9 | ## 10 | ##===----------------------------------------------------------------------===## 11 | 12 | configure_file(SwiftSubprocessConfig.cmake.in 13 | ${CMAKE_CURRENT_BINARY_DIR}/SwiftSubprocessConfig.cmake) 14 | 15 | -------------------------------------------------------------------------------- /Sources/_SubprocessCShims/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ##===----------------------------------------------------------------------===## 2 | ## 3 | ## This source file is part of the Swift.org open source project 4 | ## 5 | ## Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | ## Licensed under Apache License v2.0 with Runtime Library Exception 7 | ## 8 | ## See https://swift.org/LICENSE.txt for license information 9 | ## 10 | ##===----------------------------------------------------------------------===## 11 | 12 | add_library(_SubprocessCShims STATIC 13 | process_shims.c) 14 | target_include_directories(_SubprocessCShims PUBLIC 15 | include) 16 | 17 | install(TARGETS _SubprocessCShims 18 | EXPORT SwiftSubprocessTargets) 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Subprocess future directions 4 | title: "[Feature] Feature Request" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Subprocess 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. What executable was running? 16 | 2. What arguments are passed in? 17 | 3. What environment values are used? 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Environment (please complete the following information):** 23 | - OS [e.g. macOS 15] 24 | - Swift version (run `swift --version`) [e.g. swiftlang-6.2.0.1.23 clang-1700.3.1.3] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /cmake/modules/InstallSwiftInterface.cmake: -------------------------------------------------------------------------------- 1 | ##===----------------------------------------------------------------------===## 2 | ## 3 | ## This source file is part of the Swift.org open source project 4 | ## 5 | ## Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | ## Licensed under Apache License v2.0 with Runtime Library Exception 7 | ## 8 | ## See https://swift.org/LICENSE.txt for license information 9 | ## 10 | ##===----------------------------------------------------------------------===## 11 | 12 | # Install the generated swift interface files for the target. 13 | function(install_swift_interface target) 14 | # Swiftmodules are already in the directory structure 15 | get_target_property(module_name ${target} Swift_MODULE_NAME) 16 | if(NOT module_name) 17 | set(module_name ${target}) 18 | endif() 19 | 20 | install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${module_name}.swiftmodule" 21 | DESTINATION "${${PROJECT_NAME}_INSTALL_SWIFTMODULEDIR}" 22 | COMPONENT SwiftSubprocess_development) 23 | endfunction() 24 | -------------------------------------------------------------------------------- /Tests/TestResources/Resources/getgroups.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if canImport(Darwin) 13 | import Darwin 14 | #elseif canImport(Android) 15 | import Bionic 16 | #elseif canImport(Glibc) 17 | import Glibc 18 | #elseif canImport(Musl) 19 | import Musl 20 | #endif 21 | 22 | let ngroups = getgroups(0, nil) 23 | guard ngroups >= 0 else { 24 | perror("ngroups should be > 0") 25 | exit(1) 26 | } 27 | var groups = [gid_t](repeating: 0, count: Int(ngroups)) 28 | guard getgroups(ngroups, &groups) >= 0 else { 29 | perror("getgroups failed") 30 | exit(errno) 31 | } 32 | let result = groups.map { String($0) }.joined(separator: ",") 33 | print(result) 34 | -------------------------------------------------------------------------------- /cmake/modules/InstallExternalDependencies.cmake: -------------------------------------------------------------------------------- 1 | ##===----------------------------------------------------------------------===## 2 | ## 3 | ## This source file is part of the Swift.org open source project 4 | ## 5 | ## Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | ## Licensed under Apache License v2.0 with Runtime Library Exception 7 | ## 8 | ## See https://swift.org/LICENSE.txt for license information 9 | ## 10 | ##===----------------------------------------------------------------------===## 11 | 12 | include_guard() 13 | 14 | include(FetchContent) 15 | 16 | find_package(SwiftSystem QUIET) 17 | if(NOT SwiftSystem_FOUND) 18 | message("-- Vendoring swift-system") 19 | FetchContent_Declare(SwiftSystem 20 | GIT_REPOSITORY https://github.com/apple/swift-system.git 21 | GIT_TAG a34201439c74b53f0fd71ef11741af7e7caf01e1 # 1.4.2 22 | GIT_SHALLOW YES) 23 | list(APPEND VendoredDependencies SwiftSystem) 24 | endif() 25 | 26 | if(VendoredDependencies) 27 | FetchContent_MakeAvailable(${VendoredDependencies}) 28 | if(NOT TARGET SwiftSystem::SystemPackage) 29 | add_library(SwiftSystem::SystemPackage ALIAS SystemPackage) 30 | endif() 31 | endif() 32 | -------------------------------------------------------------------------------- /Sources/_SubprocessCShims/include/target_conditionals.h: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2021 - 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #ifndef _SHIMS_TARGET_CONDITIONALS_H 13 | #define _SHIMS_TARGET_CONDITIONALS_H 14 | 15 | #if __has_include() 16 | #include 17 | #endif 18 | 19 | #if (defined(__APPLE__) && defined(__MACH__)) 20 | #define TARGET_OS_MAC 1 21 | #else 22 | #define TARGET_OS_MAC 0 23 | #endif 24 | 25 | #if defined(__linux__) 26 | #define TARGET_OS_LINUX 1 27 | #else 28 | #define TARGET_OS_LINUX 0 29 | #endif 30 | 31 | #if defined(__FreeBSD__) 32 | #define TARGET_OS_FREEBSD 1 33 | #else 34 | #define TARGET_OS_FREEBSD 0 35 | #endif 36 | 37 | #if defined(__unix__) 38 | #define TARGET_OS_UNIX 1 39 | #else 40 | #define TARGET_OS_UNIX 0 41 | #endif 42 | 43 | #if defined(_WIN32) 44 | #define TARGET_OS_WINDOWS 1 45 | #else 46 | #define TARGET_OS_WINDOWS 0 47 | #endif 48 | 49 | #if defined(__wasi__) 50 | #define TARGET_OS_WASI 1 51 | #else 52 | #define TARGET_OS_WASI 0 53 | #endif 54 | 55 | #endif // _SHIMS_TARGET_CONDITIONALS_H 56 | -------------------------------------------------------------------------------- /Sources/Subprocess/Execution.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if canImport(System) 13 | @preconcurrency import System 14 | #else 15 | @preconcurrency import SystemPackage 16 | #endif 17 | 18 | #if canImport(Darwin) 19 | import Darwin 20 | #elseif canImport(Bionic) 21 | import Bionic 22 | #elseif canImport(Glibc) 23 | import Glibc 24 | #elseif canImport(Musl) 25 | import Musl 26 | #elseif canImport(WinSDK) 27 | @preconcurrency import WinSDK 28 | #endif 29 | 30 | /// An object that represents a subprocess that has been 31 | /// executed. You can use this object to send signals to the 32 | /// child process as well as stream its output and error. 33 | public struct Execution: Sendable { 34 | /// The process identifier of the current execution 35 | public let processIdentifier: ProcessIdentifier 36 | 37 | init( 38 | processIdentifier: ProcessIdentifier 39 | ) { 40 | self.processIdentifier = processIdentifier 41 | } 42 | } 43 | 44 | // MARK: - Output Capture 45 | internal enum OutputCapturingState: Sendable { 46 | case standardOutputCaptured(Output) 47 | case standardErrorCaptured(Error) 48 | } 49 | -------------------------------------------------------------------------------- /Tests/TestResources/Resources/windows-tester.ps1: -------------------------------------------------------------------------------- 1 | ##===----------------------------------------------------------------------===## 2 | ## 3 | ## This source file is part of the Swift.org open source project 4 | ## 5 | ## Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | ## Licensed under Apache License v2.0 with Runtime Library Exception 7 | ## 8 | ## See https://swift.org/LICENSE.txt for license information 9 | ## 10 | ##===----------------------------------------------------------------------===## 11 | 12 | param ( 13 | [string]$mode, 14 | [int]$processID 15 | ) 16 | 17 | Add-Type @" 18 | using System; 19 | using System.Runtime.InteropServices; 20 | public class NativeMethods { 21 | [DllImport("Kernel32.dll")] 22 | public static extern IntPtr GetConsoleWindow(); 23 | } 24 | "@ 25 | 26 | function GetConsoleWindow { 27 | $consoleHandle = [NativeMethods]::GetConsoleWindow() 28 | Write-Host $consoleHandle 29 | } 30 | 31 | function IsProcessSuspended { 32 | $process = Get-Process -Id $processID -ErrorAction SilentlyContinue 33 | if ($process) { 34 | $threads = $process.Threads 35 | $suspendedThreadCount = ($threads | Where-Object { $_.WaitReason -eq 'Suspended' }).Count 36 | if ($threads.Count -eq $suspendedThreadCount) { 37 | Write-Host "true" 38 | } else { 39 | Write-Host "false" 40 | } 41 | } else { 42 | Write-Host "Process not found." 43 | } 44 | } 45 | 46 | switch ($mode) { 47 | 'get-console-window' { GetConsoleWindow } 48 | 'is-process-suspended' { IsProcessSuspended -processID $processID } 49 | default { Write-Host "Invalid mode specified: [$mode]" } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/SubprocessTests/PlatformConformance.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @testable import Subprocess 13 | 14 | #if canImport(Darwin) 15 | import Darwin 16 | #elseif canImport(Bionic) 17 | import Bionic 18 | #elseif canImport(Glibc) 19 | import Glibc 20 | #elseif canImport(Musl) 21 | import Musl 22 | #elseif canImport(WinSDK) 23 | import WinSDK 24 | #endif 25 | 26 | /// This file defines protocols for platform-specific structs and 27 | /// adds retroactive conformances to them to ensure they all conform 28 | /// to a uniform shape. We opted to keep these protocols in the test 29 | /// target as opposed to making them public APIs because we don't 30 | /// directly use them in public APIs. 31 | protocol ProcessIdentifierProtocol: Sendable, Hashable, CustomStringConvertible, CustomDebugStringConvertible { 32 | #if os(Windows) 33 | var value: DWORD { get } 34 | #else 35 | var value: pid_t { get } 36 | #endif 37 | 38 | #if os(Linux) || os(Android) || os(FreeBSD) 39 | var processDescriptor: PlatformFileDescriptor { get } 40 | #endif 41 | 42 | #if os(Windows) 43 | nonisolated(unsafe) var processDescriptor: PlatformFileDescriptor { get } 44 | nonisolated(unsafe) var threadHandle: PlatformFileDescriptor { get } 45 | #endif 46 | } 47 | 48 | extension ProcessIdentifier: ProcessIdentifierProtocol {} 49 | -------------------------------------------------------------------------------- /cmake/modules/PlatformInfo.cmake: -------------------------------------------------------------------------------- 1 | ##===----------------------------------------------------------------------===## 2 | ## 3 | ## This source file is part of the Swift.org open source project 4 | ## 5 | ## Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | ## Licensed under Apache License v2.0 with Runtime Library Exception 7 | ## 8 | ## See https://swift.org/LICENSE.txt for license information 9 | ## 10 | ##===----------------------------------------------------------------------===## 11 | 12 | # TODO: This logic should migrate to CMake once CMake supports installing swiftmodules 13 | set(module_triple_command "${CMAKE_Swift_COMPILER}" -print-target-info) 14 | if(CMAKE_Swift_COMPILER_TARGET) 15 | list(APPEND module_triple_command -target ${CMAKE_Swift_COMPILER_TARGET}) 16 | endif() 17 | execute_process(COMMAND ${module_triple_command} OUTPUT_VARIABLE target_info_json) 18 | message(CONFIGURE_LOG "Swift target info: ${module_triple_command}\n" 19 | "${target_info_json}") 20 | 21 | if(NOT ${PROJECT_NAME}_MODULE_TRIPLE) 22 | string(JSON module_triple GET "${target_info_json}" "target" "moduleTriple") 23 | set(${PROJECT_NAME}_MODULE_TRIPLE "${module_triple}" CACHE STRING "Triple used for installed swift{doc,module,interface} files") 24 | mark_as_advanced(${PROJECT_NAME}_MODULE_TRIPLE) 25 | 26 | message(CONFIGURE_LOG "Swift module triple: ${module_triple}") 27 | endif() 28 | 29 | if(NOT ${PROJECT_NAME}_PLATFORM_SUBDIR) 30 | string(JSON platform GET "${target_info_json}" "target" "platform") 31 | set(${PROJECT_NAME}_PLATFORM_SUBDIR "${platform}" CACHE STRING "Platform name used for installed swift{doc,module,interface} files") 32 | mark_as_advanced(${PROJECT_NAME}_PLATFORM_SUBDIR) 33 | 34 | message(CONFIGURE_LOG "Swift platform: ${platform}") 35 | endif() 36 | 37 | if(NOT ${PROJECT_NAME}_ARCH_SUBDIR) 38 | string(JSON arch GET "${target_info_json}" "target" "arch") 39 | set(${PROJECT_NAME}_ARCH_SUBDIR "${arch}" CACHE STRING "Architecture used for setting the architecture subdirectory") 40 | mark_as_advanced(${PROJECT_NAME}_ARCH_SUBDIR) 41 | 42 | message(CONFIGURE_LOG "Swift Arch: ${arch}") 43 | endif() 44 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ##===----------------------------------------------------------------------===## 2 | ## 3 | ## This source file is part of the Swift.org open source project 4 | ## 5 | ## Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | ## Licensed under Apache License v2.0 with Runtime Library Exception 7 | ## 8 | ## See https://swift.org/LICENSE.txt for license information 9 | ## 10 | ##===----------------------------------------------------------------------===## 11 | 12 | cmake_minimum_required(VERSION 3.26...3.29) 13 | 14 | set(CMAKE_C_VISIBILITY_PRESET "hidden") 15 | 16 | project(Subprocess 17 | LANGUAGES C Swift) 18 | 19 | list(APPEND CMAKE_MODULE_PATH 20 | "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") 21 | 22 | include(EmitSwiftInterface) 23 | include(GNUInstallDirs) 24 | include(InstallExternalDependencies) 25 | include(PlatformInfo) 26 | include(InstallSwiftInterface) 27 | 28 | option(${PROJECT_NAME}_INSTALL_NESTED_SUBDIR "Install libraries under a platform and architecture subdirectory" ON) 29 | set(${PROJECT_NAME}_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/swift$<$>:_static>$<$:/${${PROJECT_NAME}_PLATFORM_SUBDIR}/${${PROJECT_NAME}_ARCH_SUBDIR}>") 30 | set(${PROJECT_NAME}_INSTALL_SWIFTMODULEDIR "${CMAKE_INSTALL_LIBDIR}/swift$<$>:_static>$<$:/${${PROJECT_NAME}_PLATFORM_SUBDIR}>") 31 | 32 | option(${PROJECT_NAME}_ENABLE_LIBRARY_EVOLUTION "Generate ABI resilient runtime libraries" NO) 33 | option(${PROJECT_NAME}_ENABLE_PRESPECIALIZATION "Enable generic metadata prespecialization" NO) 34 | 35 | add_compile_options( 36 | "$<$:SHELL:-package-name ${PROJECT_NAME}>" 37 | "$<$,$>:-enable-library-evolution>" 38 | "$<$,$>:SHELL:-Xfrontend -prespecialize-generic-metadata>") 39 | 40 | add_subdirectory(Sources) 41 | 42 | export(EXPORT SwiftSubprocessTargets 43 | FILE "cmake/SwiftSubprocess/SwiftSubprocessTargets.cmake" 44 | NAMESPACE "SwiftSubprocess::") 45 | 46 | add_subdirectory(cmake/modules) 47 | -------------------------------------------------------------------------------- /Sources/Subprocess/SubprocessFoundation/Output+Foundation.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if SubprocessFoundation 13 | 14 | #if canImport(Darwin) 15 | // On Darwin always prefer system Foundation 16 | import Foundation 17 | #else 18 | // On other platforms prefer FoundationEssentials 19 | import FoundationEssentials 20 | #endif 21 | 22 | /// A concrete `Output` type for subprocesses that collects output 23 | /// from the subprocess as data. 24 | public struct DataOutput: OutputProtocol, ErrorOutputProtocol { 25 | /// The output type for this output option 26 | public typealias OutputType = Data 27 | /// The maximum number of bytes to collect. 28 | public let maxSize: Int 29 | 30 | #if SubprocessSpan 31 | /// Create data from a raw span. 32 | public func output(from span: RawSpan) throws -> Data { 33 | return Data(span) 34 | } 35 | #else 36 | /// Create a data from sequence of 8-bit unsigned integers. 37 | public func output(from buffer: some Sequence) throws -> Data { 38 | return Data(buffer) 39 | } 40 | #endif 41 | 42 | internal init(limit: Int) { 43 | self.maxSize = limit 44 | } 45 | } 46 | 47 | extension OutputProtocol where Self == DataOutput { 48 | /// Create a `Subprocess` output that collects output as `Data` 49 | /// with given buffer limit in bytes. Subprocess throws an error 50 | /// if the child process emits more bytes than the limit. 51 | public static func data(limit: Int) -> Self { 52 | return .init(limit: limit) 53 | } 54 | } 55 | 56 | extension Data { 57 | /// Create a `Data` from `Buffer` 58 | /// - Parameter buffer: buffer to copy from 59 | public init(buffer: AsyncBufferSequence.Buffer) { 60 | self = Data(buffer.data) 61 | } 62 | } 63 | 64 | #endif // SubprocessFoundation 65 | -------------------------------------------------------------------------------- /Tests/TestResources/TestResources.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if canImport(WinSDK) 13 | @preconcurrency import WinSDK 14 | #endif 15 | 16 | // Conditionally require Foundation due to `Bundle.module` 17 | import Foundation 18 | 19 | #if canImport(System) 20 | @preconcurrency import System 21 | #else 22 | @preconcurrency import SystemPackage 23 | #endif 24 | 25 | package var prideAndPrejudice: FilePath { 26 | let path = Bundle.module.url( 27 | forResource: "PrideAndPrejudice", 28 | withExtension: "txt", 29 | subdirectory: "Resources" 30 | )!._fileSystemPath 31 | return FilePath(path) 32 | } 33 | 34 | package var theMysteriousIsland: FilePath { 35 | let path = Bundle.module.url( 36 | forResource: "TheMysteriousIsland", 37 | withExtension: "txt", 38 | subdirectory: "Resources" 39 | )!._fileSystemPath 40 | return FilePath(path) 41 | } 42 | 43 | package var getgroupsSwift: FilePath { 44 | let path = Bundle.module.url( 45 | forResource: "getgroups", 46 | withExtension: "swift", 47 | subdirectory: "Resources" 48 | )!._fileSystemPath 49 | return FilePath(path) 50 | } 51 | 52 | package var windowsTester: FilePath { 53 | let path = Bundle.module.url( 54 | forResource: "windows-tester", 55 | withExtension: "ps1", 56 | subdirectory: "Resources" 57 | )!._fileSystemPath 58 | return FilePath(path) 59 | } 60 | 61 | extension URL { 62 | package var _fileSystemPath: String { 63 | #if canImport(WinSDK) 64 | var path = self.path(percentEncoded: false) 65 | if path.starts(with: "/") { 66 | path.removeFirst() 67 | return path 68 | } 69 | return path 70 | #else 71 | return self.path(percentEncoded: false) 72 | #endif 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/Subprocess/Platforms/Subprocess+BSD.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if os(macOS) || os(FreeBSD) || os(OpenBSD) 13 | 14 | #if canImport(Darwin) 15 | import Darwin 16 | #elseif canImport(Glibc) 17 | import Glibc 18 | #endif 19 | 20 | internal import Dispatch 21 | 22 | // MARK: - Process Monitoring 23 | @Sendable 24 | internal func monitorProcessTermination( 25 | for processIdentifier: ProcessIdentifier 26 | ) async throws -> TerminationStatus { 27 | switch Result(catching: { () throws(SubprocessError.UnderlyingError) -> TerminationStatus? in try processIdentifier.reap() }) { 28 | case let .success(status?): 29 | return status 30 | case .success(nil): 31 | break 32 | case let .failure(error): 33 | throw SubprocessError( 34 | code: .init(.failedToMonitorProcess), 35 | underlyingError: error 36 | ) 37 | } 38 | return try await withCheckedThrowingContinuation { continuation in 39 | let source = DispatchSource.makeProcessSource( 40 | identifier: processIdentifier.value, 41 | eventMask: [.exit], 42 | queue: .global() 43 | ) 44 | source.setEventHandler { 45 | source.cancel() 46 | continuation.resume( 47 | with: Result(catching: { () throws(SubprocessError.UnderlyingError) -> TerminationStatus in 48 | // NOTE_EXIT may be delivered slightly before the process becomes reapable, 49 | // so we must call waitid without WNOHANG to avoid a narrow possibility of a race condition. 50 | // If waitid does block, it won't do so for very long at all. 51 | try processIdentifier.blockingReap() 52 | }).mapError { underlyingError in 53 | SubprocessError( 54 | code: .init(.failedToMonitorProcess), 55 | underlyingError: underlyingError 56 | ) 57 | }) 58 | } 59 | source.resume() 60 | } 61 | } 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /Sources/Subprocess/Span+Subprocess.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | // swift-format-ignore-file 13 | 14 | #if SubprocessSpan 15 | 16 | @_unsafeNonescapableResult 17 | @inlinable @inline(__always) 18 | @lifetime(borrow source) 19 | public func _overrideLifetime< 20 | T: ~Copyable & ~Escapable, 21 | U: ~Copyable & ~Escapable 22 | >( 23 | of dependent: consuming T, 24 | to source: borrowing U 25 | ) -> T { 26 | dependent 27 | } 28 | 29 | @_unsafeNonescapableResult 30 | @inlinable @inline(__always) 31 | @lifetime(copy source) 32 | public func _overrideLifetime< 33 | T: ~Copyable & ~Escapable, 34 | U: ~Copyable & ~Escapable 35 | >( 36 | of dependent: consuming T, 37 | copyingFrom source: consuming U 38 | ) -> T { 39 | dependent 40 | } 41 | 42 | extension Span where Element: BitwiseCopyable { 43 | 44 | internal var _bytes: RawSpan { 45 | @lifetime(copy self) 46 | @_alwaysEmitIntoClient 47 | get { 48 | let rawSpan = RawSpan(_elements: self) 49 | return _overrideLifetime(of: rawSpan, copyingFrom: self) 50 | } 51 | } 52 | } 53 | 54 | #if canImport(Glibc) || canImport(Bionic) || canImport(Musl) 55 | internal import Dispatch 56 | 57 | extension DispatchData { 58 | var bytes: RawSpan { 59 | _read { 60 | if self.count == 0 { 61 | let empty = UnsafeRawBufferPointer(start: nil, count: 0) 62 | let span = RawSpan(_unsafeBytes: empty) 63 | yield _overrideLifetime(of: span, to: self) 64 | } else { 65 | // FIXME: We cannot get a stable ptr out of DispatchData. 66 | // For now revert back to copy 67 | let array = Array(self) 68 | let ptr = array.withUnsafeBytes { return $0 } 69 | let span = RawSpan(_unsafeBytes: ptr) 70 | yield _overrideLifetime(of: span, to: self) 71 | } 72 | } 73 | } 74 | } 75 | #endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl) 76 | 77 | #endif // SubprocessSpan 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | **/.DS_Store 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | 50 | .swiftpm/ 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | -------------------------------------------------------------------------------- /Sources/Subprocess/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ##===----------------------------------------------------------------------===## 2 | ## 3 | ## This source file is part of the Swift.org open source project 4 | ## 5 | ## Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | ## Licensed under Apache License v2.0 with Runtime Library Exception 7 | ## 8 | ## See https://swift.org/LICENSE.txt for license information 9 | ## 10 | ##===----------------------------------------------------------------------===## 11 | 12 | add_library(Subprocess 13 | Execution.swift 14 | Buffer.swift 15 | Error.swift 16 | Teardown.swift 17 | Thread.swift 18 | Result.swift 19 | IO/Output.swift 20 | IO/Input.swift 21 | IO/AsyncIO+Dispatch.swift 22 | IO/AsyncIO+Linux.swift 23 | IO/AsyncIO+Windows.swift 24 | Span+Subprocess.swift 25 | AsyncBufferSequence.swift 26 | API.swift 27 | SubprocessFoundation/Span+SubprocessFoundation.swift 28 | SubprocessFoundation/Output+Foundation.swift 29 | SubprocessFoundation/Input+Foundation.swift 30 | Configuration.swift) 31 | if(WIN32) 32 | target_sources(Subprocess PRIVATE 33 | Platforms/Subprocess+Windows.swift) 34 | elseif(LINUX OR ANDROID) 35 | target_sources(Subprocess PRIVATE 36 | Platforms/Subprocess+Linux.swift 37 | Platforms/Subprocess+Unix.swift) 38 | elseif(APPLE) 39 | target_sources(Subprocess PRIVATE 40 | Platforms/Subprocess+BSD.swift 41 | Platforms/Subprocess+Darwin.swift 42 | Platforms/Subprocess+Unix.swift) 43 | target_compile_options(Subprocess PRIVATE 44 | "$<$:-DSUBPROCESS_ASYNCIO_DISPATCH>") 45 | elseif(FREEBSD OR OPENBSD) 46 | target_sources(Subprocess PRIVATE 47 | Platforms/Subprocess+BSD.swift 48 | Platforms/Subprocess+Unix.swift) 49 | target_compile_options(Subprocess PRIVATE 50 | "$<$:-DSUBPROCESS_ASYNCIO_DISPATCH>") 51 | endif() 52 | 53 | target_compile_options(Subprocess PRIVATE 54 | "$<$:SHELL:-enable-experimental-feature StrictConcurrency>" 55 | "$<$:SHELL:-enable-experimental-feature NonescapableTyeps>" 56 | "$<$:SHELL:-enable-experimental-feature LifetimeDependence>" 57 | "$<$:SHELL:-enable-experimental-feature Span>") 58 | target_link_libraries(Subprocess PUBLIC 59 | _SubprocessCShims) 60 | target_link_libraries(Subprocess PRIVATE 61 | SwiftSystem::SystemPackage) 62 | 63 | install(TARGETS Subprocess 64 | EXPORT SwiftSubprocessTargets 65 | ARCHIVE DESTINATION "${${PROJECT_NAME}_INSTALL_LIBDIR}" 66 | LIBRARY DESTINATION "${${PROJECT_NAME}_INSTALL_LIBDIR}" 67 | RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") 68 | emit_swift_interface(Subprocess) 69 | install_swift_interface(Subprocess) 70 | -------------------------------------------------------------------------------- /Tests/SubprocessTests/TestSupport.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if canImport(Darwin) 13 | // On Darwin always prefer system Foundation 14 | import Foundation 15 | #else 16 | // On other platforms prefer FoundationEssentials 17 | import FoundationEssentials 18 | #endif 19 | 20 | import Testing 21 | import Subprocess 22 | 23 | // Workaround: https://github.com/swiftlang/swift-testing/issues/543 24 | internal func _require(_ value: consuming T?) throws -> T { 25 | guard let value else { 26 | throw SubprocessError.UnderlyingError(rawValue: .max) 27 | } 28 | return value 29 | } 30 | 31 | internal func randomString(length: Int, lettersOnly: Bool = false) -> String { 32 | let letters: String 33 | if lettersOnly { 34 | letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 35 | } else { 36 | letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 37 | } 38 | return String((0.. [UInt8] { 42 | return Array(unsafeUninitializedCapacity: count) { buffer, initializedCount in 43 | for i in 0.. Bool { 51 | guard lhs != rhs else { 52 | return true 53 | } 54 | var canonicalLhs: String = (try? FileManager.default.destinationOfSymbolicLink(atPath: lhs)) ?? lhs 55 | var canonicalRhs: String = (try? FileManager.default.destinationOfSymbolicLink(atPath: rhs)) ?? rhs 56 | if !canonicalLhs.starts(with: "/") { 57 | canonicalLhs = "/\(canonicalLhs)" 58 | } 59 | if !canonicalRhs.starts(with: "/") { 60 | canonicalRhs = "/\(canonicalRhs)" 61 | } 62 | 63 | return canonicalLhs == canonicalRhs 64 | } 65 | 66 | extension Trait where Self == ConditionTrait { 67 | /// This test requires bash to run (instead of sh) 68 | public static var requiresBash: Self { 69 | enabled( 70 | if: (try? Executable.name("bash").resolveExecutablePath(in: .inherit)) != nil, 71 | "This test requires bash (install `bash` package on Linux/BSD)" 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/Subprocess/SubprocessFoundation/Span+SubprocessFoundation.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if SubprocessFoundation && SubprocessSpan 13 | 14 | #if canImport(Darwin) 15 | // On Darwin always prefer system Foundation 16 | import Foundation 17 | #else 18 | // On other platforms prefer FoundationEssentials 19 | import FoundationEssentials 20 | #endif // canImport(Darwin) 21 | 22 | internal import Dispatch 23 | 24 | extension Data { 25 | init(_ s: borrowing RawSpan) { 26 | self = s.withUnsafeBytes { Data($0) } 27 | } 28 | 29 | internal var bytes: RawSpan { 30 | // FIXME: For demo purpose only 31 | let ptr = self.withUnsafeBytes { ptr in 32 | return ptr 33 | } 34 | let span = RawSpan(_unsafeBytes: ptr) 35 | return _overrideLifetime(of: span, to: self) 36 | } 37 | } 38 | 39 | extension DataProtocol { 40 | var bytes: RawSpan { 41 | _read { 42 | if self.regions.isEmpty { 43 | let empty = UnsafeRawBufferPointer(start: nil, count: 0) 44 | let span = RawSpan(_unsafeBytes: empty) 45 | yield _overrideLifetime(of: span, to: self) 46 | } else if self.regions.count == 1 { 47 | // Easy case: there is only one region in the data 48 | let ptr = self.regions.first!.withUnsafeBytes { ptr in 49 | return ptr 50 | } 51 | let span = RawSpan(_unsafeBytes: ptr) 52 | yield _overrideLifetime(of: span, to: self) 53 | } else { 54 | // This data contains discontiguous chunks. We have to 55 | // copy and make a contiguous chunk 56 | var contiguous: ContiguousArray? 57 | for region in self.regions { 58 | if contiguous != nil { 59 | contiguous?.append(contentsOf: region) 60 | } else { 61 | contiguous = .init(region) 62 | } 63 | } 64 | let ptr = contiguous!.withUnsafeBytes { ptr in 65 | return ptr 66 | } 67 | let span = RawSpan(_unsafeBytes: ptr) 68 | yield _overrideLifetime(of: span, to: self) 69 | } 70 | } 71 | } 72 | } 73 | 74 | #endif // SubprocessFoundation 75 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | var dep: [Package.Dependency] = [ 7 | .package( 8 | url: "https://github.com/apple/swift-system", 9 | from: "1.5.0" 10 | ) 11 | ] 12 | #if !os(Windows) 13 | dep.append( 14 | .package( 15 | url: "https://github.com/apple/swift-docc-plugin", 16 | from: "1.4.5" 17 | ), 18 | ) 19 | #endif 20 | 21 | // Enable SubprocessFoundation by default 22 | var defaultTraits: Set = ["SubprocessFoundation"] 23 | 24 | #if compiler(>=6.2) 25 | // Enable SubprocessSpan when Span is available. 26 | defaultTraits.insert("SubprocessSpan") 27 | #endif 28 | 29 | let packageSwiftSettings: [SwiftSetting] = [ 30 | .define("SUBPROCESS_ASYNCIO_DISPATCH", .when(platforms: [.macOS, .custom("freebsd"), .openbsd])) 31 | ] 32 | 33 | let package = Package( 34 | name: "Subprocess", 35 | platforms: [.macOS(.v13), .iOS("99.0")], 36 | products: [ 37 | .library( 38 | name: "Subprocess", 39 | targets: ["Subprocess"] 40 | ) 41 | ], 42 | traits: [ 43 | "SubprocessFoundation", 44 | "SubprocessSpan", 45 | .default( 46 | enabledTraits: defaultTraits 47 | ), 48 | ], 49 | dependencies: dep, 50 | targets: [ 51 | .target( 52 | name: "Subprocess", 53 | dependencies: [ 54 | "_SubprocessCShims", 55 | .product(name: "SystemPackage", package: "swift-system"), 56 | ], 57 | path: "Sources/Subprocess", 58 | exclude: ["CMakeLists.txt"], 59 | swiftSettings: [ 60 | .enableExperimentalFeature("StrictConcurrency"), 61 | .enableExperimentalFeature("NonescapableTypes"), 62 | .enableExperimentalFeature("LifetimeDependence"), 63 | .enableExperimentalFeature("Span"), 64 | ] + packageSwiftSettings 65 | ), 66 | .testTarget( 67 | name: "SubprocessTests", 68 | dependencies: [ 69 | "_SubprocessCShims", 70 | "Subprocess", 71 | "TestResources", 72 | .product(name: "SystemPackage", package: "swift-system"), 73 | ], 74 | swiftSettings: [ 75 | .enableExperimentalFeature("Span") 76 | ] + packageSwiftSettings 77 | ), 78 | 79 | .target( 80 | name: "TestResources", 81 | dependencies: [ 82 | .product(name: "SystemPackage", package: "swift-system") 83 | ], 84 | path: "Tests/TestResources", 85 | resources: [ 86 | .copy("Resources") 87 | ] 88 | ), 89 | 90 | .target( 91 | name: "_SubprocessCShims", 92 | path: "Sources/_SubprocessCShims", 93 | exclude: ["CMakeLists.txt"] 94 | ), 95 | ] 96 | ) 97 | -------------------------------------------------------------------------------- /Tests/SubprocessTests/LinterTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import Testing 13 | @testable import Subprocess 14 | 15 | private func enableLintingTest() -> Bool { 16 | guard CommandLine.arguments.first(where: { $0.contains("/.build/") }) != nil else { 17 | return false 18 | } 19 | #if os(macOS) 20 | // Use xcrun 21 | do { 22 | _ = try Executable.path("/usr/bin/xcrun") 23 | .resolveExecutablePath(in: .inherit) 24 | return true 25 | } catch { 26 | return false 27 | } 28 | #else 29 | // Use swift-format directly 30 | do { 31 | _ = try Executable.name("swift-format") 32 | .resolveExecutablePath(in: .inherit) 33 | return true 34 | } catch { 35 | return false 36 | } 37 | #endif 38 | } 39 | 40 | struct SubprocessLintingTest { 41 | @Test( 42 | .enabled( 43 | if: false /* enableLintingTest() */, 44 | "Skipped until we decide on the rules" 45 | ) 46 | ) 47 | func runLinter() async throws { 48 | // META: Use Subprocess to run `swift-format` on self 49 | // to make sure it's properly linted 50 | guard 51 | let maybePath = CommandLine.arguments.first( 52 | where: { $0.contains("/.build/") } 53 | ) 54 | else { 55 | return 56 | } 57 | let sourcePath = try String( 58 | maybePath.prefix(upTo: #require(maybePath.range(of: "/.build")).lowerBound) 59 | ) 60 | print("Linting \(sourcePath)") 61 | #if os(macOS) 62 | let configuration = Configuration( 63 | executable: .path("/usr/bin/xcrun"), 64 | arguments: ["swift-format", "lint", "-s", "--recursive", sourcePath] 65 | ) 66 | #else 67 | let configuration = Configuration( 68 | executable: .name("swift-format"), 69 | arguments: ["lint", "-s", "--recursive", sourcePath] 70 | ) 71 | #endif 72 | let lintResult = try await Subprocess.run( 73 | configuration, 74 | output: .discarded, 75 | error: .string(limit: .max) 76 | ) 77 | #expect( 78 | lintResult.terminationStatus.isSuccess, 79 | "❌ `swift-format lint --recursive \(sourcePath)` failed" 80 | ) 81 | if let error = lintResult.standardError?.trimmingCharacters( 82 | in: .whitespacesAndNewlines 83 | ), !error.isEmpty { 84 | print("\(error)\n") 85 | } else { 86 | print("✅ Linting passed") 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cmake/modules/EmitSwiftInterface.cmake: -------------------------------------------------------------------------------- 1 | ##===----------------------------------------------------------------------===## 2 | ## 3 | ## This source file is part of the Swift.org open source project 4 | ## 5 | ## Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | ## Licensed under Apache License v2.0 with Runtime Library Exception 7 | ## 8 | ## See https://swift.org/LICENSE.txt for license information 9 | ## 10 | ##===----------------------------------------------------------------------===## 11 | 12 | function(emit_swift_interface target) 13 | # Generate the target-variant binary swift module when performing zippered 14 | # build 15 | # 16 | # Clean this up once CMake has nested swiftmodules in the build directory: 17 | # https://gitlab.kitware.com/cmake/cmake/-/merge_requests/10664 18 | # https://cmake.org/cmake/help/git-stage/policy/CMP0195.html 19 | 20 | # We can't expand the Swift_MODULE_NAME target property in a generator 21 | # expression or it will fail saying that the target doesn't exist. 22 | get_target_property(module_name ${target} Swift_MODULE_NAME) 23 | if(NOT module_name) 24 | set(module_name ${target}) 25 | endif() 26 | 27 | # Account for an existing swiftmodule file generated with the previous logic 28 | if(EXISTS "${CMAKE_CURRENT_BINARY_DIR}/${module_name}.swiftmodule" 29 | AND NOT IS_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${module_name}.swiftmodule") 30 | message(STATUS "Removing regular file ${CMAKE_CURRENT_BINARY_DIR}/${module_name}.swiftmodule to support nested swiftmodule generation") 31 | file(REMOVE "${CMAKE_CURRENT_BINARY_DIR}/${module_name}.swiftmodule") 32 | endif() 33 | 34 | target_compile_options(${target} PRIVATE 35 | "$<$:SHELL:-emit-module-path ${CMAKE_CURRENT_BINARY_DIR}/${module_name}.swiftmodule/${${PROJECT_NAME}_MODULE_TRIPLE}.swiftmodule>") 36 | add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${module_name}.swiftmodule/${${PROJECT_NAME}_MODULE_TRIPLE}.swiftmodule" 37 | DEPENDS ${target}) 38 | target_sources(${target} 39 | INTERFACE 40 | $) 41 | 42 | set_target_properties(${target} PROPERTIES 43 | INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}) 44 | 45 | # Generate textual swift interfaces is library-evolution is enabled 46 | if(${PROJECT_NAME}_ENABLE_LIBRARY_EVOLUTION) 47 | target_compile_options(${target} PRIVATE 48 | $<$:-emit-module-interface-path$${CMAKE_CURRENT_BINARY_DIR}/${module_name}.swiftmodule/${${PROJECT_NAME}_MODULE_TRIPLE}.swiftinterface> 49 | $<$:-emit-private-module-interface-path$${CMAKE_CURRENT_BINARY_DIR}/${module_name}.swiftmodule/${${PROJECT_NAME}_MODULE_TRIPLE}.private.swiftinterface>) 50 | target_compile_options(${target} PRIVATE 51 | $<$:-library-level$api> 52 | $<$:-Xfrontend$-require-explicit-availability=ignore>) 53 | endif() 54 | endfunction() 55 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy": { 3 | "accessLevel": "private" 4 | }, 5 | "indentConditionalCompilationBlocks": false, 6 | "indentSwitchCaseLabels": false, 7 | "indentation": { 8 | "spaces": 4 9 | }, 10 | "lineBreakAroundMultilineExpressionChainComponents": false, 11 | "lineBreakBeforeControlFlowKeywords": false, 12 | "lineBreakBeforeEachArgument": false, 13 | "lineBreakBeforeEachGenericRequirement": false, 14 | "lineBreakBetweenDeclarationAttributes": false, 15 | "lineLength": 9999, 16 | "maximumBlankLines": 1, 17 | "multiElementCollectionTrailingCommas": true, 18 | "noAssignmentInExpressions": { 19 | "allowedFunctions": [ 20 | "XCTAssertNoThrow" 21 | ] 22 | }, 23 | "prioritizeKeepingFunctionOutputTogether": false, 24 | "respectsExistingLineBreaks": true, 25 | "rules": { 26 | "AllPublicDeclarationsHaveDocumentation": false, 27 | "AlwaysUseLiteralForEmptyCollectionInit": false, 28 | "AlwaysUseLowerCamelCase": false, 29 | "AmbiguousTrailingClosureOverload": false, 30 | "AvoidRetroactiveConformances": false, 31 | "BeginDocumentationCommentWithOneLineSummary": false, 32 | "DoNotUseSemicolons": false, 33 | "DontRepeatTypeInStaticProperties": false, 34 | "FileScopedDeclarationPrivacy": false, 35 | "FullyIndirectEnum": false, 36 | "GroupNumericLiterals": false, 37 | "IdentifiersMustBeASCII": false, 38 | "NeverForceUnwrap": false, 39 | "NeverUseForceTry": false, 40 | "NeverUseImplicitlyUnwrappedOptionals": false, 41 | "NoAccessLevelOnExtensionDeclaration": false, 42 | "NoAssignmentInExpressions": false, 43 | "NoBlockComments": false, 44 | "NoCasesWithOnlyFallthrough": false, 45 | "NoEmptyLinesOpeningClosingBraces": false, 46 | "NoEmptyTrailingClosureParentheses": false, 47 | "NoLabelsInCasePatterns": false, 48 | "NoLeadingUnderscores": false, 49 | "NoParensAroundConditions": false, 50 | "NoPlaygroundLiterals": false, 51 | "NoVoidReturnOnFunctionSignature": false, 52 | "OmitExplicitReturns": false, 53 | "OneCasePerLine": false, 54 | "OneVariableDeclarationPerLine": false, 55 | "OnlyOneTrailingClosureArgument": false, 56 | "OrderedImports": false, 57 | "ReplaceForEachWithForLoop": false, 58 | "ReturnVoidInsteadOfEmptyTuple": false, 59 | "TypeNamesShouldBeCapitalized": false, 60 | "UseEarlyExits": false, 61 | "UseExplicitNilCheckInConditions": false, 62 | "UseLetInEveryBoundCaseVariable": false, 63 | "UseShorthandTypeNames": false, 64 | "UseSingleLinePropertyGetter": false, 65 | "UseSynthesizedInitializer": false, 66 | "UseTripleSlashForDocumentationComments": false, 67 | "UseWhereClausesInForLoops": false, 68 | "ValidateDocumentationComments": false 69 | }, 70 | "spacesAroundRangeFormationOperators": false, 71 | "spacesBeforeEndOfLineComments": 1, 72 | "tabWidth": 4, 73 | "version": 1 74 | } 75 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull request 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | 10 | jobs: 11 | tests: 12 | name: Test 13 | uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main 14 | with: 15 | linux_os_versions: '["amazonlinux2", "bookworm", "noble", "jammy", "rhel-ubi9"]' 16 | linux_swift_versions: '["6.1", "nightly-main"]' 17 | linux_pre_build_command: | 18 | if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy 19 | apt-get update -y 20 | 21 | # Test dependencies 22 | apt-get install -y procps 23 | elif command -v dnf >/dev/null 2>&1 ; then # rhel-ubi9 24 | dnf update -y 25 | 26 | # Test dependencies 27 | dnf install -y procps 28 | elif command -v yum >/dev/null 2>&1 ; then # amazonlinux2 29 | yum update -y 30 | 31 | # Test dependencies 32 | yum install -y procps 33 | fi 34 | linux_build_command: 'swift-format lint -s -r --configuration ./.swift-format . && swift test && swift test -c release && swift test --disable-default-traits' 35 | windows_swift_versions: '["6.1", "nightly-main"]' 36 | windows_build_command: | 37 | Invoke-Program swift test 38 | Invoke-Program swift test -c release 39 | Invoke-Program swift test --disable-default-traits 40 | enable_macos_checks: true 41 | macos_xcode_versions: '["16.3"]' 42 | macos_build_command: 'xcrun swift-format lint -s -r --configuration ./.swift-format . && xcrun swift test && xcrun swift test -c release && xcrun swift test --disable-default-traits' 43 | enable_linux_static_sdk_build: true 44 | enable_android_sdk_build: true 45 | android_ndk_versions: '["r27d", "r29"]' 46 | linux_static_sdk_versions: '["6.1", "nightly-6.2"]' 47 | linux_static_sdk_build_command: | 48 | for triple in aarch64-swift-linux-musl x86_64-swift-linux-musl ; do 49 | swift build --swift-sdk "\$triple" 50 | swift build --swift-sdk "\$triple" --disable-default-traits 51 | done 52 | # empty line to ignore the --swift-sdk given by swiftlang/github-workflows/.github/workflows/scripts/install-and-build-with-sdk.sh \ 53 | 54 | soundness: 55 | name: Soundness 56 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main 57 | with: 58 | license_header_check_project_name: "Swift.org" 59 | docs_check_enabled: false 60 | format_check_enabled: false 61 | unacceptable_language_check_enabled: false 62 | api_breakage_check_enabled: false 63 | 64 | cmake_build: 65 | name: CMake Build 66 | runs-on: ubuntu-latest 67 | container: swiftlang/swift:nightly-noble 68 | steps: 69 | - name: checkout sources 70 | uses: actions/checkout@v1 71 | - name: Install dependencies 72 | shell: bash 73 | run: apt update && apt install -y cmake ninja-build 74 | - name: Configure Project 75 | shell: bash 76 | run: cmake -G 'Ninja' -B build -S . -DCMAKE_C_COMPILER=clang -DCMAKE_Swift_COMPILER=swiftc -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=YES 77 | - name: Build Project 78 | shell: bash 79 | run: cmake --build build 80 | -------------------------------------------------------------------------------- /Tests/SubprocessTests/LinuxTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if os(Linux) || os(Android) 13 | 14 | #if canImport(Android) 15 | import Android 16 | import Foundation 17 | #elseif canImport(Glibc) 18 | import Glibc 19 | #elseif canImport(Musl) 20 | import Musl 21 | #endif 22 | 23 | import FoundationEssentials 24 | 25 | import Testing 26 | import _SubprocessCShims 27 | @testable import Subprocess 28 | 29 | // MARK: PlatformOption Tests 30 | @Suite(.serialized) 31 | struct SubprocessLinuxTests { 32 | @Test func testSuspendResumeProcess() async throws { 33 | func blockAndWaitForStatus( 34 | pid: pid_t, 35 | waitThread: inout pthread_t?, 36 | targetSignal: Int32, 37 | _ handler: @Sendable @escaping (Int32) -> Void 38 | ) async throws { 39 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 40 | do { 41 | waitThread = try pthread_create { 42 | var suspendedStatus: Int32 = 0 43 | let rc = waitpid(pid, &suspendedStatus, targetSignal) 44 | if rc == -1 { 45 | continuation.resume(throwing: SubprocessError.UnderlyingError(rawValue: errno)) 46 | return 47 | } 48 | handler(suspendedStatus) 49 | continuation.resume() 50 | } 51 | } catch { 52 | continuation.resume(throwing: error) 53 | } 54 | } 55 | } 56 | 57 | _ = try await Subprocess.run( 58 | // This will intentionally hang 59 | .path("/usr/bin/sleep"), 60 | arguments: ["infinity"], 61 | error: .discarded 62 | ) { subprocess, standardOutput in 63 | // First suspend the process 64 | try subprocess.send(signal: .suspend) 65 | var thread1: pthread_t? = nil 66 | try await blockAndWaitForStatus( 67 | pid: subprocess.processIdentifier.value, 68 | waitThread: &thread1, 69 | targetSignal: WSTOPPED 70 | ) { status in 71 | #expect(_was_process_suspended(status) > 0) 72 | } 73 | // Now resume the process 74 | try subprocess.send(signal: .resume) 75 | var thread2: pthread_t? = nil 76 | try await blockAndWaitForStatus( 77 | pid: subprocess.processIdentifier.value, 78 | waitThread: &thread2, 79 | targetSignal: WCONTINUED 80 | ) { status in 81 | #expect(_was_process_suspended(status) == 0) 82 | } 83 | 84 | // Now kill the process 85 | try subprocess.send(signal: .terminate) 86 | for try await _ in standardOutput {} 87 | 88 | if let thread1 { 89 | pthread_join(thread1, nil) 90 | } 91 | if let thread2 { 92 | pthread_join(thread2, nil) 93 | } 94 | } 95 | } 96 | 97 | @Test func testUniqueProcessIdentifier() async throws { 98 | _ = try await Subprocess.run( 99 | .path("/bin/echo"), 100 | output: .discarded, 101 | error: .discarded 102 | ) { subprocess in 103 | if subprocess.processIdentifier.processDescriptor > 0 { 104 | var statinfo = stat() 105 | try #require(fstat(subprocess.processIdentifier.processDescriptor, &statinfo) == 0) 106 | 107 | // In kernel 6.9+, st_ino globally uniquely identifies the process 108 | #expect(statinfo.st_ino > 0) 109 | } 110 | } 111 | } 112 | } 113 | #endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl) 114 | -------------------------------------------------------------------------------- /Sources/_SubprocessCShims/include/process_shims.h: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #ifndef process_shims_h 13 | #define process_shims_h 14 | 15 | #include "target_conditionals.h" 16 | 17 | #if !TARGET_OS_WINDOWS 18 | #include 19 | #include 20 | 21 | #if _POSIX_SPAWN 22 | #include 23 | #endif 24 | 25 | #if TARGET_OS_LINUX 26 | #include 27 | #include 28 | #endif // TARGET_OS_LINUX 29 | 30 | #if TARGET_OS_FREEBSD 31 | #include 32 | #endif 33 | 34 | #if TARGET_OS_LINUX || TARGET_OS_FREEBSD 35 | #include 36 | #include 37 | #endif // TARGET_OS_LINUX || TARGET_OS_FREEBSD 38 | 39 | #ifdef __cplusplus 40 | extern "C" { 41 | #endif 42 | 43 | int _subprocess_pthread_create( 44 | pthread_t * _Nonnull ptr, 45 | pthread_attr_t const * _Nullable attr, 46 | void * _Nullable (* _Nonnull start)(void * _Nullable), 47 | void * _Nullable context 48 | ); 49 | 50 | #if __has_include() 51 | vm_size_t _subprocess_vm_size(void); 52 | #endif 53 | 54 | #if TARGET_OS_MAC 55 | int _subprocess_spawn( 56 | pid_t * _Nonnull pid, 57 | const char * _Nonnull exec_path, 58 | const posix_spawn_file_actions_t _Nullable * _Nonnull file_actions, 59 | const posix_spawnattr_t _Nullable * _Nonnull spawn_attrs, 60 | char * _Nullable const args[_Nonnull], 61 | char * _Nullable const env[_Nullable], 62 | uid_t * _Nullable uid, 63 | gid_t * _Nullable gid, 64 | int number_of_sgroups, const gid_t * _Nullable sgroups, 65 | int create_session 66 | ); 67 | #endif // TARGET_OS_MAC 68 | 69 | int _subprocess_fork_exec( 70 | pid_t * _Nonnull pid, 71 | int * _Nonnull pidfd, 72 | const char * _Nonnull exec_path, 73 | const char * _Nullable working_directory, 74 | const int file_descriptors[_Nonnull], 75 | char * _Nullable const args[_Nonnull], 76 | char * _Nullable const env[_Nullable], 77 | uid_t * _Nullable uid, 78 | gid_t * _Nullable gid, 79 | gid_t * _Nullable process_group_id, 80 | int number_of_sgroups, const gid_t * _Nullable sgroups, 81 | int create_session 82 | ); 83 | 84 | int _was_process_exited(int status); 85 | int _get_exit_code(int status); 86 | int _was_process_signaled(int status); 87 | int _get_signal_code(int status); 88 | int _was_process_suspended(int status); 89 | 90 | void _subprocess_lock_environ(void); 91 | void _subprocess_unlock_environ(void); 92 | char * _Nullable * _Nullable _subprocess_get_environ(void); 93 | 94 | int _subprocess_pdkill(int pidfd, int signal); 95 | 96 | #if TARGET_OS_UNIX && !TARGET_OS_FREEBSD 97 | int _shims_snprintf( 98 | char * _Nonnull str, 99 | int len, 100 | const char * _Nonnull format, 101 | char * _Nonnull str1, 102 | char * _Nonnull str2 103 | ); 104 | #endif 105 | 106 | #if TARGET_OS_LINUX 107 | int _pidfd_open(pid_t pid); 108 | 109 | // P_PIDFD is only defined on Linux Kernel 5.4 and above 110 | // Define our value if it's not available 111 | #ifndef P_PIDFD 112 | #define P_PIDFD 3 113 | #endif 114 | 115 | #endif 116 | 117 | #ifdef __cplusplus 118 | } // extern "C" 119 | #endif 120 | 121 | #endif // !TARGET_OS_WINDOWS 122 | 123 | #if TARGET_OS_WINDOWS 124 | 125 | #include 126 | 127 | #ifdef __cplusplus 128 | extern "C" { 129 | #endif 130 | 131 | #ifndef _WINDEF_ 132 | typedef unsigned long DWORD; 133 | typedef int BOOL; 134 | #endif 135 | 136 | BOOL _subprocess_windows_send_vm_close(DWORD pid); 137 | unsigned int _subprocess_windows_get_errno(void); 138 | 139 | /// Get the value of `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`. 140 | /// 141 | /// This function is provided because `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` is a 142 | /// complex macro and cannot be imported directly into Swift. 143 | DWORD_PTR _subprocess_PROC_THREAD_ATTRIBUTE_HANDLE_LIST(void); 144 | 145 | #ifdef __cplusplus 146 | } // extern "C" 147 | #endif 148 | 149 | #endif 150 | 151 | #endif /* process_shims_h */ 152 | -------------------------------------------------------------------------------- /Sources/Subprocess/Result.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if canImport(System) 13 | @preconcurrency import System 14 | #else 15 | @preconcurrency import SystemPackage 16 | #endif 17 | 18 | // MARK: - Result 19 | 20 | /// A simple wrapper around the generic result returned by the 21 | /// `run` closure with the corresponding termination status of 22 | /// the child process. 23 | public struct ExecutionResult { 24 | /// The termination status of the child process 25 | public let terminationStatus: TerminationStatus 26 | /// The result returned by the closure passed to `.run` methods 27 | public let value: Result 28 | 29 | internal init(terminationStatus: TerminationStatus, value: Result) { 30 | self.terminationStatus = terminationStatus 31 | self.value = value 32 | } 33 | } 34 | 35 | /// The result of a subprocess execution with its collected 36 | /// standard output and standard error. 37 | public struct CollectedResult< 38 | Output: OutputProtocol, 39 | Error: OutputProtocol 40 | >: Sendable { 41 | /// The process identifier for the executed subprocess 42 | public let processIdentifier: ProcessIdentifier 43 | /// The termination status of the executed subprocess 44 | public let terminationStatus: TerminationStatus 45 | /// The captured standard output of the executed subprocess. 46 | public let standardOutput: Output.OutputType 47 | /// The captured standard error of the executed subprocess. 48 | public let standardError: Error.OutputType 49 | 50 | internal init( 51 | processIdentifier: ProcessIdentifier, 52 | terminationStatus: TerminationStatus, 53 | standardOutput: Output.OutputType, 54 | standardError: Error.OutputType 55 | ) { 56 | self.processIdentifier = processIdentifier 57 | self.terminationStatus = terminationStatus 58 | self.standardOutput = standardOutput 59 | self.standardError = standardError 60 | } 61 | } 62 | 63 | // MARK: - CollectedResult Conformances 64 | 65 | extension CollectedResult: Equatable where Output.OutputType: Equatable, Error.OutputType: Equatable {} 66 | 67 | extension CollectedResult: Hashable where Output.OutputType: Hashable, Error.OutputType: Hashable {} 68 | 69 | extension CollectedResult: CustomStringConvertible 70 | where Output.OutputType: CustomStringConvertible, Error.OutputType: CustomStringConvertible { 71 | /// A textual representation of the collected result. 72 | public var description: String { 73 | return """ 74 | CollectedResult( 75 | processIdentifier: \(self.processIdentifier), 76 | terminationStatus: \(self.terminationStatus.description), 77 | standardOutput: \(self.standardOutput.description) 78 | standardError: \(self.standardError.description) 79 | ) 80 | """ 81 | } 82 | } 83 | 84 | extension CollectedResult: CustomDebugStringConvertible 85 | where Output.OutputType: CustomDebugStringConvertible, Error.OutputType: CustomDebugStringConvertible { 86 | /// A debug-oriented textual representation of the collected result. 87 | public var debugDescription: String { 88 | return """ 89 | CollectedResult( 90 | processIdentifier: \(self.processIdentifier), 91 | terminationStatus: \(self.terminationStatus.description), 92 | standardOutput: \(self.standardOutput.debugDescription) 93 | standardError: \(self.standardError.debugDescription) 94 | ) 95 | """ 96 | } 97 | } 98 | 99 | // MARK: - ExecutionResult Conformances 100 | extension ExecutionResult: Equatable where Result: Equatable {} 101 | 102 | extension ExecutionResult: Hashable where Result: Hashable {} 103 | 104 | extension ExecutionResult: CustomStringConvertible where Result: CustomStringConvertible { 105 | /// A textual representation of the execution result. 106 | public var description: String { 107 | return """ 108 | ExecutionResult( 109 | terminationStatus: \(self.terminationStatus.description), 110 | value: \(self.value.description) 111 | ) 112 | """ 113 | } 114 | } 115 | 116 | extension ExecutionResult: CustomDebugStringConvertible where Result: CustomDebugStringConvertible { 117 | /// A debug-oriented textual representation of this execution result. 118 | public var debugDescription: String { 119 | return """ 120 | ExecutionResult( 121 | terminationStatus: \(self.terminationStatus.debugDescription), 122 | value: \(self.value.debugDescription) 123 | ) 124 | """ 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Tests/SubprocessTests/DarwinTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if canImport(Darwin) 13 | 14 | import Foundation 15 | 16 | import _SubprocessCShims 17 | import Testing 18 | 19 | #if canImport(System) 20 | @preconcurrency import System 21 | #else 22 | @preconcurrency import SystemPackage 23 | #endif 24 | @testable import Subprocess 25 | 26 | // MARK: PlatformOptions Tests 27 | @Suite(.serialized) 28 | struct SubprocessDarwinTests { 29 | @Test func testSubprocessPlatformOptionsProcessConfiguratorUpdateSpawnAttr() async throws { 30 | var platformOptions = PlatformOptions() 31 | platformOptions.preSpawnProcessConfigurator = { spawnAttr, _ in 32 | // Set POSIX_SPAWN_SETSID flag, which implies calls 33 | // to setsid 34 | var flags: Int16 = 0 35 | posix_spawnattr_getflags(&spawnAttr, &flags) 36 | posix_spawnattr_setflags(&spawnAttr, flags | Int16(POSIX_SPAWN_SETSID)) 37 | } 38 | // Check the process ID (pid), process group ID (pgid), and 39 | // controlling terminal's process group ID (tpgid) 40 | let psResult = try await Subprocess.run( 41 | .path("/bin/bash"), 42 | arguments: ["-c", "ps -o pid,pgid,tpgid -p $$"], 43 | platformOptions: platformOptions, 44 | output: .string(limit: .max) 45 | ) 46 | try assertNewSessionCreated(with: psResult) 47 | } 48 | 49 | @Test func testSubprocessPlatformOptionsProcessConfiguratorUpdateFileAction() async throws { 50 | let intendedWorkingDir = FileManager.default.temporaryDirectory.path() 51 | var platformOptions = PlatformOptions() 52 | platformOptions.preSpawnProcessConfigurator = { _, fileAttr in 53 | // Change the working directory 54 | intendedWorkingDir.withCString { path in 55 | _ = posix_spawn_file_actions_addchdir_np(&fileAttr, path) 56 | } 57 | } 58 | let pwdResult = try await Subprocess.run( 59 | .path("/bin/pwd"), 60 | platformOptions: platformOptions, 61 | output: .string(limit: .max) 62 | ) 63 | #expect(pwdResult.terminationStatus.isSuccess) 64 | let currentDir = try #require( 65 | pwdResult.standardOutput 66 | ).trimmingCharacters(in: .whitespacesAndNewlines) 67 | // On Darwin, /var is linked to /private/var; /tmp is linked /private/tmp 68 | var expected = FilePath(intendedWorkingDir) 69 | if expected.starts(with: "/var") || expected.starts(with: "/tmp") { 70 | expected = FilePath("/private").appending(expected.components) 71 | } 72 | #expect(FilePath(currentDir) == expected) 73 | } 74 | 75 | @Test func testSuspendResumeProcess() async throws { 76 | _ = try await Subprocess.run( 77 | // This will intentionally hang 78 | .path("/bin/cat"), 79 | error: .discarded 80 | ) { subprocess, standardOutput in 81 | // First suspend the process 82 | try subprocess.send(signal: .suspend) 83 | var suspendedStatus: Int32 = 0 84 | waitpid(subprocess.processIdentifier.value, &suspendedStatus, WNOHANG | WUNTRACED) 85 | #expect(_was_process_suspended(suspendedStatus) > 0) 86 | // Now resume the process 87 | try subprocess.send(signal: .resume) 88 | var resumedStatus: Int32 = 0 89 | waitpid(subprocess.processIdentifier.value, &resumedStatus, WNOHANG | WUNTRACED) 90 | #expect(_was_process_suspended(resumedStatus) == 0) 91 | 92 | // Now kill the process 93 | try subprocess.send(signal: .terminate) 94 | for try await _ in standardOutput {} 95 | } 96 | } 97 | } 98 | 99 | extension FileDescriptor { 100 | internal func readUntilEOF(upToLength maxLength: Int) async throws -> Data { 101 | return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 102 | let dispatchIO = DispatchIO( 103 | type: .stream, 104 | fileDescriptor: self.rawValue, 105 | queue: .global() 106 | ) { error in 107 | if error != 0 { 108 | continuation.resume(throwing: POSIXError(.init(rawValue: error) ?? .ENODEV)) 109 | } 110 | } 111 | var buffer: Data = Data() 112 | dispatchIO.read( 113 | offset: 0, 114 | length: maxLength, 115 | queue: .global() 116 | ) { done, data, error in 117 | guard error == 0 else { 118 | continuation.resume(throwing: POSIXError(.init(rawValue: error) ?? .ENODEV)) 119 | return 120 | } 121 | if let data = data { 122 | buffer += Data(data) 123 | } 124 | if done { 125 | dispatchIO.close() 126 | continuation.resume(returning: buffer) 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | #endif // canImport(Darwin) 134 | -------------------------------------------------------------------------------- /Sources/Subprocess/Buffer.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | // swift-format-ignore-file 13 | 14 | #if canImport(Darwin) || canImport(Glibc) || canImport(Android) || canImport(Musl) 15 | @preconcurrency internal import Dispatch 16 | #endif 17 | 18 | extension AsyncBufferSequence { 19 | /// A immutable collection of bytes 20 | public struct Buffer: Sendable { 21 | #if SUBPROCESS_ASYNCIO_DISPATCH 22 | // We need to keep the backingData alive while Slice is alive 23 | internal let backingData: DispatchData 24 | internal let data: DispatchData.Region 25 | 26 | internal init(data: DispatchData.Region, backingData: DispatchData) { 27 | self.data = data 28 | self.backingData = backingData 29 | } 30 | 31 | internal static func createFrom(_ data: DispatchData) -> [Buffer] { 32 | let slices = data.regions 33 | // In most (all?) cases data should only have one slice 34 | if _fastPath(slices.count == 1) { 35 | return [.init(data: slices[0], backingData: data)] 36 | } 37 | return slices.map { .init(data: $0, backingData: data) } 38 | } 39 | #else 40 | internal let data: [UInt8] 41 | 42 | internal init(data: [UInt8]) { 43 | self.data = data 44 | } 45 | 46 | internal static func createFrom(_ data: [UInt8]) -> [Buffer] { 47 | return [.init(data: data)] 48 | } 49 | #endif // SUBPROCESS_ASYNCIO_DISPATCH 50 | } 51 | } 52 | 53 | // MARK: - Properties 54 | extension AsyncBufferSequence.Buffer { 55 | /// Number of bytes stored in the buffer 56 | public var count: Int { 57 | return self.data.count 58 | } 59 | 60 | /// A Boolean value indicating whether the collection is empty. 61 | public var isEmpty: Bool { 62 | return self.data.isEmpty 63 | } 64 | } 65 | 66 | // MARK: - Accessors 67 | extension AsyncBufferSequence.Buffer { 68 | /// Access the raw bytes stored in this buffer 69 | /// - Parameter body: A closure with an `UnsafeRawBufferPointer` parameter that 70 | /// points to the contiguous storage for the buffer. If no such storage exists, 71 | /// the method creates it. The argument is valid only for the duration of the 72 | /// closure's execution. 73 | /// - Returns: The return value of the body closure. 74 | public func withUnsafeBytes( 75 | _ body: (UnsafeRawBufferPointer) throws -> ResultType 76 | ) rethrows -> ResultType { 77 | return try self.data.withUnsafeBytes(body) 78 | } 79 | 80 | #if SubprocessSpan 81 | // swift-format-ignore 82 | // Access the storage backing this Buffer 83 | public var bytes: RawSpan { 84 | @lifetime(borrow self) 85 | borrowing get { 86 | let ptr = self.data.withUnsafeBytes { $0 } 87 | let bytes = RawSpan(_unsafeBytes: ptr) 88 | return _overrideLifetime(of: bytes, to: self) 89 | } 90 | } 91 | #endif // SubprocessSpan 92 | } 93 | 94 | // MARK: - Hashable, Equatable 95 | extension AsyncBufferSequence.Buffer: Equatable, Hashable { 96 | #if SUBPROCESS_ASYNCIO_DISPATCH 97 | /// Returns a Boolean value that indicates whether two buffers are equal. 98 | public static func == (lhs: AsyncBufferSequence.Buffer, rhs: AsyncBufferSequence.Buffer) -> Bool { 99 | return lhs.data == rhs.data 100 | } 101 | 102 | /// Hashes the essential components of this value by feeding them into the given hasher. 103 | public func hash(into hasher: inout Hasher) { 104 | return self.data.hash(into: &hasher) 105 | } 106 | #endif 107 | // else Compiler generated conformances 108 | } 109 | 110 | #if SUBPROCESS_ASYNCIO_DISPATCH 111 | extension DispatchData.Region { 112 | static func == (lhs: DispatchData.Region, rhs: DispatchData.Region) -> Bool { 113 | return lhs.withUnsafeBytes { lhsBytes in 114 | return rhs.withUnsafeBytes { rhsBytes in 115 | return lhsBytes.elementsEqual(rhsBytes) 116 | } 117 | } 118 | } 119 | 120 | internal func hash(into hasher: inout Hasher) { 121 | return self.withUnsafeBytes { ptr in 122 | return hasher.combine(bytes: ptr) 123 | } 124 | } 125 | } 126 | #if !canImport(Darwin) || !SubprocessFoundation 127 | /// `DispatchData.Region` is defined in Foundation, but we can't depend on Foundation when the SubprocessFoundation trait is disabled. 128 | extension DispatchData { 129 | typealias Region = _ContiguousBufferView 130 | 131 | var regions: [Region] { 132 | contiguousBufferViews 133 | } 134 | 135 | internal struct _ContiguousBufferView: @unchecked Sendable, RandomAccessCollection { 136 | typealias Element = UInt8 137 | 138 | internal let bytes: UnsafeBufferPointer 139 | 140 | internal var startIndex: Int { self.bytes.startIndex } 141 | internal var endIndex: Int { self.bytes.endIndex } 142 | 143 | internal init(bytes: UnsafeBufferPointer) { 144 | self.bytes = bytes 145 | } 146 | 147 | internal func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> ResultType) rethrows -> ResultType { 148 | return try body(UnsafeRawBufferPointer(self.bytes)) 149 | } 150 | 151 | subscript(position: Int) -> UInt8 { 152 | _read { 153 | yield self.bytes[position] 154 | } 155 | } 156 | } 157 | 158 | internal var contiguousBufferViews: [_ContiguousBufferView] { 159 | var slices = [_ContiguousBufferView]() 160 | enumerateBytes { (bytes, index, stop) in 161 | slices.append(_ContiguousBufferView(bytes: bytes)) 162 | } 163 | return slices 164 | } 165 | } 166 | #endif 167 | #endif 168 | -------------------------------------------------------------------------------- /Sources/Subprocess/SubprocessFoundation/Input+Foundation.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if SubprocessFoundation 13 | 14 | #if canImport(Darwin) 15 | // On Darwin always prefer system Foundation 16 | import Foundation 17 | #else 18 | // On other platforms prefer FoundationEssentials 19 | import FoundationEssentials 20 | #endif // canImport(Darwin) 21 | 22 | #if canImport(System) 23 | @preconcurrency import System 24 | #else 25 | @preconcurrency import SystemPackage 26 | #endif 27 | 28 | internal import Dispatch 29 | 30 | /// A concrete input type for subprocesses that reads input from data. 31 | public struct DataInput: InputProtocol { 32 | private let data: Data 33 | 34 | /// Asynchronously write the input to the subprocess using the 35 | /// write file descriptor. 36 | public func write(with writer: StandardInputWriter) async throws { 37 | _ = try await writer.write(self.data) 38 | } 39 | 40 | internal init(data: Data) { 41 | self.data = data 42 | } 43 | } 44 | 45 | /// A concrete input type for subprocesses that accepts input from 46 | /// a specified sequence of data. 47 | public struct DataSequenceInput< 48 | InputSequence: Sequence & Sendable 49 | >: InputProtocol where InputSequence.Element == Data { 50 | private let sequence: InputSequence 51 | 52 | /// Asynchronously write the input to the subprocess using the 53 | /// write file descriptor. 54 | public func write(with writer: StandardInputWriter) async throws { 55 | var buffer = Data() 56 | for chunk in self.sequence { 57 | buffer.append(chunk) 58 | } 59 | _ = try await writer.write(buffer) 60 | } 61 | 62 | internal init(underlying: InputSequence) { 63 | self.sequence = underlying 64 | } 65 | } 66 | 67 | /// A concrete `Input` type for subprocesses that reads input 68 | /// from a given async sequence of `Data`. 69 | public struct DataAsyncSequenceInput< 70 | InputSequence: AsyncSequence & Sendable 71 | >: InputProtocol where InputSequence.Element == Data { 72 | private let sequence: InputSequence 73 | 74 | private func writeChunk(_ chunk: Data, with writer: StandardInputWriter) async throws { 75 | _ = try await writer.write(chunk) 76 | } 77 | 78 | /// Asynchronously write the input to the subprocess using the 79 | /// write file descriptor. 80 | public func write(with writer: StandardInputWriter) async throws { 81 | for try await chunk in self.sequence { 82 | try await self.writeChunk(chunk, with: writer) 83 | } 84 | } 85 | 86 | internal init(underlying: InputSequence) { 87 | self.sequence = underlying 88 | } 89 | } 90 | 91 | extension InputProtocol { 92 | /// Create a Subprocess input from a `Data` 93 | public static func data(_ data: Data) -> Self where Self == DataInput { 94 | return DataInput(data: data) 95 | } 96 | 97 | /// Create a Subprocess input from a `Sequence` of `Data`. 98 | public static func sequence( 99 | _ sequence: InputSequence 100 | ) -> Self where Self == DataSequenceInput { 101 | return .init(underlying: sequence) 102 | } 103 | 104 | /// Create a Subprocess input from a `AsyncSequence` of `Data`. 105 | public static func sequence( 106 | _ asyncSequence: InputSequence 107 | ) -> Self where Self == DataAsyncSequenceInput { 108 | return .init(underlying: asyncSequence) 109 | } 110 | } 111 | 112 | extension StandardInputWriter { 113 | /// Write a `Data` to the standard input of the subprocess. 114 | /// - Parameter data: The sequence of bytes to write. 115 | /// - Returns number of bytes written. 116 | public func write( 117 | _ data: Data 118 | ) async throws -> Int { 119 | return try await AsyncIO.shared.write(data, to: self.diskIO) 120 | } 121 | 122 | /// Write a AsyncSequence of Data to the standard input of the subprocess. 123 | /// - Parameter asyncSequence: The sequence of bytes to write. 124 | /// - Returns number of bytes written. 125 | public func write( 126 | _ asyncSequence: AsyncSendableSequence 127 | ) async throws -> Int where AsyncSendableSequence.Element == Data { 128 | var buffer = Data() 129 | for try await data in asyncSequence { 130 | buffer.append(data) 131 | } 132 | return try await self.write(buffer) 133 | } 134 | } 135 | 136 | #if SUBPROCESS_ASYNCIO_DISPATCH 137 | extension AsyncIO { 138 | internal func write( 139 | _ data: Data, 140 | to diskIO: borrowing IOChannel 141 | ) async throws -> Int { 142 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 143 | let dispatchData = data.withUnsafeBytes { 144 | return DispatchData( 145 | bytesNoCopy: $0, 146 | deallocator: .custom( 147 | nil, 148 | { 149 | // noop 150 | } 151 | ) 152 | ) 153 | } 154 | self.write(dispatchData, to: diskIO) { writtenLength, error in 155 | if let error = error { 156 | continuation.resume(throwing: error) 157 | } else { 158 | continuation.resume(returning: writtenLength) 159 | } 160 | } 161 | } 162 | } 163 | } 164 | #else 165 | extension Data: AsyncIO._ContiguousBytes {} 166 | 167 | extension AsyncIO { 168 | internal func write( 169 | _ data: Data, 170 | to diskIO: borrowing IOChannel 171 | ) async throws -> Int { 172 | return try await self._write(data, to: diskIO) 173 | } 174 | } 175 | #endif // SUBPROCESS_ASYNCIO_DISPATCH 176 | 177 | #endif // SubprocessFoundation 178 | -------------------------------------------------------------------------------- /Sources/Subprocess/IO/AsyncIO+Dispatch.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | /// Darwin AsyncIO implementation based on DispatchIO 13 | 14 | // MARK: - macOS (DispatchIO) 15 | #if SUBPROCESS_ASYNCIO_DISPATCH 16 | 17 | #if canImport(System) 18 | @preconcurrency import System 19 | #else 20 | @preconcurrency import SystemPackage 21 | #endif 22 | 23 | internal import Dispatch 24 | 25 | final class AsyncIO: Sendable { 26 | static let shared: AsyncIO = AsyncIO() 27 | 28 | internal init() {} 29 | 30 | internal func shutdown() { /* noop on Darwin */ } 31 | 32 | internal func read( 33 | from diskIO: borrowing IOChannel, 34 | upTo maxLength: Int 35 | ) async throws -> DispatchData? { 36 | return try await self.read( 37 | from: diskIO.channel, 38 | upTo: maxLength, 39 | ) 40 | } 41 | 42 | internal func read( 43 | from dispatchIO: DispatchIO, 44 | upTo maxLength: Int 45 | ) async throws -> DispatchData? { 46 | return try await withCheckedThrowingContinuation { continuation in 47 | var buffer: DispatchData = .empty 48 | dispatchIO.read( 49 | offset: 0, 50 | length: maxLength, 51 | queue: DispatchQueue(label: "SubprocessReadQueue") 52 | ) { done, data, error in 53 | if error != 0 { 54 | continuation.resume( 55 | throwing: SubprocessError( 56 | code: .init(.failedToReadFromSubprocess), 57 | underlyingError: .init(rawValue: error) 58 | ) 59 | ) 60 | return 61 | } 62 | if let data { 63 | if buffer.isEmpty { 64 | buffer = data 65 | } else { 66 | buffer.append(data) 67 | } 68 | } 69 | if done { 70 | if !buffer.isEmpty { 71 | continuation.resume(returning: buffer) 72 | } else { 73 | continuation.resume(returning: nil) 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | #if SubprocessSpan 81 | internal func write( 82 | _ span: borrowing RawSpan, 83 | to diskIO: borrowing IOChannel 84 | ) async throws -> Int { 85 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 86 | span.withUnsafeBytes { 87 | let dispatchData = DispatchData( 88 | bytesNoCopy: $0, 89 | deallocator: .custom( 90 | nil, 91 | { 92 | // noop 93 | } 94 | ) 95 | ) 96 | 97 | self.write(dispatchData, to: diskIO) { writtenLength, error in 98 | if let error { 99 | continuation.resume(throwing: error) 100 | } else { 101 | continuation.resume(returning: writtenLength) 102 | } 103 | } 104 | } 105 | } 106 | } 107 | #endif // SubprocessSpan 108 | 109 | internal func write( 110 | _ array: [UInt8], 111 | to diskIO: borrowing IOChannel 112 | ) async throws -> Int { 113 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 114 | array.withUnsafeBytes { 115 | let dispatchData = DispatchData( 116 | bytesNoCopy: $0, 117 | deallocator: .custom( 118 | nil, 119 | { 120 | // noop 121 | } 122 | ) 123 | ) 124 | 125 | self.write(dispatchData, to: diskIO) { writtenLength, error in 126 | if let error { 127 | continuation.resume(throwing: error) 128 | } else { 129 | continuation.resume(returning: writtenLength) 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | internal func write( 137 | _ dispatchData: DispatchData, 138 | to diskIO: borrowing IOChannel, 139 | queue: DispatchQueue = .global(), 140 | completion: @escaping (Int, Error?) -> Void 141 | ) { 142 | diskIO.channel.write( 143 | offset: 0, 144 | data: dispatchData, 145 | queue: queue 146 | ) { done, unwritten, error in 147 | guard done else { 148 | // Wait until we are done writing or encountered some error 149 | return 150 | } 151 | 152 | let unwrittenLength = unwritten?.count ?? 0 153 | let writtenLength = dispatchData.count - unwrittenLength 154 | guard error != 0 else { 155 | completion(writtenLength, nil) 156 | return 157 | } 158 | completion( 159 | writtenLength, 160 | SubprocessError( 161 | code: .init(.failedToWriteToSubprocess), 162 | underlyingError: .init(rawValue: error) 163 | ) 164 | ) 165 | } 166 | } 167 | } 168 | 169 | #if canImport(Darwin) 170 | // Dispatch has a -user-module-version of 54 in the macOS 15.3 SDK 171 | #if canImport(Dispatch, _version: "54") 172 | // DispatchData is annotated as Sendable 173 | #else 174 | // Retroactively conform DispatchData to Sendable 175 | extension DispatchData: @retroactive @unchecked Sendable {} 176 | #endif // canImport(Dispatch, _version: "54") 177 | #else 178 | extension DispatchData: @retroactive @unchecked Sendable {} 179 | #endif // canImport(Darwin) 180 | 181 | #endif // SUBPROCESS_ASYNCIO_DISPATCH 182 | -------------------------------------------------------------------------------- /Sources/Subprocess/Error.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if canImport(Darwin) 13 | import Darwin 14 | #elseif canImport(Bionic) 15 | import Bionic 16 | #elseif canImport(Glibc) 17 | import Glibc 18 | #elseif canImport(Musl) 19 | import Musl 20 | #elseif canImport(WinSDK) 21 | @preconcurrency import WinSDK 22 | #endif 23 | 24 | /// Error thrown from Subprocess 25 | public struct SubprocessError: Swift.Error, Hashable, Sendable { 26 | /// The error code of this error 27 | public let code: SubprocessError.Code 28 | /// The underlying error that caused this error, if any 29 | public let underlyingError: UnderlyingError? 30 | } 31 | 32 | // MARK: - Error Codes 33 | extension SubprocessError { 34 | /// A SubprocessError Code 35 | public struct Code: Hashable, Sendable { 36 | internal enum Storage: Hashable, Sendable { 37 | case spawnFailed 38 | case executableNotFound(String) 39 | case failedToChangeWorkingDirectory(String) 40 | case failedToReadFromSubprocess 41 | case failedToWriteToSubprocess 42 | case failedToMonitorProcess 43 | case streamOutputExceedsLimit(Int) 44 | case asyncIOFailed(String) 45 | case outputBufferLimitExceeded(Int) 46 | // Signal 47 | case failedToSendSignal(Int32) 48 | // Windows Only 49 | case failedToTerminate 50 | case failedToSuspend 51 | case failedToResume 52 | case failedToCreatePipe 53 | case invalidWindowsPath(String) 54 | } 55 | 56 | /// The numeric value of this code. 57 | public var value: Int { 58 | switch self.storage { 59 | case .spawnFailed: 60 | return 0 61 | case .executableNotFound(_): 62 | return 1 63 | case .failedToChangeWorkingDirectory(_): 64 | return 2 65 | case .failedToReadFromSubprocess: 66 | return 3 67 | case .failedToWriteToSubprocess: 68 | return 4 69 | case .failedToMonitorProcess: 70 | return 5 71 | case .streamOutputExceedsLimit(_): 72 | return 6 73 | case .asyncIOFailed(_): 74 | return 7 75 | case .outputBufferLimitExceeded(_): 76 | return 8 77 | case .failedToSendSignal(_): 78 | return 9 79 | case .failedToTerminate: 80 | return 10 81 | case .failedToSuspend: 82 | return 11 83 | case .failedToResume: 84 | return 12 85 | case .failedToCreatePipe: 86 | return 13 87 | case .invalidWindowsPath(_): 88 | return 14 89 | } 90 | } 91 | 92 | internal let storage: Storage 93 | 94 | internal init(_ storage: Storage) { 95 | self.storage = storage 96 | } 97 | } 98 | } 99 | 100 | // MARK: - Description 101 | extension SubprocessError: CustomStringConvertible, CustomDebugStringConvertible { 102 | /// A textual representation of this subprocess error. 103 | public var description: String { 104 | switch self.code.storage { 105 | case .spawnFailed: 106 | return "Failed to spawn the new process with underlying error: \(self.underlyingError!)" 107 | case .executableNotFound(let executableName): 108 | return "Executable \"\(executableName)\" is not found or cannot be executed." 109 | case .failedToChangeWorkingDirectory(let workingDirectory): 110 | return "Failed to set working directory to \"\(workingDirectory)\"." 111 | case .failedToReadFromSubprocess: 112 | return "Failed to read bytes from the child process with underlying error: \(self.underlyingError!)" 113 | case .failedToWriteToSubprocess: 114 | return "Failed to write bytes to the child process." 115 | case .failedToMonitorProcess: 116 | return "Failed to monitor the state of child process with underlying error: \(self.underlyingError.map { "\($0)" } ?? "nil")" 117 | case .streamOutputExceedsLimit(let limit): 118 | return "Failed to create output from current buffer because the output limit (\(limit)) was reached." 119 | case .asyncIOFailed(let reason): 120 | return "An error occurred within the AsyncIO subsystem: \(reason). Underlying error: \(self.underlyingError!)" 121 | case .outputBufferLimitExceeded(let limit): 122 | return "Output exceeds the limit of \(limit) bytes." 123 | case .failedToSendSignal(let signal): 124 | return "Failed to send signal \(signal) to the child process." 125 | case .failedToTerminate: 126 | return "Failed to terminate the child process." 127 | case .failedToSuspend: 128 | return "Failed to suspend the child process." 129 | case .failedToResume: 130 | return "Failed to resume the child process." 131 | case .failedToCreatePipe: 132 | return "Failed to create a pipe to communicate to child process." 133 | case .invalidWindowsPath(let badPath): 134 | return "\"\(badPath)\" is not a valid Windows path." 135 | } 136 | } 137 | 138 | /// A debug-oriented textual representation of this subprocess error. 139 | public var debugDescription: String { self.description } 140 | } 141 | 142 | extension SubprocessError { 143 | /// The underlying error that caused this SubprocessError. 144 | /// - On Unix-like systems, `UnderlyingError` wraps `errno` from libc; 145 | /// - On Windows, `UnderlyingError` wraps Windows Error code 146 | public struct UnderlyingError: Swift.Error, RawRepresentable, Hashable, Sendable { 147 | #if os(Windows) 148 | /// The type for the raw value of the underlying error. 149 | public typealias RawValue = DWORD 150 | #else 151 | /// The type for the raw value of the underlying error. 152 | public typealias RawValue = Int32 153 | #endif 154 | 155 | /// The platform specific value for this underlying error. 156 | public let rawValue: RawValue 157 | /// Initialize a `UnderlyingError` with given error value 158 | public init(rawValue: RawValue) { 159 | self.rawValue = rawValue 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Sources/Subprocess/Teardown.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import _SubprocessCShims 13 | 14 | #if canImport(Darwin) 15 | import Darwin 16 | #elseif canImport(Bionic) 17 | import Bionic 18 | #elseif canImport(Glibc) 19 | import Glibc 20 | #elseif canImport(Musl) 21 | import Musl 22 | #elseif canImport(WinSDK) 23 | @preconcurrency import WinSDK 24 | #endif 25 | 26 | /// A step in the graceful shutdown teardown sequence. 27 | /// It consists of an action to perform on the child process and the 28 | /// duration allowed for the child process to exit before proceeding 29 | /// to the next step. 30 | public struct TeardownStep: Sendable, Hashable { 31 | internal enum Storage: Sendable, Hashable { 32 | #if !os(Windows) 33 | case sendSignal(Signal, allowedDuration: Duration) 34 | #endif 35 | case gracefulShutDown(allowedDuration: Duration) 36 | case kill 37 | } 38 | var storage: Storage 39 | 40 | #if !os(Windows) 41 | /// Sends `signal` to the process and allows `allowedDurationToExit` 42 | /// for the process to exit before proceeding to the next step. 43 | /// The final step in the sequence will always send a `.kill` signal. 44 | public static func send( 45 | signal: Signal, 46 | allowedDurationToNextStep: Duration 47 | ) -> Self { 48 | return Self( 49 | storage: .sendSignal( 50 | signal, 51 | allowedDuration: allowedDurationToNextStep 52 | ) 53 | ) 54 | } 55 | #endif // !os(Windows) 56 | 57 | /// Attempt to perform a graceful shutdown and allows 58 | /// `allowedDurationToNextStep` for the process to exit 59 | /// before proceeding to the next step: 60 | /// - On Unix: send `SIGTERM` 61 | /// - On Windows: 62 | /// 1. Attempt to send `VM_CLOSE` if the child process is a GUI process; 63 | /// 2. Attempt to send `CTRL_C_EVENT` to console; 64 | /// 3. Attempt to send `CTRL_BREAK_EVENT` to process group. 65 | public static func gracefulShutDown( 66 | allowedDurationToNextStep: Duration 67 | ) -> Self { 68 | return Self( 69 | storage: .gracefulShutDown( 70 | allowedDuration: allowedDurationToNextStep 71 | ) 72 | ) 73 | } 74 | } 75 | 76 | extension Execution { 77 | /// Performs a sequence of teardown steps on the Subprocess. 78 | /// Teardown sequence always ends with a `.kill` signal 79 | /// - Parameter sequence: The steps to perform. 80 | public func teardown(using sequence: some Sequence & Sendable) async { 81 | await withUncancelledTask { 82 | await runTeardownSequence(sequence) 83 | } 84 | } 85 | } 86 | 87 | internal enum TeardownStepCompletion { 88 | case processHasExited 89 | case processStillAlive 90 | case killedTheProcess 91 | } 92 | 93 | extension Execution { 94 | internal func gracefulShutDown( 95 | allowedDurationToNextStep duration: Duration 96 | ) async { 97 | #if os(Windows) 98 | // 1. Attempt to send WM_CLOSE to the main window 99 | if _subprocess_windows_send_vm_close( 100 | processIdentifier.value 101 | ) { 102 | try? await Task.sleep(for: duration) 103 | } 104 | 105 | // 2. Attempt to attach to the console and send CTRL_C_EVENT 106 | if AttachConsole(processIdentifier.value) { 107 | // Disable Ctrl-C handling in this process 108 | if SetConsoleCtrlHandler(nil, true) { 109 | if GenerateConsoleCtrlEvent(DWORD(CTRL_C_EVENT), 0) { 110 | // We successfully sent the event. wait for the process to exit 111 | try? await Task.sleep(for: duration) 112 | } 113 | // Re-enable Ctrl-C handling 114 | SetConsoleCtrlHandler(nil, false) 115 | } 116 | // Detach console 117 | FreeConsole() 118 | } 119 | 120 | // 3. Attempt to send CTRL_BREAK_EVENT to the process group 121 | if GenerateConsoleCtrlEvent(DWORD(CTRL_BREAK_EVENT), processIdentifier.value) { 122 | // Wait for process to exit 123 | try? await Task.sleep(for: duration) 124 | } 125 | #else 126 | // Send SIGTERM 127 | try? self.send( 128 | signal: .terminate, 129 | toProcessGroup: false 130 | ) 131 | #endif 132 | } 133 | 134 | internal func runTeardownSequence( 135 | _ sequence: some Sequence & Sendable 136 | ) async { 137 | // First insert the `.kill` step 138 | let finalSequence = sequence + [TeardownStep(storage: .kill)] 139 | for step in finalSequence { 140 | let stepCompletion: TeardownStepCompletion 141 | guard self.isPotentiallyStillAlive() else { 142 | // Early return since the process has already exited 143 | return 144 | } 145 | 146 | switch step.storage { 147 | case .gracefulShutDown(let allowedDuration): 148 | stepCompletion = await withTaskGroup(of: TeardownStepCompletion.self) { group in 149 | group.addTask { 150 | do { 151 | try await Task.sleep(for: allowedDuration) 152 | return .processStillAlive 153 | } catch { 154 | // teardown(using:) cancels this task 155 | // when process has exited 156 | return .processHasExited 157 | } 158 | } 159 | await self.gracefulShutDown( 160 | allowedDurationToNextStep: allowedDuration 161 | ) 162 | return await group.next()! 163 | } 164 | #if !os(Windows) 165 | case .sendSignal(let signal, let allowedDuration): 166 | stepCompletion = await withTaskGroup(of: TeardownStepCompletion.self) { group in 167 | group.addTask { 168 | do { 169 | try await Task.sleep(for: allowedDuration) 170 | return .processStillAlive 171 | } catch { 172 | // teardown(using:) cancels this task 173 | // when process has exited 174 | return .processHasExited 175 | } 176 | } 177 | try? self.send(signal: signal, toProcessGroup: false) 178 | return await group.next()! 179 | } 180 | #endif // !os(Windows) 181 | case .kill: 182 | #if os(Windows) 183 | try? self.terminate( 184 | withExitCode: 0 185 | ) 186 | #else 187 | try? self.send(signal: .kill, toProcessGroup: false) 188 | #endif 189 | stepCompletion = .killedTheProcess 190 | } 191 | 192 | switch stepCompletion { 193 | case .killedTheProcess, .processHasExited: 194 | return 195 | case .processStillAlive: 196 | // Continue to next step 197 | break 198 | } 199 | } 200 | } 201 | 202 | private func isPotentiallyStillAlive() -> Bool { 203 | // Non-blockingly check whether the current execution has already exited 204 | // Note here we do NOT want to reap the exit status because we are still 205 | // running monitorProcessTermination() 206 | #if os(Windows) 207 | return WaitForSingleObject(self.processIdentifier.processDescriptor, 0) == WAIT_TIMEOUT 208 | #else 209 | return kill(self.processIdentifier.value, 0) == 0 210 | #endif 211 | } 212 | } 213 | 214 | func withUncancelledTask( 215 | returning: Result.Type = Result.self, 216 | _ body: @Sendable @escaping () async -> Result 217 | ) async -> Result { 218 | // This looks unstructured but it isn't, please note that we `await` `.value` of this task. 219 | // The reason we need this separate `Task` is that in general, we cannot assume that code performs to our 220 | // expectations if the task we run it on is already cancelled. However, in some cases we need the code to 221 | // run regardless -- even if our task is already cancelled. Therefore, we create a new, uncanceled task here. 222 | await Task { 223 | await body() 224 | }.value 225 | } 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Subprocess 4 | 5 | Subprocess is a cross-platform package for spawning processes in Swift. 6 | 7 | 8 | ## Getting Started 9 | 10 | To use `Subprocess` in a [SwiftPM](https://swift.org/package-manager/) project, add it as a package dependency to your `Package.swift`: 11 | 12 | 13 | ```swift 14 | dependencies: [ 15 | .package(url: "https://github.com/swiftlang/swift-subprocess.git", branch: "main") 16 | ] 17 | ``` 18 | Then, add the `Subprocess` module to your target dependencies: 19 | 20 | ```swift 21 | .target( 22 | name: "MyTarget", 23 | dependencies: [ 24 | .product(name: "Subprocess", package: "swift-subprocess") 25 | ] 26 | ) 27 | ``` 28 | 29 | `Subprocess` offers two [package traits](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0450-swiftpm-package-traits.md): 30 | 31 | - `SubprocessFoundation`: includes a dependency on `Foundation` and adds extensions on Foundation types like `Data`. This trait is enabled by default. 32 | - `SubprocessSpan`: makes Subprocess's API, mainly `OutputProtocol`, `RawSpan`-based. This trait is enabled whenever `RawSpan` is available and should only be disabled when `RawSpan` is not available. 33 | 34 | Please find the API proposal [here](https://github.com/swiftlang/swift-foundation/blob/main/Proposals/0007-swift-subprocess.md). 35 | 36 | ### Swift Versions 37 | 38 | The minimum supported Swift version is **Swift 6.1**. 39 | 40 | To experiment with the `SubprocessSpan` trait, Swift 6.2 is required. Currently, you can download the Swift 6.2 toolchain (`main` development snapshot) [here](https://www.swift.org/install/macos/#development-snapshots). 41 | 42 | 43 | ## Feature Overview 44 | 45 | ### Run and Asynchronously Collect Output 46 | 47 | The easiest way to spawn a process with `Subprocess` is to simply run it and await its `CollectedResult`: 48 | 49 | ```swift 50 | import Subprocess 51 | 52 | let result = try await run(.name("ls"), output: .string(limit: 4096)) 53 | 54 | print(result.processIdentifier) // prints 1234 55 | print(result.terminationStatus) // prints exited(0) 56 | 57 | print(result.standardOutput) // prints LICENSE Package.swift ... 58 | ``` 59 | 60 | ### Run with Custom Closure 61 | 62 | To have more precise control over input and output, you can provide a custom closure that executes while the child process is active. Inside this closure, you have the ability to manage the subprocess’s state (like suspending or terminating it) and stream its standard output and standard error as an `AsyncSequence`: 63 | 64 | ```swift 65 | import Subprocess 66 | 67 | // Monitor Nginx log via `tail -f` 68 | async let monitorResult = run( 69 | .path("/usr/bin/tail"), 70 | arguments: ["-f", "/path/to/nginx.log"] 71 | ) { execution, standardOutput in 72 | for try await line in standardOutput.lines() { 73 | // Parse the log text 74 | if line.contains("500") { 75 | // Oh no, 500 error 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | ### Customizable Execution 82 | 83 | You can set various parameters when running the child process, such as `Arguments`, `Environment`, and working directory: 84 | 85 | ```swift 86 | import Subprocess 87 | 88 | let result = try await run( 89 | .path("/bin/ls"), 90 | arguments: ["-a"], 91 | // Inherit the environment values from parent process and 92 | // add `NewKey=NewValue` 93 | environment: .inherit.updating(["NewKey": "NewValue"]), 94 | workingDirectory: "/Users/", 95 | output: .string(limit: 4096) 96 | ) 97 | ``` 98 | 99 | ### Platform Specific Options and “Escape Hatches” 100 | 101 | `Subprocess` provides **platform-specific** configuration options, like setting `uid` and `gid` on Unix and adjusting window style on Windows, through the `PlatformOptions` struct. Check out the `PlatformOptions` documentation for a complete list of configurable parameters across different platforms. 102 | 103 | `PlatformOptions` also allows access to platform-specific spawning constructs and customizations via a closure. 104 | 105 | ```swift 106 | import Darwin 107 | import Subprocess 108 | 109 | var platformOptions = PlatformOptions() 110 | let intendedWorkingDir = "/path/to/directory" 111 | platformOptions.preSpawnProcessConfigurator = { spawnAttr, fileAttr in 112 | // Set POSIX_SPAWN_SETSID flag, which implies calls 113 | // to setsid 114 | var flags: Int16 = 0 115 | posix_spawnattr_getflags(&spawnAttr, &flags) 116 | posix_spawnattr_setflags(&spawnAttr, flags | Int16(POSIX_SPAWN_SETSID)) 117 | 118 | // Change the working directory 119 | intendedWorkingDir.withCString { path in 120 | _ = posix_spawn_file_actions_addchdir_np(&fileAttr, path) 121 | } 122 | } 123 | 124 | let result = try await run(.path("/bin/exe"), platformOptions: platformOptions) 125 | ``` 126 | 127 | 128 | ### Flexible Input and Output Configurations 129 | 130 | By default, `Subprocess`: 131 | - Doesn't send any input to the child process's standard input 132 | - Requires you to specify how to capture the output 133 | - Ignores the child process's standard error 134 | 135 | You can tailor how `Subprocess` handles the standard input, standard output, and standard error by setting the `input`, `output`, and `error` parameters: 136 | 137 | ```swift 138 | let content = "Hello Subprocess" 139 | 140 | // Send "Hello Subprocess" to the standard input of `cat` 141 | let result = try await run(.name("cat"), input: .string(content), output: .string(limit: 4096)) 142 | 143 | // Collect both standard error and standard output as Data 144 | let result = try await run(.name("cat"), output: .data(limit: 4096), error: .data(limit: 4096)) 145 | ``` 146 | 147 | `Subprocess` supports these input options: 148 | 149 | #### `NoInput` 150 | 151 | This option means no input is sent to the subprocess. 152 | 153 | Use it by setting `.none` for `input`. 154 | 155 | #### `FileDescriptorInput` 156 | 157 | This option reads input from a specified `FileDescriptor`. If `closeAfterSpawningProcess` is set to `true`, the subprocess will close the file descriptor after spawning. If `false`, you are responsible for closing it, even if the subprocess fails to spawn. 158 | 159 | Use it by setting `.fileDescriptor(_:closeAfterSpawningProcess:)` for `input`. 160 | 161 | #### `StringInput` 162 | 163 | This option reads input from a type conforming to `StringProtocol` using the specified encoding. 164 | 165 | Use it by setting `.string(_:)` or `.string(_:using:)` for `input`. 166 | 167 | #### `ArrayInput` 168 | 169 | This option reads input from an array of `UInt8`. 170 | 171 | Use it by setting `.array` for `input`. 172 | 173 | #### `DataInput` (available with `SubprocessFoundation` trait) 174 | 175 | This option reads input from a given `Data`. 176 | 177 | Use it by setting `.data` for `input`. 178 | 179 | #### `DataSequenceInput` (available with `SubprocessFoundation` trait) 180 | 181 | This option reads input from a sequence of `Data`. 182 | 183 | Use it by setting `.sequence(_:)` for `input`. 184 | 185 | #### `DataAsyncSequenceInput` (available with `SubprocessFoundation` trait) 186 | 187 | This option reads input from an async sequence of `Data`. 188 | 189 | Use it by setting `.sequence(_:)` for `input`. 190 | 191 | --- 192 | 193 | `Subprocess` also supports these output options: 194 | 195 | #### `DiscardedOutput` 196 | 197 | This option means the `Subprocess` won’t collect or redirect output from the child process. 198 | 199 | Use it by setting `.discarded` for `output` or `error`. 200 | 201 | #### `FileDescriptorOutput` 202 | 203 | This option writes output to a specified `FileDescriptor`. You can choose to have the `Subprocess` close the file descriptor after spawning. 204 | 205 | Use it by setting `.fileDescriptor(_:closeAfterSpawningProcess:)` for `output` or `error`. 206 | 207 | #### `StringOutput` 208 | 209 | This option collects output as a `String` with the given encoding. 210 | 211 | Use it by setting `.string(limit:)` or `.string(limit:encoding:)` for `output` or `error`. 212 | 213 | #### `BytesOutput` 214 | 215 | This option collects output as `[UInt8]`. 216 | 217 | Use it by setting `.bytes(limit:)` for `output` or `error`. 218 | 219 | #### `DataOutput` (available with `SubprocessFoundation` trait) 220 | 221 | This option collects output as `Data`. 222 | 223 | Use it by setting `.data(limit:)` for `output` or `error`. 224 | 225 | 226 | ### Cross-platform support 227 | 228 | `Subprocess` works on macOS, Linux, and Windows, with feature parity on all platforms as well as platform-specific options for each. 229 | 230 | The table below describes the current level of support that Subprocess has for various platforms: 231 | 232 | | **Platform** | **Support Status** | 233 | | ---------------------------------- | ------------------ | 234 | | **macOS** | Supported | 235 | | **iOS** | Not supported | 236 | | **watchOS** | Not supported | 237 | | **tvOS** | Not supported | 238 | | **visionOS** | Not supported | 239 | | **Ubuntu 20.04** | Supported | 240 | | **Ubuntu 22.04** | Supported | 241 | | **Ubuntu 24.04** | Supported | 242 | | **Red Hat Universal Base Image 9** | Supported | 243 | | **Debian 12** | Supported | 244 | | **Amazon Linux 2** | Supported | 245 | | **Windows** | Supported | 246 | 247 |

(back to top)

248 | 249 | 250 | ## Documentation 251 | 252 | The latest API documentation can be viewed by running the following command: 253 | 254 | ``` 255 | swift package --disable-sandbox preview-documentation --target Subprocess 256 | ``` 257 | 258 |

(back to top)

259 | 260 | 261 | ## Contributing to Subprocess 262 | 263 | Subprocess is part of the Foundation project. Discussion and evolution take place on [Swift Foundation Forum](https://forums.swift.org/c/related-projects/foundation/99). 264 | 265 | If you find something that looks like a bug, please open a [Bug Report][bugreport]! Fill out as many details as you can. 266 | 267 | [bugreport]: https://github.com/swiftlang/swift-subprocess/issues/new?assignees=&labels=bug&template=bug_report.md 268 | 269 |

(back to top)

270 | 271 | 272 | ## Code of Conduct 273 | 274 | Like all Swift.org projects, we would like the Subprocess project to foster a diverse and friendly community. We expect contributors to adhere to the [Swift.org Code of Conduct](https://swift.org/code-of-conduct/). 275 | 276 | 277 |

(back to top)

278 | 279 | ## Contact information 280 | 281 | The Foundation Workgroup communicates with the broader Swift community using the [forum](https://forums.swift.org/c/related-projects/foundation/99) for general discussions. 282 | 283 | The workgroup can also be contacted privately by messaging [@foundation-workgroup](https://forums.swift.org/new-message?groupname=foundation-workgroup) on the Swift Forums. 284 | 285 | 286 |

(back to top)

287 | -------------------------------------------------------------------------------- /Sources/Subprocess/Thread.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if canImport(Darwin) 13 | import Darwin 14 | import os 15 | #elseif canImport(Glibc) 16 | import Glibc 17 | #elseif canImport(Bionic) 18 | import Bionic 19 | #elseif canImport(Musl) 20 | import Musl 21 | #elseif canImport(WinSDK) 22 | import WinSDK 23 | #endif 24 | 25 | internal import Dispatch 26 | import _SubprocessCShims 27 | 28 | #if canImport(Synchronization) 29 | import Synchronization 30 | #endif 31 | 32 | #if canImport(WinSDK) 33 | private typealias MutexType = CRITICAL_SECTION 34 | private typealias ConditionType = CONDITION_VARIABLE 35 | private typealias ThreadType = HANDLE 36 | #else 37 | private typealias MutexType = pthread_mutex_t 38 | private typealias ConditionType = pthread_cond_t 39 | private typealias ThreadType = pthread_t 40 | #endif 41 | 42 | internal func runOnBackgroundThread( 43 | _ body: @Sendable @escaping () throws -> Result 44 | ) async throws -> Result { 45 | // Only executed once 46 | _setupWorkerThread 47 | 48 | let result = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 49 | let workItem = BackgroundWorkItem(body, continuation: continuation) 50 | _workQueue.enqueue(workItem) 51 | } 52 | return result 53 | } 54 | 55 | private struct BackgroundWorkItem { 56 | private let work: @Sendable () -> Void 57 | 58 | init( 59 | _ body: @Sendable @escaping () throws -> Result, 60 | continuation: CheckedContinuation 61 | ) { 62 | self.work = { 63 | do { 64 | let result = try body() 65 | continuation.resume(returning: result) 66 | } catch { 67 | continuation.resume(throwing: error) 68 | } 69 | } 70 | } 71 | 72 | func run() { 73 | self.work() 74 | } 75 | } 76 | 77 | // We can't use Mutex directly here because we need the underlying `pthread_mutex_t` to be 78 | // exposed so we can use it with `pthread_cond_wait`. 79 | private final class WorkQueue: Sendable { 80 | private nonisolated(unsafe) var queue: [BackgroundWorkItem] 81 | internal nonisolated(unsafe) let mutex: UnsafeMutablePointer 82 | internal nonisolated(unsafe) let waitCondition: UnsafeMutablePointer 83 | 84 | init() { 85 | self.queue = [] 86 | self.mutex = UnsafeMutablePointer.allocate(capacity: 1) 87 | self.waitCondition = UnsafeMutablePointer.allocate(capacity: 1) 88 | #if canImport(WinSDK) 89 | InitializeCriticalSection(self.mutex) 90 | InitializeConditionVariable(self.waitCondition) 91 | #else 92 | pthread_mutex_init(self.mutex, nil) 93 | pthread_cond_init(self.waitCondition, nil) 94 | #endif 95 | } 96 | 97 | func withLock(_ body: (inout [BackgroundWorkItem]) throws -> R) rethrows -> R { 98 | try withUnsafeUnderlyingLock { _, queue in 99 | try body(&queue) 100 | } 101 | } 102 | 103 | private func withUnsafeUnderlyingLock( 104 | condition: (inout [BackgroundWorkItem]) -> Bool = { _ in false }, 105 | body: (UnsafeMutablePointer, inout [BackgroundWorkItem]) throws -> R 106 | ) rethrows -> R { 107 | #if canImport(WinSDK) 108 | EnterCriticalSection(self.mutex) 109 | defer { 110 | LeaveCriticalSection(self.mutex) 111 | } 112 | #else 113 | pthread_mutex_lock(self.mutex) 114 | defer { 115 | pthread_mutex_unlock(mutex) 116 | } 117 | #endif 118 | if condition(&queue) { 119 | #if canImport(WinSDK) 120 | SleepConditionVariableCS(self.waitCondition, mutex, INFINITE) 121 | #else 122 | pthread_cond_wait(self.waitCondition, mutex) 123 | #endif 124 | } 125 | return try body(mutex, &queue) 126 | } 127 | 128 | // Only called in worker thread. Sleeps the thread if there's no more item 129 | func dequeue() -> BackgroundWorkItem? { 130 | return self.withUnsafeUnderlyingLock { queue in 131 | // Sleep the worker thread if there's no more work 132 | queue.isEmpty 133 | } body: { mutex, queue in 134 | guard !queue.isEmpty else { 135 | return nil 136 | } 137 | return queue.removeFirst() 138 | } 139 | } 140 | 141 | // Only called in parent thread. Signals wait condition to wake up worker thread 142 | func enqueue(_ workItem: BackgroundWorkItem) { 143 | self.withLock { queue in 144 | queue.append(workItem) 145 | #if canImport(WinSDK) 146 | WakeConditionVariable(self.waitCondition) 147 | #else 148 | pthread_cond_signal(self.waitCondition) 149 | #endif 150 | } 151 | } 152 | 153 | func shutdown() { 154 | self.withLock { queue in 155 | queue.removeAll() 156 | #if canImport(WinSDK) 157 | WakeConditionVariable(self.waitCondition) 158 | #else 159 | pthread_cond_signal(self.waitCondition) 160 | #endif 161 | } 162 | } 163 | } 164 | 165 | private let _workQueue = WorkQueue() 166 | private let _workQueueShutdownFlag = AtomicCounter() 167 | 168 | // Okay to be unlocked global mutable because this value is only set once like dispatch_once 169 | private nonisolated(unsafe) var _workerThread: Result = .failure(SubprocessError(code: .init(.spawnFailed), underlyingError: nil)) 170 | 171 | private let _setupWorkerThread: () = { 172 | do { 173 | #if canImport(WinSDK) 174 | let workerThread = try begin_thread_x { 175 | while true { 176 | // This dequeue call will suspend the thread if there's no more work left 177 | guard let workItem = _workQueue.dequeue() else { 178 | break 179 | } 180 | workItem.run() 181 | } 182 | return 0 183 | } 184 | #else 185 | let workerThread = try pthread_create { 186 | while true { 187 | // This dequeue call will suspend the thread if there's no more work left 188 | guard let workItem = _workQueue.dequeue() else { 189 | break 190 | } 191 | workItem.run() 192 | } 193 | } 194 | #endif 195 | _workerThread = .success(workerThread) 196 | } catch { 197 | _workerThread = .failure(error) 198 | } 199 | 200 | atexit { 201 | _shutdownWorkerThread() 202 | } 203 | }() 204 | 205 | private func _shutdownWorkerThread() { 206 | guard case .success(let thread) = _workerThread else { 207 | return 208 | } 209 | guard _workQueueShutdownFlag.addOne() == 1 else { 210 | // We already shutdown this thread 211 | return 212 | } 213 | _workQueue.shutdown() 214 | #if canImport(WinSDK) 215 | WaitForSingleObject(thread, INFINITE) 216 | CloseHandle(thread) 217 | DeleteCriticalSection(_workQueue.mutex) 218 | // We do not need to destroy CONDITION_VARIABLE 219 | #else 220 | pthread_join(thread, nil) 221 | pthread_mutex_destroy(_workQueue.mutex) 222 | pthread_cond_destroy(_workQueue.waitCondition) 223 | #endif 224 | _workQueue.mutex.deallocate() 225 | _workQueue.waitCondition.deallocate() 226 | } 227 | 228 | // MARK: - AtomicCounter 229 | 230 | #if canImport(Darwin) 231 | // Unfortunately on Darwin we cannot unconditionally use Atomic since it requires macOS 15 232 | internal struct AtomicCounter: ~Copyable { 233 | private let storage: OSAllocatedUnfairLock 234 | 235 | internal init() { 236 | self.storage = .init(initialState: 0) 237 | } 238 | 239 | internal func addOne() -> UInt8 { 240 | return self.storage.withLock { 241 | $0 += 1 242 | return $0 243 | } 244 | } 245 | } 246 | #else 247 | internal struct AtomicCounter: ~Copyable { 248 | 249 | private let storage: Atomic 250 | 251 | internal init() { 252 | self.storage = Atomic(0) 253 | } 254 | 255 | internal func addOne() -> UInt8 { 256 | return self.storage.add(1, ordering: .sequentiallyConsistent).newValue 257 | } 258 | } 259 | #endif 260 | 261 | // MARK: - Thread Creation Primitives 262 | #if canImport(WinSDK) 263 | /// Microsoft documentation for `CreateThread` states: 264 | /// > A thread in an executable that calls the C run-time library (CRT) 265 | /// > should use the _beginthreadex and _endthreadex functions for 266 | /// > thread management rather than CreateThread and ExitThread 267 | internal func begin_thread_x( 268 | _ body: @Sendable @escaping () -> UInt32 269 | ) throws(SubprocessError.UnderlyingError) -> HANDLE { 270 | final class Context { 271 | let body: @Sendable () -> UInt32 272 | init(body: @Sendable @escaping () -> UInt32) { 273 | self.body = body 274 | } 275 | } 276 | 277 | func proc(_ context: UnsafeMutableRawPointer?) -> UInt32 { 278 | return Unmanaged.fromOpaque(context!).takeRetainedValue().body() 279 | } 280 | 281 | let threadHandleValue = _beginthreadex( 282 | nil, 283 | 0, 284 | proc, 285 | Unmanaged.passRetained(Context(body: body)).toOpaque(), 286 | 0, 287 | nil 288 | ) 289 | guard threadHandleValue != 0, 290 | let threadHandle = HANDLE(bitPattern: threadHandleValue) 291 | else { 292 | // _beginthreadex uses errno instead of GetLastError() 293 | let capturedError = _subprocess_windows_get_errno() 294 | throw SubprocessError.UnderlyingError(rawValue: DWORD(capturedError)) 295 | } 296 | 297 | return threadHandle 298 | } 299 | #else 300 | 301 | internal func pthread_create( 302 | _ body: @Sendable @escaping () -> () 303 | ) throws(SubprocessError.UnderlyingError) -> pthread_t { 304 | final class Context { 305 | let body: @Sendable () -> () 306 | init(body: @Sendable @escaping () -> Void) { 307 | self.body = body 308 | } 309 | } 310 | func proc(_ context: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? { 311 | (Unmanaged.fromOpaque(context!).takeRetainedValue() as! Context).body() 312 | return context 313 | } 314 | #if canImport(Glibc) || canImport(Bionic) 315 | var thread = pthread_t() 316 | #else 317 | var thread: pthread_t? 318 | #endif 319 | let rc = _subprocess_pthread_create( 320 | &thread, 321 | nil, 322 | proc, 323 | Unmanaged.passRetained(Context(body: body)).toOpaque() 324 | ) 325 | if rc != 0 { 326 | throw SubprocessError.UnderlyingError(rawValue: rc) 327 | } 328 | #if canImport(Glibc) || canImport(Bionic) 329 | return thread 330 | #else 331 | return thread! 332 | #endif 333 | } 334 | 335 | #endif // canImport(WinSDK) 336 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Tests/SubprocessTests/AsyncIOTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if canImport(System) 13 | @preconcurrency import System 14 | #else 15 | @preconcurrency import SystemPackage 16 | #endif 17 | 18 | #if canImport(Darwin) 19 | import Darwin 20 | #elseif canImport(Glibc) 21 | import Glibc 22 | #elseif canImport(Bionic) 23 | import Bionic 24 | #elseif canImport(Musl) 25 | import Musl 26 | #elseif canImport(WinSDK) 27 | import WinSDK 28 | #endif 29 | 30 | import Testing 31 | import Dispatch 32 | import Foundation 33 | import TestResources 34 | import _SubprocessCShims 35 | @testable import Subprocess 36 | 37 | @Suite("Subprocess.AsyncIO Unit Tests", .serialized) 38 | struct SubprocessAsyncIOTests {} 39 | 40 | // MARK: - Basic Functionality Tests 41 | extension SubprocessAsyncIOTests { 42 | @Test func testBasicReadWrite() async throws { 43 | let testData = randomData(count: 1024) 44 | try await runReadWriteTest { readIO, readTestBed in 45 | let readData = try #require( 46 | try await readIO.read(from: readTestBed.ioChannel, upTo: .max) 47 | ) 48 | #expect(Array(readData) == testData) 49 | } writer: { writeIO, writeTestBed in 50 | _ = try await writeIO.write(testData, to: writeTestBed.ioChannel) 51 | try await writeTestBed.finish() 52 | } 53 | } 54 | 55 | @Test func testMultipleSequentialReadWrite() async throws { 56 | var _chunks: [[UInt8]] = [] 57 | for _ in 0..<10 { 58 | // Generate some that's short 59 | _chunks.append(randomData(count: Int.random(in: 1..<512))) 60 | } 61 | for _ in 0..<10 { 62 | // Generate some that are longer than buffer size 63 | _chunks.append(randomData(count: Int.random(in: Subprocess.readBufferSize.. Void, 240 | writer: @escaping @Sendable (AsyncIO, consuming SubprocessAsyncIOTests.TestBed) async throws -> Void 241 | ) async throws { 242 | try await withThrowingTaskGroup { group in 243 | // First create the pipe 244 | var pipe = try CreatedPipe(closeWhenDone: true, purpose: .input) 245 | 246 | let readChannel: IOChannel? = pipe.readFileDescriptor()?.createIOChannel() 247 | let writeChannel: IOChannel? = pipe.writeFileDescriptor()?.createIOChannel() 248 | 249 | var readBox: IOChannel? = consume readChannel 250 | var writeBox: IOChannel? = consume writeChannel 251 | 252 | let readIO = AsyncIO.shared 253 | let writeIO = AsyncIO() 254 | 255 | group.addTask { 256 | var readIOContainer: IOChannel? = readBox.take() 257 | let readTestBed = try TestBed(ioChannel: _require(readIOContainer.take())) 258 | try await reader(readIO, readTestBed) 259 | } 260 | group.addTask { 261 | var writeIOContainer: IOChannel? = writeBox.take() 262 | let writeTestBed = try TestBed(ioChannel: _require(writeIOContainer.take())) 263 | try await writer(writeIO, writeTestBed) 264 | } 265 | 266 | try await group.waitForAll() 267 | // Teardown 268 | // readIO shutdown is done via `atexit`. 269 | writeIO.shutdown() 270 | } 271 | } 272 | } 273 | 274 | extension SubprocessAsyncIOTests.TestBed { 275 | consuming func finish() async throws { 276 | #if SUBPROCESS_ASYNCIO_DISPATCH 277 | try _safelyClose(.dispatchIO(self.ioChannel.channel)) 278 | #elseif canImport(WinSDK) 279 | try _safelyClose(.handle(self.ioChannel.channel)) 280 | #else 281 | try _safelyClose(.fileDescriptor(self.ioChannel.channel)) 282 | #endif 283 | } 284 | 285 | func delay(_ duration: Duration) async throws { 286 | try await Task.sleep(for: duration) 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /Sources/Subprocess/IO/Input.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if canImport(System) 13 | @preconcurrency import System 14 | #else 15 | @preconcurrency import SystemPackage 16 | #endif 17 | 18 | #if canImport(WinSDK) 19 | @preconcurrency import WinSDK 20 | #endif 21 | 22 | #if SubprocessFoundation 23 | 24 | #if canImport(Darwin) 25 | // On Darwin always prefer system Foundation 26 | import Foundation 27 | #else 28 | // On other platforms prefer FoundationEssentials 29 | import FoundationEssentials 30 | #endif 31 | 32 | #endif // SubprocessFoundation 33 | 34 | // MARK: - Input 35 | 36 | /// InputProtocol defines a type that serves as the input source for a subprocess. 37 | /// 38 | /// The protocol defines the `write(with:)` method that a type must 39 | /// implement to serve as the input source. 40 | public protocol InputProtocol: Sendable, ~Copyable { 41 | /// Asynchronously write the input to the subprocess using the 42 | /// write file descriptor 43 | func write(with writer: StandardInputWriter) async throws 44 | } 45 | 46 | /// A concrete input type for subprocesses that indicates the absence 47 | /// of input to the subprocess. 48 | /// 49 | /// On Unix-like systems, `NoInput` redirects the standard input of the subprocess to /dev/null, 50 | /// while on Windows, it redirects to `NUL`. 51 | public struct NoInput: InputProtocol { 52 | internal func createPipe() throws -> CreatedPipe { 53 | #if os(Windows) 54 | let devnullFd: FileDescriptor = try .openDevNull(withAccessMode: .writeOnly) 55 | let devnull = HANDLE(bitPattern: _get_osfhandle(devnullFd.rawValue))! 56 | #else 57 | let devnull: FileDescriptor = try .openDevNull(withAccessMode: .readOnly) 58 | #endif 59 | return CreatedPipe( 60 | readFileDescriptor: .init(devnull, closeWhenDone: true), 61 | writeFileDescriptor: nil 62 | ) 63 | } 64 | 65 | /// Asynchronously write the input to the subprocess that uses the 66 | /// write file descriptor. 67 | public func write(with writer: StandardInputWriter) async throws { 68 | fatalError("Unexpected call to \(#function)") 69 | } 70 | 71 | internal init() {} 72 | } 73 | 74 | /// A concrete input type for subprocesses that reads input from a specified FileDescriptor. 75 | /// 76 | /// Developers have the option to instruct the Subprocess to automatically close the provided 77 | /// FileDescriptor after the subprocess is spawned. 78 | public struct FileDescriptorInput: InputProtocol { 79 | private let fileDescriptor: FileDescriptor 80 | private let closeAfterSpawningProcess: Bool 81 | 82 | internal func createPipe() throws -> CreatedPipe { 83 | #if canImport(WinSDK) 84 | let readFd = HANDLE(bitPattern: _get_osfhandle(self.fileDescriptor.rawValue))! 85 | #else 86 | let readFd = self.fileDescriptor 87 | #endif 88 | return CreatedPipe( 89 | readFileDescriptor: .init( 90 | readFd, 91 | closeWhenDone: self.closeAfterSpawningProcess 92 | ), 93 | writeFileDescriptor: nil 94 | ) 95 | } 96 | 97 | /// Asynchronously write the input to the subprocess that use the 98 | /// write file descriptor. 99 | public func write(with writer: StandardInputWriter) async throws { 100 | fatalError("Unexpected call to \(#function)") 101 | } 102 | 103 | internal init( 104 | fileDescriptor: FileDescriptor, 105 | closeAfterSpawningProcess: Bool 106 | ) { 107 | self.fileDescriptor = fileDescriptor 108 | self.closeAfterSpawningProcess = closeAfterSpawningProcess 109 | } 110 | } 111 | 112 | /// A concrete `Input` type for subprocesses that reads input 113 | /// from a given type conforming to `StringProtocol`. 114 | /// Developers can specify the string encoding to use when 115 | /// encoding the string to data, which defaults to UTF-8. 116 | public struct StringInput< 117 | InputString: StringProtocol & Sendable, 118 | Encoding: Unicode.Encoding 119 | >: InputProtocol { 120 | private let string: InputString 121 | 122 | /// Asynchronously write the input to the subprocess that use the 123 | /// write file descriptor. 124 | public func write(with writer: StandardInputWriter) async throws { 125 | guard let array = self.string.byteArray(using: Encoding.self) else { 126 | return 127 | } 128 | _ = try await writer.write(array) 129 | } 130 | 131 | internal init(string: InputString, encoding: Encoding.Type) { 132 | self.string = string 133 | } 134 | } 135 | 136 | /// A concrete input type for subprocesses that reads input from 137 | /// a given `UInt8` Array. 138 | public struct ArrayInput: InputProtocol { 139 | private let array: [UInt8] 140 | 141 | /// Asynchronously write the input to the subprocess using the 142 | /// write file descriptor 143 | public func write(with writer: StandardInputWriter) async throws { 144 | _ = try await writer.write(self.array) 145 | } 146 | 147 | internal init(array: [UInt8]) { 148 | self.array = array 149 | } 150 | } 151 | 152 | /// A concrete input type that the run closure uses to write custom input 153 | /// into the subprocess. 154 | internal struct CustomWriteInput: InputProtocol { 155 | /// Asynchronously write the input to the subprocess using the 156 | /// write file descriptor. 157 | public func write(with writer: StandardInputWriter) async throws { 158 | fatalError("Unexpected call to \(#function)") 159 | } 160 | 161 | internal init() {} 162 | } 163 | 164 | extension InputProtocol where Self == NoInput { 165 | /// Create a Subprocess input that specifies there is no input 166 | public static var none: Self { .init() } 167 | } 168 | 169 | extension InputProtocol where Self == FileDescriptorInput { 170 | /// Create a Subprocess input from a `FileDescriptor` and 171 | /// specify whether the `FileDescriptor` should be closed 172 | /// after the process is spawned. 173 | public static func fileDescriptor( 174 | _ fd: FileDescriptor, 175 | closeAfterSpawningProcess: Bool 176 | ) -> Self { 177 | return .init( 178 | fileDescriptor: fd, 179 | closeAfterSpawningProcess: closeAfterSpawningProcess 180 | ) 181 | } 182 | 183 | /// Create a Subprocess input that reads from the standard input of 184 | /// current process. 185 | /// 186 | /// The file descriptor isn't closed afterwards. 187 | public static var standardInput: Self { 188 | return Self.fileDescriptor( 189 | .standardInput, 190 | closeAfterSpawningProcess: false 191 | ) 192 | } 193 | } 194 | 195 | extension InputProtocol { 196 | /// Create a Subprocess input from a `Array` of `UInt8`. 197 | public static func array( 198 | _ array: [UInt8] 199 | ) -> Self where Self == ArrayInput { 200 | return ArrayInput(array: array) 201 | } 202 | 203 | /// Create a Subprocess input from a type that conforms to `StringProtocol` 204 | public static func string< 205 | InputString: StringProtocol & Sendable 206 | >( 207 | _ string: InputString 208 | ) -> Self where Self == StringInput { 209 | return .init(string: string, encoding: UTF8.self) 210 | } 211 | 212 | /// Create a Subprocess input from a type that conforms to `StringProtocol` 213 | public static func string< 214 | InputString: StringProtocol & Sendable, 215 | Encoding: Unicode.Encoding 216 | >( 217 | _ string: InputString, 218 | using encoding: Encoding.Type 219 | ) -> Self where Self == StringInput { 220 | return .init(string: string, encoding: encoding) 221 | } 222 | } 223 | 224 | extension InputProtocol { 225 | internal func createPipe() throws -> CreatedPipe { 226 | if let noInput = self as? NoInput { 227 | return try noInput.createPipe() 228 | } else if let fdInput = self as? FileDescriptorInput { 229 | return try fdInput.createPipe() 230 | } 231 | // Base implementation 232 | return try CreatedPipe(closeWhenDone: true, purpose: .input) 233 | } 234 | } 235 | 236 | // MARK: - StandardInputWriter 237 | 238 | /// A writer that writes to the standard input of the subprocess. 239 | public final actor StandardInputWriter: Sendable { 240 | 241 | internal var diskIO: IOChannel 242 | 243 | init(diskIO: consuming IOChannel) { 244 | self.diskIO = diskIO 245 | } 246 | 247 | /// Write an array of 8-bit unsigned integers to the standard input of the subprocess. 248 | /// - Parameter array: The sequence of bytes to write. 249 | /// - Returns: the number of bytes written. 250 | public func write( 251 | _ array: [UInt8] 252 | ) async throws -> Int { 253 | return try await AsyncIO.shared.write(array, to: self.diskIO) 254 | } 255 | 256 | #if SubprocessSpan 257 | /// Write a raw span to the standard input of the subprocess. 258 | /// 259 | /// - Parameter `span`: The span to write. 260 | /// - Returns: the number of bytes written. 261 | public func write(_ span: borrowing RawSpan) async throws -> Int { 262 | return try await AsyncIO.shared.write(span, to: self.diskIO) 263 | } 264 | #endif 265 | 266 | /// Write a type that conforms to StringProtocol to the standard input of the subprocess. 267 | /// - Parameters: 268 | /// - string: The string to write. 269 | /// - encoding: The encoding to use when converting string to bytes 270 | /// - Returns: number of bytes written. 271 | public func write( 272 | _ string: some StringProtocol, 273 | using encoding: Encoding.Type = UTF8.self 274 | ) async throws -> Int { 275 | if let array = string.byteArray(using: encoding) { 276 | return try await self.write(array) 277 | } 278 | return 0 279 | } 280 | 281 | /// Signal all writes are finished 282 | public func finish() async throws { 283 | try self.diskIO.safelyClose() 284 | } 285 | } 286 | 287 | extension StringProtocol { 288 | #if SubprocessFoundation 289 | private func convertEncoding( 290 | _ encoding: Encoding.Type 291 | ) -> String.Encoding? { 292 | switch encoding { 293 | case is UTF8.Type: 294 | return .utf8 295 | case is UTF16.Type: 296 | return .utf16 297 | case is UTF32.Type: 298 | return .utf32 299 | default: 300 | return nil 301 | } 302 | } 303 | #endif 304 | package func byteArray(using encoding: Encoding.Type) -> [UInt8]? { 305 | if Encoding.self == Unicode.ASCII.self { 306 | let isASCII = self.utf8.allSatisfy { 307 | return Character(Unicode.Scalar($0)).isASCII 308 | } 309 | 310 | guard isASCII else { 311 | return nil 312 | } 313 | return Array(self.utf8) 314 | } 315 | if Encoding.self == UTF8.self { 316 | return Array(self.utf8) 317 | } 318 | if Encoding.self == UTF16.self { 319 | return Array(self.utf16).flatMap { input in 320 | var uint16: UInt16 = input 321 | return withUnsafeBytes(of: &uint16) { ptr in 322 | Array(ptr) 323 | } 324 | } 325 | } 326 | #if SubprocessFoundation 327 | if let stringEncoding = self.convertEncoding(encoding), 328 | let encoded = self.data(using: stringEncoding) 329 | { 330 | return Array(encoded) 331 | } 332 | return nil 333 | #else 334 | return nil 335 | #endif 336 | } 337 | } 338 | 339 | extension String { 340 | package init( 341 | decodingBytes bytes: [T], 342 | as encoding: Encoding.Type 343 | ) { 344 | self = bytes.withUnsafeBytes { raw in 345 | String( 346 | decoding: raw.bindMemory(to: Encoding.CodeUnit.self).lazy.map { $0 }, 347 | as: encoding 348 | ) 349 | } 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /Tests/SubprocessTests/ProcessMonitoringTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if canImport(System) 13 | @preconcurrency import System 14 | #else 15 | @preconcurrency import SystemPackage 16 | #endif 17 | 18 | #if canImport(Darwin) 19 | import Darwin 20 | #elseif canImport(Glibc) 21 | import Glibc 22 | #elseif canImport(Android) 23 | import Android 24 | #elseif canImport(Musl) 25 | import Musl 26 | #elseif canImport(WinSDK) 27 | import WinSDK 28 | #endif 29 | 30 | import Testing 31 | import Dispatch 32 | import Foundation 33 | import TestResources 34 | import _SubprocessCShims 35 | @testable import Subprocess 36 | 37 | @Suite("Subprocess Process Monitoring Unit Tests", .serialized) 38 | struct SubprocessProcessMonitoringTests { 39 | 40 | init() { 41 | #if os(Linux) || os(Android) 42 | _setupMonitorSignalHandler() 43 | #endif 44 | } 45 | 46 | private func immediateExitProcess(withExitCode code: Int) -> Configuration { 47 | #if os(Windows) 48 | return Configuration( 49 | executable: .name("cmd.exe"), 50 | arguments: ["/c", "exit \(code)"] 51 | ) 52 | #else 53 | return Configuration( 54 | executable: .path("/bin/sh"), 55 | arguments: ["-c", "exit \(code)"] 56 | ) 57 | #endif 58 | } 59 | 60 | private func longRunningProcess(withTimeOutSeconds timeout: Double? = nil) -> Configuration { 61 | #if os(Windows) 62 | let waitTime = timeout ?? 99999 63 | return Configuration( 64 | executable: .name("powershell.exe"), 65 | arguments: ["-Command", "Start-Sleep -Seconds \(waitTime)"] 66 | ) 67 | #else 68 | let waitTime = timeout.map { "\($0)" } ?? "infinite" 69 | return Configuration( 70 | executable: .path("/bin/sleep"), 71 | arguments: [waitTime] 72 | ) 73 | #endif 74 | } 75 | 76 | private func devNullInputPipe() throws -> CreatedPipe { 77 | #if os(Windows) 78 | let devnullFd: FileDescriptor = try .openDevNull(withAccessMode: .writeOnly) 79 | let devnull = try #require(HANDLE(bitPattern: _get_osfhandle(devnullFd.rawValue))) 80 | #else 81 | let devnull: FileDescriptor = try .openDevNull(withAccessMode: .readOnly) 82 | #endif 83 | return CreatedPipe( 84 | readFileDescriptor: .init(devnull, closeWhenDone: true), 85 | writeFileDescriptor: nil 86 | ) 87 | } 88 | 89 | private func devNullOutputPipe() throws -> CreatedPipe { 90 | #if os(Windows) 91 | let devnullFd: FileDescriptor = try .openDevNull(withAccessMode: .writeOnly) 92 | let devnull = try #require(HANDLE(bitPattern: _get_osfhandle(devnullFd.rawValue))) 93 | #else 94 | let devnull: FileDescriptor = try .openDevNull(withAccessMode: .writeOnly) 95 | #endif 96 | return CreatedPipe( 97 | readFileDescriptor: nil, 98 | writeFileDescriptor: .init(devnull, closeWhenDone: true) 99 | ) 100 | } 101 | 102 | private func withSpawnedExecution( 103 | config: Configuration, 104 | _ body: (Execution) async throws -> Void 105 | ) async throws { 106 | let spawnResult = try await config.spawn( 107 | withInput: self.devNullInputPipe(), 108 | outputPipe: self.devNullOutputPipe(), 109 | errorPipe: self.devNullOutputPipe() 110 | ) 111 | defer { 112 | spawnResult.execution.processIdentifier.close() 113 | } 114 | try await body(spawnResult.execution) 115 | } 116 | } 117 | 118 | // MARK: - Basic Functionality Tests 119 | extension SubprocessProcessMonitoringTests { 120 | @Test func testNormalExit() async throws { 121 | let config = self.immediateExitProcess(withExitCode: 0) 122 | try await withSpawnedExecution(config: config) { execution in 123 | let monitorResult = try await monitorProcessTermination( 124 | for: execution.processIdentifier 125 | ) 126 | 127 | #expect(monitorResult.isSuccess) 128 | } 129 | } 130 | 131 | @Test func testExitCode() async throws { 132 | let config = self.immediateExitProcess(withExitCode: 42) 133 | try await withSpawnedExecution(config: config) { execution in 134 | let monitorResult = try await monitorProcessTermination( 135 | for: execution.processIdentifier 136 | ) 137 | 138 | #expect(monitorResult == .exited(42)) 139 | } 140 | } 141 | 142 | #if !os(Windows) 143 | @Test func testExitViaSignal() async throws { 144 | let config = Configuration( 145 | executable: .path("/usr/bin/tail"), 146 | arguments: ["-f", "/dev/null"] 147 | ) 148 | try await withSpawnedExecution(config: config) { execution in 149 | // Send signal to process 150 | try execution.send(signal: .terminate) 151 | 152 | let result = try await monitorProcessTermination( 153 | for: execution.processIdentifier 154 | ) 155 | #expect(result == .unhandledException(SIGTERM)) 156 | } 157 | } 158 | #endif 159 | } 160 | 161 | // MARK: - Edge Cases 162 | extension SubprocessProcessMonitoringTests { 163 | @Test func testAlreadyTerminatedProcess() async throws { 164 | let config = self.immediateExitProcess(withExitCode: 0) 165 | try await withSpawnedExecution(config: config) { execution in 166 | // Manually wait for the process to make sure it exits 167 | #if os(Windows) 168 | WaitForSingleObject( 169 | execution.processIdentifier.processDescriptor, 170 | INFINITE 171 | ) 172 | #else 173 | var siginfo = siginfo_t() 174 | waitid( 175 | P_PID, 176 | id_t(execution.processIdentifier.value), 177 | &siginfo, 178 | WEXITED | WNOWAIT 179 | ) 180 | #endif 181 | // Now make sure monitorProcessTermination() can still get the correct result 182 | let monitorResult = try await monitorProcessTermination( 183 | for: execution.processIdentifier 184 | ) 185 | #expect(monitorResult == .exited(0)) 186 | } 187 | } 188 | 189 | @Test func testCanMonitorLongRunningProcess() async throws { 190 | let config = self.longRunningProcess(withTimeOutSeconds: 1) 191 | try await withSpawnedExecution(config: config) { execution in 192 | let monitorResult = try await monitorProcessTermination( 193 | for: execution.processIdentifier 194 | ) 195 | 196 | #expect(monitorResult.isSuccess) 197 | } 198 | } 199 | 200 | @Test func testInvalidProcessIdentifier() async throws { 201 | #if os(Windows) 202 | let expectedError = SubprocessError( 203 | code: .init(.failedToMonitorProcess), 204 | underlyingError: .init(rawValue: DWORD(ERROR_INVALID_PARAMETER)) 205 | ) 206 | let processIdentifier = ProcessIdentifier( 207 | value: .max, processDescriptor: INVALID_HANDLE_VALUE, threadHandle: INVALID_HANDLE_VALUE 208 | ) 209 | #elseif os(Linux) || os(Android) || os(FreeBSD) 210 | let expectedError = SubprocessError( 211 | code: .init(.failedToMonitorProcess), 212 | underlyingError: .init(rawValue: ECHILD) 213 | ) 214 | let processIdentifier = ProcessIdentifier( 215 | value: .max, processDescriptor: -1 216 | ) 217 | #else 218 | let expectedError = SubprocessError( 219 | code: .init(.failedToMonitorProcess), 220 | underlyingError: .init(rawValue: ECHILD) 221 | ) 222 | let processIdentifier = ProcessIdentifier(value: .max) 223 | #endif 224 | await #expect(throws: expectedError) { 225 | _ = try await monitorProcessTermination(for: processIdentifier) 226 | } 227 | } 228 | 229 | @Test func testDoesNotReapUnrelatedChildProcess() async throws { 230 | // Make sure we don't reap child exit status that we didn't spawn 231 | let child1 = self.immediateExitProcess(withExitCode: 0) 232 | let child2 = self.immediateExitProcess(withExitCode: 0) 233 | try await withSpawnedExecution(config: child1) { child1Execution in 234 | try await withSpawnedExecution(config: child2) { child2Execution in 235 | // Monitor child2, but make sure we don't reap child1's status 236 | let status = try await monitorProcessTermination( 237 | for: child2Execution.processIdentifier 238 | ) 239 | #expect(status.isSuccess) 240 | // Make sure we can still fetch child 1 241 | #if os(Windows) 242 | let rc = WaitForSingleObject( 243 | child1Execution.processIdentifier.processDescriptor, 244 | INFINITE 245 | ) 246 | #expect(rc == WAIT_OBJECT_0) 247 | var child1Status: DWORD = 0 248 | let rc2 = GetExitCodeProcess( 249 | child1Execution.processIdentifier.processDescriptor, 250 | &child1Status 251 | ) 252 | #expect(rc2 == true) 253 | #expect(child1Status == 0) 254 | #else 255 | var siginfo = siginfo_t() 256 | let rc = waitid( 257 | P_PID, 258 | id_t(child1Execution.processIdentifier.value), 259 | &siginfo, 260 | WEXITED 261 | ) 262 | #expect(rc == 0) 263 | #expect(siginfo.si_code == CLD_EXITED) 264 | #expect(siginfo.si_status == 0) 265 | #endif 266 | } 267 | } 268 | } 269 | } 270 | 271 | // MARK: Concurrency Tests 272 | extension SubprocessProcessMonitoringTests { 273 | @Test func testCanMonitorProcessConcurrently() async throws { 274 | let testCount = 100 275 | try await withThrowingTaskGroup { group in 276 | for _ in 0.. Buffer? { 56 | // If we have more left in buffer, use that 57 | guard self.buffer.isEmpty else { 58 | return self.buffer.removeFirst() 59 | } 60 | // Read more data 61 | let data = try await AsyncIO.shared.read( 62 | from: self.diskIO, 63 | upTo: self.preferredBufferSize 64 | ) 65 | guard let data else { 66 | // We finished reading. Close the file descriptor now 67 | #if SUBPROCESS_ASYNCIO_DISPATCH 68 | try _safelyClose(.dispatchIO(self.diskIO)) 69 | #elseif canImport(WinSDK) 70 | try _safelyClose(.handle(self.diskIO)) 71 | #else 72 | try _safelyClose(.fileDescriptor(self.diskIO)) 73 | #endif 74 | return nil 75 | } 76 | let createdBuffers = Buffer.createFrom(data) 77 | // Most (all?) cases there should be only one buffer 78 | // because DispatchData are mostly contiguous 79 | if _fastPath(createdBuffers.count == 1) { 80 | // No need to push to the stack 81 | return createdBuffers[0] 82 | } 83 | self.buffer = createdBuffers 84 | return self.buffer.removeFirst() 85 | } 86 | } 87 | 88 | private let diskIO: DiskIO 89 | private let preferredBufferSize: Int? 90 | 91 | internal init(diskIO: DiskIO, preferredBufferSize: Int?) { 92 | self.diskIO = diskIO 93 | self.preferredBufferSize = preferredBufferSize 94 | } 95 | 96 | /// Creates a iterator for this asynchronous sequence. 97 | public func makeAsyncIterator() -> Iterator { 98 | return Iterator( 99 | diskIO: self.diskIO, 100 | preferredBufferSize: self.preferredBufferSize 101 | ) 102 | } 103 | 104 | /// Creates a line sequence to iterate through this `AsyncBufferSequence` line by line. 105 | public func lines() -> LineSequence { 106 | return LineSequence( 107 | underlying: self, 108 | encoding: UTF8.self, 109 | bufferingPolicy: .maxLineLength(128 * 1024) 110 | ) 111 | } 112 | 113 | /// Creates a line sequence to iterate through a `AsyncBufferSequence` line by line. 114 | /// - Parameters: 115 | /// - encoding: The taget encoding to encoding Strings to 116 | /// - bufferingPolicy: How should back-pressure be handled 117 | /// - Returns: A `LineSequence` to iterate though this `AsyncBufferSequence` line by line 118 | public func lines( 119 | encoding: Encoding.Type, 120 | bufferingPolicy: LineSequence.BufferingPolicy = .maxLineLength(128 * 1024) 121 | ) -> LineSequence { 122 | return LineSequence(underlying: self, encoding: encoding, bufferingPolicy: bufferingPolicy) 123 | } 124 | } 125 | 126 | // MARK: - LineSequence 127 | extension AsyncBufferSequence { 128 | /// Line sequence parses and splits an asynchronous sequence of buffers into lines. 129 | /// 130 | /// It is the preferred method to convert `Buffer` to `String` 131 | public struct LineSequence: AsyncSequence, Sendable { 132 | /// The element type for the asynchronous sequence. 133 | public typealias Element = String 134 | 135 | private let base: AsyncBufferSequence 136 | private let bufferingPolicy: BufferingPolicy 137 | 138 | /// The iterator for line sequence. 139 | public struct AsyncIterator: AsyncIteratorProtocol { 140 | /// The element type for this Iterator. 141 | public typealias Element = String 142 | 143 | private var source: AsyncBufferSequence.AsyncIterator 144 | private var buffer: [Encoding.CodeUnit] 145 | private var underlyingBuffer: [Encoding.CodeUnit] 146 | private var underlyingBufferIndex: Array.Index 147 | private var leftover: Encoding.CodeUnit? 148 | private var eofReached: Bool 149 | private let bufferingPolicy: BufferingPolicy 150 | 151 | internal init( 152 | underlyingIterator: AsyncBufferSequence.AsyncIterator, 153 | bufferingPolicy: BufferingPolicy 154 | ) { 155 | self.source = underlyingIterator 156 | self.buffer = [] 157 | self.underlyingBuffer = [] 158 | self.underlyingBufferIndex = self.underlyingBuffer.startIndex 159 | self.leftover = nil 160 | self.eofReached = false 161 | self.bufferingPolicy = bufferingPolicy 162 | } 163 | 164 | /// Retrieves the next line, or returns nil if the sequence ends. 165 | public mutating func next() async throws -> String? { 166 | 167 | func loadBuffer() async throws -> [Encoding.CodeUnit]? { 168 | guard !self.eofReached else { 169 | return nil 170 | } 171 | 172 | guard let buffer = try await self.source.next() else { 173 | self.eofReached = true 174 | return nil 175 | } 176 | #if SUBPROCESS_ASYNCIO_DISPATCH 177 | // Unfortunately here we _have to_ copy the bytes out because 178 | // DispatchIO (rightfully) reuses buffer, which means `buffer.data` 179 | // has the same address on all iterations, therefore we can't directly 180 | // create the result array from buffer.data 181 | 182 | // Calculate how many CodePoint elements we have 183 | let elementCount = buffer.data.count / MemoryLayout.stride 184 | 185 | // Create array by copying from the buffer reinterpreted as CodePoint 186 | let result: Array = buffer.data.withUnsafeBytes { ptr -> Array in 187 | return Array( 188 | UnsafeBufferPointer(start: ptr.baseAddress?.assumingMemoryBound(to: Encoding.CodeUnit.self), count: elementCount) 189 | ) 190 | } 191 | #else 192 | // Cast data to CodeUnit type 193 | let result = buffer.withUnsafeBytes { ptr in 194 | return ptr.withMemoryRebound(to: Encoding.CodeUnit.self) { codeUnitPtr in 195 | return Array(codeUnitPtr) 196 | } 197 | } 198 | #endif 199 | return result.isEmpty ? nil : result 200 | } 201 | 202 | func yield() -> String? { 203 | defer { 204 | self.buffer.removeAll(keepingCapacity: true) 205 | } 206 | if self.buffer.isEmpty { 207 | return nil 208 | } 209 | return String(decoding: self.buffer, as: Encoding.self) 210 | } 211 | 212 | func nextFromSource() async throws -> Encoding.CodeUnit? { 213 | if underlyingBufferIndex >= underlyingBuffer.count { 214 | guard let buf = try await loadBuffer() else { 215 | return nil 216 | } 217 | underlyingBuffer = buf 218 | underlyingBufferIndex = buf.startIndex 219 | } 220 | let result = underlyingBuffer[underlyingBufferIndex] 221 | underlyingBufferIndex = underlyingBufferIndex.advanced(by: 1) 222 | return result 223 | } 224 | 225 | func nextCodeUnit() async throws -> Encoding.CodeUnit? { 226 | defer { leftover = nil } 227 | if let leftover = leftover { 228 | return leftover 229 | } 230 | return try await nextFromSource() 231 | } 232 | 233 | // https://en.wikipedia.org/wiki/Newline#Unicode 234 | let lineFeed = Encoding.CodeUnit(0x0A) 235 | /// let verticalTab = Encoding.CodeUnit(0x0B) 236 | /// let formFeed = Encoding.CodeUnit(0x0C) 237 | let carriageReturn = Encoding.CodeUnit(0x0D) 238 | // carriageReturn + lineFeed 239 | let newLine1: Encoding.CodeUnit 240 | let newLine2: Encoding.CodeUnit 241 | let lineSeparator1: Encoding.CodeUnit 242 | let lineSeparator2: Encoding.CodeUnit 243 | let lineSeparator3: Encoding.CodeUnit 244 | let paragraphSeparator1: Encoding.CodeUnit 245 | let paragraphSeparator2: Encoding.CodeUnit 246 | let paragraphSeparator3: Encoding.CodeUnit 247 | switch Encoding.CodeUnit.self { 248 | case is UInt8.Type: 249 | newLine1 = Encoding.CodeUnit(0xC2) 250 | newLine2 = Encoding.CodeUnit(0x85) 251 | 252 | lineSeparator1 = Encoding.CodeUnit(0xE2) 253 | lineSeparator2 = Encoding.CodeUnit(0x80) 254 | lineSeparator3 = Encoding.CodeUnit(0xA8) 255 | 256 | paragraphSeparator1 = Encoding.CodeUnit(0xE2) 257 | paragraphSeparator2 = Encoding.CodeUnit(0x80) 258 | paragraphSeparator3 = Encoding.CodeUnit(0xA9) 259 | case is UInt16.Type, is UInt32.Type: 260 | // UTF16 and UTF32 use one byte for all 261 | newLine1 = Encoding.CodeUnit(0x0085) 262 | newLine2 = Encoding.CodeUnit(0x0085) 263 | 264 | lineSeparator1 = Encoding.CodeUnit(0x2028) 265 | lineSeparator2 = Encoding.CodeUnit(0x2028) 266 | lineSeparator3 = Encoding.CodeUnit(0x2028) 267 | 268 | paragraphSeparator1 = Encoding.CodeUnit(0x2029) 269 | paragraphSeparator2 = Encoding.CodeUnit(0x2029) 270 | paragraphSeparator3 = Encoding.CodeUnit(0x2029) 271 | default: 272 | fatalError("Unknown encoding type \(Encoding.self)") 273 | } 274 | 275 | while let first = try await nextCodeUnit() { 276 | // Throw if we exceed max line length 277 | if case .maxLineLength(let maxLength) = self.bufferingPolicy, buffer.count >= maxLength { 278 | throw SubprocessError( 279 | code: .init(.streamOutputExceedsLimit(maxLength)), 280 | underlyingError: nil 281 | ) 282 | } 283 | 284 | buffer.append(first) 285 | switch first { 286 | case carriageReturn: 287 | // Swallow up any subsequent LF 288 | guard let next = try await nextFromSource() else { 289 | return yield() // if we ran out of bytes, the last byte was a CR 290 | } 291 | buffer.append(next) 292 | guard next == lineFeed else { 293 | // if the next character was not an LF, save it for the next iteration and still return a line 294 | leftover = buffer.removeLast() 295 | return yield() 296 | } 297 | return yield() 298 | case newLine1 where Encoding.CodeUnit.self is UInt8.Type: // this may be used to compose other UTF8 characters 299 | guard let next = try await nextFromSource() else { 300 | // technically invalid UTF8 but it should be repaired to "\u{FFFD}" 301 | return yield() 302 | } 303 | buffer.append(next) 304 | guard next == newLine2 else { 305 | continue 306 | } 307 | return yield() 308 | case lineSeparator1 where Encoding.CodeUnit.self is UInt8.Type, 309 | paragraphSeparator1 where Encoding.CodeUnit.self is UInt8.Type: 310 | // Try to read: 80 [A8 | A9]. 311 | // If we can't, then we put the byte in the buffer for error correction 312 | guard let next = try await nextFromSource() else { 313 | return yield() 314 | } 315 | buffer.append(next) 316 | guard next == lineSeparator2 || next == paragraphSeparator2 else { 317 | continue 318 | } 319 | guard let fin = try await nextFromSource() else { 320 | return yield() 321 | } 322 | buffer.append(fin) 323 | guard fin == lineSeparator3 || fin == paragraphSeparator3 else { 324 | continue 325 | } 326 | return yield() 327 | case lineFeed.. AsyncIterator { 344 | return AsyncIterator( 345 | underlyingIterator: self.base.makeAsyncIterator(), 346 | bufferingPolicy: self.bufferingPolicy 347 | ) 348 | } 349 | 350 | internal init( 351 | underlying: AsyncBufferSequence, 352 | encoding: Encoding.Type, 353 | bufferingPolicy: BufferingPolicy 354 | ) { 355 | self.base = underlying 356 | self.bufferingPolicy = bufferingPolicy 357 | } 358 | } 359 | } 360 | 361 | extension AsyncBufferSequence.LineSequence { 362 | /// A strategy that handles the exhaustion of a buffer’s capacity. 363 | public enum BufferingPolicy: Sendable { 364 | /// Continue to add to the buffer, without imposing a limit 365 | /// on the number of buffered elements (line length). 366 | case unbounded 367 | /// Impose a max buffer size (line length) limit. 368 | /// Subprocess **will throw an error** if the number of buffered 369 | /// elements (line length) exceeds the limit 370 | case maxLineLength(Int) 371 | } 372 | } 373 | 374 | // MARK: - Page Size 375 | import _SubprocessCShims 376 | 377 | #if canImport(Darwin) 378 | import Darwin 379 | internal import MachO.dyld 380 | 381 | private let _pageSize: Int = { 382 | Int(_subprocess_vm_size()) 383 | }() 384 | #elseif canImport(WinSDK) 385 | @preconcurrency import WinSDK 386 | private let _pageSize: Int = { 387 | var sysInfo: SYSTEM_INFO = SYSTEM_INFO() 388 | GetSystemInfo(&sysInfo) 389 | return Int(sysInfo.dwPageSize) 390 | }() 391 | #elseif os(WASI) 392 | // WebAssembly defines a fixed page size 393 | private let _pageSize: Int = 65_536 394 | #elseif canImport(Android) 395 | @preconcurrency import Android 396 | private let _pageSize: Int = Int(getpagesize()) 397 | #elseif canImport(Glibc) 398 | @preconcurrency import Glibc 399 | private let _pageSize: Int = Int(getpagesize()) 400 | #elseif canImport(Musl) 401 | @preconcurrency import Musl 402 | private let _pageSize: Int = Int(getpagesize()) 403 | #elseif canImport(C) 404 | private let _pageSize: Int = Int(getpagesize()) 405 | #endif // canImport(Darwin) 406 | 407 | @inline(__always) 408 | internal var readBufferSize: Int { 409 | return _pageSize 410 | } 411 | -------------------------------------------------------------------------------- /Sources/Subprocess/IO/Output.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | #if canImport(System) 13 | @preconcurrency import System 14 | #else 15 | @preconcurrency import SystemPackage 16 | #endif 17 | 18 | #if canImport(WinSDK) 19 | @preconcurrency import WinSDK 20 | #endif 21 | 22 | internal import Dispatch 23 | 24 | // MARK: - Output 25 | 26 | /// Output protocol specifies the set of methods that a type must implement to 27 | /// serve as the output target for a subprocess. 28 | /// 29 | /// Instead of developing custom implementations of `OutputProtocol`, use the 30 | /// default implementations provided by the `Subprocess` library to specify the 31 | /// output handling requirements. 32 | public protocol OutputProtocol: Sendable, ~Copyable { 33 | associatedtype OutputType: Sendable 34 | 35 | #if SubprocessSpan 36 | /// Convert the output from span to expected output type 37 | func output(from span: RawSpan) throws -> OutputType 38 | #endif 39 | 40 | /// Convert the output from buffer to expected output type 41 | func output(from buffer: some Sequence) throws -> OutputType 42 | 43 | /// The max amount of data to collect for this output. 44 | var maxSize: Int { get } 45 | } 46 | 47 | extension OutputProtocol { 48 | /// The max amount of data to collect for this output. 49 | public var maxSize: Int { 128 * 1024 } 50 | } 51 | 52 | /// A concrete output type for subprocesses that indicates that the 53 | /// subprocess should not collect or redirect output from the child 54 | /// process. 55 | /// 56 | /// On Unix-like systems, `DiscardedOutput` redirects the 57 | /// standard output of the subprocess to `/dev/null`, while on Windows, 58 | /// redirects the output to `NUL`. 59 | public struct DiscardedOutput: OutputProtocol, ErrorOutputProtocol { 60 | /// The type for the output. 61 | public typealias OutputType = Void 62 | 63 | internal func createPipe() throws -> CreatedPipe { 64 | 65 | #if os(Windows) 66 | let devnullFd: FileDescriptor = try .openDevNull(withAccessMode: .writeOnly) 67 | let devnull = HANDLE(bitPattern: _get_osfhandle(devnullFd.rawValue))! 68 | #else 69 | let devnull: FileDescriptor = try .openDevNull(withAccessMode: .writeOnly) 70 | #endif 71 | return CreatedPipe( 72 | readFileDescriptor: nil, 73 | writeFileDescriptor: .init(devnull, closeWhenDone: true) 74 | ) 75 | } 76 | 77 | internal init() {} 78 | } 79 | 80 | /// A concrete output type for subprocesses that writes output 81 | /// to a specified file descriptor. 82 | /// 83 | /// Developers have the option to instruct the `Subprocess` to automatically 84 | /// close the related `FileDescriptor` after the subprocess is spawned. 85 | public struct FileDescriptorOutput: OutputProtocol, ErrorOutputProtocol { 86 | /// The type for this output. 87 | public typealias OutputType = Void 88 | 89 | private let closeAfterSpawningProcess: Bool 90 | private let fileDescriptor: FileDescriptor 91 | 92 | internal func createPipe() throws -> CreatedPipe { 93 | #if canImport(WinSDK) 94 | let writeFd = HANDLE(bitPattern: _get_osfhandle(self.fileDescriptor.rawValue))! 95 | #else 96 | let writeFd = self.fileDescriptor 97 | #endif 98 | return CreatedPipe( 99 | readFileDescriptor: nil, 100 | writeFileDescriptor: .init( 101 | writeFd, 102 | closeWhenDone: self.closeAfterSpawningProcess 103 | ) 104 | ) 105 | } 106 | 107 | internal init( 108 | fileDescriptor: FileDescriptor, 109 | closeAfterSpawningProcess: Bool 110 | ) { 111 | self.fileDescriptor = fileDescriptor 112 | self.closeAfterSpawningProcess = closeAfterSpawningProcess 113 | } 114 | } 115 | 116 | /// A concrete `Output` type for subprocesses that collects output 117 | /// from the subprocess as `String` with the given encoding. 118 | public struct StringOutput: OutputProtocol, ErrorOutputProtocol { 119 | /// The type for this output. 120 | public typealias OutputType = String? 121 | /// The max number of bytes to collect. 122 | public let maxSize: Int 123 | 124 | #if SubprocessSpan 125 | /// Create a string from a raw span. 126 | public func output(from span: RawSpan) throws -> String? { 127 | // FIXME: Span to String 128 | var array: [UInt8] = [] 129 | for index in 0..) throws -> String? { 138 | // FIXME: Span to String 139 | let array = Array(buffer) 140 | return String(decodingBytes: array, as: Encoding.self) 141 | } 142 | 143 | internal init(limit: Int, encoding: Encoding.Type) { 144 | self.maxSize = limit 145 | } 146 | } 147 | 148 | /// A concrete `Output` type for subprocesses that collects output from 149 | /// the subprocess as `[UInt8]`. 150 | public struct BytesOutput: OutputProtocol, ErrorOutputProtocol { 151 | /// The output type for this output option 152 | public typealias OutputType = [UInt8] 153 | /// The max number of bytes to collect 154 | public let maxSize: Int 155 | 156 | internal func captureOutput( 157 | from diskIO: consuming IOChannel 158 | ) async throws -> [UInt8] { 159 | #if SUBPROCESS_ASYNCIO_DISPATCH 160 | var result: DispatchData? = nil 161 | #else 162 | var result: [UInt8]? = nil 163 | #endif 164 | do { 165 | var maxLength = self.maxSize 166 | if maxLength != .max { 167 | // If we actually have a max length, attempt to read one 168 | // more byte to determine whether output exceeds the limit 169 | maxLength += 1 170 | } 171 | result = try await AsyncIO.shared.read(from: diskIO, upTo: maxLength) 172 | } catch { 173 | try diskIO.safelyClose() 174 | throw error 175 | } 176 | try diskIO.safelyClose() 177 | 178 | if let result, result.count > self.maxSize { 179 | throw SubprocessError( 180 | code: .init(.outputBufferLimitExceeded(self.maxSize)), 181 | underlyingError: nil 182 | ) 183 | } 184 | #if SUBPROCESS_ASYNCIO_DISPATCH 185 | return result?.array() ?? [] 186 | #else 187 | return result ?? [] 188 | #endif 189 | } 190 | 191 | #if SubprocessSpan 192 | /// Create an Array from `RawSpawn`. 193 | /// Not implemented 194 | public func output(from span: RawSpan) throws -> [UInt8] { 195 | fatalError("Not implemented") 196 | } 197 | #endif 198 | /// Create an Array from `Sequence`. 199 | /// Not implemented 200 | public func output(from buffer: some Sequence) throws -> [UInt8] { 201 | fatalError("Not implemented") 202 | } 203 | 204 | internal init(limit: Int) { 205 | self.maxSize = limit 206 | } 207 | } 208 | 209 | /// A concrete `Output` type for subprocesses that redirects the child output to 210 | /// the `.standardOutput` (a sequence) or `.standardError` property of 211 | /// `Execution`. This output type is only applicable to the `run()` family that 212 | /// takes a custom closure. 213 | internal struct SequenceOutput: OutputProtocol { 214 | /// The output type for this output option 215 | public typealias OutputType = Void 216 | 217 | internal init() {} 218 | } 219 | 220 | extension OutputProtocol where Self == DiscardedOutput { 221 | /// Create a Subprocess output that discards the output 222 | public static var discarded: Self { .init() } 223 | } 224 | 225 | extension OutputProtocol where Self == FileDescriptorOutput { 226 | /// Create a Subprocess output that writes output to a `FileDescriptor` 227 | /// and optionally close the `FileDescriptor` once process spawned. 228 | public static func fileDescriptor( 229 | _ fd: FileDescriptor, 230 | closeAfterSpawningProcess: Bool 231 | ) -> Self { 232 | return .init(fileDescriptor: fd, closeAfterSpawningProcess: closeAfterSpawningProcess) 233 | } 234 | 235 | /// Create a Subprocess output that writes output to the standard output of 236 | /// current process. 237 | /// 238 | /// The file descriptor isn't closed afterwards. 239 | public static var standardOutput: Self { 240 | return Self.fileDescriptor( 241 | .standardOutput, 242 | closeAfterSpawningProcess: false 243 | ) 244 | } 245 | 246 | /// Create a Subprocess output that write output to the standard error of 247 | /// current process. 248 | /// 249 | /// The file descriptor isn't closed afterwards. 250 | public static var standardError: Self { 251 | return Self.fileDescriptor( 252 | .standardError, 253 | closeAfterSpawningProcess: false 254 | ) 255 | } 256 | } 257 | 258 | extension OutputProtocol where Self == StringOutput { 259 | /// Create a `Subprocess` output that collects output as UTF8 String 260 | /// with a buffer limit in bytes. Subprocess throws an error if the 261 | /// child process emits more bytes than the limit. 262 | public static func string(limit: Int) -> Self { 263 | return .init(limit: limit, encoding: UTF8.self) 264 | } 265 | } 266 | 267 | extension OutputProtocol { 268 | /// Create a `Subprocess` output that collects output as 269 | /// `String` using the given encoding up to limit in bytes. 270 | /// Subprocess throws an error if the child process emits 271 | /// more bytes than the limit. 272 | public static func string( 273 | limit: Int, 274 | encoding: Encoding.Type 275 | ) -> Self where Self == StringOutput { 276 | return .init(limit: limit, encoding: encoding) 277 | } 278 | } 279 | 280 | extension OutputProtocol where Self == BytesOutput { 281 | /// Create a `Subprocess` output that collects output as 282 | /// `Buffer` with a buffer limit in bytes. Subprocess throws 283 | /// an error if the child process emits more bytes than the limit. 284 | public static func bytes(limit: Int) -> Self { 285 | return .init(limit: limit) 286 | } 287 | } 288 | 289 | // MARK: - ErrorOutputProtocol 290 | 291 | /// Error output protocol specifies the set of methods that a type must implement to 292 | /// serve as the error output target for a subprocess. 293 | /// 294 | /// Instead of developing custom implementations of `ErrorOutputProtocol`, use the 295 | /// default implementations provided by the `Subprocess` library to specify the 296 | /// output handling requirements. 297 | public protocol ErrorOutputProtocol: OutputProtocol {} 298 | 299 | /// A concrete error output type for subprocesses that combines the standard error 300 | /// output with the standard output stream. 301 | /// 302 | /// When `CombinedErrorOutput` is used as the error output for a subprocess, both 303 | /// standard output and standard error from the child process are merged into a 304 | /// single output stream. This is equivalent to using shell redirection like `2>&1`. 305 | /// 306 | /// This output type is useful when you want to capture or redirect both output 307 | /// streams together, making it possible to process all subprocess output as a unified 308 | /// stream rather than handling standard output and standard error separately. 309 | public struct CombinedErrorOutput: ErrorOutputProtocol { 310 | public typealias OutputType = Void 311 | } 312 | 313 | extension ErrorOutputProtocol { 314 | internal func createPipe(from outputPipe: borrowing CreatedPipe) throws -> CreatedPipe { 315 | if self is CombinedErrorOutput { 316 | return try CreatedPipe(duplicating: outputPipe) 317 | } 318 | return try createPipe() 319 | } 320 | } 321 | 322 | extension ErrorOutputProtocol where Self == CombinedErrorOutput { 323 | /// Creates an error output that combines standard error with standard output. 324 | /// 325 | /// When using `combineWithOutput`, both standard output and standard error from 326 | /// the child process are merged into a single output stream. This is equivalent 327 | /// to using shell redirection like `2>&1`. 328 | /// 329 | /// This is useful when you want to capture or redirect both output streams 330 | /// together, making it possible to process all subprocess output as a unified 331 | /// stream rather than handling standard output and standard error separately 332 | /// 333 | /// - Returns: A `CombinedErrorOutput` instance that merges standard error 334 | /// with standard output. 335 | public static var combineWithOutput: Self { 336 | return CombinedErrorOutput() 337 | } 338 | } 339 | 340 | // MARK: - Span Default Implementations 341 | #if SubprocessSpan 342 | extension OutputProtocol { 343 | /// Create an Array from `Sequence`. 344 | public func output(from buffer: some Sequence) throws -> OutputType { 345 | guard let rawBytes: UnsafeRawBufferPointer = buffer as? UnsafeRawBufferPointer else { 346 | fatalError("Unexpected input type passed: \(type(of: buffer))") 347 | } 348 | let span = RawSpan(_unsafeBytes: rawBytes) 349 | return try self.output(from: span) 350 | } 351 | } 352 | #endif 353 | 354 | // MARK: - Default Implementations 355 | extension OutputProtocol { 356 | @_disfavoredOverload 357 | internal func createPipe() throws -> CreatedPipe { 358 | if let discard = self as? DiscardedOutput { 359 | return try discard.createPipe() 360 | } else if let fdOutput = self as? FileDescriptorOutput { 361 | return try fdOutput.createPipe() 362 | } 363 | // Base pipe based implementation for everything else 364 | return try CreatedPipe(closeWhenDone: true, purpose: .output) 365 | } 366 | 367 | /// Capture the output from the subprocess up to maxSize 368 | @_disfavoredOverload 369 | internal func captureOutput( 370 | from diskIO: consuming IOChannel? 371 | ) async throws -> OutputType { 372 | if OutputType.self == Void.self { 373 | try diskIO?.safelyClose() 374 | return () as! OutputType 375 | } 376 | // `diskIO` is only `nil` for any types that conform to `OutputProtocol` 377 | // and have `Void` as ``OutputType` (i.e. `DiscardedOutput`). Since we 378 | // made sure `OutputType` is not `Void` on the line above, `diskIO` 379 | // must not be nil; otherwise, this is a programmer error. 380 | guard var diskIO else { 381 | fatalError( 382 | "Internal Inconsistency Error: diskIO must not be nil when OutputType is not Void" 383 | ) 384 | } 385 | 386 | if let bytesOutput = self as? BytesOutput { 387 | return try await bytesOutput.captureOutput(from: diskIO) as! Self.OutputType 388 | } 389 | 390 | #if SUBPROCESS_ASYNCIO_DISPATCH 391 | var result: DispatchData? = nil 392 | #else 393 | var result: [UInt8]? = nil 394 | #endif 395 | do { 396 | var maxLength = self.maxSize 397 | if maxLength != .max { 398 | // If we actually have a max length, attempt to read one 399 | // more byte to determine whether output exceeds the limit 400 | maxLength += 1 401 | } 402 | result = try await AsyncIO.shared.read(from: diskIO, upTo: maxLength) 403 | } catch { 404 | try diskIO.safelyClose() 405 | throw error 406 | } 407 | 408 | try diskIO.safelyClose() 409 | if let result, result.count > self.maxSize { 410 | throw SubprocessError( 411 | code: .init(.outputBufferLimitExceeded(self.maxSize)), 412 | underlyingError: nil 413 | ) 414 | } 415 | 416 | #if SUBPROCESS_ASYNCIO_DISPATCH 417 | return try self.output(from: result ?? .empty) 418 | #else 419 | return try self.output(from: result ?? []) 420 | #endif 421 | } 422 | } 423 | 424 | extension OutputProtocol where OutputType == Void { 425 | internal func captureOutput(from fileDescriptor: consuming IOChannel?) async throws {} 426 | 427 | #if SubprocessSpan 428 | /// Convert the output from raw span to expected output type 429 | public func output(from span: RawSpan) throws { 430 | fatalError("Unexpected call to \(#function)") 431 | } 432 | #endif 433 | /// Convert the output from a sequence of 8-bit unsigned integers to expected output type. 434 | public func output(from buffer: some Sequence) throws { 435 | fatalError("Unexpected call to \(#function)") 436 | } 437 | } 438 | 439 | #if SubprocessSpan 440 | extension OutputProtocol { 441 | #if SUBPROCESS_ASYNCIO_DISPATCH 442 | internal func output(from data: DispatchData) throws -> OutputType { 443 | guard !data.isEmpty else { 444 | let empty = UnsafeRawBufferPointer(start: nil, count: 0) 445 | let span = RawSpan(_unsafeBytes: empty) 446 | return try self.output(from: span) 447 | } 448 | 449 | return try data.withUnsafeBytes { ptr in 450 | let bufferPtr = UnsafeRawBufferPointer(start: ptr, count: data.count) 451 | let span = RawSpan(_unsafeBytes: bufferPtr) 452 | return try self.output(from: span) 453 | } 454 | } 455 | #else 456 | internal func output(from data: [UInt8]) throws -> OutputType { 457 | guard !data.isEmpty else { 458 | let empty = UnsafeRawBufferPointer(start: nil, count: 0) 459 | let span = RawSpan(_unsafeBytes: empty) 460 | return try self.output(from: span) 461 | } 462 | 463 | return try data.withUnsafeBufferPointer { ptr in 464 | let span = RawSpan(_unsafeBytes: UnsafeRawBufferPointer(ptr)) 465 | return try self.output(from: span) 466 | } 467 | } 468 | #endif // SUBPROCESS_ASYNCIO_DISPATCH 469 | } 470 | #endif 471 | 472 | extension DispatchData { 473 | internal func array() -> [UInt8] { 474 | var result: [UInt8]? 475 | self.enumerateBytes { buffer, byteIndex, stop in 476 | let currentChunk = Array(UnsafeRawBufferPointer(buffer)) 477 | if result == nil { 478 | result = currentChunk 479 | } else { 480 | result?.append(contentsOf: currentChunk) 481 | } 482 | } 483 | return result ?? [] 484 | } 485 | } 486 | 487 | extension FileDescriptor { 488 | internal static func openDevNull( 489 | withAccessMode mode: FileDescriptor.AccessMode 490 | ) throws -> FileDescriptor { 491 | #if os(Windows) 492 | let devnull: FileDescriptor = try .open("NUL", mode) 493 | #else 494 | let devnull: FileDescriptor = try .open("/dev/null", mode) 495 | #endif 496 | return devnull 497 | } 498 | } 499 | --------------------------------------------------------------------------------