├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── pull_request.yml ├── .gitignore ├── .license_header_template ├── .licenseignore ├── .spi.yml ├── .swift-version ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Subprocess │ ├── API.swift │ ├── AsyncBufferSequence.swift │ ├── Buffer.swift │ ├── Configuration.swift │ ├── Error.swift │ ├── Execution.swift │ ├── IO │ │ ├── Input.swift │ │ └── Output.swift │ ├── Platforms │ │ ├── Subprocess+Darwin.swift │ │ ├── Subprocess+Linux.swift │ │ ├── Subprocess+Unix.swift │ │ └── Subprocess+Windows.swift │ ├── Result.swift │ ├── Span+Subprocess.swift │ ├── SubprocessFoundation │ │ ├── Input+Foundation.swift │ │ ├── Output+Foundation.swift │ │ └── Span+SubprocessFoundation.swift │ └── Teardown.swift └── _SubprocessCShims │ ├── include │ ├── process_shims.h │ └── target_conditionals.h │ └── process_shims.c └── Tests ├── SubprocessTests ├── SubprocessTests+Darwin.swift ├── SubprocessTests+Linting.swift ├── SubprocessTests+Linux.swift ├── SubprocessTests+Unix.swift ├── SubprocessTests+Windows.swift └── TestSupport.swift └── TestResources ├── Resources ├── TheMysteriousIsland.txt ├── getgroups.swift └── windows-tester.ps1 └── TestResources.swift /.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 | -------------------------------------------------------------------------------- /.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/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull request 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | 7 | jobs: 8 | tests: 9 | name: Test 10 | uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main 11 | with: 12 | linux_os_versions: '["noble", "jammy", "focal", "rhel-ubi9"]' 13 | linux_swift_versions: '["6.1", "nightly-main"]' 14 | linux_build_command: 'swift build' 15 | windows_swift_versions: '["6.1", "nightly-main"]' 16 | windows_build_command: 'swift build' 17 | enable_macos_checks: true 18 | macos_xcode_versions: '["16.3"]' 19 | 20 | soundness: 21 | name: Soundness 22 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main 23 | with: 24 | license_header_check_project_name: "Swift.org" 25 | docs_check_enabled: false 26 | format_check_enabled: false 27 | unacceptable_language_check_enabled: false 28 | api_breakage_check_enabled: false 29 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | - documentation_targets: [Subprocess] 5 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 6.1.0 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | let availabilityMacro: SwiftSetting = .enableExperimentalFeature( 7 | "AvailabilityMacro=SubprocessSpan: macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, visionOS 9999", 8 | ) 9 | 10 | var dep: [Package.Dependency] = [ 11 | .package( 12 | url: "https://github.com/apple/swift-system", 13 | from: "1.4.2" 14 | ) 15 | ] 16 | #if !os(Windows) 17 | dep.append( 18 | .package( 19 | url: "https://github.com/apple/swift-docc-plugin", 20 | from: "1.4.3" 21 | ), 22 | ) 23 | #endif 24 | 25 | // Enable SubprocessFoundation by default 26 | var defaultTraits: Set = ["SubprocessFoundation"] 27 | #if compiler(>=6.2) 28 | // Enable SubprocessSpan when Span is available 29 | defaultTraits.insert("SubprocessSpan") 30 | #endif 31 | 32 | let package = Package( 33 | name: "Subprocess", 34 | platforms: [.macOS(.v13), .iOS("99.0")], 35 | products: [ 36 | .library( 37 | name: "Subprocess", 38 | targets: ["Subprocess"] 39 | ) 40 | ], 41 | traits: [ 42 | "SubprocessFoundation", 43 | "SubprocessSpan", 44 | .default( 45 | enabledTraits: defaultTraits 46 | ), 47 | ], 48 | dependencies: dep, 49 | targets: [ 50 | .target( 51 | name: "Subprocess", 52 | dependencies: [ 53 | "_SubprocessCShims", 54 | .product(name: "SystemPackage", package: "swift-system"), 55 | ], 56 | path: "Sources/Subprocess", 57 | swiftSettings: [ 58 | .enableExperimentalFeature("StrictConcurrency"), 59 | .enableExperimentalFeature("NonescapableTypes"), 60 | .enableExperimentalFeature("LifetimeDependence"), 61 | .enableExperimentalFeature("Span"), 62 | availabilityMacro, 63 | ] 64 | ), 65 | .testTarget( 66 | name: "SubprocessTests", 67 | dependencies: [ 68 | "_SubprocessCShims", 69 | "Subprocess", 70 | "TestResources", 71 | .product(name: "SystemPackage", package: "swift-system"), 72 | ], 73 | swiftSettings: [ 74 | .enableExperimentalFeature("Span"), 75 | availabilityMacro, 76 | ] 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 | ), 94 | ] 95 | ) 96 | -------------------------------------------------------------------------------- /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, adding 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’ 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 minimal 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 Asynchonously 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")) 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 | let result = try await run( 68 | .path("/bin/dd"), 69 | arguments: ["if=/path/to/document"] 70 | ) { execution in 71 | var contents = "" 72 | for try await chunk in execution.standardOutput { 73 | let string = chunk.withUnsafeBytes { String(decoding: $0, as: UTF8.self) } 74 | contents += string 75 | if string == "Done" { 76 | // Stop execution 77 | await execution.teardown( 78 | using: [ 79 | .gracefulShutDown( 80 | allowedDurationToNextStep: .seconds(0.5) 81 | ) 82 | ] 83 | ) 84 | return contents 85 | } 86 | } 87 | return contents 88 | } 89 | ``` 90 | 91 | ### Running Unmonitored Processes 92 | 93 | While `Subprocess` is designed with Swift’s structural concurrency in mind, it also provides a lower level, synchronous method for launching child processes. However, since `Subprocess` can’t synchronously monitor child process’s state or handle cleanup, you’ll need to attach a FileDescriptor to each I/O directly. Remember to close the `FileDescriptor` once you’re finished. 94 | 95 | ```swift 96 | import Subprocess 97 | 98 | let input: FileDescriptor = ... 99 | 100 | input.closeAfter { 101 | let pid = try runDetached(.path("/bin/daemon"), input: input) 102 | // ... other opeartions 103 | } 104 | ``` 105 | 106 | ### Customizable Execution 107 | 108 | You can set various parameters when running the child process, such as `Arguments`, `Environment`, and working directory: 109 | 110 | ```swift 111 | import Subprocess 112 | 113 | let result = try await run( 114 | .path("/bin/ls"), 115 | arguments: ["-a"], 116 | // Inherit the environment values from parent process and 117 | // add `NewKey=NewValue` 118 | environment: .inherit.updating(["NewKey": "NewValue"]), 119 | workingDirectory: "/Users/", 120 | ) 121 | ``` 122 | 123 | ### Platform Specific Options and “Escape Hatches” 124 | 125 | `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. 126 | 127 | `PlatformOptions` also allows access to platform-specific spawning constructs and customizations via a closure. 128 | 129 | ```swift 130 | import Darwin 131 | import Subprocess 132 | 133 | var platformOptions = PlatformOptions() 134 | let intendedWorkingDir = "/path/to/directory" 135 | platformOptions.preSpawnProcessConfigurator = { spawnAttr, fileAttr in 136 | // Set POSIX_SPAWN_SETSID flag, which implies calls 137 | // to setsid 138 | var flags: Int16 = 0 139 | posix_spawnattr_getflags(&spawnAttr, &flags) 140 | posix_spawnattr_setflags(&spawnAttr, flags | Int16(POSIX_SPAWN_SETSID)) 141 | 142 | // Change the working directory 143 | intendedWorkingDir.withCString { path in 144 | _ = posix_spawn_file_actions_addchdir_np(&fileAttr, path) 145 | } 146 | } 147 | 148 | let result = try await run(.path("/bin/exe"), platformOptions: platformOptions) 149 | ``` 150 | 151 | 152 | ### Flexible Input and Output Configurations 153 | 154 | By default, `Subprocess`: 155 | - Doesn’t send any input to the child process’s standard input 156 | - Captures the child process’s standard output as a `String`, up to 128kB 157 | - Ignores the child process’s standard error 158 | 159 | You can tailor how `Subprocess` handles the standard input, standard output, and standard error by setting the `input`, `output`, and `error` parameters: 160 | 161 | ```swift 162 | let content = "Hello Subprocess" 163 | 164 | // Send "Hello Subprocess" to the standard input of `cat` 165 | let result = try await run(.name("cat"), input: .string(content, using: UTF8.self)) 166 | 167 | // Collect both standard error and standard output as Data 168 | let result = try await run(.name("cat"), output: .data, error: .data) 169 | ``` 170 | 171 | `Subprocess` supports these input options: 172 | 173 | #### `NoInput` 174 | 175 | This option means no input is sent to the subprocess. 176 | 177 | Use it by setting `.none` for `input`. 178 | 179 | #### `FileDescriptorInput` 180 | 181 | 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. 182 | 183 | Use it by setting `.fileDescriptor(closeAfterSpawningProcess:)` for `input`. 184 | 185 | #### `StringInput` 186 | 187 | This option reads input from a type conforming to `StringProtocol` using the specified encoding. 188 | 189 | Use it by setting `.string(using:)` for `input`. 190 | 191 | #### `ArrayInput` 192 | 193 | This option reads input from an array of `UInt8`. 194 | 195 | Use it by setting `.array` for `input`. 196 | 197 | #### `DataInput` (available with `SubprocessFoundation` trait) 198 | 199 | This option reads input from a given `Data`. 200 | 201 | Use it by setting `.data` for `input`. 202 | 203 | #### `DataSequenceInput` (available with `SubprocessFoundation` trait) 204 | 205 | This option reads input from a sequence of `Data`. 206 | 207 | Use it by setting `.sequence` for `input`. 208 | 209 | #### `DataAsyncSequenceInput` (available with `SubprocessFoundation` trait) 210 | 211 | This option reads input from an async sequence of `Data`. 212 | 213 | Use it by setting `.asyncSequence` for `input`. 214 | 215 | --- 216 | 217 | `Subprocess` also supports these output options: 218 | 219 | #### `DiscardedOutput` 220 | 221 | This option means the `Subprocess` won’t collect or redirect output from the child process. 222 | 223 | Use it by setting `.discarded` for `input` or `error`. 224 | 225 | #### `FileDescriptorOutput` 226 | 227 | This option writes output to a specified `FileDescriptor`. You can choose to have the `Subprocess` close the file descriptor after spawning. 228 | 229 | Use it by setting `.fileDescriptor(closeAfterSpawningProcess:)` for `input` or `error`. 230 | 231 | #### `StringOutput` 232 | 233 | This option collects output as a `String` with the given encoding. 234 | 235 | Use it by setting `.string` or `.string(limit:encoding:)` for `input` or `error`. 236 | 237 | #### `BytesOutput` 238 | 239 | This option collects output as `[UInt8]`. 240 | 241 | Use it by setting `.bytes` or `.bytes(limit:)` for `input` or `error`. 242 | 243 | #### `SequenceOutput`: 244 | 245 | This option redirects the child output to the `.standardOutput` or `.standardError` property of `Execution`. It’s only for the `run()` family that takes a custom closure. 246 | 247 | 248 | ### Cross-platform support 249 | 250 | `Subprocess` works on macOS, Linux, and Windows, with feature parity on all platforms as well as platform-specific options for each. 251 | 252 | The table below describes the current level of support that Subprocess has for various platforms: 253 | 254 | | **Platform** | **Support Status** | 255 | |---|---| 256 | | **macOS** | Supported | 257 | | **iOS** | Not supported | 258 | | **watchOS** | Not supported | 259 | | **tvOS** | Not supported | 260 | | **visionOS** | Not supported | 261 | | **Ubuntu 22.04** | Supported | 262 | | **Windows** | Supported | 263 | 264 |

(back to top)

265 | 266 | 267 | ## Documentation 268 | 269 | The latest API documentation can be viewed by running the following command: 270 | 271 | ``` 272 | swift package --disable-sandbox preview-documentation --target Subprocess 273 | ``` 274 | 275 |

(back to top)

276 | 277 | 278 | ## Contributing to Subprocess 279 | 280 | 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). 281 | 282 | If you find something that looks like a bug, please open a [Bug Report][bugreport]! Fill out as many details as you can. 283 | 284 | [bugreport]: https://github.com/swiftlang/swift-subprocess/issues/new?assignees=&labels=bug&template=bug_report.md 285 | 286 |

(back to top)

287 | 288 | 289 | ## Code of Conduct 290 | 291 | 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/). 292 | 293 | 294 |

(back to top)

295 | 296 | ## Contact information 297 | 298 | The Foundation Workgroup communicates with the broader Swift community using the [forum](https://forums.swift.org/c/related-projects/foundation/99) for general discussions. 299 | 300 | The workgroup can also be contacted privately by messaging [@foundation-workgroup](https://forums.swift.org/new-message?groupname=foundation-workgroup) on the Swift Forums. 301 | 302 | 303 |

(back to top)

304 | -------------------------------------------------------------------------------- /Sources/Subprocess/AsyncBufferSequence.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 !os(Windows) 19 | internal import Dispatch 20 | #endif 21 | 22 | #if SubprocessSpan 23 | @available(SubprocessSpan, *) 24 | #endif 25 | public struct AsyncBufferSequence: AsyncSequence, Sendable { 26 | public typealias Failure = any Swift.Error 27 | public typealias Element = Buffer 28 | 29 | #if os(Windows) 30 | internal typealias DiskIO = FileDescriptor 31 | #else 32 | internal typealias DiskIO = DispatchIO 33 | #endif 34 | 35 | @_nonSendable 36 | public struct Iterator: AsyncIteratorProtocol { 37 | public typealias Element = Buffer 38 | 39 | private let diskIO: DiskIO 40 | private var buffer: [UInt8] 41 | private var currentPosition: Int 42 | private var finished: Bool 43 | 44 | internal init(diskIO: DiskIO) { 45 | self.diskIO = diskIO 46 | self.buffer = [] 47 | self.currentPosition = 0 48 | self.finished = false 49 | } 50 | 51 | public func next() async throws -> Buffer? { 52 | let data = try await self.diskIO.readChunk( 53 | upToLength: readBufferSize 54 | ) 55 | if data == nil { 56 | // We finished reading. Close the file descriptor now 57 | #if os(Windows) 58 | try self.diskIO.close() 59 | #else 60 | self.diskIO.close() 61 | #endif 62 | return nil 63 | } 64 | return data 65 | } 66 | } 67 | 68 | private let diskIO: DiskIO 69 | 70 | internal init(diskIO: DiskIO) { 71 | self.diskIO = diskIO 72 | } 73 | 74 | public func makeAsyncIterator() -> Iterator { 75 | return Iterator(diskIO: self.diskIO) 76 | } 77 | } 78 | 79 | // MARK: - Page Size 80 | import _SubprocessCShims 81 | 82 | #if canImport(Darwin) 83 | import Darwin 84 | internal import MachO.dyld 85 | 86 | private let _pageSize: Int = { 87 | Int(_subprocess_vm_size()) 88 | }() 89 | #elseif canImport(WinSDK) 90 | import WinSDK 91 | private let _pageSize: Int = { 92 | var sysInfo: SYSTEM_INFO = SYSTEM_INFO() 93 | GetSystemInfo(&sysInfo) 94 | return Int(sysInfo.dwPageSize) 95 | }() 96 | #elseif os(WASI) 97 | // WebAssembly defines a fixed page size 98 | private let _pageSize: Int = 65_536 99 | #elseif canImport(Android) 100 | @preconcurrency import Android 101 | private let _pageSize: Int = Int(getpagesize()) 102 | #elseif canImport(Glibc) 103 | @preconcurrency import Glibc 104 | private let _pageSize: Int = Int(getpagesize()) 105 | #elseif canImport(Musl) 106 | @preconcurrency import Musl 107 | private let _pageSize: Int = Int(getpagesize()) 108 | #elseif canImport(C) 109 | private let _pageSize: Int = Int(getpagesize()) 110 | #endif // canImport(Darwin) 111 | 112 | @inline(__always) 113 | internal var readBufferSize: Int { 114 | return _pageSize 115 | } 116 | -------------------------------------------------------------------------------- /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 | @preconcurrency internal import Dispatch 13 | 14 | #if SubprocessSpan 15 | @available(SubprocessSpan, *) 16 | #endif 17 | extension AsyncBufferSequence { 18 | /// A immutable collection of bytes 19 | public struct Buffer: Sendable { 20 | #if os(Windows) 21 | private var data: [UInt8] 22 | 23 | internal init(data: [UInt8]) { 24 | self.data = data 25 | } 26 | #else 27 | private var data: DispatchData 28 | 29 | internal init(data: DispatchData) { 30 | self.data = data 31 | } 32 | #endif 33 | } 34 | } 35 | 36 | // MARK: - Properties 37 | #if SubprocessSpan 38 | @available(SubprocessSpan, *) 39 | #endif 40 | extension AsyncBufferSequence.Buffer { 41 | /// Number of bytes stored in the buffer 42 | public var count: Int { 43 | return self.data.count 44 | } 45 | 46 | /// A Boolean value indicating whether the collection is empty. 47 | public var isEmpty: Bool { 48 | return self.data.isEmpty 49 | } 50 | } 51 | 52 | // MARK: - Accessors 53 | #if SubprocessSpan 54 | @available(SubprocessSpan, *) 55 | #endif 56 | extension AsyncBufferSequence.Buffer { 57 | #if !SubprocessSpan 58 | /// Access the raw bytes stored in this buffer 59 | /// - Parameter body: A closure with an `UnsafeRawBufferPointer` parameter that 60 | /// points to the contiguous storage for the type. If no such storage exists, 61 | /// the method creates it. If body has a return value, this method also returns 62 | /// that value. The argument is valid only for the duration of the 63 | /// closure’s SequenceOutput. 64 | /// - Returns: The return value, if any, of the body closure parameter. 65 | public func withUnsafeBytes( 66 | _ body: (UnsafeRawBufferPointer) throws -> ResultType 67 | ) rethrows -> ResultType { 68 | return try self._withUnsafeBytes(body) 69 | } 70 | #endif // !SubprocessSpan 71 | 72 | internal func _withUnsafeBytes( 73 | _ body: (UnsafeRawBufferPointer) throws -> ResultType 74 | ) rethrows -> ResultType { 75 | #if os(Windows) 76 | return try self.data.withUnsafeBytes(body) 77 | #else 78 | // Although DispatchData was designed to be uncontiguous, in practice 79 | // we found that almost all DispatchData are contiguous. Therefore 80 | // we can access this body in O(1) most of the time. 81 | return try self.data.withUnsafeBytes { ptr in 82 | let bytes = UnsafeRawBufferPointer(start: ptr, count: self.data.count) 83 | return try body(bytes) 84 | } 85 | #endif 86 | } 87 | 88 | #if SubprocessSpan 89 | // Access the storge backing this Buffer 90 | public var bytes: RawSpan { 91 | var backing: SpanBacking? 92 | #if os(Windows) 93 | self.data.withUnsafeBufferPointer { 94 | backing = .pointer($0) 95 | } 96 | #else 97 | self.data.enumerateBytes { buffer, byteIndex, stop in 98 | if _fastPath(backing == nil) { 99 | // In practice, almost all `DispatchData` is contiguous 100 | backing = .pointer(buffer) 101 | } else { 102 | // This DispatchData is not contiguous. We need to copy 103 | // the bytes out 104 | let contents = Array(buffer) 105 | switch backing! { 106 | case .pointer(let ptr): 107 | // Convert the ptr to array 108 | let existing = Array(ptr) 109 | backing = .array(existing + contents) 110 | case .array(let array): 111 | backing = .array(array + contents) 112 | } 113 | } 114 | } 115 | #endif 116 | guard let backing = backing else { 117 | let empty = UnsafeRawBufferPointer(start: nil, count: 0) 118 | let span = RawSpan(_unsafeBytes: empty) 119 | return _overrideLifetime(of: span, to: self) 120 | } 121 | switch backing { 122 | case .pointer(let ptr): 123 | let span = RawSpan(_unsafeElements: ptr) 124 | return _overrideLifetime(of: span, to: self) 125 | case .array(let array): 126 | let ptr = array.withUnsafeBytes { $0 } 127 | let span = RawSpan(_unsafeBytes: ptr) 128 | return _overrideLifetime(of: span, to: self) 129 | } 130 | } 131 | #endif // SubprocessSpan 132 | 133 | private enum SpanBacking { 134 | case pointer(UnsafeBufferPointer) 135 | case array([UInt8]) 136 | } 137 | } 138 | 139 | // MARK: - Hashable, Equatable 140 | #if SubprocessSpan 141 | @available(SubprocessSpan, *) 142 | #endif 143 | extension AsyncBufferSequence.Buffer: Equatable, Hashable { 144 | #if os(Windows) 145 | // Compiler generated conformances 146 | #else 147 | public static func == (lhs: AsyncBufferSequence.Buffer, rhs: AsyncBufferSequence.Buffer) -> Bool { 148 | return lhs.data.elementsEqual(rhs.data) 149 | } 150 | 151 | public func hash(into hasher: inout Hasher) { 152 | self.data.withUnsafeBytes { ptr in 153 | let bytes = UnsafeRawBufferPointer( 154 | start: ptr, 155 | count: self.data.count 156 | ) 157 | hasher.combine(bytes: bytes) 158 | } 159 | } 160 | #endif 161 | } 162 | -------------------------------------------------------------------------------- /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 | 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 | // Signal 44 | case failedToSendSignal(Int32) 45 | // Windows Only 46 | case failedToTerminate 47 | case failedToSuspend 48 | case failedToResume 49 | case failedToCreatePipe 50 | case invalidWindowsPath(String) 51 | } 52 | 53 | public var value: Int { 54 | switch self.storage { 55 | case .spawnFailed: 56 | return 0 57 | case .executableNotFound(_): 58 | return 1 59 | case .failedToChangeWorkingDirectory(_): 60 | return 2 61 | case .failedToReadFromSubprocess: 62 | return 3 63 | case .failedToWriteToSubprocess: 64 | return 4 65 | case .failedToMonitorProcess: 66 | return 5 67 | case .failedToSendSignal(_): 68 | return 6 69 | case .failedToTerminate: 70 | return 7 71 | case .failedToSuspend: 72 | return 8 73 | case .failedToResume: 74 | return 9 75 | case .failedToCreatePipe: 76 | return 10 77 | case .invalidWindowsPath(_): 78 | return 11 79 | } 80 | } 81 | 82 | internal let storage: Storage 83 | 84 | internal init(_ storage: Storage) { 85 | self.storage = storage 86 | } 87 | } 88 | } 89 | 90 | // MARK: - Description 91 | extension SubprocessError: CustomStringConvertible, CustomDebugStringConvertible { 92 | public var description: String { 93 | switch self.code.storage { 94 | case .spawnFailed: 95 | return "Failed to spawn the new process with underlying error: \(self.underlyingError!)" 96 | case .executableNotFound(let executableName): 97 | return "Executable \"\(executableName)\" is not found or cannot be executed." 98 | case .failedToChangeWorkingDirectory(let workingDirectory): 99 | return "Failed to set working directory to \"\(workingDirectory)\"." 100 | case .failedToReadFromSubprocess: 101 | return "Failed to read bytes from the child process with underlying error: \(self.underlyingError!)" 102 | case .failedToWriteToSubprocess: 103 | return "Failed to write bytes to the child process." 104 | case .failedToMonitorProcess: 105 | return "Failed to monitor the state of child process with underlying error: \(self.underlyingError!)" 106 | case .failedToSendSignal(let signal): 107 | return "Failed to send signal \(signal) to the child process." 108 | case .failedToTerminate: 109 | return "Failed to terminate the child process." 110 | case .failedToSuspend: 111 | return "Failed to suspend the child process." 112 | case .failedToResume: 113 | return "Failed to resume the child process." 114 | case .failedToCreatePipe: 115 | return "Failed to create a pipe to communicate to child process." 116 | case .invalidWindowsPath(let badPath): 117 | return "\"\(badPath)\" is not a valid Windows path." 118 | } 119 | } 120 | 121 | public var debugDescription: String { self.description } 122 | } 123 | 124 | extension SubprocessError { 125 | /// The underlying error that caused this SubprocessError. 126 | /// - On Unix-like systems, `UnderlyingError` wraps `errno` from libc; 127 | /// - On Windows, `UnderlyingError` wraps Windows Error code 128 | public struct UnderlyingError: Swift.Error, RawRepresentable, Hashable, Sendable { 129 | #if os(Windows) 130 | public typealias RawValue = DWORD 131 | #else 132 | public typealias RawValue = Int32 133 | #endif 134 | 135 | public let rawValue: RawValue 136 | 137 | public init(rawValue: RawValue) { 138 | self.rawValue = rawValue 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /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 | import WinSDK 28 | #endif 29 | 30 | /// An object that repersents 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 | #if SubprocessSpan 34 | @available(SubprocessSpan, *) 35 | #endif 36 | public struct Execution: Sendable { 37 | /// The process identifier of the current execution 38 | public let processIdentifier: ProcessIdentifier 39 | 40 | #if os(Windows) 41 | internal let consoleBehavior: PlatformOptions.ConsoleBehavior 42 | 43 | init( 44 | processIdentifier: ProcessIdentifier, 45 | consoleBehavior: PlatformOptions.ConsoleBehavior 46 | ) { 47 | self.processIdentifier = processIdentifier 48 | self.consoleBehavior = consoleBehavior 49 | } 50 | #else 51 | init( 52 | processIdentifier: ProcessIdentifier 53 | ) { 54 | self.processIdentifier = processIdentifier 55 | } 56 | #endif // os(Windows) 57 | } 58 | 59 | // MARK: - Output Capture 60 | internal enum OutputCapturingState: Sendable { 61 | case standardOutputCaptured(Output) 62 | case standardErrorCaptured(Error) 63 | } 64 | -------------------------------------------------------------------------------- /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 SubprocessFoundation 19 | 20 | #if canImport(Darwin) 21 | // On Darwin always prefer system Foundation 22 | import Foundation 23 | #else 24 | // On other platforms prefer FoundationEssentials 25 | import FoundationEssentials 26 | #endif 27 | 28 | #endif // SubprocessFoundation 29 | 30 | // MARK: - Input 31 | 32 | /// `InputProtocol` defines the `write(with:)` method that a type 33 | /// must implement to serve as the input source for a subprocess. 34 | public protocol InputProtocol: Sendable, ~Copyable { 35 | /// Asynchronously write the input to the subprocess using the 36 | /// write file descriptor 37 | func write(with writer: StandardInputWriter) async throws 38 | } 39 | 40 | /// A concrete `Input` type for subprocesses that indicates 41 | /// the absence of input to the subprocess. On Unix-like systems, 42 | /// `NoInput` redirects the standard input of the subprocess 43 | /// to `/dev/null`, while on Windows, it does not bind any 44 | /// file handle to the subprocess standard input handle. 45 | public struct NoInput: InputProtocol { 46 | internal func createPipe() throws -> CreatedPipe { 47 | #if os(Windows) 48 | // On Windows, instead of binding to dev null, 49 | // we don't set the input handle in the `STARTUPINFOW` 50 | // to signal no input 51 | return CreatedPipe( 52 | readFileDescriptor: nil, 53 | writeFileDescriptor: nil 54 | ) 55 | #else 56 | let devnull: FileDescriptor = try .openDevNull(withAcessMode: .readOnly) 57 | return CreatedPipe( 58 | readFileDescriptor: .init(devnull, closeWhenDone: true), 59 | writeFileDescriptor: nil 60 | ) 61 | #endif 62 | } 63 | 64 | public func write(with writer: StandardInputWriter) async throws { 65 | // noop 66 | } 67 | 68 | internal init() {} 69 | } 70 | 71 | /// A concrete `Input` type for subprocesses that 72 | /// reads input from a specified `FileDescriptor`. 73 | /// Developers have the option to instruct the `Subprocess` to 74 | /// automatically close the provided `FileDescriptor` 75 | /// after the subprocess is spawned. 76 | public struct FileDescriptorInput: InputProtocol { 77 | private let fileDescriptor: FileDescriptor 78 | private let closeAfterSpawningProcess: Bool 79 | 80 | internal func createPipe() throws -> CreatedPipe { 81 | return CreatedPipe( 82 | readFileDescriptor: .init( 83 | self.fileDescriptor, 84 | closeWhenDone: self.closeAfterSpawningProcess 85 | ), 86 | writeFileDescriptor: nil 87 | ) 88 | } 89 | 90 | public func write(with writer: StandardInputWriter) async throws { 91 | // noop 92 | } 93 | 94 | internal init( 95 | fileDescriptor: FileDescriptor, 96 | closeAfterSpawningProcess: Bool 97 | ) { 98 | self.fileDescriptor = fileDescriptor 99 | self.closeAfterSpawningProcess = closeAfterSpawningProcess 100 | } 101 | } 102 | 103 | /// A concrete `Input` type for subprocesses that reads input 104 | /// from a given type conforming to `StringProtocol`. 105 | /// Developers can specify the string encoding to use when 106 | /// encoding the string to data, which defaults to UTF-8. 107 | public struct StringInput< 108 | InputString: StringProtocol & Sendable, 109 | Encoding: Unicode.Encoding 110 | >: InputProtocol { 111 | private let string: InputString 112 | 113 | public func write(with writer: StandardInputWriter) async throws { 114 | guard let array = self.string.byteArray(using: Encoding.self) else { 115 | return 116 | } 117 | _ = try await writer.write(array) 118 | } 119 | 120 | internal init(string: InputString, encoding: Encoding.Type) { 121 | self.string = string 122 | } 123 | } 124 | 125 | /// A concrete `Input` type for subprocesses that reads input 126 | /// from a given `UInt8` Array. 127 | public struct ArrayInput: InputProtocol { 128 | private let array: [UInt8] 129 | 130 | public func write(with writer: StandardInputWriter) async throws { 131 | _ = try await writer.write(self.array) 132 | } 133 | 134 | internal init(array: [UInt8]) { 135 | self.array = array 136 | } 137 | } 138 | 139 | /// A concrete `Input` type for subprocess that indicates that 140 | /// the Subprocess should read its input from `StandardInputWriter`. 141 | internal struct CustomWriteInput: InputProtocol { 142 | public func write(with writer: StandardInputWriter) async throws { 143 | // noop 144 | } 145 | 146 | internal init() {} 147 | } 148 | 149 | extension InputProtocol where Self == NoInput { 150 | /// Create a Subprocess input that specfies there is no input 151 | public static var none: Self { .init() } 152 | } 153 | 154 | extension InputProtocol where Self == FileDescriptorInput { 155 | /// Create a Subprocess input from a `FileDescriptor` and 156 | /// specify whether the `FileDescriptor` should be closed 157 | /// after the process is spawned. 158 | public static func fileDescriptor( 159 | _ fd: FileDescriptor, 160 | closeAfterSpawningProcess: Bool 161 | ) -> Self { 162 | return .init( 163 | fileDescriptor: fd, 164 | closeAfterSpawningProcess: closeAfterSpawningProcess 165 | ) 166 | } 167 | } 168 | 169 | extension InputProtocol { 170 | /// Create a Subprocess input from a `Array` of `UInt8`. 171 | public static func array( 172 | _ array: [UInt8] 173 | ) -> Self where Self == ArrayInput { 174 | return ArrayInput(array: array) 175 | } 176 | 177 | /// Create a Subprocess input from a type that conforms to `StringProtocol` 178 | public static func string< 179 | InputString: StringProtocol & Sendable 180 | >( 181 | _ string: InputString 182 | ) -> Self where Self == StringInput { 183 | return .init(string: string, encoding: UTF8.self) 184 | } 185 | 186 | /// Create a Subprocess input from a type that conforms to `StringProtocol` 187 | public static func string< 188 | InputString: StringProtocol & Sendable, 189 | Encoding: Unicode.Encoding 190 | >( 191 | _ string: InputString, 192 | using encoding: Encoding.Type 193 | ) -> Self where Self == StringInput { 194 | return .init(string: string, encoding: encoding) 195 | } 196 | } 197 | 198 | extension InputProtocol { 199 | internal func createPipe() throws -> CreatedPipe { 200 | if let noInput = self as? NoInput { 201 | return try noInput.createPipe() 202 | } else if let fdInput = self as? FileDescriptorInput { 203 | return try fdInput.createPipe() 204 | } 205 | // Base implementation 206 | return try CreatedPipe(closeWhenDone: true) 207 | } 208 | } 209 | 210 | // MARK: - StandardInputWriter 211 | 212 | /// A writer that writes to the standard input of the subprocess. 213 | public final actor StandardInputWriter: Sendable { 214 | 215 | internal var diskIO: TrackedPlatformDiskIO 216 | 217 | init(diskIO: consuming TrackedPlatformDiskIO) { 218 | self.diskIO = diskIO 219 | } 220 | 221 | /// Write an array of UInt8 to the standard input of the subprocess. 222 | /// - Parameter array: The sequence of bytes to write. 223 | /// - Returns number of bytes written. 224 | public func write( 225 | _ array: [UInt8] 226 | ) async throws -> Int { 227 | return try await self.diskIO.write(array) 228 | } 229 | 230 | /// Write a `RawSpan` to the standard input of the subprocess. 231 | /// - Parameter span: The span to write 232 | /// - Returns number of bytes written 233 | #if SubprocessSpan 234 | @available(SubprocessSpan, *) 235 | public func write(_ span: borrowing RawSpan) async throws -> Int { 236 | return try await self.diskIO.write(span) 237 | } 238 | #endif 239 | 240 | /// Write a StringProtocol to the standard input of the subprocess. 241 | /// - Parameters: 242 | /// - string: The string to write. 243 | /// - encoding: The encoding to use when converting string to bytes 244 | /// - Returns number of bytes written. 245 | public func write( 246 | _ string: some StringProtocol, 247 | using encoding: Encoding.Type = UTF8.self 248 | ) async throws -> Int { 249 | if let array = string.byteArray(using: encoding) { 250 | return try await self.write(array) 251 | } 252 | return 0 253 | } 254 | 255 | /// Signal all writes are finished 256 | public func finish() async throws { 257 | try self.diskIO.safelyClose() 258 | } 259 | } 260 | 261 | extension StringProtocol { 262 | #if SubprocessFoundation 263 | private func convertEncoding( 264 | _ encoding: Encoding.Type 265 | ) -> String.Encoding? { 266 | switch encoding { 267 | case is UTF8.Type: 268 | return .utf8 269 | case is UTF16.Type: 270 | return .utf16 271 | case is UTF32.Type: 272 | return .utf32 273 | default: 274 | return nil 275 | } 276 | } 277 | #endif 278 | package func byteArray(using encoding: Encoding.Type) -> [UInt8]? { 279 | if Encoding.self == Unicode.ASCII.self { 280 | let isASCII = self.utf8.allSatisfy { 281 | return Character(Unicode.Scalar($0)).isASCII 282 | } 283 | 284 | guard isASCII else { 285 | return nil 286 | } 287 | return Array(self.utf8) 288 | } 289 | if Encoding.self == UTF8.self { 290 | return Array(self.utf8) 291 | } 292 | if Encoding.self == UTF16.self { 293 | return Array(self.utf16).flatMap { input in 294 | var uint16: UInt16 = input 295 | return withUnsafeBytes(of: &uint16) { ptr in 296 | Array(ptr) 297 | } 298 | } 299 | } 300 | #if SubprocessFoundation 301 | if let stringEncoding = self.convertEncoding(encoding), 302 | let encoded = self.data(using: stringEncoding) 303 | { 304 | return Array(encoded) 305 | } 306 | return nil 307 | #else 308 | return nil 309 | #endif 310 | } 311 | } 312 | 313 | extension String { 314 | package init( 315 | decodingBytes bytes: [T], 316 | as encoding: Encoding.Type 317 | ) { 318 | self = bytes.withUnsafeBytes { raw in 319 | String( 320 | decoding: raw.bindMemory(to: Encoding.CodeUnit.self).lazy.map { $0 }, 321 | as: encoding 322 | ) 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /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 | internal import Dispatch 18 | 19 | // MARK: - Output 20 | 21 | /// `OutputProtocol` specifies the set of methods that a type 22 | /// must implement to serve as the output target for a subprocess. 23 | /// Instead of developing custom implementations of `OutputProtocol`, 24 | /// it is recommended to utilize the default implementations provided 25 | /// by the `Subprocess` library to specify the output handling requirements. 26 | #if SubprocessSpan 27 | @available(SubprocessSpan, *) 28 | #endif 29 | public protocol OutputProtocol: Sendable, ~Copyable { 30 | associatedtype OutputType: Sendable 31 | 32 | #if SubprocessSpan 33 | /// Convert the output from span to expected output type 34 | func output(from span: RawSpan) throws -> OutputType 35 | #endif 36 | 37 | /// Convert the output from buffer to expected output type 38 | func output(from buffer: some Sequence) throws -> OutputType 39 | 40 | /// The max amount of data to collect for this output. 41 | var maxSize: Int { get } 42 | } 43 | 44 | #if SubprocessSpan 45 | @available(SubprocessSpan, *) 46 | #endif 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 53 | /// the `Subprocess` should not collect or redirect output 54 | /// from the child process. On Unix-like systems, `DiscardedOutput` 55 | /// redirects the standard output of the subprocess to `/dev/null`, 56 | /// while on Windows, it does not bind any file handle to the 57 | /// subprocess standard output handle. 58 | #if SubprocessSpan 59 | @available(SubprocessSpan, *) 60 | #endif 61 | public struct DiscardedOutput: OutputProtocol { 62 | public typealias OutputType = Void 63 | 64 | internal func createPipe() throws -> CreatedPipe { 65 | #if os(Windows) 66 | // On Windows, instead of binding to dev null, 67 | // we don't set the input handle in the `STARTUPINFOW` 68 | // to signal no output 69 | return CreatedPipe( 70 | readFileDescriptor: nil, 71 | writeFileDescriptor: nil 72 | ) 73 | #else 74 | let devnull: FileDescriptor = try .openDevNull(withAcessMode: .readOnly) 75 | return CreatedPipe( 76 | readFileDescriptor: nil, 77 | writeFileDescriptor: .init(devnull, closeWhenDone: true) 78 | ) 79 | #endif 80 | } 81 | 82 | internal init() {} 83 | } 84 | 85 | /// A concrete `Output` type for subprocesses that 86 | /// writes output to a specified `FileDescriptor`. 87 | /// Developers have the option to instruct the `Subprocess` to 88 | /// automatically close the provided `FileDescriptor` 89 | /// after the subprocess is spawned. 90 | #if SubprocessSpan 91 | @available(SubprocessSpan, *) 92 | #endif 93 | public struct FileDescriptorOutput: OutputProtocol { 94 | public typealias OutputType = Void 95 | 96 | private let closeAfterSpawningProcess: Bool 97 | private let fileDescriptor: FileDescriptor 98 | 99 | internal func createPipe() throws -> CreatedPipe { 100 | return CreatedPipe( 101 | readFileDescriptor: nil, 102 | writeFileDescriptor: .init( 103 | self.fileDescriptor, 104 | closeWhenDone: self.closeAfterSpawningProcess 105 | ) 106 | ) 107 | } 108 | 109 | internal init( 110 | fileDescriptor: FileDescriptor, 111 | closeAfterSpawningProcess: Bool 112 | ) { 113 | self.fileDescriptor = fileDescriptor 114 | self.closeAfterSpawningProcess = closeAfterSpawningProcess 115 | } 116 | } 117 | 118 | /// A concrete `Output` type for subprocesses that collects output 119 | /// from the subprocess as `String` with the given encoding. 120 | /// This option must be used with he `run()` method that 121 | /// returns a `CollectedResult`. 122 | #if SubprocessSpan 123 | @available(SubprocessSpan, *) 124 | #endif 125 | public struct StringOutput: OutputProtocol { 126 | public typealias OutputType = String? 127 | public let maxSize: Int 128 | 129 | #if SubprocessSpan 130 | public func output(from span: RawSpan) throws -> String? { 131 | // FIXME: Span to String 132 | var array: [UInt8] = [] 133 | for index in 0..) throws -> String? { 140 | // FIXME: Span to String 141 | let array = Array(buffer) 142 | return String(decodingBytes: array, as: Encoding.self) 143 | } 144 | #endif 145 | 146 | internal init(limit: Int, encoding: Encoding.Type) { 147 | self.maxSize = limit 148 | } 149 | } 150 | 151 | /// A concrete `Output` type for subprocesses that collects output 152 | /// from the subprocess as `[UInt8]`. This option must be used with 153 | /// the `run()` method that returns a `CollectedResult` 154 | #if SubprocessSpan 155 | @available(SubprocessSpan, *) 156 | #endif 157 | public struct BytesOutput: OutputProtocol { 158 | public typealias OutputType = [UInt8] 159 | public let maxSize: Int 160 | 161 | internal func captureOutput( 162 | from diskIO: consuming TrackedPlatformDiskIO? 163 | ) async throws -> [UInt8] { 164 | var diskIOBox: TrackedPlatformDiskIO? = consume diskIO 165 | return try await withCheckedThrowingContinuation { continuation in 166 | let _diskIO = diskIOBox.take() 167 | guard let _diskIO = _diskIO else { 168 | // Show not happen due to type system constraints 169 | fatalError("Trying to capture output without file descriptor") 170 | } 171 | _diskIO.readUntilEOF(upToLength: self.maxSize) { result in 172 | switch result { 173 | case .success(let data): 174 | // FIXME: remove workaround for 175 | // rdar://143992296 176 | // https://github.com/swiftlang/swift-subprocess/issues/3 177 | #if os(Windows) 178 | continuation.resume(returning: data) 179 | #else 180 | continuation.resume(returning: data.array()) 181 | #endif 182 | case .failure(let error): 183 | continuation.resume(throwing: error) 184 | } 185 | } 186 | } 187 | } 188 | 189 | #if SubprocessSpan 190 | public func output(from span: RawSpan) throws -> [UInt8] { 191 | fatalError("Not implemented") 192 | } 193 | #else 194 | public func output(from buffer: some Sequence) throws -> [UInt8] { 195 | fatalError("Not implemented") 196 | } 197 | #endif 198 | 199 | internal init(limit: Int) { 200 | self.maxSize = limit 201 | } 202 | } 203 | 204 | /// A concrete `Output` type for subprocesses that redirects 205 | /// the child output to the `.standardOutput` (a sequence) or `.standardError` 206 | /// property of `Execution`. This output type is 207 | /// only applicable to the `run()` family that takes a custom closure. 208 | #if SubprocessSpan 209 | @available(SubprocessSpan, *) 210 | #endif 211 | internal struct SequenceOutput: OutputProtocol { 212 | public typealias OutputType = Void 213 | 214 | internal init() {} 215 | } 216 | 217 | #if SubprocessSpan 218 | @available(SubprocessSpan, *) 219 | #endif 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 | #if SubprocessSpan 226 | @available(SubprocessSpan, *) 227 | #endif 228 | extension OutputProtocol where Self == FileDescriptorOutput { 229 | /// Create a Subprocess output that writes output to a `FileDescriptor` 230 | /// and optionally close the `FileDescriptor` once process spawned. 231 | public static func fileDescriptor( 232 | _ fd: FileDescriptor, 233 | closeAfterSpawningProcess: Bool 234 | ) -> Self { 235 | return .init(fileDescriptor: fd, closeAfterSpawningProcess: closeAfterSpawningProcess) 236 | } 237 | } 238 | 239 | #if SubprocessSpan 240 | @available(SubprocessSpan, *) 241 | #endif 242 | extension OutputProtocol where Self == StringOutput { 243 | /// Create a `Subprocess` output that collects output as 244 | /// UTF8 String with 128kb limit. 245 | public static var string: Self { 246 | .init(limit: 128 * 1024, encoding: UTF8.self) 247 | } 248 | } 249 | 250 | #if SubprocessSpan 251 | @available(SubprocessSpan, *) 252 | #endif 253 | extension OutputProtocol { 254 | /// Create a `Subprocess` output that collects output as 255 | /// `String` using the given encoding up to limit it bytes. 256 | public static func string( 257 | limit: Int, 258 | encoding: Encoding.Type 259 | ) -> Self where Self == StringOutput { 260 | return .init(limit: limit, encoding: encoding) 261 | } 262 | } 263 | 264 | #if SubprocessSpan 265 | @available(SubprocessSpan, *) 266 | #endif 267 | extension OutputProtocol where Self == BytesOutput { 268 | /// Create a `Subprocess` output that collects output as 269 | /// `Buffer` with 128kb limit. 270 | public static var bytes: Self { .init(limit: 128 * 1024) } 271 | 272 | /// Create a `Subprocess` output that collects output as 273 | /// `Buffer` up to limit it bytes. 274 | public static func bytes(limit: Int) -> Self { 275 | return .init(limit: limit) 276 | } 277 | } 278 | 279 | // MARK: - Span Default Implementations 280 | #if SubprocessSpan 281 | @available(SubprocessSpan, *) 282 | extension OutputProtocol { 283 | public func output(from buffer: some Sequence) throws -> OutputType { 284 | guard let rawBytes: UnsafeRawBufferPointer = buffer as? UnsafeRawBufferPointer else { 285 | fatalError("Unexpected input type passed: \(type(of: buffer))") 286 | } 287 | let span = RawSpan(_unsafeBytes: rawBytes) 288 | return try self.output(from: span) 289 | } 290 | } 291 | #endif 292 | 293 | 294 | // MARK: - Default Implementations 295 | #if SubprocessSpan 296 | @available(SubprocessSpan, *) 297 | #endif 298 | extension OutputProtocol { 299 | @_disfavoredOverload 300 | internal func createPipe() throws -> CreatedPipe { 301 | if let discard = self as? DiscardedOutput { 302 | return try discard.createPipe() 303 | } else if let fdOutput = self as? FileDescriptorOutput { 304 | return try fdOutput.createPipe() 305 | } 306 | // Base pipe based implementation for everything else 307 | return try CreatedPipe(closeWhenDone: true) 308 | } 309 | 310 | /// Capture the output from the subprocess up to maxSize 311 | @_disfavoredOverload 312 | internal func captureOutput( 313 | from diskIO: consuming TrackedPlatformDiskIO? 314 | ) async throws -> OutputType { 315 | if let bytesOutput = self as? BytesOutput { 316 | return try await bytesOutput.captureOutput(from: diskIO) as! Self.OutputType 317 | } 318 | var diskIOBox: TrackedPlatformDiskIO? = consume diskIO 319 | return try await withCheckedThrowingContinuation { continuation in 320 | if OutputType.self == Void.self { 321 | continuation.resume(returning: () as! OutputType) 322 | return 323 | } 324 | guard let _diskIO = diskIOBox.take() else { 325 | // Show not happen due to type system constraints 326 | fatalError("Trying to capture output without file descriptor") 327 | } 328 | 329 | _diskIO.readUntilEOF(upToLength: self.maxSize) { result in 330 | do { 331 | switch result { 332 | case .success(let data): 333 | // FIXME: remove workaround for 334 | // rdar://143992296 335 | // https://github.com/swiftlang/swift-subprocess/issues/3 336 | let output = try self.output(from: data) 337 | continuation.resume(returning: output) 338 | case .failure(let error): 339 | continuation.resume(throwing: error) 340 | } 341 | } catch { 342 | continuation.resume(throwing: error) 343 | } 344 | } 345 | } 346 | } 347 | } 348 | 349 | #if SubprocessSpan 350 | @available(SubprocessSpan, *) 351 | #endif 352 | extension OutputProtocol where OutputType == Void { 353 | internal func captureOutput(from fileDescriptor: consuming TrackedPlatformDiskIO?) async throws {} 354 | 355 | #if SubprocessSpan 356 | /// Convert the output from Data to expected output type 357 | public func output(from span: RawSpan) throws { 358 | // noop 359 | } 360 | #else 361 | public func output(from buffer: some Sequence) throws { 362 | // noop 363 | } 364 | #endif // SubprocessSpan 365 | } 366 | 367 | #if SubprocessSpan 368 | @available(SubprocessSpan, *) 369 | extension OutputProtocol { 370 | #if os(Windows) 371 | internal func output(from data: [UInt8]) throws -> OutputType { 372 | guard !data.isEmpty else { 373 | let empty = UnsafeRawBufferPointer(start: nil, count: 0) 374 | let span = RawSpan(_unsafeBytes: empty) 375 | return try self.output(from: span) 376 | } 377 | 378 | return try data.withUnsafeBufferPointer { ptr in 379 | let span = RawSpan(_unsafeBytes: UnsafeRawBufferPointer(ptr)) 380 | return try self.output(from: span) 381 | } 382 | } 383 | #else 384 | internal func output(from data: DispatchData) throws -> OutputType { 385 | guard !data.isEmpty else { 386 | let empty = UnsafeRawBufferPointer(start: nil, count: 0) 387 | let span = RawSpan(_unsafeBytes: empty) 388 | return try self.output(from: span) 389 | } 390 | 391 | return try data.withUnsafeBytes { ptr in 392 | let bufferPtr = UnsafeRawBufferPointer(start: ptr, count: data.count) 393 | let span = RawSpan(_unsafeBytes: bufferPtr) 394 | return try self.output(from: span) 395 | } 396 | } 397 | #endif // os(Windows) 398 | } 399 | #endif 400 | 401 | extension DispatchData { 402 | internal func array() -> [UInt8] { 403 | var result: [UInt8]? 404 | self.enumerateBytes { buffer, byteIndex, stop in 405 | let currentChunk = Array(UnsafeRawBufferPointer(buffer)) 406 | if result == nil { 407 | result = currentChunk 408 | } else { 409 | result?.append(contentsOf: currentChunk) 410 | } 411 | } 412 | return result ?? [] 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /Sources/Subprocess/Platforms/Subprocess+Darwin.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 Darwin 15 | internal import Dispatch 16 | #if canImport(System) 17 | @preconcurrency import System 18 | #else 19 | @preconcurrency import SystemPackage 20 | #endif 21 | 22 | import _SubprocessCShims 23 | 24 | #if SubprocessFoundation 25 | 26 | #if canImport(Darwin) 27 | // On Darwin always prefer system Foundation 28 | import Foundation 29 | #else 30 | // On other platforms prefer FoundationEssentials 31 | import FoundationEssentials 32 | #endif 33 | 34 | #endif // SubprocessFoundation 35 | 36 | // MARK: - PlatformOptions 37 | 38 | /// The collection of platform-specific settings 39 | /// to configure the subprocess when running 40 | public struct PlatformOptions: Sendable { 41 | public var qualityOfService: QualityOfService = .default 42 | /// Set user ID for the subprocess 43 | public var userID: uid_t? = nil 44 | /// Set the real and effective group ID and the saved 45 | /// set-group-ID of the subprocess, equivalent to calling 46 | /// `setgid()` on the child process. 47 | /// Group ID is used to control permissions, particularly 48 | /// for file access. 49 | public var groupID: gid_t? = nil 50 | /// Set list of supplementary group IDs for the subprocess 51 | public var supplementaryGroups: [gid_t]? = nil 52 | /// Set the process group for the subprocess, equivalent to 53 | /// calling `setpgid()` on the child process. 54 | /// Process group ID is used to group related processes for 55 | /// controlling signals. 56 | public var processGroupID: pid_t? = nil 57 | /// Creates a session and sets the process group ID 58 | /// i.e. Detach from the terminal. 59 | public var createSession: Bool = false 60 | /// An ordered list of steps in order to tear down the child 61 | /// process in case the parent task is cancelled before 62 | /// the child proces terminates. 63 | /// Always ends in sending a `.kill` signal at the end. 64 | public var teardownSequence: [TeardownStep] = [] 65 | /// A closure to configure platform-specific 66 | /// spawning constructs. This closure enables direct 67 | /// configuration or override of underlying platform-specific 68 | /// spawn settings that `Subprocess` utilizes internally, 69 | /// in cases where Subprocess does not provide higher-level 70 | /// APIs for such modifications. 71 | /// 72 | /// On Darwin, Subprocess uses `posix_spawn()` as the 73 | /// underlying spawning mechanism. This closure allows 74 | /// modification of the `posix_spawnattr_t` spawn attribute 75 | /// and file actions `posix_spawn_file_actions_t` before 76 | /// they are sent to `posix_spawn()`. 77 | public var preSpawnProcessConfigurator: 78 | ( 79 | @Sendable ( 80 | inout posix_spawnattr_t?, 81 | inout posix_spawn_file_actions_t? 82 | ) throws -> Void 83 | )? = nil 84 | 85 | public init() {} 86 | } 87 | 88 | extension PlatformOptions { 89 | #if SubprocessFoundation 90 | public typealias QualityOfService = Foundation.QualityOfService 91 | #else 92 | /// Constants that indicate the nature and importance of work to the system. 93 | /// 94 | /// Work with higher quality of service classes receive more resources 95 | /// than work with lower quality of service classes whenever 96 | /// there’s resource contention. 97 | public enum QualityOfService: Int, Sendable { 98 | /// Used for work directly involved in providing an 99 | /// interactive UI. For example, processing control 100 | /// events or drawing to the screen. 101 | case userInteractive = 0x21 102 | /// Used for performing work that has been explicitly requested 103 | /// by the user, and for which results must be immediately 104 | /// presented in order to allow for further user interaction. 105 | /// For example, loading an email after a user has selected 106 | /// it in a message list. 107 | case userInitiated = 0x19 108 | /// Used for performing work which the user is unlikely to be 109 | /// immediately waiting for the results. This work may have been 110 | /// requested by the user or initiated automatically, and often 111 | /// operates at user-visible timescales using a non-modal 112 | /// progress indicator. For example, periodic content updates 113 | /// or bulk file operations, such as media import. 114 | case utility = 0x11 115 | /// Used for work that is not user initiated or visible. 116 | /// In general, a user is unaware that this work is even happening. 117 | /// For example, pre-fetching content, search indexing, backups, 118 | /// or syncing of data with external systems. 119 | case background = 0x09 120 | /// Indicates no explicit quality of service information. 121 | /// Whenever possible, an appropriate quality of service is determined 122 | /// from available sources. Otherwise, some quality of service level 123 | /// between `.userInteractive` and `.utility` is used. 124 | case `default` = -1 125 | } 126 | #endif 127 | } 128 | 129 | extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible { 130 | internal func description(withIndent indent: Int) -> String { 131 | let indent = String(repeating: " ", count: indent * 4) 132 | return """ 133 | PlatformOptions( 134 | \(indent) qualityOfService: \(self.qualityOfService), 135 | \(indent) userID: \(String(describing: userID)), 136 | \(indent) groupID: \(String(describing: groupID)), 137 | \(indent) supplementaryGroups: \(String(describing: supplementaryGroups)), 138 | \(indent) processGroupID: \(String(describing: processGroupID)), 139 | \(indent) createSession: \(createSession), 140 | \(indent) preSpawnProcessConfigurator: \(self.preSpawnProcessConfigurator == nil ? "not set" : "set") 141 | \(indent)) 142 | """ 143 | } 144 | 145 | public var description: String { 146 | return self.description(withIndent: 0) 147 | } 148 | 149 | public var debugDescription: String { 150 | return self.description(withIndent: 0) 151 | } 152 | } 153 | 154 | // MARK: - Spawn 155 | extension Configuration { 156 | #if SubprocessSpan 157 | @available(SubprocessSpan, *) 158 | #endif 159 | internal func spawn( 160 | withInput inputPipe: consuming CreatedPipe, 161 | outputPipe: consuming CreatedPipe, 162 | errorPipe: consuming CreatedPipe 163 | ) throws -> SpawnResult { 164 | // Instead of checking if every possible executable path 165 | // is valid, spawn each directly and catch ENOENT 166 | let possiblePaths = self.executable.possibleExecutablePaths( 167 | withPathValue: self.environment.pathValue() 168 | ) 169 | var inputPipeBox: CreatedPipe? = consume inputPipe 170 | var outputPipeBox: CreatedPipe? = consume outputPipe 171 | var errorPipeBox: CreatedPipe? = consume errorPipe 172 | 173 | return try self.preSpawn { args throws -> SpawnResult in 174 | let (env, uidPtr, gidPtr, supplementaryGroups) = args 175 | var _inputPipe = inputPipeBox.take()! 176 | var _outputPipe = outputPipeBox.take()! 177 | var _errorPipe = errorPipeBox.take()! 178 | 179 | let inputReadFileDescriptor: TrackedFileDescriptor? = _inputPipe.readFileDescriptor() 180 | let inputWriteFileDescriptor: TrackedFileDescriptor? = _inputPipe.writeFileDescriptor() 181 | let outputReadFileDescriptor: TrackedFileDescriptor? = _outputPipe.readFileDescriptor() 182 | let outputWriteFileDescriptor: TrackedFileDescriptor? = _outputPipe.writeFileDescriptor() 183 | let errorReadFileDescriptor: TrackedFileDescriptor? = _errorPipe.readFileDescriptor() 184 | let errorWriteFileDescriptor: TrackedFileDescriptor? = _errorPipe.writeFileDescriptor() 185 | 186 | for possibleExecutablePath in possiblePaths { 187 | var pid: pid_t = 0 188 | 189 | // Setup Arguments 190 | let argv: [UnsafeMutablePointer?] = self.arguments.createArgs( 191 | withExecutablePath: possibleExecutablePath 192 | ) 193 | defer { 194 | for ptr in argv { ptr?.deallocate() } 195 | } 196 | 197 | // Setup file actions and spawn attributes 198 | var fileActions: posix_spawn_file_actions_t? = nil 199 | var spawnAttributes: posix_spawnattr_t? = nil 200 | // Setup stdin, stdout, and stderr 201 | posix_spawn_file_actions_init(&fileActions) 202 | defer { 203 | posix_spawn_file_actions_destroy(&fileActions) 204 | } 205 | 206 | // Input 207 | var result: Int32 = -1 208 | if inputReadFileDescriptor != nil { 209 | result = posix_spawn_file_actions_adddup2( 210 | &fileActions, inputReadFileDescriptor!.platformDescriptor(), 0) 211 | guard result == 0 else { 212 | try self.safelyCloseMultuple( 213 | inputRead: inputReadFileDescriptor, 214 | inputWrite: inputWriteFileDescriptor, 215 | outputRead: outputReadFileDescriptor, 216 | outputWrite: outputWriteFileDescriptor, 217 | errorRead: errorReadFileDescriptor, 218 | errorWrite: errorWriteFileDescriptor 219 | ) 220 | throw SubprocessError( 221 | code: .init(.spawnFailed), 222 | underlyingError: .init(rawValue: result) 223 | ) 224 | } 225 | } 226 | if inputWriteFileDescriptor != nil { 227 | // Close parent side 228 | result = posix_spawn_file_actions_addclose( 229 | &fileActions, inputWriteFileDescriptor!.platformDescriptor() 230 | ) 231 | guard result == 0 else { 232 | try self.safelyCloseMultuple( 233 | inputRead: inputReadFileDescriptor, 234 | inputWrite: inputWriteFileDescriptor, 235 | outputRead: outputReadFileDescriptor, 236 | outputWrite: outputWriteFileDescriptor, 237 | errorRead: errorReadFileDescriptor, 238 | errorWrite: errorWriteFileDescriptor 239 | ) 240 | throw SubprocessError( 241 | code: .init(.spawnFailed), 242 | underlyingError: .init(rawValue: result) 243 | ) 244 | } 245 | } 246 | // Output 247 | if outputWriteFileDescriptor != nil { 248 | result = posix_spawn_file_actions_adddup2( 249 | &fileActions, outputWriteFileDescriptor!.platformDescriptor(), 1 250 | ) 251 | guard result == 0 else { 252 | try self.safelyCloseMultuple( 253 | inputRead: inputReadFileDescriptor, 254 | inputWrite: inputWriteFileDescriptor, 255 | outputRead: outputReadFileDescriptor, 256 | outputWrite: outputWriteFileDescriptor, 257 | errorRead: errorReadFileDescriptor, 258 | errorWrite: errorWriteFileDescriptor 259 | ) 260 | throw SubprocessError( 261 | code: .init(.spawnFailed), 262 | underlyingError: .init(rawValue: result) 263 | ) 264 | } 265 | } 266 | if outputReadFileDescriptor != nil { 267 | // Close parent side 268 | result = posix_spawn_file_actions_addclose( 269 | &fileActions, outputReadFileDescriptor!.platformDescriptor() 270 | ) 271 | guard result == 0 else { 272 | try self.safelyCloseMultuple( 273 | inputRead: inputReadFileDescriptor, 274 | inputWrite: inputWriteFileDescriptor, 275 | outputRead: outputReadFileDescriptor, 276 | outputWrite: outputWriteFileDescriptor, 277 | errorRead: errorReadFileDescriptor, 278 | errorWrite: errorWriteFileDescriptor 279 | ) 280 | throw SubprocessError( 281 | code: .init(.spawnFailed), 282 | underlyingError: .init(rawValue: result) 283 | ) 284 | } 285 | } 286 | // Error 287 | if errorWriteFileDescriptor != nil { 288 | result = posix_spawn_file_actions_adddup2( 289 | &fileActions, errorWriteFileDescriptor!.platformDescriptor(), 2 290 | ) 291 | guard result == 0 else { 292 | try self.safelyCloseMultuple( 293 | inputRead: inputReadFileDescriptor, 294 | inputWrite: inputWriteFileDescriptor, 295 | outputRead: outputReadFileDescriptor, 296 | outputWrite: outputWriteFileDescriptor, 297 | errorRead: errorReadFileDescriptor, 298 | errorWrite: errorWriteFileDescriptor 299 | ) 300 | throw SubprocessError( 301 | code: .init(.spawnFailed), 302 | underlyingError: .init(rawValue: result) 303 | ) 304 | } 305 | } 306 | if errorReadFileDescriptor != nil { 307 | // Close parent side 308 | result = posix_spawn_file_actions_addclose( 309 | &fileActions, errorReadFileDescriptor!.platformDescriptor() 310 | ) 311 | guard result == 0 else { 312 | try self.safelyCloseMultuple( 313 | inputRead: inputReadFileDescriptor, 314 | inputWrite: inputWriteFileDescriptor, 315 | outputRead: outputReadFileDescriptor, 316 | outputWrite: outputWriteFileDescriptor, 317 | errorRead: errorReadFileDescriptor, 318 | errorWrite: errorWriteFileDescriptor 319 | ) 320 | throw SubprocessError( 321 | code: .init(.spawnFailed), 322 | underlyingError: .init(rawValue: result) 323 | ) 324 | } 325 | } 326 | // Setup spawnAttributes 327 | posix_spawnattr_init(&spawnAttributes) 328 | defer { 329 | posix_spawnattr_destroy(&spawnAttributes) 330 | } 331 | var noSignals = sigset_t() 332 | var allSignals = sigset_t() 333 | sigemptyset(&noSignals) 334 | sigfillset(&allSignals) 335 | posix_spawnattr_setsigmask(&spawnAttributes, &noSignals) 336 | posix_spawnattr_setsigdefault(&spawnAttributes, &allSignals) 337 | // Configure spawnattr 338 | var spawnAttributeError: Int32 = 0 339 | var flags: Int32 = POSIX_SPAWN_CLOEXEC_DEFAULT | POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF 340 | if let pgid = self.platformOptions.processGroupID { 341 | flags |= POSIX_SPAWN_SETPGROUP 342 | spawnAttributeError = posix_spawnattr_setpgroup(&spawnAttributes, pid_t(pgid)) 343 | } 344 | spawnAttributeError = posix_spawnattr_setflags(&spawnAttributes, Int16(flags)) 345 | // Set QualityOfService 346 | // spanattr_qos seems to only accept `QOS_CLASS_UTILITY` or `QOS_CLASS_BACKGROUND` 347 | // and returns an error of `EINVAL` if anything else is provided 348 | if spawnAttributeError == 0 && self.platformOptions.qualityOfService == .utility { 349 | spawnAttributeError = posix_spawnattr_set_qos_class_np(&spawnAttributes, QOS_CLASS_UTILITY) 350 | } else if spawnAttributeError == 0 && self.platformOptions.qualityOfService == .background { 351 | spawnAttributeError = posix_spawnattr_set_qos_class_np(&spawnAttributes, QOS_CLASS_BACKGROUND) 352 | } 353 | 354 | // Setup cwd 355 | let intendedWorkingDir = self.workingDirectory.string 356 | let chdirError: Int32 = intendedWorkingDir.withPlatformString { workDir in 357 | return posix_spawn_file_actions_addchdir_np(&fileActions, workDir) 358 | } 359 | 360 | // Error handling 361 | if chdirError != 0 || spawnAttributeError != 0 { 362 | try self.safelyCloseMultuple( 363 | inputRead: inputReadFileDescriptor, 364 | inputWrite: inputWriteFileDescriptor, 365 | outputRead: outputReadFileDescriptor, 366 | outputWrite: outputWriteFileDescriptor, 367 | errorRead: errorReadFileDescriptor, 368 | errorWrite: errorWriteFileDescriptor 369 | ) 370 | 371 | let error: SubprocessError 372 | if spawnAttributeError != 0 { 373 | error = SubprocessError( 374 | code: .init(.spawnFailed), 375 | underlyingError: .init(rawValue: spawnAttributeError) 376 | ) 377 | } else { 378 | error = SubprocessError( 379 | code: .init(.spawnFailed), 380 | underlyingError: .init(rawValue: spawnAttributeError) 381 | ) 382 | } 383 | throw error 384 | } 385 | // Run additional config 386 | if let spawnConfig = self.platformOptions.preSpawnProcessConfigurator { 387 | try spawnConfig(&spawnAttributes, &fileActions) 388 | } 389 | 390 | // Spawn 391 | let spawnError: CInt = possibleExecutablePath.withCString { exePath in 392 | return supplementaryGroups.withOptionalUnsafeBufferPointer { sgroups in 393 | return _subprocess_spawn( 394 | &pid, 395 | exePath, 396 | &fileActions, 397 | &spawnAttributes, 398 | argv, 399 | env, 400 | uidPtr, 401 | gidPtr, 402 | Int32(supplementaryGroups?.count ?? 0), 403 | sgroups?.baseAddress, 404 | self.platformOptions.createSession ? 1 : 0 405 | ) 406 | } 407 | } 408 | // Spawn error 409 | if spawnError != 0 { 410 | if spawnError == ENOENT { 411 | // Move on to another possible path 412 | continue 413 | } 414 | // Throw all other errors 415 | try self.safelyCloseMultuple( 416 | inputRead: inputReadFileDescriptor, 417 | inputWrite: inputWriteFileDescriptor, 418 | outputRead: outputReadFileDescriptor, 419 | outputWrite: outputWriteFileDescriptor, 420 | errorRead: errorReadFileDescriptor, 421 | errorWrite: errorWriteFileDescriptor 422 | ) 423 | throw SubprocessError( 424 | code: .init(.spawnFailed), 425 | underlyingError: .init(rawValue: spawnError) 426 | ) 427 | } 428 | 429 | // After spawn finishes, close all child side fds 430 | try self.safelyCloseMultuple( 431 | inputRead: inputReadFileDescriptor, 432 | inputWrite: nil, 433 | outputRead: nil, 434 | outputWrite: outputWriteFileDescriptor, 435 | errorRead: nil, 436 | errorWrite: errorWriteFileDescriptor 437 | ) 438 | 439 | let execution = Execution( 440 | processIdentifier: .init(value: pid) 441 | ) 442 | return SpawnResult( 443 | execution: execution, 444 | inputWriteEnd: inputWriteFileDescriptor?.createPlatformDiskIO(), 445 | outputReadEnd: outputReadFileDescriptor?.createPlatformDiskIO(), 446 | errorReadEnd: errorReadFileDescriptor?.createPlatformDiskIO() 447 | ) 448 | } 449 | 450 | // If we reach this point, it means either the executable path 451 | // or working directory is not valid. Since posix_spawn does not 452 | // provide which one is not valid, here we make a best effort guess 453 | // by checking whether the working directory is valid. This technically 454 | // still causes TOUTOC issue, but it's the best we can do for error recovery. 455 | try self.safelyCloseMultuple( 456 | inputRead: inputReadFileDescriptor, 457 | inputWrite: inputWriteFileDescriptor, 458 | outputRead: outputReadFileDescriptor, 459 | outputWrite: outputWriteFileDescriptor, 460 | errorRead: errorReadFileDescriptor, 461 | errorWrite: errorWriteFileDescriptor 462 | ) 463 | let workingDirectory = self.workingDirectory.string 464 | guard Configuration.pathAccessible(workingDirectory, mode: F_OK) else { 465 | throw SubprocessError( 466 | code: .init(.failedToChangeWorkingDirectory(workingDirectory)), 467 | underlyingError: .init(rawValue: ENOENT) 468 | ) 469 | } 470 | throw SubprocessError( 471 | code: .init(.executableNotFound(self.executable.description)), 472 | underlyingError: .init(rawValue: ENOENT) 473 | ) 474 | } 475 | } 476 | } 477 | 478 | // Special keys used in Error's user dictionary 479 | extension String { 480 | static let debugDescriptionErrorKey = "NSDebugDescription" 481 | } 482 | 483 | // MARK: - Process Monitoring 484 | @Sendable 485 | internal func monitorProcessTermination( 486 | forProcessWithIdentifier pid: ProcessIdentifier 487 | ) async throws -> TerminationStatus { 488 | return try await withCheckedThrowingContinuation { continuation in 489 | let source = DispatchSource.makeProcessSource( 490 | identifier: pid.value, 491 | eventMask: [.exit], 492 | queue: .global() 493 | ) 494 | source.setEventHandler { 495 | source.cancel() 496 | var siginfo = siginfo_t() 497 | let rc = waitid(P_PID, id_t(pid.value), &siginfo, WEXITED) 498 | guard rc == 0 else { 499 | continuation.resume( 500 | throwing: SubprocessError( 501 | code: .init(.failedToMonitorProcess), 502 | underlyingError: .init(rawValue: errno) 503 | ) 504 | ) 505 | return 506 | } 507 | switch siginfo.si_code { 508 | case .init(CLD_EXITED): 509 | continuation.resume(returning: .exited(siginfo.si_status)) 510 | return 511 | case .init(CLD_KILLED), .init(CLD_DUMPED): 512 | continuation.resume(returning: .unhandledException(siginfo.si_status)) 513 | case .init(CLD_TRAPPED), .init(CLD_STOPPED), .init(CLD_CONTINUED), .init(CLD_NOOP): 514 | // Ignore these signals because they are not related to 515 | // process exiting 516 | break 517 | default: 518 | fatalError("Unexpected exit status: \(siginfo.si_code)") 519 | } 520 | } 521 | source.resume() 522 | } 523 | } 524 | 525 | #endif // canImport(Darwin) 526 | -------------------------------------------------------------------------------- /Sources/Subprocess/Platforms/Subprocess+Linux.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(Glibc) || canImport(Android) || canImport(Musl) 13 | 14 | #if canImport(System) 15 | @preconcurrency import System 16 | #else 17 | @preconcurrency import SystemPackage 18 | #endif 19 | 20 | #if canImport(Glibc) 21 | import Glibc 22 | #elseif canImport(Android) 23 | import Android 24 | #elseif canImport(Musl) 25 | import Musl 26 | #endif 27 | 28 | internal import Dispatch 29 | 30 | import Synchronization 31 | import _SubprocessCShims 32 | 33 | // Linux specific implementations 34 | extension Configuration { 35 | #if SubprocessSpan 36 | @available(SubprocessSpan, *) 37 | #endif 38 | internal func spawn( 39 | withInput inputPipe: consuming CreatedPipe, 40 | outputPipe: consuming CreatedPipe, 41 | errorPipe: consuming CreatedPipe 42 | ) throws -> SpawnResult { 43 | _setupMonitorSignalHandler() 44 | 45 | // Instead of checking if every possible executable path 46 | // is valid, spawn each directly and catch ENOENT 47 | let possiblePaths = self.executable.possibleExecutablePaths( 48 | withPathValue: self.environment.pathValue() 49 | ) 50 | var inputPipeBox: CreatedPipe? = consume inputPipe 51 | var outputPipeBox: CreatedPipe? = consume outputPipe 52 | var errorPipeBox: CreatedPipe? = consume errorPipe 53 | 54 | return try self.preSpawn { args throws -> SpawnResult in 55 | let (env, uidPtr, gidPtr, supplementaryGroups) = args 56 | 57 | var _inputPipe = inputPipeBox.take()! 58 | var _outputPipe = outputPipeBox.take()! 59 | var _errorPipe = errorPipeBox.take()! 60 | 61 | let inputReadFileDescriptor: TrackedFileDescriptor? = _inputPipe.readFileDescriptor() 62 | let inputWriteFileDescriptor: TrackedFileDescriptor? = _inputPipe.writeFileDescriptor() 63 | let outputReadFileDescriptor: TrackedFileDescriptor? = _outputPipe.readFileDescriptor() 64 | let outputWriteFileDescriptor: TrackedFileDescriptor? = _outputPipe.writeFileDescriptor() 65 | let errorReadFileDescriptor: TrackedFileDescriptor? = _errorPipe.readFileDescriptor() 66 | let errorWriteFileDescriptor: TrackedFileDescriptor? = _errorPipe.writeFileDescriptor() 67 | 68 | for possibleExecutablePath in possiblePaths { 69 | var processGroupIDPtr: UnsafeMutablePointer? = nil 70 | if let processGroupID = self.platformOptions.processGroupID { 71 | processGroupIDPtr = .allocate(capacity: 1) 72 | processGroupIDPtr?.pointee = gid_t(processGroupID) 73 | } 74 | // Setup Arguments 75 | let argv: [UnsafeMutablePointer?] = self.arguments.createArgs( 76 | withExecutablePath: possibleExecutablePath 77 | ) 78 | defer { 79 | for ptr in argv { ptr?.deallocate() } 80 | } 81 | // Setup input 82 | let fileDescriptors: [CInt] = [ 83 | inputReadFileDescriptor?.platformDescriptor() ?? -1, 84 | inputWriteFileDescriptor?.platformDescriptor() ?? -1, 85 | outputWriteFileDescriptor?.platformDescriptor() ?? -1, 86 | outputReadFileDescriptor?.platformDescriptor() ?? -1, 87 | errorWriteFileDescriptor?.platformDescriptor() ?? -1, 88 | errorWriteFileDescriptor?.platformDescriptor() ?? -1, 89 | ] 90 | 91 | let workingDirectory: String = self.workingDirectory.string 92 | // Spawn 93 | var pid: pid_t = 0 94 | let spawnError: CInt = possibleExecutablePath.withCString { exePath in 95 | return workingDirectory.withCString { workingDir in 96 | return supplementaryGroups.withOptionalUnsafeBufferPointer { sgroups in 97 | return fileDescriptors.withUnsafeBufferPointer { fds in 98 | return _subprocess_fork_exec( 99 | &pid, 100 | exePath, 101 | workingDir, 102 | fds.baseAddress!, 103 | argv, 104 | env, 105 | uidPtr, 106 | gidPtr, 107 | processGroupIDPtr, 108 | CInt(supplementaryGroups?.count ?? 0), 109 | sgroups?.baseAddress, 110 | self.platformOptions.createSession ? 1 : 0, 111 | self.platformOptions.preSpawnProcessConfigurator 112 | ) 113 | } 114 | } 115 | } 116 | } 117 | // Spawn error 118 | if spawnError != 0 { 119 | if spawnError == ENOENT || spawnError == EACCES { 120 | // Move on to another possible path 121 | continue 122 | } 123 | // Throw all other errors 124 | try self.safelyCloseMultuple( 125 | inputRead: inputReadFileDescriptor, 126 | inputWrite: inputWriteFileDescriptor, 127 | outputRead: outputReadFileDescriptor, 128 | outputWrite: outputWriteFileDescriptor, 129 | errorRead: errorReadFileDescriptor, 130 | errorWrite: errorWriteFileDescriptor 131 | ) 132 | throw SubprocessError( 133 | code: .init(.spawnFailed), 134 | underlyingError: .init(rawValue: spawnError) 135 | ) 136 | } 137 | func captureError(_ work: () throws -> Void) -> (any Swift.Error)? { 138 | do { 139 | try work() 140 | return nil 141 | } catch { 142 | return error 143 | } 144 | } 145 | // After spawn finishes, close all child side fds 146 | try self.safelyCloseMultuple( 147 | inputRead: inputReadFileDescriptor, 148 | inputWrite: nil, 149 | outputRead: nil, 150 | outputWrite: outputWriteFileDescriptor, 151 | errorRead: nil, 152 | errorWrite: errorWriteFileDescriptor 153 | ) 154 | 155 | let execution = Execution( 156 | processIdentifier: .init(value: pid) 157 | ) 158 | return SpawnResult( 159 | execution: execution, 160 | inputWriteEnd: inputWriteFileDescriptor?.createPlatformDiskIO(), 161 | outputReadEnd: outputReadFileDescriptor?.createPlatformDiskIO(), 162 | errorReadEnd: errorReadFileDescriptor?.createPlatformDiskIO() 163 | ) 164 | } 165 | 166 | // If we reach this point, it means either the executable path 167 | // or working directory is not valid. Since posix_spawn does not 168 | // provide which one is not valid, here we make a best effort guess 169 | // by checking whether the working directory is valid. This technically 170 | // still causes TOUTOC issue, but it's the best we can do for error recovery. 171 | try self.safelyCloseMultuple( 172 | inputRead: inputReadFileDescriptor, 173 | inputWrite: inputWriteFileDescriptor, 174 | outputRead: outputReadFileDescriptor, 175 | outputWrite: outputWriteFileDescriptor, 176 | errorRead: errorReadFileDescriptor, 177 | errorWrite: errorWriteFileDescriptor 178 | ) 179 | let workingDirectory = self.workingDirectory.string 180 | guard Configuration.pathAccessible(workingDirectory, mode: F_OK) else { 181 | throw SubprocessError( 182 | code: .init(.failedToChangeWorkingDirectory(workingDirectory)), 183 | underlyingError: .init(rawValue: ENOENT) 184 | ) 185 | } 186 | throw SubprocessError( 187 | code: .init(.executableNotFound(self.executable.description)), 188 | underlyingError: .init(rawValue: ENOENT) 189 | ) 190 | } 191 | } 192 | } 193 | 194 | // MARK: - Platform Specific Options 195 | 196 | /// The collection of platform-specific settings 197 | /// to configure the subprocess when running 198 | public struct PlatformOptions: Sendable { 199 | // Set user ID for the subprocess 200 | public var userID: uid_t? = nil 201 | /// Set the real and effective group ID and the saved 202 | /// set-group-ID of the subprocess, equivalent to calling 203 | /// `setgid()` on the child process. 204 | /// Group ID is used to control permissions, particularly 205 | /// for file access. 206 | public var groupID: gid_t? = nil 207 | // Set list of supplementary group IDs for the subprocess 208 | public var supplementaryGroups: [gid_t]? = nil 209 | /// Set the process group for the subprocess, equivalent to 210 | /// calling `setpgid()` on the child process. 211 | /// Process group ID is used to group related processes for 212 | /// controlling signals. 213 | public var processGroupID: pid_t? = nil 214 | // Creates a session and sets the process group ID 215 | // i.e. Detach from the terminal. 216 | public var createSession: Bool = false 217 | /// An ordered list of steps in order to tear down the child 218 | /// process in case the parent task is cancelled before 219 | /// the child proces terminates. 220 | /// Always ends in sending a `.kill` signal at the end. 221 | public var teardownSequence: [TeardownStep] = [] 222 | /// A closure to configure platform-specific 223 | /// spawning constructs. This closure enables direct 224 | /// configuration or override of underlying platform-specific 225 | /// spawn settings that `Subprocess` utilizes internally, 226 | /// in cases where Subprocess does not provide higher-level 227 | /// APIs for such modifications. 228 | /// 229 | /// On Linux, Subprocess uses `fork/exec` as the 230 | /// underlying spawning mechanism. This closure is called 231 | /// after `fork()` but before `exec()`. You may use it to 232 | /// call any necessary process setup functions. 233 | public var preSpawnProcessConfigurator: (@convention(c) @Sendable () -> Void)? = nil 234 | 235 | public init() {} 236 | } 237 | 238 | extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible { 239 | internal func description(withIndent indent: Int) -> String { 240 | let indent = String(repeating: " ", count: indent * 4) 241 | return """ 242 | PlatformOptions( 243 | \(indent) userID: \(String(describing: userID)), 244 | \(indent) groupID: \(String(describing: groupID)), 245 | \(indent) supplementaryGroups: \(String(describing: supplementaryGroups)), 246 | \(indent) processGroupID: \(String(describing: processGroupID)), 247 | \(indent) createSession: \(createSession), 248 | \(indent) preSpawnProcessConfigurator: \(self.preSpawnProcessConfigurator == nil ? "not set" : "set") 249 | \(indent)) 250 | """ 251 | } 252 | 253 | public var description: String { 254 | return self.description(withIndent: 0) 255 | } 256 | 257 | public var debugDescription: String { 258 | return self.description(withIndent: 0) 259 | } 260 | } 261 | 262 | // Special keys used in Error's user dictionary 263 | extension String { 264 | static let debugDescriptionErrorKey = "DebugDescription" 265 | } 266 | 267 | // MARK: - Process Monitoring 268 | @Sendable 269 | internal func monitorProcessTermination( 270 | forProcessWithIdentifier pid: ProcessIdentifier 271 | ) async throws -> TerminationStatus { 272 | return try await withCheckedThrowingContinuation { continuation in 273 | _childProcessContinuations.withLock { continuations in 274 | if let existing = continuations.removeValue(forKey: pid.value), 275 | case .status(let existingStatus) = existing 276 | { 277 | // We already have existing status to report 278 | continuation.resume(returning: existingStatus) 279 | } else { 280 | // Save the continuation for handler 281 | continuations[pid.value] = .continuation(continuation) 282 | } 283 | } 284 | } 285 | } 286 | 287 | private enum ContinuationOrStatus { 288 | case continuation(CheckedContinuation) 289 | case status(TerminationStatus) 290 | } 291 | 292 | private let _childProcessContinuations: 293 | Mutex< 294 | [pid_t: ContinuationOrStatus] 295 | > = Mutex([:]) 296 | 297 | private let signalSource: SendableSourceSignal = SendableSourceSignal() 298 | 299 | private extension siginfo_t { 300 | var si_status: Int32 { 301 | #if canImport(Glibc) 302 | return _sifields._sigchld.si_status 303 | #elseif canImport(Musl) 304 | return __si_fields.__si_common.__second.__sigchld.si_status 305 | #elseif canImport(Bionic) 306 | return _sifields._sigchld._status 307 | #endif 308 | } 309 | 310 | var si_pid: pid_t { 311 | #if canImport(Glibc) 312 | return _sifields._sigchld.si_pid 313 | #elseif canImport(Musl) 314 | return __si_fields.__si_common.__first.__piduid.si_pid 315 | #elseif canImport(Bionic) 316 | return _sifields._kill._pid 317 | #endif 318 | } 319 | } 320 | 321 | private let setup: () = { 322 | signalSource.setEventHandler { 323 | while true { 324 | var siginfo = siginfo_t() 325 | guard waitid(P_ALL, id_t(0), &siginfo, WEXITED) == 0 || errno == EINTR else { 326 | return 327 | } 328 | var status: TerminationStatus? = nil 329 | switch siginfo.si_code { 330 | case .init(CLD_EXITED): 331 | status = .exited(siginfo.si_status) 332 | case .init(CLD_KILLED), .init(CLD_DUMPED): 333 | status = .unhandledException(siginfo.si_status) 334 | case .init(CLD_TRAPPED), .init(CLD_STOPPED), .init(CLD_CONTINUED): 335 | // Ignore these signals because they are not related to 336 | // process exiting 337 | break 338 | default: 339 | fatalError("Unexpected exit status: \(siginfo.si_code)") 340 | } 341 | if let status = status { 342 | _childProcessContinuations.withLock { continuations in 343 | let pid = siginfo.si_pid 344 | if let existing = continuations.removeValue(forKey: pid), 345 | case .continuation(let c) = existing 346 | { 347 | c.resume(returning: status) 348 | } else { 349 | // We don't have continuation yet, just state status 350 | continuations[pid] = .status(status) 351 | } 352 | } 353 | } 354 | } 355 | } 356 | signalSource.resume() 357 | }() 358 | 359 | /// Unchecked Sendable here since this class is only explicitly 360 | /// initialzied once during the lifetime of the process 361 | final class SendableSourceSignal: @unchecked Sendable { 362 | private let signalSource: DispatchSourceSignal 363 | 364 | func setEventHandler(handler: @escaping DispatchSourceHandler) { 365 | self.signalSource.setEventHandler(handler: handler) 366 | } 367 | 368 | func resume() { 369 | self.signalSource.resume() 370 | } 371 | 372 | init() { 373 | self.signalSource = DispatchSource.makeSignalSource( 374 | signal: SIGCHLD, 375 | queue: .global() 376 | ) 377 | } 378 | } 379 | 380 | private func _setupMonitorSignalHandler() { 381 | // Only executed once 382 | setup 383 | } 384 | 385 | #endif // canImport(Glibc) || canImport(Android) || canImport(Musl) 386 | -------------------------------------------------------------------------------- /Sources/Subprocess/Platforms/Subprocess+Unix.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) || canImport(Glibc) || canImport(Android) || canImport(Musl) 13 | 14 | #if canImport(System) 15 | @preconcurrency import System 16 | #else 17 | @preconcurrency import SystemPackage 18 | #endif 19 | 20 | import _SubprocessCShims 21 | 22 | #if canImport(Darwin) 23 | import Darwin 24 | #elseif canImport(Android) 25 | import Android 26 | #elseif canImport(Glibc) 27 | import Glibc 28 | #elseif canImport(Musl) 29 | import Musl 30 | #endif 31 | 32 | internal import Dispatch 33 | 34 | // MARK: - Signals 35 | 36 | /// Signals are standardized messages sent to a running program 37 | /// to trigger specific behavior, such as quitting or error handling. 38 | public struct Signal: Hashable, Sendable { 39 | /// The underlying platform specific value for the signal 40 | public let rawValue: Int32 41 | 42 | private init(rawValue: Int32) { 43 | self.rawValue = rawValue 44 | } 45 | 46 | /// The `.interrupt` signal is sent to a process by its 47 | /// controlling terminal when a user wishes to interrupt 48 | /// the process. 49 | public static var interrupt: Self { .init(rawValue: SIGINT) } 50 | /// The `.terminate` signal is sent to a process to request its 51 | /// termination. Unlike the `.kill` signal, it can be caught 52 | /// and interpreted or ignored by the process. This allows 53 | /// the process to perform nice termination releasing resources 54 | /// and saving state if appropriate. `.interrupt` is nearly 55 | /// identical to `.terminate`. 56 | public static var terminate: Self { .init(rawValue: SIGTERM) } 57 | /// The `.suspend` signal instructs the operating system 58 | /// to stop a process for later resumption. 59 | public static var suspend: Self { .init(rawValue: SIGSTOP) } 60 | /// The `resume` signal instructs the operating system to 61 | /// continue (restart) a process previously paused by the 62 | /// `.suspend` signal. 63 | public static var resume: Self { .init(rawValue: SIGCONT) } 64 | /// The `.kill` signal is sent to a process to cause it to 65 | /// terminate immediately (kill). In contrast to `.terminate` 66 | /// and `.interrupt`, this signal cannot be caught or ignored, 67 | /// and the receiving process cannot perform any 68 | /// clean-up upon receiving this signal. 69 | public static var kill: Self { .init(rawValue: SIGKILL) } 70 | /// The `.terminalClosed` signal is sent to a process when 71 | /// its controlling terminal is closed. In modern systems, 72 | /// this signal usually means that the controlling pseudo 73 | /// or virtual terminal has been closed. 74 | public static var terminalClosed: Self { .init(rawValue: SIGHUP) } 75 | /// The `.quit` signal is sent to a process by its controlling 76 | /// terminal when the user requests that the process quit 77 | /// and perform a core dump. 78 | public static var quit: Self { .init(rawValue: SIGQUIT) } 79 | /// The `.userDefinedOne` signal is sent to a process to indicate 80 | /// user-defined conditions. 81 | public static var userDefinedOne: Self { .init(rawValue: SIGUSR1) } 82 | /// The `.userDefinedTwo` signal is sent to a process to indicate 83 | /// user-defined conditions. 84 | public static var userDefinedTwo: Self { .init(rawValue: SIGUSR2) } 85 | /// The `.alarm` signal is sent to a process when the corresponding 86 | /// time limit is reached. 87 | public static var alarm: Self { .init(rawValue: SIGALRM) } 88 | /// The `.windowSizeChange` signal is sent to a process when 89 | /// its controlling terminal changes its size (a window change). 90 | public static var windowSizeChange: Self { .init(rawValue: SIGWINCH) } 91 | } 92 | 93 | // MARK: - ProcessIdentifier 94 | 95 | /// A platform independent identifier for a Subprocess. 96 | public struct ProcessIdentifier: Sendable, Hashable, Codable { 97 | /// The platform specific process identifier value 98 | public let value: pid_t 99 | 100 | public init(value: pid_t) { 101 | self.value = value 102 | } 103 | } 104 | 105 | extension ProcessIdentifier: CustomStringConvertible, CustomDebugStringConvertible { 106 | public var description: String { "\(self.value)" } 107 | 108 | public var debugDescription: String { "\(self.value)" } 109 | } 110 | 111 | #if SubprocessSpan 112 | @available(SubprocessSpan, *) 113 | #endif 114 | extension Execution { 115 | /// Send the given signal to the child process. 116 | /// - Parameters: 117 | /// - signal: The signal to send. 118 | /// - shouldSendToProcessGroup: Whether this signal should be sent to 119 | /// the entire process group. 120 | public func send( 121 | signal: Signal, 122 | toProcessGroup shouldSendToProcessGroup: Bool = false 123 | ) throws { 124 | try Self.send( 125 | signal: signal, 126 | to: self.processIdentifier, 127 | toProcessGroup: shouldSendToProcessGroup 128 | ) 129 | } 130 | 131 | internal static func send( 132 | signal: Signal, 133 | to processIdentifier: ProcessIdentifier, 134 | toProcessGroup shouldSendToProcessGroup: Bool 135 | ) throws { 136 | let pid = shouldSendToProcessGroup ? -(processIdentifier.value) : processIdentifier.value 137 | guard kill(pid, signal.rawValue) == 0 else { 138 | throw SubprocessError( 139 | code: .init(.failedToSendSignal(signal.rawValue)), 140 | underlyingError: .init(rawValue: errno) 141 | ) 142 | } 143 | } 144 | } 145 | 146 | // MARK: - Environment Resolution 147 | extension Environment { 148 | internal static let pathVariableName = "PATH" 149 | 150 | internal func pathValue() -> String? { 151 | switch self.config { 152 | case .inherit(let overrides): 153 | // If PATH value exists in overrides, use it 154 | if let value = overrides[Self.pathVariableName] { 155 | return value 156 | } 157 | // Fall back to current process 158 | return Self.currentEnvironmentValues()[Self.pathVariableName] 159 | case .custom(let fullEnvironment): 160 | if let value = fullEnvironment[Self.pathVariableName] { 161 | return value 162 | } 163 | return nil 164 | case .rawBytes(let rawBytesArray): 165 | let needle: [UInt8] = Array("\(Self.pathVariableName)=".utf8) 166 | for row in rawBytesArray { 167 | guard row.starts(with: needle) else { 168 | continue 169 | } 170 | // Attempt to 171 | let pathValue = row.dropFirst(needle.count) 172 | return String(decoding: pathValue, as: UTF8.self) 173 | } 174 | return nil 175 | } 176 | } 177 | 178 | // This method follows the standard "create" rule: `env` needs to be 179 | // manually deallocated 180 | internal func createEnv() -> [UnsafeMutablePointer?] { 181 | func createFullCString( 182 | fromKey keyContainer: StringOrRawBytes, 183 | value valueContainer: StringOrRawBytes 184 | ) -> UnsafeMutablePointer { 185 | let rawByteKey: UnsafeMutablePointer = keyContainer.createRawBytes() 186 | let rawByteValue: UnsafeMutablePointer = valueContainer.createRawBytes() 187 | defer { 188 | rawByteKey.deallocate() 189 | rawByteValue.deallocate() 190 | } 191 | /// length = `key` + `=` + `value` + `\null` 192 | let totalLength = keyContainer.count + 1 + valueContainer.count + 1 193 | let fullString: UnsafeMutablePointer = .allocate(capacity: totalLength) 194 | #if canImport(Darwin) 195 | _ = snprintf(ptr: fullString, totalLength, "%s=%s", rawByteKey, rawByteValue) 196 | #else 197 | _ = _shims_snprintf(fullString, CInt(totalLength), "%s=%s", rawByteKey, rawByteValue) 198 | #endif 199 | return fullString 200 | } 201 | 202 | var env: [UnsafeMutablePointer?] = [] 203 | switch self.config { 204 | case .inherit(let updates): 205 | var current = Self.currentEnvironmentValues() 206 | for (key, value) in updates { 207 | // Remove the value from current to override it 208 | current.removeValue(forKey: key) 209 | let fullString = "\(key)=\(value)" 210 | env.append(strdup(fullString)) 211 | } 212 | // Add the rest of `current` to env 213 | for (key, value) in current { 214 | let fullString = "\(key)=\(value)" 215 | env.append(strdup(fullString)) 216 | } 217 | case .custom(let customValues): 218 | for (key, value) in customValues { 219 | let fullString = "\(key)=\(value)" 220 | env.append(strdup(fullString)) 221 | } 222 | case .rawBytes(let rawBytesArray): 223 | for rawBytes in rawBytesArray { 224 | env.append(strdup(rawBytes)) 225 | } 226 | } 227 | env.append(nil) 228 | return env 229 | } 230 | 231 | internal static func withCopiedEnv(_ body: ([UnsafeMutablePointer]) -> R) -> R { 232 | var values: [UnsafeMutablePointer] = [] 233 | // This lock is taken by calls to getenv, so we want as few callouts to other code as possible here. 234 | _subprocess_lock_environ() 235 | guard 236 | let environments: UnsafeMutablePointer?> = 237 | _subprocess_get_environ() 238 | else { 239 | _subprocess_unlock_environ() 240 | return body([]) 241 | } 242 | var curr = environments 243 | while let value = curr.pointee { 244 | values.append(strdup(value)) 245 | curr = curr.advanced(by: 1) 246 | } 247 | _subprocess_unlock_environ() 248 | defer { values.forEach { free($0) } } 249 | return body(values) 250 | } 251 | } 252 | 253 | // MARK: Args Creation 254 | extension Arguments { 255 | // This method follows the standard "create" rule: `args` needs to be 256 | // manually deallocated 257 | internal func createArgs(withExecutablePath executablePath: String) -> [UnsafeMutablePointer?] { 258 | var argv: [UnsafeMutablePointer?] = self.storage.map { $0.createRawBytes() } 259 | // argv[0] = executable path 260 | if let override = self.executablePathOverride { 261 | argv.insert(override.createRawBytes(), at: 0) 262 | } else { 263 | argv.insert(strdup(executablePath), at: 0) 264 | } 265 | argv.append(nil) 266 | return argv 267 | } 268 | } 269 | 270 | // MARK: - Executable Searching 271 | extension Executable { 272 | internal static var defaultSearchPaths: Set { 273 | return Set([ 274 | "/usr/bin", 275 | "/bin", 276 | "/usr/sbin", 277 | "/sbin", 278 | "/usr/local/bin", 279 | ]) 280 | } 281 | 282 | internal func resolveExecutablePath(withPathValue pathValue: String?) throws -> String { 283 | switch self.storage { 284 | case .executable(let executableName): 285 | // If the executableName in is already a full path, return it directly 286 | if Configuration.pathAccessible(executableName, mode: X_OK) { 287 | return executableName 288 | } 289 | // Get $PATH from environment 290 | let searchPaths: Set 291 | if let pathValue = pathValue { 292 | let localSearchPaths = pathValue.split(separator: ":").map { String($0) } 293 | searchPaths = Set(localSearchPaths).union(Self.defaultSearchPaths) 294 | } else { 295 | searchPaths = Self.defaultSearchPaths 296 | } 297 | 298 | for path in searchPaths { 299 | let fullPath = "\(path)/\(executableName)" 300 | let fileExists = Configuration.pathAccessible(fullPath, mode: X_OK) 301 | if fileExists { 302 | return fullPath 303 | } 304 | } 305 | throw SubprocessError( 306 | code: .init(.executableNotFound(executableName)), 307 | underlyingError: nil 308 | ) 309 | case .path(let executablePath): 310 | // Use path directly 311 | return executablePath.string 312 | } 313 | } 314 | } 315 | 316 | // MARK: - PreSpawn 317 | extension Configuration { 318 | internal typealias PreSpawnArgs = ( 319 | env: [UnsafeMutablePointer?], 320 | uidPtr: UnsafeMutablePointer?, 321 | gidPtr: UnsafeMutablePointer?, 322 | supplementaryGroups: [gid_t]? 323 | ) 324 | 325 | internal func preSpawn( 326 | _ work: (PreSpawnArgs) throws -> Result 327 | ) throws -> Result { 328 | // Prepare environment 329 | let env = self.environment.createEnv() 330 | defer { 331 | for ptr in env { ptr?.deallocate() } 332 | } 333 | 334 | var uidPtr: UnsafeMutablePointer? = nil 335 | if let userID = self.platformOptions.userID { 336 | uidPtr = .allocate(capacity: 1) 337 | uidPtr?.pointee = userID 338 | } 339 | defer { 340 | uidPtr?.deallocate() 341 | } 342 | var gidPtr: UnsafeMutablePointer? = nil 343 | if let groupID = self.platformOptions.groupID { 344 | gidPtr = .allocate(capacity: 1) 345 | gidPtr?.pointee = groupID 346 | } 347 | defer { 348 | gidPtr?.deallocate() 349 | } 350 | var supplementaryGroups: [gid_t]? 351 | if let groupsValue = self.platformOptions.supplementaryGroups { 352 | supplementaryGroups = groupsValue 353 | } 354 | return try work( 355 | ( 356 | env: env, 357 | uidPtr: uidPtr, 358 | gidPtr: gidPtr, 359 | supplementaryGroups: supplementaryGroups 360 | ) 361 | ) 362 | } 363 | 364 | internal static func pathAccessible(_ path: String, mode: Int32) -> Bool { 365 | return path.withCString { 366 | return access($0, mode) == 0 367 | } 368 | } 369 | } 370 | 371 | // MARK: - FileDescriptor extensions 372 | extension FileDescriptor { 373 | internal static func ssp_pipe() throws -> ( 374 | readEnd: FileDescriptor, 375 | writeEnd: FileDescriptor 376 | ) { 377 | try pipe() 378 | } 379 | 380 | internal static func openDevNull( 381 | withAcessMode mode: FileDescriptor.AccessMode 382 | ) throws -> FileDescriptor { 383 | let devnull: FileDescriptor = try .open("/dev/null", mode) 384 | return devnull 385 | } 386 | 387 | internal var platformDescriptor: PlatformFileDescriptor { 388 | return self.rawValue 389 | } 390 | } 391 | 392 | internal typealias PlatformFileDescriptor = CInt 393 | internal typealias TrackedPlatformDiskIO = TrackedDispatchIO 394 | 395 | extension TrackedFileDescriptor { 396 | internal consuming func createPlatformDiskIO() -> TrackedPlatformDiskIO { 397 | let dispatchIO: DispatchIO = DispatchIO( 398 | type: .stream, 399 | fileDescriptor: self.platformDescriptor(), 400 | queue: .global(), 401 | cleanupHandler: { error in 402 | // Close the file descriptor 403 | if self.closeWhenDone { 404 | try? self.safelyClose() 405 | } 406 | } 407 | ) 408 | return .init(dispatchIO, closeWhenDone: self.closeWhenDone) 409 | } 410 | } 411 | 412 | // MARK: - TrackedDispatchIO extensions 413 | extension DispatchIO { 414 | #if SubprocessSpan 415 | @available(SubprocessSpan, *) 416 | #endif 417 | internal func readChunk(upToLength maxLength: Int) async throws -> AsyncBufferSequence.Buffer? { 418 | return try await withCheckedThrowingContinuation { continuation in 419 | var buffer: DispatchData = .empty 420 | self.read( 421 | offset: 0, 422 | length: maxLength, 423 | queue: .global() 424 | ) { done, data, error in 425 | if error != 0 { 426 | continuation.resume( 427 | throwing: SubprocessError( 428 | code: .init(.failedToReadFromSubprocess), 429 | underlyingError: .init(rawValue: error) 430 | ) 431 | ) 432 | return 433 | } 434 | if let data = data { 435 | if buffer.isEmpty { 436 | buffer = data 437 | } else { 438 | buffer.append(data) 439 | } 440 | } 441 | if done { 442 | if !buffer.isEmpty { 443 | continuation.resume(returning: AsyncBufferSequence.Buffer(data: buffer)) 444 | } else { 445 | continuation.resume(returning: nil) 446 | } 447 | } 448 | } 449 | } 450 | } 451 | } 452 | 453 | extension TrackedDispatchIO { 454 | #if SubprocessSpan 455 | @available(SubprocessSpan, *) 456 | #endif 457 | internal consuming func readUntilEOF( 458 | upToLength maxLength: Int, 459 | resultHandler: sending @escaping (Swift.Result) -> Void 460 | ) { 461 | var buffer: DispatchData? 462 | self.dispatchIO.read( 463 | offset: 0, 464 | length: maxLength, 465 | queue: .global() 466 | ) { done, data, error in 467 | guard error == 0, let chunkData = data else { 468 | self.dispatchIO.close() 469 | resultHandler( 470 | .failure( 471 | SubprocessError( 472 | code: .init(.failedToReadFromSubprocess), 473 | underlyingError: .init(rawValue: error) 474 | ) 475 | ) 476 | ) 477 | return 478 | } 479 | // Close dispatchIO if we are done 480 | if done { 481 | self.dispatchIO.close() 482 | } 483 | // Easy case: if we are done and buffer is nil, this means 484 | // there is only one chunk of data 485 | if done && buffer == nil { 486 | buffer = chunkData 487 | resultHandler(.success(chunkData)) 488 | return 489 | } 490 | 491 | if buffer == nil { 492 | buffer = chunkData 493 | } else { 494 | buffer?.append(chunkData) 495 | } 496 | 497 | if done { 498 | resultHandler(.success(buffer!)) 499 | return 500 | } 501 | } 502 | } 503 | 504 | #if SubprocessSpan 505 | @available(SubprocessSpan, *) 506 | internal func write( 507 | _ span: borrowing RawSpan 508 | ) async throws -> Int { 509 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 510 | let dispatchData = span.withUnsafeBytes { 511 | return DispatchData( 512 | bytesNoCopy: $0, 513 | deallocator: .custom( 514 | nil, 515 | { 516 | // noop 517 | } 518 | ) 519 | ) 520 | } 521 | self.write(dispatchData) { writtenLength, error in 522 | if let error = error { 523 | continuation.resume(throwing: error) 524 | } else { 525 | continuation.resume(returning: writtenLength) 526 | } 527 | } 528 | } 529 | } 530 | #endif // SubprocessSpan 531 | 532 | internal func write( 533 | _ array: [UInt8] 534 | ) async throws -> Int { 535 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 536 | let dispatchData = array.withUnsafeBytes { 537 | return DispatchData( 538 | bytesNoCopy: $0, 539 | deallocator: .custom( 540 | nil, 541 | { 542 | // noop 543 | } 544 | ) 545 | ) 546 | } 547 | self.write(dispatchData) { writtenLength, error in 548 | if let error = error { 549 | continuation.resume(throwing: error) 550 | } else { 551 | continuation.resume(returning: writtenLength) 552 | } 553 | } 554 | } 555 | } 556 | 557 | internal func write( 558 | _ dispatchData: DispatchData, 559 | queue: DispatchQueue = .global(), 560 | completion: @escaping (Int, Error?) -> Void 561 | ) { 562 | self.dispatchIO.write( 563 | offset: 0, 564 | data: dispatchData, 565 | queue: queue 566 | ) { done, unwritten, error in 567 | guard done else { 568 | // Wait until we are done writing or encountered some error 569 | return 570 | } 571 | 572 | let unwrittenLength = unwritten?.count ?? 0 573 | let writtenLength = dispatchData.count - unwrittenLength 574 | guard error != 0 else { 575 | completion(writtenLength, nil) 576 | return 577 | } 578 | completion( 579 | writtenLength, 580 | SubprocessError( 581 | code: .init(.failedToWriteToSubprocess), 582 | underlyingError: .init(rawValue: error) 583 | ) 584 | ) 585 | } 586 | } 587 | } 588 | 589 | #endif // canImport(Darwin) || canImport(Glibc) || canImport(Android) || canImport(Musl) 590 | -------------------------------------------------------------------------------- /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` closures with the corresponding `TerminationStatus` 22 | /// of 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 | #if SubprocessSpan 38 | @available(SubprocessSpan, *) 39 | #endif 40 | public struct CollectedResult< 41 | Output: OutputProtocol, 42 | Error: OutputProtocol 43 | >: Sendable { 44 | /// The process identifier for the executed subprocess 45 | public let processIdentifier: ProcessIdentifier 46 | /// The termination status of the executed subprocess 47 | public let terminationStatus: TerminationStatus 48 | public let standardOutput: Output.OutputType 49 | public let standardError: Error.OutputType 50 | 51 | internal init( 52 | processIdentifier: ProcessIdentifier, 53 | terminationStatus: TerminationStatus, 54 | standardOutput: Output.OutputType, 55 | standardError: Error.OutputType 56 | ) { 57 | self.processIdentifier = processIdentifier 58 | self.terminationStatus = terminationStatus 59 | self.standardOutput = standardOutput 60 | self.standardError = standardError 61 | } 62 | } 63 | 64 | // MARK: - CollectedResult Conformances 65 | #if SubprocessSpan 66 | @available(SubprocessSpan, *) 67 | #endif 68 | extension CollectedResult: Equatable where Output.OutputType: Equatable, Error.OutputType: Equatable {} 69 | 70 | #if SubprocessSpan 71 | @available(SubprocessSpan, *) 72 | #endif 73 | extension CollectedResult: Hashable where Output.OutputType: Hashable, Error.OutputType: Hashable {} 74 | 75 | #if SubprocessSpan 76 | @available(SubprocessSpan, *) 77 | #endif 78 | extension CollectedResult: Codable where Output.OutputType: Codable, Error.OutputType: Codable {} 79 | 80 | #if SubprocessSpan 81 | @available(SubprocessSpan, *) 82 | #endif 83 | extension CollectedResult: CustomStringConvertible 84 | where Output.OutputType: CustomStringConvertible, Error.OutputType: CustomStringConvertible { 85 | public var description: String { 86 | return """ 87 | CollectedResult( 88 | processIdentifier: \(self.processIdentifier), 89 | terminationStatus: \(self.terminationStatus.description), 90 | standardOutput: \(self.standardOutput.description) 91 | standardError: \(self.standardError.description) 92 | ) 93 | """ 94 | } 95 | } 96 | 97 | #if SubprocessSpan 98 | @available(SubprocessSpan, *) 99 | #endif 100 | extension CollectedResult: CustomDebugStringConvertible 101 | where Output.OutputType: CustomDebugStringConvertible, Error.OutputType: CustomDebugStringConvertible { 102 | public var debugDescription: String { 103 | return """ 104 | CollectedResult( 105 | processIdentifier: \(self.processIdentifier), 106 | terminationStatus: \(self.terminationStatus.description), 107 | standardOutput: \(self.standardOutput.debugDescription) 108 | standardError: \(self.standardError.debugDescription) 109 | ) 110 | """ 111 | } 112 | } 113 | 114 | // MARK: - ExecutionResult Conformances 115 | extension ExecutionResult: Equatable where Result: Equatable {} 116 | 117 | extension ExecutionResult: Hashable where Result: Hashable {} 118 | 119 | extension ExecutionResult: Codable where Result: Codable {} 120 | 121 | extension ExecutionResult: CustomStringConvertible where Result: CustomStringConvertible { 122 | public var description: String { 123 | return """ 124 | ExecutionResult( 125 | terminationStatus: \(self.terminationStatus.description), 126 | value: \(self.value.description) 127 | ) 128 | """ 129 | } 130 | } 131 | 132 | extension ExecutionResult: CustomDebugStringConvertible where Result: CustomDebugStringConvertible { 133 | public var debugDescription: String { 134 | return """ 135 | ExecutionResult( 136 | terminationStatus: \(self.terminationStatus.debugDescription), 137 | value: \(self.value.debugDescription) 138 | ) 139 | """ 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /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 | @available(SubprocessSpan, *) 43 | extension Span where Element: BitwiseCopyable { 44 | 45 | internal var _bytes: RawSpan { 46 | @lifetime(copy self) 47 | @_alwaysEmitIntoClient 48 | get { 49 | let rawSpan = RawSpan(_elements: self) 50 | return _overrideLifetime(of: rawSpan, copyingFrom: self) 51 | } 52 | } 53 | } 54 | 55 | #if canImport(Glibc) || canImport(Bionic) || canImport(Musl) 56 | internal import Dispatch 57 | 58 | @available(SubprocessSpan, *) 59 | extension DispatchData { 60 | var bytes: RawSpan { 61 | _read { 62 | if self.count == 0 { 63 | let empty = UnsafeRawBufferPointer(start: nil, count: 0) 64 | let span = RawSpan(_unsafeBytes: empty) 65 | yield _overrideLifetime(of: span, to: self) 66 | } else { 67 | // FIXME: We cannot get a stable ptr out of DispatchData. 68 | // For now revert back to copy 69 | let array = Array(self) 70 | let ptr = array.withUnsafeBytes { return $0 } 71 | let span = RawSpan(_unsafeBytes: ptr) 72 | yield _overrideLifetime(of: span, to: self) 73 | } 74 | } 75 | } 76 | } 77 | #endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl) 78 | 79 | #endif // SubprocessSpan 80 | -------------------------------------------------------------------------------- /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 31 | /// from a given `Data`. 32 | public struct DataInput: InputProtocol { 33 | private let data: Data 34 | 35 | public func write(with writer: StandardInputWriter) async throws { 36 | _ = try await writer.write(self.data) 37 | } 38 | 39 | internal init(data: Data) { 40 | self.data = data 41 | } 42 | } 43 | 44 | /// A concrete `Input` type for subprocesses that accepts input 45 | /// from a specified sequence of `Data`. 46 | public struct DataSequenceInput< 47 | InputSequence: Sequence & Sendable 48 | >: InputProtocol where InputSequence.Element == Data { 49 | private let sequence: InputSequence 50 | 51 | public func write(with writer: StandardInputWriter) async throws { 52 | var buffer = Data() 53 | for chunk in self.sequence { 54 | buffer.append(chunk) 55 | } 56 | _ = try await writer.write(buffer) 57 | } 58 | 59 | internal init(underlying: InputSequence) { 60 | self.sequence = underlying 61 | } 62 | } 63 | 64 | /// A concrete `Input` type for subprocesses that reads input 65 | /// from a given async sequence of `Data`. 66 | public struct DataAsyncSequenceInput< 67 | InputSequence: AsyncSequence & Sendable 68 | >: InputProtocol where InputSequence.Element == Data { 69 | private let sequence: InputSequence 70 | 71 | private func writeChunk(_ chunk: Data, with writer: StandardInputWriter) async throws { 72 | _ = try await writer.write(chunk) 73 | } 74 | 75 | public func write(with writer: StandardInputWriter) async throws { 76 | for try await chunk in self.sequence { 77 | try await self.writeChunk(chunk, with: writer) 78 | } 79 | } 80 | 81 | internal init(underlying: InputSequence) { 82 | self.sequence = underlying 83 | } 84 | } 85 | 86 | extension InputProtocol { 87 | /// Create a Subprocess input from a `Data` 88 | public static func data(_ data: Data) -> Self where Self == DataInput { 89 | return DataInput(data: data) 90 | } 91 | 92 | /// Create a Subprocess input from a `Sequence` of `Data`. 93 | public static func sequence( 94 | _ sequence: InputSequence 95 | ) -> Self where Self == DataSequenceInput { 96 | return .init(underlying: sequence) 97 | } 98 | 99 | /// Create a Subprocess input from a `AsyncSequence` of `Data`. 100 | public static func sequence( 101 | _ asyncSequence: InputSequence 102 | ) -> Self where Self == DataAsyncSequenceInput { 103 | return .init(underlying: asyncSequence) 104 | } 105 | } 106 | 107 | extension StandardInputWriter { 108 | /// Write a `Data` to the standard input of the subprocess. 109 | /// - Parameter data: The sequence of bytes to write. 110 | /// - Returns number of bytes written. 111 | public func write( 112 | _ data: Data 113 | ) async throws -> Int { 114 | return try await self.diskIO.write(data) 115 | } 116 | 117 | /// Write a AsyncSequence of Data to the standard input of the subprocess. 118 | /// - Parameter sequence: The sequence of bytes to write. 119 | /// - Returns number of bytes written. 120 | public func write( 121 | _ asyncSequence: AsyncSendableSequence 122 | ) async throws -> Int where AsyncSendableSequence.Element == Data { 123 | var buffer = Data() 124 | for try await data in asyncSequence { 125 | buffer.append(data) 126 | } 127 | return try await self.write(buffer) 128 | } 129 | } 130 | 131 | #if os(Windows) 132 | extension TrackedFileDescriptor { 133 | internal func write( 134 | _ data: Data 135 | ) async throws -> Int { 136 | let fileDescriptor = self.fileDescriptor 137 | return try await withCheckedThrowingContinuation { continuation in 138 | // TODO: Figure out a better way to asynchornously write 139 | DispatchQueue.global(qos: .userInitiated).async { 140 | data.withUnsafeBytes { 141 | Self.write( 142 | $0, 143 | to: fileDescriptor 144 | ) { writtenLength, error in 145 | if let error = error { 146 | continuation.resume(throwing: error) 147 | } else { 148 | continuation.resume(returning: writtenLength) 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | } 156 | #else 157 | extension TrackedDispatchIO { 158 | internal func write( 159 | _ data: Data 160 | ) async throws -> Int { 161 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 162 | let dispatchData = data.withUnsafeBytes { 163 | return DispatchData( 164 | bytesNoCopy: $0, 165 | deallocator: .custom( 166 | nil, 167 | { 168 | // noop 169 | } 170 | ) 171 | ) 172 | } 173 | self.write(dispatchData) { writtenLength, error in 174 | if let error = error { 175 | continuation.resume(throwing: error) 176 | } else { 177 | continuation.resume(returning: writtenLength) 178 | } 179 | } 180 | } 181 | } 182 | } 183 | #endif // os(Windows) 184 | 185 | #endif // SubprocessFoundation 186 | -------------------------------------------------------------------------------- /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`. This option must be used with 24 | /// the `run()` method that returns a `CollectedResult` 25 | #if SubprocessSpan 26 | @available(SubprocessSpan, *) 27 | #endif 28 | public struct DataOutput: OutputProtocol { 29 | public typealias OutputType = Data 30 | public let maxSize: Int 31 | 32 | #if SubprocessSpan 33 | public func output(from span: RawSpan) throws -> Data { 34 | return Data(span) 35 | } 36 | #else 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 | #if SubprocessSpan 48 | @available(SubprocessSpan, *) 49 | #endif 50 | extension OutputProtocol where Self == DataOutput { 51 | /// Create a `Subprocess` output that collects output as `Data` 52 | /// up to 128kb. 53 | public static var data: Self { 54 | return .data(limit: 128 * 1024) 55 | } 56 | 57 | /// Create a `Subprocess` output that collects output as `Data` 58 | /// with given max number of bytes to collect. 59 | public static func data(limit: Int) -> Self { 60 | return .init(limit: limit) 61 | } 62 | } 63 | 64 | // MARK: - Workarounds 65 | #if SubprocessSpan 66 | @available(SubprocessSpan, *) 67 | extension OutputProtocol { 68 | @_disfavoredOverload 69 | public func output(from data: some DataProtocol) throws -> OutputType { 70 | // FIXME: remove workaround for 71 | // rdar://143992296 72 | // https://github.com/swiftlang/swift-subprocess/issues/3 73 | return try self.output(from: data.bytes) 74 | } 75 | } 76 | #endif 77 | 78 | #endif // SubprocessFoundation 79 | -------------------------------------------------------------------------------- /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 | @available(SubprocessSpan, *) 25 | extension Data { 26 | init(_ s: borrowing RawSpan) { 27 | self = s.withUnsafeBytes { Data($0) } 28 | } 29 | 30 | public var bytes: RawSpan { 31 | // FIXME: For demo purpose only 32 | let ptr = self.withUnsafeBytes { ptr in 33 | return ptr 34 | } 35 | let span = RawSpan(_unsafeBytes: ptr) 36 | return _overrideLifetime(of: span, to: self) 37 | } 38 | } 39 | 40 | @available(SubprocessSpan, *) 41 | extension DataProtocol { 42 | var bytes: RawSpan { 43 | _read { 44 | if self.regions.isEmpty { 45 | let empty = UnsafeRawBufferPointer(start: nil, count: 0) 46 | let span = RawSpan(_unsafeBytes: empty) 47 | yield _overrideLifetime(of: span, to: self) 48 | } else if self.regions.count == 1 { 49 | // Easy case: there is only one region in the data 50 | let ptr = self.regions.first!.withUnsafeBytes { ptr in 51 | return ptr 52 | } 53 | let span = RawSpan(_unsafeBytes: ptr) 54 | yield _overrideLifetime(of: span, to: self) 55 | } else { 56 | // This data contains discontiguous chunks. We have to 57 | // copy and make a contiguous chunk 58 | var contiguous: ContiguousArray? 59 | for region in self.regions { 60 | if contiguous != nil { 61 | contiguous?.append(contentsOf: region) 62 | } else { 63 | contiguous = .init(region) 64 | } 65 | } 66 | let ptr = contiguous!.withUnsafeBytes { ptr in 67 | return ptr 68 | } 69 | let span = RawSpan(_unsafeBytes: ptr) 70 | yield _overrideLifetime(of: span, to: self) 71 | } 72 | } 73 | } 74 | } 75 | 76 | #endif // SubprocessFoundation 77 | -------------------------------------------------------------------------------- /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 | 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 | #if SubprocessSpan 77 | @available(SubprocessSpan, *) 78 | #endif 79 | extension Execution { 80 | /// Performs a sequence of teardown steps on the Subprocess. 81 | /// Teardown sequence always ends with a `.kill` signal 82 | /// - Parameter sequence: The steps to perform. 83 | public func teardown(using sequence: some Sequence & Sendable) async { 84 | await Self.runTeardownSequence(sequence, on: self.processIdentifier) 85 | } 86 | 87 | internal static func teardown( 88 | using sequence: some Sequence & Sendable, 89 | on processIdentifier: ProcessIdentifier 90 | ) async { 91 | await withUncancelledTask { 92 | await Self.runTeardownSequence(sequence, on: processIdentifier) 93 | } 94 | } 95 | } 96 | 97 | internal enum TeardownStepCompletion { 98 | case processHasExited 99 | case processStillAlive 100 | case killedTheProcess 101 | } 102 | 103 | #if SubprocessSpan 104 | @available(SubprocessSpan, *) 105 | #endif 106 | extension Execution { 107 | internal static func gracefulShutDown( 108 | _ processIdentifier: ProcessIdentifier, 109 | allowedDurationToNextStep duration: Duration 110 | ) async { 111 | #if os(Windows) 112 | guard 113 | let processHandle = OpenProcess( 114 | DWORD(PROCESS_QUERY_INFORMATION | SYNCHRONIZE), 115 | false, 116 | processIdentifier.value 117 | ) 118 | else { 119 | // Nothing more we can do 120 | return 121 | } 122 | defer { 123 | CloseHandle(processHandle) 124 | } 125 | 126 | // 1. Attempt to send WM_CLOSE to the main window 127 | if _subprocess_windows_send_vm_close( 128 | processIdentifier.value 129 | ) { 130 | try? await Task.sleep(for: duration) 131 | } 132 | 133 | // 2. Attempt to attach to the console and send CTRL_C_EVENT 134 | if AttachConsole(processIdentifier.value) { 135 | // Disable Ctrl-C handling in this process 136 | if SetConsoleCtrlHandler(nil, true) { 137 | if GenerateConsoleCtrlEvent(DWORD(CTRL_C_EVENT), 0) { 138 | // We successfully sent the event. wait for the process to exit 139 | try? await Task.sleep(for: duration) 140 | } 141 | // Re-enable Ctrl-C handling 142 | SetConsoleCtrlHandler(nil, false) 143 | } 144 | // Detach console 145 | FreeConsole() 146 | } 147 | 148 | // 3. Attempt to send CTRL_BREAK_EVENT to the process group 149 | if GenerateConsoleCtrlEvent(DWORD(CTRL_BREAK_EVENT), processIdentifier.value) { 150 | // Wait for process to exit 151 | try? await Task.sleep(for: duration) 152 | } 153 | #else 154 | // Send SIGTERM 155 | try? self.send( 156 | signal: .terminate, 157 | to: processIdentifier, 158 | toProcessGroup: false 159 | ) 160 | #endif 161 | } 162 | 163 | internal static func runTeardownSequence( 164 | _ sequence: some Sequence & Sendable, 165 | on processIdentifier: ProcessIdentifier 166 | ) async { 167 | // First insert the `.kill` step 168 | let finalSequence = sequence + [TeardownStep(storage: .kill)] 169 | for step in finalSequence { 170 | let stepCompletion: TeardownStepCompletion 171 | 172 | switch step.storage { 173 | case .gracefulShutDown(let allowedDuration): 174 | stepCompletion = await withTaskGroup(of: TeardownStepCompletion.self) { group in 175 | group.addTask { 176 | do { 177 | try await Task.sleep(for: allowedDuration) 178 | return .processStillAlive 179 | } catch { 180 | // teardown(using:) cancells this task 181 | // when process has exited 182 | return .processHasExited 183 | } 184 | } 185 | await self.gracefulShutDown( 186 | processIdentifier, 187 | allowedDurationToNextStep: allowedDuration 188 | ) 189 | return await group.next()! 190 | } 191 | #if !os(Windows) 192 | case .sendSignal(let signal, let allowedDuration): 193 | stepCompletion = await withTaskGroup(of: TeardownStepCompletion.self) { group in 194 | group.addTask { 195 | do { 196 | try await Task.sleep(for: allowedDuration) 197 | return .processStillAlive 198 | } catch { 199 | // teardown(using:) cancells this task 200 | // when process has exited 201 | return .processHasExited 202 | } 203 | } 204 | try? self.send(signal: signal, to: processIdentifier, toProcessGroup: false) 205 | return await group.next()! 206 | } 207 | #endif // !os(Windows) 208 | case .kill: 209 | #if os(Windows) 210 | try? self.terminate( 211 | processIdentifier, 212 | withExitCode: 0 213 | ) 214 | #else 215 | try? self.send(signal: .kill, to: processIdentifier, toProcessGroup: false) 216 | #endif 217 | stepCompletion = .killedTheProcess 218 | } 219 | 220 | switch stepCompletion { 221 | case .killedTheProcess, .processHasExited: 222 | return 223 | case .processStillAlive: 224 | // Continue to next step 225 | break 226 | } 227 | } 228 | } 229 | } 230 | 231 | func withUncancelledTask( 232 | returning: Result.Type = Result.self, 233 | _ body: @Sendable @escaping () async -> Result 234 | ) async -> Result { 235 | // This looks unstructured but it isn't, please note that we `await` `.value` of this task. 236 | // The reason we need this separate `Task` is that in general, we cannot assume that code performs to our 237 | // expectations if the task we run it on is already cancelled. However, in some cases we need the code to 238 | // run regardless -- even if our task is already cancelled. Therefore, we create a new, uncancelled task here. 239 | await Task { 240 | await body() 241 | }.value 242 | } 243 | -------------------------------------------------------------------------------- /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 | 20 | #if _POSIX_SPAWN 21 | #include 22 | #endif 23 | 24 | #if __has_include() 25 | vm_size_t _subprocess_vm_size(void); 26 | #endif 27 | 28 | #if TARGET_OS_MAC 29 | int _subprocess_spawn( 30 | pid_t * _Nonnull pid, 31 | const char * _Nonnull exec_path, 32 | const posix_spawn_file_actions_t _Nullable * _Nonnull file_actions, 33 | const posix_spawnattr_t _Nullable * _Nonnull spawn_attrs, 34 | char * _Nullable const args[_Nonnull], 35 | char * _Nullable const env[_Nullable], 36 | uid_t * _Nullable uid, 37 | gid_t * _Nullable gid, 38 | int number_of_sgroups, const gid_t * _Nullable sgroups, 39 | int create_session 40 | ); 41 | #endif // TARGET_OS_MAC 42 | 43 | int _subprocess_fork_exec( 44 | pid_t * _Nonnull pid, 45 | const char * _Nonnull exec_path, 46 | const char * _Nullable working_directory, 47 | const int file_descriptors[_Nonnull], 48 | char * _Nullable const args[_Nonnull], 49 | char * _Nullable const env[_Nullable], 50 | uid_t * _Nullable uid, 51 | gid_t * _Nullable gid, 52 | gid_t * _Nullable process_group_id, 53 | int number_of_sgroups, const gid_t * _Nullable sgroups, 54 | int create_session, 55 | void (* _Nullable configurator)(void) 56 | ); 57 | 58 | int _was_process_exited(int status); 59 | int _get_exit_code(int status); 60 | int _was_process_signaled(int status); 61 | int _get_signal_code(int status); 62 | int _was_process_suspended(int status); 63 | 64 | void _subprocess_lock_environ(void); 65 | void _subprocess_unlock_environ(void); 66 | char * _Nullable * _Nullable _subprocess_get_environ(void); 67 | 68 | #if TARGET_OS_LINUX 69 | int _shims_snprintf( 70 | char * _Nonnull str, 71 | int len, 72 | const char * _Nonnull format, 73 | char * _Nonnull str1, 74 | char * _Nonnull str2 75 | ); 76 | #endif 77 | 78 | #endif // !TARGET_OS_WINDOWS 79 | 80 | #if TARGET_OS_WINDOWS 81 | 82 | #ifndef _WINDEF_ 83 | typedef unsigned long DWORD; 84 | typedef int BOOL; 85 | #endif 86 | 87 | BOOL _subprocess_windows_send_vm_close(DWORD pid); 88 | 89 | #endif 90 | 91 | #endif /* process_shims_h */ 92 | -------------------------------------------------------------------------------- /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(__unix__) 32 | #define TARGET_OS_BSD 1 33 | #else 34 | #define TARGET_OS_BSD 0 35 | #endif 36 | 37 | #if defined(_WIN32) 38 | #define TARGET_OS_WINDOWS 1 39 | #else 40 | #define TARGET_OS_WINDOWS 0 41 | #endif 42 | 43 | #if defined(__wasi__) 44 | #define TARGET_OS_WASI 1 45 | #else 46 | #define TARGET_OS_WASI 0 47 | #endif 48 | 49 | #endif // _SHIMS_TARGET_CONDITIONALS_H 50 | -------------------------------------------------------------------------------- /Sources/_SubprocessCShims/process_shims.c: -------------------------------------------------------------------------------- 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 "include/target_conditionals.h" 13 | 14 | #if TARGET_OS_LINUX 15 | // For posix_spawn_file_actions_addchdir_np 16 | #define _GNU_SOURCE 1 17 | #endif 18 | 19 | #include "include/process_shims.h" 20 | 21 | #if TARGET_OS_WINDOWS 22 | #include 23 | #else 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | 35 | #include 36 | 37 | #if __has_include() 38 | #include 39 | #elif defined(_WIN32) 40 | #include 41 | #elif __has_include() 42 | #include 43 | extern char **environ; 44 | #endif 45 | 46 | int _was_process_exited(int status) { 47 | return WIFEXITED(status); 48 | } 49 | 50 | int _get_exit_code(int status) { 51 | return WEXITSTATUS(status); 52 | } 53 | 54 | int _was_process_signaled(int status) { 55 | return WIFSIGNALED(status); 56 | } 57 | 58 | int _get_signal_code(int status) { 59 | return WTERMSIG(status); 60 | } 61 | 62 | int _was_process_suspended(int status) { 63 | return WIFSTOPPED(status); 64 | } 65 | 66 | #if TARGET_OS_LINUX 67 | #include 68 | 69 | int _shims_snprintf( 70 | char * _Nonnull str, 71 | int len, 72 | const char * _Nonnull format, 73 | char * _Nonnull str1, 74 | char * _Nonnull str2 75 | ) { 76 | return snprintf(str, len, format, str1, str2); 77 | } 78 | #endif 79 | 80 | #if __has_include() 81 | vm_size_t _subprocess_vm_size(void) { 82 | // This shim exists because vm_page_size is not marked const, and therefore looks like global mutable state to Swift. 83 | return vm_page_size; 84 | } 85 | #endif 86 | 87 | // MARK: - Private Helpers 88 | static pthread_mutex_t _subprocess_fork_lock = PTHREAD_MUTEX_INITIALIZER; 89 | 90 | // Used after fork, before exec 91 | static int _subprocess_block_everything_but_something_went_seriously_wrong_signals(sigset_t *old_mask) { 92 | sigset_t mask; 93 | int r = 0; 94 | r |= sigfillset(&mask); 95 | r |= sigdelset(&mask, SIGABRT); 96 | r |= sigdelset(&mask, SIGBUS); 97 | r |= sigdelset(&mask, SIGFPE); 98 | r |= sigdelset(&mask, SIGILL); 99 | r |= sigdelset(&mask, SIGKILL); 100 | r |= sigdelset(&mask, SIGSEGV); 101 | r |= sigdelset(&mask, SIGSTOP); 102 | r |= sigdelset(&mask, SIGSYS); 103 | r |= sigdelset(&mask, SIGTRAP); 104 | 105 | r |= pthread_sigmask(SIG_BLOCK, &mask, old_mask); 106 | return r; 107 | } 108 | 109 | #define _subprocess_precondition(__cond) do { \ 110 | int eval = (__cond); \ 111 | if (!eval) { \ 112 | __builtin_trap(); \ 113 | } \ 114 | } while(0) 115 | 116 | #if __DARWIN_NSIG 117 | # define _SUBPROCESS_SIG_MAX __DARWIN_NSIG 118 | #else 119 | # define _SUBPROCESS_SIG_MAX 32 120 | #endif 121 | 122 | 123 | // MARK: - Darwin (posix_spawn) 124 | #if TARGET_OS_MAC 125 | static int _subprocess_spawn_prefork( 126 | pid_t * _Nonnull pid, 127 | const char * _Nonnull exec_path, 128 | const posix_spawn_file_actions_t _Nullable * _Nonnull file_actions, 129 | const posix_spawnattr_t _Nullable * _Nonnull spawn_attrs, 130 | char * _Nullable const args[_Nonnull], 131 | char * _Nullable const env[_Nullable], 132 | uid_t * _Nullable uid, 133 | gid_t * _Nullable gid, 134 | int number_of_sgroups, const gid_t * _Nullable sgroups, 135 | int create_session 136 | ) { 137 | #define write_error_and_exit int error = errno; \ 138 | write(pipefd[1], &error, sizeof(error));\ 139 | close(pipefd[1]); \ 140 | _exit(EXIT_FAILURE) 141 | 142 | // Set `POSIX_SPAWN_SETEXEC` flag since we are forking ourselves 143 | short flags = 0; 144 | int rc = posix_spawnattr_getflags(spawn_attrs, &flags); 145 | if (rc != 0) { 146 | return rc; 147 | } 148 | 149 | rc = posix_spawnattr_setflags( 150 | (posix_spawnattr_t *)spawn_attrs, flags | POSIX_SPAWN_SETEXEC 151 | ); 152 | if (rc != 0) { 153 | return rc; 154 | } 155 | // Setup pipe to catch exec failures from child 156 | int pipefd[2]; 157 | if (pipe(pipefd) != 0) { 158 | return errno; 159 | } 160 | // Set FD_CLOEXEC so the pipe is automatically closed when exec succeeds 161 | flags = fcntl(pipefd[0], F_GETFD); 162 | if (flags == -1) { 163 | close(pipefd[0]); 164 | close(pipefd[1]); 165 | return errno; 166 | } 167 | flags |= FD_CLOEXEC; 168 | if (fcntl(pipefd[0], F_SETFD, flags) == -1) { 169 | close(pipefd[0]); 170 | close(pipefd[1]); 171 | return errno; 172 | } 173 | 174 | flags = fcntl(pipefd[1], F_GETFD); 175 | if (flags == -1) { 176 | close(pipefd[0]); 177 | close(pipefd[1]); 178 | return errno; 179 | } 180 | flags |= FD_CLOEXEC; 181 | if (fcntl(pipefd[1], F_SETFD, flags) == -1) { 182 | close(pipefd[0]); 183 | close(pipefd[1]); 184 | return errno; 185 | } 186 | 187 | // Finally, fork 188 | #pragma GCC diagnostic push 189 | #pragma GCC diagnostic ignored "-Wdeprecated" 190 | pid_t childPid = fork(); 191 | #pragma GCC diagnostic pop 192 | if (childPid < 0) { 193 | close(pipefd[0]); 194 | close(pipefd[1]); 195 | return errno; 196 | } 197 | 198 | if (childPid == 0) { 199 | // Child process 200 | close(pipefd[0]); // Close unused read end 201 | 202 | // Perform setups 203 | if (number_of_sgroups > 0 && sgroups != NULL) { 204 | if (setgroups(number_of_sgroups, sgroups) != 0) { 205 | write_error_and_exit; 206 | } 207 | } 208 | 209 | if (uid != NULL) { 210 | if (setuid(*uid) != 0) { 211 | write_error_and_exit; 212 | } 213 | } 214 | 215 | if (gid != NULL) { 216 | if (setgid(*gid) != 0) { 217 | write_error_and_exit; 218 | } 219 | } 220 | 221 | if (create_session != 0) { 222 | (void)setsid(); 223 | } 224 | 225 | // Use posix_spawnas exec 226 | int error = posix_spawn(pid, exec_path, file_actions, spawn_attrs, args, env); 227 | // If we reached this point, something went wrong 228 | write(pipefd[1], &error, sizeof(error)); 229 | close(pipefd[1]); 230 | _exit(EXIT_FAILURE); 231 | } else { 232 | // Parent process 233 | // Close unused write end 234 | close(pipefd[1]); 235 | // Communicate child pid back 236 | *pid = childPid; 237 | // Read from the pipe until pipe is closed 238 | // either due to exec succeeds or error is written 239 | while (TRUE) { 240 | int childError = 0; 241 | ssize_t read_rc = read(pipefd[0], &childError, sizeof(childError)); 242 | if (read_rc == 0) { 243 | // exec worked! 244 | close(pipefd[0]); 245 | return 0; 246 | } else if (read_rc > 0) { 247 | // Child exec failed and reported back 248 | close(pipefd[0]); 249 | return childError; 250 | } else { 251 | // Read failed 252 | if (errno == EINTR) { 253 | continue; 254 | } else { 255 | close(pipefd[0]); 256 | return errno; 257 | } 258 | } 259 | } 260 | } 261 | } 262 | 263 | int _subprocess_spawn( 264 | pid_t * _Nonnull pid, 265 | const char * _Nonnull exec_path, 266 | const posix_spawn_file_actions_t _Nullable * _Nonnull file_actions, 267 | const posix_spawnattr_t _Nullable * _Nonnull spawn_attrs, 268 | char * _Nullable const args[_Nonnull], 269 | char * _Nullable const env[_Nullable], 270 | uid_t * _Nullable uid, 271 | gid_t * _Nullable gid, 272 | int number_of_sgroups, const gid_t * _Nullable sgroups, 273 | int create_session 274 | ) { 275 | int require_pre_fork = uid != NULL || 276 | gid != NULL || 277 | number_of_sgroups > 0 || 278 | create_session > 0; 279 | 280 | if (require_pre_fork != 0) { 281 | int rc = _subprocess_spawn_prefork( 282 | pid, 283 | exec_path, 284 | file_actions, spawn_attrs, 285 | args, env, 286 | uid, gid, number_of_sgroups, sgroups, create_session 287 | ); 288 | return rc; 289 | } 290 | 291 | // Spawn 292 | return posix_spawn(pid, exec_path, file_actions, spawn_attrs, args, env); 293 | } 294 | 295 | #endif // TARGET_OS_MAC 296 | 297 | // MARK: - Linux (fork/exec + posix_spawn fallback) 298 | #if TARGET_OS_LINUX 299 | #ifndef __GLIBC_PREREQ 300 | #define __GLIBC_PREREQ(maj, min) 0 301 | #endif 302 | 303 | #if _POSIX_SPAWN 304 | static int _subprocess_is_addchdir_np_available() { 305 | #if defined(__GLIBC__) && !__GLIBC_PREREQ(2, 29) 306 | // Glibc versions prior to 2.29 don't support posix_spawn_file_actions_addchdir_np, impacting: 307 | // - Amazon Linux 2 (EoL mid-2025) 308 | return 0; 309 | #elif defined(__OpenBSD__) || defined(__QNX__) 310 | // Currently missing as of: 311 | // - OpenBSD 7.5 (April 2024) 312 | // - QNX 8 (December 2023) 313 | return 0; 314 | #elif defined(__GLIBC__) || TARGET_OS_DARWIN || defined(__FreeBSD__) || (defined(__ANDROID__) && __ANDROID_API__ >= 34) || defined(__musl__) 315 | // Pre-standard posix_spawn_file_actions_addchdir_np version available in: 316 | // - Solaris 11.3 (October 2015) 317 | // - Glibc 2.29 (February 2019) 318 | // - macOS 10.15 (October 2019) 319 | // - musl 1.1.24 (October 2019) 320 | // - FreeBSD 13.1 (May 2022) 321 | // - Android 14 (October 2023) 322 | return 1; 323 | #else 324 | // Standardized posix_spawn_file_actions_addchdir version (POSIX.1-2024, June 2024) available in: 325 | // - Solaris 11.4 (August 2018) 326 | // - NetBSD 10.0 (March 2024) 327 | return 1; 328 | #endif 329 | } 330 | 331 | static int _subprocess_addchdir_np( 332 | posix_spawn_file_actions_t *file_actions, 333 | const char * __restrict path 334 | ) { 335 | #if defined(__GLIBC__) && !__GLIBC_PREREQ(2, 29) 336 | // Glibc versions prior to 2.29 don't support posix_spawn_file_actions_addchdir_np, impacting: 337 | // - Amazon Linux 2 (EoL mid-2025) 338 | return ENOTSUP; 339 | #elif defined(__ANDROID__) && __ANDROID_API__ < 34 340 | // Android versions prior to 14 (API level 34) don't support posix_spawn_file_actions_addchdir_np 341 | return ENOTSUP; 342 | #elif defined(__OpenBSD__) || defined(__QNX__) 343 | // Currently missing as of: 344 | // - OpenBSD 7.5 (April 2024) 345 | // - QNX 8 (December 2023) 346 | return ENOTSUP; 347 | #elif defined(__GLIBC__) || TARGET_OS_DARWIN || defined(__FreeBSD__) || defined(__ANDROID__) || defined(__musl__) 348 | // Pre-standard posix_spawn_file_actions_addchdir_np version available in: 349 | // - Solaris 11.3 (October 2015) 350 | // - Glibc 2.29 (February 2019) 351 | // - macOS 10.15 (October 2019) 352 | // - musl 1.1.24 (October 2019) 353 | // - FreeBSD 13.1 (May 2022) 354 | // - Android 14 (API level 34) (October 2023) 355 | return posix_spawn_file_actions_addchdir_np(file_actions, path); 356 | #else 357 | // Standardized posix_spawn_file_actions_addchdir version (POSIX.1-2024, June 2024) available in: 358 | // - Solaris 11.4 (August 2018) 359 | // - NetBSD 10.0 (March 2024) 360 | return posix_spawn_file_actions_addchdir(file_actions, path); 361 | #endif 362 | } 363 | 364 | static int _subprocess_posix_spawn_fallback( 365 | pid_t * _Nonnull pid, 366 | const char * _Nonnull exec_path, 367 | const char * _Nullable working_directory, 368 | const int file_descriptors[_Nonnull], 369 | char * _Nullable const args[_Nonnull], 370 | char * _Nullable const env[_Nullable], 371 | gid_t * _Nullable process_group_id 372 | ) { 373 | // Setup stdin, stdout, and stderr 374 | posix_spawn_file_actions_t file_actions; 375 | 376 | int rc = posix_spawn_file_actions_init(&file_actions); 377 | if (rc != 0) { return rc; } 378 | if (file_descriptors[0] >= 0) { 379 | rc = posix_spawn_file_actions_adddup2( 380 | &file_actions, file_descriptors[0], STDIN_FILENO 381 | ); 382 | if (rc != 0) { return rc; } 383 | } 384 | if (file_descriptors[2] >= 0) { 385 | rc = posix_spawn_file_actions_adddup2( 386 | &file_actions, file_descriptors[2], STDOUT_FILENO 387 | ); 388 | if (rc != 0) { return rc; } 389 | } 390 | if (file_descriptors[4] >= 0) { 391 | rc = posix_spawn_file_actions_adddup2( 392 | &file_actions, file_descriptors[4], STDERR_FILENO 393 | ); 394 | if (rc != 0) { return rc; } 395 | } 396 | // Setup working directory 397 | rc = _subprocess_addchdir_np(&file_actions, working_directory); 398 | if (rc != 0) { 399 | return rc; 400 | } 401 | 402 | // Close parent side 403 | if (file_descriptors[1] >= 0) { 404 | rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[1]); 405 | if (rc != 0) { return rc; } 406 | } 407 | if (file_descriptors[3] >= 0) { 408 | rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[3]); 409 | if (rc != 0) { return rc; } 410 | } 411 | if (file_descriptors[5] >= 0) { 412 | rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[5]); 413 | if (rc != 0) { return rc; } 414 | } 415 | 416 | // Setup spawnattr 417 | posix_spawnattr_t spawn_attr; 418 | rc = posix_spawnattr_init(&spawn_attr); 419 | if (rc != 0) { return rc; } 420 | // Masks 421 | sigset_t no_signals; 422 | sigset_t all_signals; 423 | sigemptyset(&no_signals); 424 | sigfillset(&all_signals); 425 | rc = posix_spawnattr_setsigmask(&spawn_attr, &no_signals); 426 | if (rc != 0) { return rc; } 427 | rc = posix_spawnattr_setsigdefault(&spawn_attr, &all_signals); 428 | if (rc != 0) { return rc; } 429 | // Flags 430 | short flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF; 431 | if (process_group_id != NULL) { 432 | flags |= POSIX_SPAWN_SETPGROUP; 433 | rc = posix_spawnattr_setpgroup(&spawn_attr, *process_group_id); 434 | if (rc != 0) { return rc; } 435 | } 436 | rc = posix_spawnattr_setflags(&spawn_attr, flags); 437 | 438 | // Spawn! 439 | rc = posix_spawn( 440 | pid, exec_path, 441 | &file_actions, &spawn_attr, 442 | args, env 443 | ); 444 | posix_spawn_file_actions_destroy(&file_actions); 445 | posix_spawnattr_destroy(&spawn_attr); 446 | return rc; 447 | } 448 | #endif // _POSIX_SPAWN 449 | 450 | int _subprocess_fork_exec( 451 | pid_t * _Nonnull pid, 452 | const char * _Nonnull exec_path, 453 | const char * _Nullable working_directory, 454 | const int file_descriptors[_Nonnull], 455 | char * _Nullable const args[_Nonnull], 456 | char * _Nullable const env[_Nullable], 457 | uid_t * _Nullable uid, 458 | gid_t * _Nullable gid, 459 | gid_t * _Nullable process_group_id, 460 | int number_of_sgroups, const gid_t * _Nullable sgroups, 461 | int create_session, 462 | void (* _Nullable configurator)(void) 463 | ) { 464 | #define write_error_and_exit int error = errno; \ 465 | write(pipefd[1], &error, sizeof(error));\ 466 | close(pipefd[1]); \ 467 | _exit(EXIT_FAILURE) 468 | 469 | int require_pre_fork = _subprocess_is_addchdir_np_available() == 0 || 470 | uid != NULL || 471 | gid != NULL || 472 | process_group_id != NULL || 473 | (number_of_sgroups > 0 && sgroups != NULL) || 474 | create_session || 475 | configurator != NULL; 476 | 477 | #if _POSIX_SPAWN 478 | // If posix_spawn is available on this platform and 479 | // we do not require prefork, use posix_spawn if possible. 480 | // 481 | // (Glibc's posix_spawn does not support 482 | // `POSIX_SPAWN_SETEXEC` therefore we have to keep 483 | // using fork/exec if `require_pre_fork` is true. 484 | if (require_pre_fork == 0) { 485 | return _subprocess_posix_spawn_fallback( 486 | pid, exec_path, 487 | working_directory, 488 | file_descriptors, 489 | args, env, 490 | process_group_id 491 | ); 492 | } 493 | #endif 494 | 495 | // Setup pipe to catch exec failures from child 496 | int pipefd[2]; 497 | if (pipe(pipefd) != 0) { 498 | return errno; 499 | } 500 | // Set FD_CLOEXEC so the pipe is automatically closed when exec succeeds 501 | short flags = fcntl(pipefd[0], F_GETFD); 502 | if (flags == -1) { 503 | close(pipefd[0]); 504 | close(pipefd[1]); 505 | return errno; 506 | } 507 | flags |= FD_CLOEXEC; 508 | if (fcntl(pipefd[0], F_SETFD, flags) == -1) { 509 | close(pipefd[0]); 510 | close(pipefd[1]); 511 | return errno; 512 | } 513 | 514 | flags = fcntl(pipefd[1], F_GETFD); 515 | if (flags == -1) { 516 | close(pipefd[0]); 517 | close(pipefd[1]); 518 | return errno; 519 | } 520 | flags |= FD_CLOEXEC; 521 | if (fcntl(pipefd[1], F_SETFD, flags) == -1) { 522 | close(pipefd[0]); 523 | close(pipefd[1]); 524 | return errno; 525 | } 526 | 527 | // Protect the signal masking below 528 | // Note that we only unlock in parent since child 529 | // will be exec'd anyway 530 | int rc = pthread_mutex_lock(&_subprocess_fork_lock); 531 | _subprocess_precondition(rc == 0); 532 | // Block all signals on this thread 533 | sigset_t old_sigmask; 534 | rc = _subprocess_block_everything_but_something_went_seriously_wrong_signals(&old_sigmask); 535 | if (rc != 0) { 536 | close(pipefd[0]); 537 | close(pipefd[1]); 538 | return errno; 539 | } 540 | 541 | // Finally, fork 542 | #pragma GCC diagnostic push 543 | #pragma GCC diagnostic ignored "-Wdeprecated" 544 | pid_t childPid = fork(); 545 | #pragma GCC diagnostic pop 546 | if (childPid < 0) { 547 | // Fork failed 548 | close(pipefd[0]); 549 | close(pipefd[1]); 550 | return errno; 551 | } 552 | 553 | if (childPid == 0) { 554 | // Child process 555 | close(pipefd[0]); // Close unused read end 556 | 557 | // Reset signal handlers 558 | for (int signo = 1; signo < _SUBPROCESS_SIG_MAX; signo++) { 559 | if (signo == SIGKILL || signo == SIGSTOP) { 560 | continue; 561 | } 562 | void (*err_ptr)(int) = signal(signo, SIG_DFL); 563 | if (err_ptr != SIG_ERR) { 564 | continue; 565 | } 566 | 567 | if (errno == EINVAL) { 568 | break; // probably too high of a signal 569 | } 570 | 571 | write_error_and_exit; 572 | } 573 | 574 | // Reset signal mask 575 | sigset_t sigset = { 0 }; 576 | sigemptyset(&sigset); 577 | int rc = sigprocmask(SIG_SETMASK, &sigset, NULL) != 0; 578 | if (rc != 0) { 579 | write_error_and_exit; 580 | } 581 | 582 | // Perform setups 583 | if (working_directory != NULL) { 584 | if (chdir(working_directory) != 0) { 585 | write_error_and_exit; 586 | } 587 | } 588 | 589 | if (uid != NULL) { 590 | if (setuid(*uid) != 0) { 591 | write_error_and_exit; 592 | } 593 | } 594 | 595 | if (gid != NULL) { 596 | if (setgid(*gid) != 0) { 597 | write_error_and_exit; 598 | } 599 | } 600 | 601 | if (number_of_sgroups > 0 && sgroups != NULL) { 602 | if (setgroups(number_of_sgroups, sgroups) != 0) { 603 | write_error_and_exit; 604 | } 605 | } 606 | 607 | if (create_session != 0) { 608 | (void)setsid(); 609 | } 610 | 611 | if (process_group_id != NULL) { 612 | (void)setpgid(0, *process_group_id); 613 | } 614 | 615 | // Bind stdin, stdout, and stderr 616 | if (file_descriptors[0] >= 0) { 617 | rc = dup2(file_descriptors[0], STDIN_FILENO); 618 | if (rc < 0) { 619 | write_error_and_exit; 620 | } 621 | } 622 | if (file_descriptors[2] >= 0) { 623 | rc = dup2(file_descriptors[2], STDOUT_FILENO); 624 | if (rc < 0) { 625 | write_error_and_exit; 626 | } 627 | } 628 | if (file_descriptors[4] >= 0) { 629 | rc = dup2(file_descriptors[4], STDERR_FILENO); 630 | if (rc < 0) { 631 | int error = errno; 632 | write(pipefd[1], &error, sizeof(error)); 633 | close(pipefd[1]); 634 | _exit(EXIT_FAILURE); 635 | } 636 | } 637 | // Close parent side 638 | if (file_descriptors[1] >= 0) { 639 | rc = close(file_descriptors[1]); 640 | } 641 | if (file_descriptors[3] >= 0) { 642 | rc = close(file_descriptors[3]); 643 | } 644 | if (file_descriptors[4] >= 0) { 645 | rc = close(file_descriptors[4]); 646 | } 647 | if (rc != 0) { 648 | int error = errno; 649 | write(pipefd[1], &error, sizeof(error)); 650 | close(pipefd[1]); 651 | _exit(EXIT_FAILURE); 652 | } 653 | // Run custom configuratior 654 | if (configurator != NULL) { 655 | configurator(); 656 | } 657 | // Finally, exec 658 | execve(exec_path, args, env); 659 | // If we reached this point, something went wrong 660 | write_error_and_exit; 661 | } else { 662 | // Parent process 663 | close(pipefd[1]); // Close unused write end 664 | 665 | // Restore old signmask 666 | rc = pthread_sigmask(SIG_SETMASK, &old_sigmask, NULL); 667 | if (rc != 0) { 668 | close(pipefd[0]); 669 | return errno; 670 | } 671 | 672 | // Unlock 673 | rc = pthread_mutex_unlock(&_subprocess_fork_lock); 674 | _subprocess_precondition(rc == 0); 675 | 676 | // Communicate child pid back 677 | *pid = childPid; 678 | // Read from the pipe until pipe is closed 679 | // either due to exec succeeds or error is written 680 | while (1) { 681 | int childError = 0; 682 | ssize_t read_rc = read(pipefd[0], &childError, sizeof(childError)); 683 | if (read_rc == 0) { 684 | // exec worked! 685 | close(pipefd[0]); 686 | return 0; 687 | } else if (read_rc > 0) { 688 | // Child exec failed and reported back 689 | close(pipefd[0]); 690 | return childError; 691 | } else { 692 | // Read failed 693 | if (errno == EINTR) { 694 | continue; 695 | } else { 696 | close(pipefd[0]); 697 | return errno; 698 | } 699 | } 700 | } 701 | } 702 | } 703 | 704 | #endif // TARGET_OS_LINUX 705 | 706 | #endif // !TARGET_OS_WINDOWS 707 | 708 | #pragma mark - Environment Locking 709 | 710 | #if __has_include() 711 | #import 712 | void _subprocess_lock_environ(void) { 713 | environ_lock_np(); 714 | } 715 | 716 | void _subprocess_unlock_environ(void) { 717 | environ_unlock_np(); 718 | } 719 | #else 720 | void _subprocess_lock_environ(void) { /* noop */ } 721 | void _subprocess_unlock_environ(void) { /* noop */ } 722 | #endif 723 | 724 | char ** _subprocess_get_environ(void) { 725 | #if __has_include() 726 | return *_NSGetEnviron(); 727 | #elif defined(_WIN32) 728 | #include 729 | return _environ; 730 | #elif TARGET_OS_WASI 731 | return __wasilibc_get_environ(); 732 | #elif __has_include() 733 | return environ; 734 | #endif 735 | } 736 | 737 | 738 | #if TARGET_OS_WINDOWS 739 | 740 | typedef struct { 741 | DWORD pid; 742 | HWND mainWindow; 743 | } CallbackContext; 744 | 745 | static BOOL CALLBACK enumWindowsCallback( 746 | HWND hwnd, 747 | LPARAM lParam 748 | ) { 749 | CallbackContext *context = (CallbackContext *)lParam; 750 | DWORD pid; 751 | GetWindowThreadProcessId(hwnd, &pid); 752 | if (pid == context->pid) { 753 | context->mainWindow = hwnd; 754 | return FALSE; // Stop enumeration 755 | } 756 | return TRUE; // Continue enumeration 757 | } 758 | 759 | BOOL _subprocess_windows_send_vm_close( 760 | DWORD pid 761 | ) { 762 | // First attempt to find the Window associate 763 | // with this process 764 | CallbackContext context = {0}; 765 | context.pid = pid; 766 | EnumWindows(enumWindowsCallback, (LPARAM)&context); 767 | 768 | if (context.mainWindow != NULL) { 769 | if (SendMessage(context.mainWindow, WM_CLOSE, 0, 0)) { 770 | return TRUE; 771 | } 772 | } 773 | 774 | return FALSE; 775 | } 776 | 777 | #endif 778 | 779 | -------------------------------------------------------------------------------- /Tests/SubprocessTests/SubprocessTests+Darwin.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 | guard #available(SubprocessSpan , *) else { 31 | return 32 | } 33 | var platformOptions = PlatformOptions() 34 | platformOptions.preSpawnProcessConfigurator = { spawnAttr, _ in 35 | // Set POSIX_SPAWN_SETSID flag, which implies calls 36 | // to setsid 37 | var flags: Int16 = 0 38 | posix_spawnattr_getflags(&spawnAttr, &flags) 39 | posix_spawnattr_setflags(&spawnAttr, flags | Int16(POSIX_SPAWN_SETSID)) 40 | } 41 | // Check the proces ID (pid), pross group ID (pgid), and 42 | // controling terminal's process group ID (tpgid) 43 | let psResult = try await Subprocess.run( 44 | .path("/bin/bash"), 45 | arguments: ["-c", "ps -o pid,pgid,tpgid -p $$"], 46 | platformOptions: platformOptions, 47 | output: .string 48 | ) 49 | try assertNewSessionCreated(with: psResult) 50 | } 51 | 52 | @Test func testSubprocessPlatformOptionsProcessConfiguratorUpdateFileAction() async throws { 53 | guard #available(SubprocessSpan , *) else { 54 | return 55 | } 56 | let intendedWorkingDir = FileManager.default.temporaryDirectory.path() 57 | var platformOptions = PlatformOptions() 58 | platformOptions.preSpawnProcessConfigurator = { _, fileAttr in 59 | // Change the working directory 60 | intendedWorkingDir.withCString { path in 61 | _ = posix_spawn_file_actions_addchdir_np(&fileAttr, path) 62 | } 63 | } 64 | let pwdResult = try await Subprocess.run( 65 | .path("/bin/pwd"), 66 | platformOptions: platformOptions, 67 | output: .string 68 | ) 69 | #expect(pwdResult.terminationStatus.isSuccess) 70 | let currentDir = try #require( 71 | pwdResult.standardOutput 72 | ).trimmingCharacters(in: .whitespacesAndNewlines) 73 | // On Darwin, /var is linked to /private/var; /tmp is linked /private/tmp 74 | var expected = FilePath(intendedWorkingDir) 75 | if expected.starts(with: "/var") || expected.starts(with: "/tmp") { 76 | expected = FilePath("/private").appending(expected.components) 77 | } 78 | #expect(FilePath(currentDir) == expected) 79 | } 80 | 81 | @Test func testSuspendResumeProcess() async throws { 82 | guard #available(SubprocessSpan , *) else { 83 | return 84 | } 85 | _ = try await Subprocess.run( 86 | // This will intentionally hang 87 | .path("/bin/cat"), 88 | error: .discarded 89 | ) { subprocess, standardOutput in 90 | // First suspend the procss 91 | try subprocess.send(signal: .suspend) 92 | var suspendedStatus: Int32 = 0 93 | waitpid(subprocess.processIdentifier.value, &suspendedStatus, WNOHANG | WUNTRACED) 94 | #expect(_was_process_suspended(suspendedStatus) > 0) 95 | // Now resume the process 96 | try subprocess.send(signal: .resume) 97 | var resumedStatus: Int32 = 0 98 | waitpid(subprocess.processIdentifier.value, &resumedStatus, WNOHANG | WUNTRACED) 99 | #expect(_was_process_suspended(resumedStatus) == 0) 100 | 101 | // Now kill the process 102 | try subprocess.send(signal: .terminate) 103 | for try await _ in standardOutput {} 104 | } 105 | } 106 | } 107 | 108 | #endif // canImport(Darwin) 109 | -------------------------------------------------------------------------------- /Tests/SubprocessTests/SubprocessTests+Linting.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 | guard #available(SubprocessSpan , *) else { 49 | return 50 | } 51 | // META: Use Subprocess to run `swift-format` on self 52 | // to make sure it's properly linted 53 | guard 54 | let maybePath = CommandLine.arguments.first( 55 | where: { $0.contains("/.build/") } 56 | ) 57 | else { 58 | return 59 | } 60 | let sourcePath = String( 61 | maybePath.prefix(upTo: maybePath.range(of: "/.build")!.lowerBound) 62 | ) 63 | print("Linting \(sourcePath)") 64 | #if os(macOS) 65 | let configuration = Configuration( 66 | executable: .path("/usr/bin/xcrun"), 67 | arguments: ["swift-format", "lint", "-s", "--recursive", sourcePath] 68 | ) 69 | #else 70 | let configuration = Configuration( 71 | executable: .name("swift-format"), 72 | arguments: ["lint", "-s", "--recursive", sourcePath] 73 | ) 74 | #endif 75 | let lintResult = try await Subprocess.run( 76 | configuration, 77 | output: .discarded, 78 | error: .string 79 | ) 80 | #expect( 81 | lintResult.terminationStatus.isSuccess, 82 | "❌ `swift-format lint --recursive \(sourcePath)` failed" 83 | ) 84 | if let error = lintResult.standardError?.trimmingCharacters( 85 | in: .whitespacesAndNewlines 86 | ), !error.isEmpty { 87 | print("\(error)\n") 88 | } else { 89 | print("✅ Linting passed") 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/SubprocessTests/SubprocessTests+Linux.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(Glibc) || canImport(Bionic) || canImport(Musl) 13 | 14 | #if canImport(Bionic) 15 | import Bionic 16 | #elseif canImport(Glibc) 17 | import Glibc 18 | #elseif canImport(Musl) 19 | import Musl 20 | #endif 21 | 22 | import FoundationEssentials 23 | 24 | import Testing 25 | @testable import Subprocess 26 | 27 | // MARK: PlatformOption Tests 28 | @Suite(.serialized) 29 | struct SubprocessLinuxTests { 30 | @Test func testSubprocessPlatfomOptionsPreSpawnProcessConfigurator() async throws { 31 | var platformOptions = PlatformOptions() 32 | platformOptions.preSpawnProcessConfigurator = { 33 | setgid(4321) 34 | } 35 | let idResult = try await Subprocess.run( 36 | .path("/usr/bin/id"), 37 | arguments: ["-g"], 38 | platformOptions: platformOptions, 39 | output: .string 40 | ) 41 | #expect(idResult.terminationStatus.isSuccess) 42 | let id = try #require(idResult.standardOutput) 43 | #expect( 44 | id.trimmingCharacters(in: .whitespacesAndNewlines) == "\(4321)" 45 | ) 46 | } 47 | 48 | @Test func testSuspendResumeProcess() async throws { 49 | func isProcessSuspended(_ pid: pid_t) throws -> Bool { 50 | let status = try Data( 51 | contentsOf: URL(filePath: "/proc/\(pid)/status") 52 | ) 53 | let statusString = try #require( 54 | String(data: status, encoding: .utf8) 55 | ) 56 | // Parse the status string 57 | let stats = statusString.split(separator: "\n") 58 | if let index = stats.firstIndex( 59 | where: { $0.hasPrefix("State:") } 60 | ) { 61 | let processState = stats[index].split( 62 | separator: ":" 63 | ).map { 64 | $0.trimmingCharacters( 65 | in: .whitespacesAndNewlines 66 | ) 67 | } 68 | 69 | return processState[1].hasPrefix("T") 70 | } 71 | return false 72 | } 73 | 74 | _ = try await Subprocess.run( 75 | // This will intentionally hang 76 | .path("/usr/bin/sleep"), 77 | arguments: ["infinity"], 78 | error: .discarded 79 | ) { subprocess, standardOutput in 80 | // First suspend the procss 81 | try subprocess.send(signal: .suspend) 82 | #expect( 83 | try isProcessSuspended(subprocess.processIdentifier.value) 84 | ) 85 | // Now resume the process 86 | try subprocess.send(signal: .resume) 87 | #expect( 88 | try isProcessSuspended(subprocess.processIdentifier.value) == false 89 | ) 90 | // Now kill the process 91 | try subprocess.send(signal: .terminate) 92 | for try await _ in standardOutput {} 93 | } 94 | } 95 | } 96 | 97 | #endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl) 98 | -------------------------------------------------------------------------------- /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 | internal func randomString(length: Int, lettersOnly: Bool = false) -> String { 21 | let letters: String 22 | if lettersOnly { 23 | letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 24 | } else { 25 | letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 26 | } 27 | return String((0.. Bool { 31 | guard lhs != rhs else { 32 | return true 33 | } 34 | var canonicalLhs: String = (try? FileManager.default.destinationOfSymbolicLink(atPath: lhs)) ?? lhs 35 | var canonicalRhs: String = (try? FileManager.default.destinationOfSymbolicLink(atPath: rhs)) ?? rhs 36 | if !canonicalLhs.starts(with: "/") { 37 | canonicalLhs = "/\(canonicalLhs)" 38 | } 39 | if !canonicalRhs.starts(with: "/") { 40 | canonicalRhs = "/\(canonicalRhs)" 41 | } 42 | 43 | return canonicalLhs == canonicalRhs 44 | } 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | import WinSDK 14 | #endif 15 | 16 | // Confitionally 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 | --------------------------------------------------------------------------------