├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── swift.yml ├── .gitignore ├── .spi.yml ├── .swift-format ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Package.swift ├── Package@swift-6.0.swift ├── Proposal.md ├── 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/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: [macos-latest, ubuntu-latest] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: swift build -v 21 | - name: Run tests 22 | run: swift test -v 23 | -------------------------------------------------------------------------------- /.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 | # .swiftpm 50 | 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 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [Subprocess] 5 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy" : { 3 | "accessLevel" : "private" 4 | }, 5 | "indentation" : { 6 | "spaces" : 4 7 | }, 8 | "indentConditionalCompilationBlocks" : false, 9 | "indentSwitchCaseLabels" : false, 10 | "lineBreakAroundMultilineExpressionChainComponents" : false, 11 | "lineBreakBeforeControlFlowKeywords" : false, 12 | "lineBreakBeforeEachArgument" : true, 13 | "lineBreakBeforeEachGenericRequirement" : true, 14 | "lineLength" : 120, 15 | "maximumBlankLines" : 1, 16 | "prioritizeKeepingFunctionOutputTogether" : true, 17 | "respectsExistingLineBreaks" : true, 18 | "rules" : { 19 | "AllPublicDeclarationsHaveDocumentation" : false, 20 | "AlwaysUseLowerCamelCase" : false, 21 | "AmbiguousTrailingClosureOverload" : true, 22 | "BeginDocumentationCommentWithOneLineSummary" : false, 23 | "DoNotUseSemicolons" : true, 24 | "DontRepeatTypeInStaticProperties" : true, 25 | "FileScopedDeclarationPrivacy" : true, 26 | "FullyIndirectEnum" : true, 27 | "GroupNumericLiterals" : true, 28 | "IdentifiersMustBeASCII" : true, 29 | "NeverForceUnwrap" : false, 30 | "NeverUseForceTry" : false, 31 | "NeverUseImplicitlyUnwrappedOptionals" : false, 32 | "NoAccessLevelOnExtensionDeclaration" : true, 33 | "NoAssignmentInExpressions" : true, 34 | "NoBlockComments" : true, 35 | "NoCasesWithOnlyFallthrough" : true, 36 | "NoEmptyTrailingClosureParentheses" : true, 37 | "NoLabelsInCasePatterns" : false, 38 | "NoLeadingUnderscores" : false, 39 | "NoParensAroundConditions" : true, 40 | "NoVoidReturnOnFunctionSignature" : true, 41 | "OneCasePerLine" : true, 42 | "OneVariableDeclarationPerLine" : true, 43 | "OnlyOneTrailingClosureArgument" : true, 44 | "OrderedImports" : false, 45 | "ReturnVoidInsteadOfEmptyTuple" : true, 46 | "UseEarlyExits" : true, 47 | "UseLetInEveryBoundCaseVariable" : false, 48 | "UseShorthandTypeNames" : true, 49 | "UseSingleLinePropertyGetter" : false, 50 | "UseSynthesizedInitializer" : false, 51 | "UseTripleSlashForDocumentationComments" : true, 52 | "UseWhereClausesInForLoops" : false, 53 | "ValidateDocumentationComments" : false 54 | }, 55 | "spacesAroundRangeFormationOperators" : false, 56 | "tabWidth" : 4, 57 | "version" : 1 58 | } -------------------------------------------------------------------------------- /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", 8 | ) 9 | 10 | var dep: [Package.Dependency] = [ 11 | .package( 12 | url: "https://github.com/apple/swift-system", 13 | from: "1.0.0" 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)], 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 | -------------------------------------------------------------------------------- /Package@swift-6.0.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 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" 8 | ) 9 | 10 | let package = Package( 11 | name: "Subprocess", 12 | platforms: [.macOS(.v13)], 13 | products: [ 14 | .library( 15 | name: "Subprocess", 16 | targets: ["Subprocess"] 17 | ) 18 | ], 19 | dependencies: [ 20 | .package( 21 | url: "https://github.com/apple/swift-system", 22 | from: "1.0.0" 23 | ), 24 | .package( 25 | url: "https://github.com/apple/swift-docc-plugin", 26 | from: "1.4.3" 27 | ), 28 | ], 29 | targets: [ 30 | .target( 31 | name: "Subprocess", 32 | dependencies: [ 33 | "_SubprocessCShims", 34 | .product(name: "SystemPackage", package: "swift-system"), 35 | ], 36 | path: "Sources/Subprocess", 37 | exclude: [ 38 | "Span+Subprocess.swift", 39 | "SubprocessFoundation/Span+SubprocessFoundation.swift", 40 | ], 41 | swiftSettings: [ 42 | .enableExperimentalFeature("StrictConcurrency"), 43 | .define("SubprocessFoundation"), 44 | availabilityMacro, 45 | ] 46 | ), 47 | .testTarget( 48 | name: "SubprocessTests", 49 | dependencies: [ 50 | "_SubprocessCShims", 51 | "Subprocess", 52 | "TestResources", 53 | .product(name: "SystemPackage", package: "swift-system"), 54 | ], 55 | swiftSettings: [ 56 | availabilityMacro 57 | ] 58 | ), 59 | 60 | .target( 61 | name: "TestResources", 62 | dependencies: [ 63 | .product(name: "SystemPackage", package: "swift-system") 64 | ], 65 | path: "Tests/TestResources", 66 | resources: [ 67 | .copy("Resources") 68 | ] 69 | ), 70 | 71 | .target( 72 | name: "_SubprocessCShims", 73 | path: "Sources/_SubprocessCShims" 74 | ), 75 | ] 76 | ) 77 | -------------------------------------------------------------------------------- /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: "Subprocess") 25 | ] 26 | ) 27 | ``` 28 | 29 | On Swift 6.1 and above, `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 | 35 | ### Swift Versions 36 | 37 | The minimal supported Swift version is Swift 6.0. 38 | 39 | 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). 40 | 41 | 42 | ## Feature Overview 43 | 44 | ### Run and Asynchonously Collect Output 45 | 46 | The easiest way to spawn a process with `Subprocess` is to simply run it and await its `CollectedResult`: 47 | 48 | ```swift 49 | import Subprocess 50 | 51 | let result = try await run(.name("ls")) 52 | 53 | print(result.processIdentifier) // prints 1234 54 | print(result.terminationStatus) // prints exited(0) 55 | 56 | print(result.standardOutput) // prints LICENSE Package.swift ... 57 | ``` 58 | 59 | ### Run with Custom Closure 60 | 61 | 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`: 62 | 63 | ```swift 64 | import Subprocess 65 | 66 | let result = try await run( 67 | .path("/bin/dd"), 68 | arguments: ["if=/path/to/document"] 69 | ) { execution in 70 | var contents = "" 71 | for try await chunk in execution.standardOutput { 72 | let string = chunk.withUnsafeBytes { String(decoding: $0, as: UTF8.self) } 73 | contents += string 74 | if string == "Done" { 75 | // Stop execution 76 | await execution.teardown( 77 | using: [ 78 | .gracefulShutDown( 79 | allowedDurationToNextStep: .seconds(0.5) 80 | ) 81 | ] 82 | ) 83 | return contents 84 | } 85 | } 86 | return contents 87 | } 88 | ``` 89 | 90 | ### Running Unmonitored Processes 91 | 92 | 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. 93 | 94 | ```swift 95 | import Subprocess 96 | 97 | let input: FileDescriptor = ... 98 | 99 | input.closeAfter { 100 | let pid = try runDetached(.path("/bin/daemon"), input: input) 101 | // ... other opeartions 102 | } 103 | ``` 104 | 105 | ### Customizable Execution 106 | 107 | You can set various parameters when running the child process, such as `Arguments`, `Environment`, and working directory: 108 | 109 | ```swift 110 | import Subprocess 111 | 112 | let result = try await run( 113 | .path("/bin/ls"), 114 | arguments: ["-a"], 115 | // Inherit the environment values from parent process and 116 | // add `NewKey=NewValue` 117 | environment: .inherit.updating(["NewKey": "NewValue"]), 118 | workingDirectory: "/Users/", 119 | ) 120 | ``` 121 | 122 | ### Platform Specific Options and “Escape Hatches” 123 | 124 | `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. 125 | 126 | `PlatformOptions` also allows access to platform-specific spawning constructs and customizations via a closure. 127 | 128 | ```swift 129 | import Darwin 130 | import Subprocess 131 | 132 | var platformOptions = PlatformOptions() 133 | let intendedWorkingDir = "/path/to/directory" 134 | platformOptions.preSpawnProcessConfigurator = { spawnAttr, fileAttr in 135 | // Set POSIX_SPAWN_SETSID flag, which implies calls 136 | // to setsid 137 | var flags: Int16 = 0 138 | posix_spawnattr_getflags(&spawnAttr, &flags) 139 | posix_spawnattr_setflags(&spawnAttr, flags | Int16(POSIX_SPAWN_SETSID)) 140 | 141 | // Change the working directory 142 | intendedWorkingDir.withCString { path in 143 | _ = posix_spawn_file_actions_addchdir_np(&fileAttr, path) 144 | } 145 | } 146 | 147 | let result = try await run(.path("/bin/exe"), platformOptions: platformOptions) 148 | ``` 149 | 150 | 151 | ### Flexible Input and Output Configurations 152 | 153 | By default, `Subprocess`: 154 | - Doesn’t send any input to the child process’s standard input 155 | - Captures the child process’s standard output as a `String`, up to 128kB 156 | - Ignores the child process’s standard error 157 | 158 | You can tailor how `Subprocess` handles the standard input, standard output, and standard error by setting the `input`, `output`, and `error` parameters: 159 | 160 | ```swift 161 | let content = "Hello Subprocess" 162 | 163 | // Send "Hello Subprocess" to the standard input of `cat` 164 | let result = try await run(.name("cat"), input: .string(content, using: UTF8.self)) 165 | 166 | // Collect both standard error and standard output as Data 167 | let result = try await run(.name("cat"), output: .data, error: .data) 168 | ``` 169 | 170 | `Subprocess` supports these input options: 171 | 172 | #### `NoInput` 173 | 174 | This option means no input is sent to the subprocess. 175 | 176 | Use it by setting `.none` for `input`. 177 | 178 | #### `FileDescriptorInput` 179 | 180 | 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. 181 | 182 | Use it by setting `.fileDescriptor(closeAfterSpawningProcess:)` for `input`. 183 | 184 | #### `StringInput` 185 | 186 | This option reads input from a type conforming to `StringProtocol` using the specified encoding. 187 | 188 | Use it by setting `.string(using:)` for `input`. 189 | 190 | #### `ArrayInput` 191 | 192 | This option reads input from an array of `UInt8`. 193 | 194 | Use it by setting `.array` for `input`. 195 | 196 | #### `DataInput` (available with `SubprocessFoundation` trait) 197 | 198 | This option reads input from a given `Data`. 199 | 200 | Use it by setting `.data` for `input`. 201 | 202 | #### `DataSequenceInput` (available with `SubprocessFoundation` trait) 203 | 204 | This option reads input from a sequence of `Data`. 205 | 206 | Use it by setting `.sequence` for `input`. 207 | 208 | #### `DataAsyncSequenceInput` (available with `SubprocessFoundation` trait) 209 | 210 | This option reads input from an async sequence of `Data`. 211 | 212 | Use it by setting `.asyncSequence` for `input`. 213 | 214 | --- 215 | 216 | `Subprocess` also supports these output options: 217 | 218 | #### `DiscardedOutput` 219 | 220 | This option means the `Subprocess` won’t collect or redirect output from the child process. 221 | 222 | Use it by setting `.discarded` for `input` or `error`. 223 | 224 | #### `FileDescriptorOutput` 225 | 226 | This option writes output to a specified `FileDescriptor`. You can choose to have the `Subprocess` close the file descriptor after spawning. 227 | 228 | Use it by setting `.fileDescriptor(closeAfterSpawningProcess:)` for `input` or `error`. 229 | 230 | #### `StringOutput` 231 | 232 | This option collects output as a `String` with the given encoding. 233 | 234 | Use it by setting `.string` or `.string(limit:encoding:)` for `input` or `error`. 235 | 236 | #### `BytesOutput` 237 | 238 | This option collects output as `[UInt8]`. 239 | 240 | Use it by setting `.bytes` or `.bytes(limit:)` for `input` or `error`. 241 | 242 | #### `SequenceOutput`: 243 | 244 | 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. 245 | 246 | 247 | ### Cross-platform support 248 | 249 | `Subprocess` works on macOS, Linux, and Windows, with feature parity on all platforms as well as platform-specific options for each. 250 | 251 | The table below describes the current level of support that Subprocess has for various platforms: 252 | 253 | | **Platform** | **Support Status** | 254 | |---|---| 255 | | **macOS** | Supported | 256 | | **iOS** | Not supported | 257 | | **watchOS** | Not supported | 258 | | **tvOS** | Not supported | 259 | | **visionOS** | Not supported | 260 | | **Ubuntu 22.04** | Supported | 261 | | **Windows** | Supported | 262 | 263 |

(back to top)

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

(back to top)

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

(back to top)

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

(back to top)

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

(back to top)

303 | -------------------------------------------------------------------------------- /Sources/Subprocess/API.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 | import System 14 | #else 15 | @preconcurrency import SystemPackage 16 | #endif 17 | 18 | // MARK: - Collected Result 19 | 20 | /// Run a executable with given parameters asynchrously and returns 21 | /// a `CollectedResult` containing the output of the child process. 22 | /// - Parameters: 23 | /// - executable: The executable to run. 24 | /// - arguments: The arguments to pass to the executable. 25 | /// - environment: The environment in which to run the executable. 26 | /// - workingDirectory: The working directory in which to run the executable. 27 | /// - platformOptions: The platform specific options to use 28 | /// when running the executable. 29 | /// - input: The input to send to the executable. 30 | /// - output: The method to use for redirecting the standard output. 31 | /// - error: The method to use for redirecting the standard error. 32 | /// - Returns a CollectedResult containing the result of the run. 33 | #if SubprocessSpan 34 | @available(SubprocessSpan, *) 35 | #endif 36 | public func run< 37 | Input: InputProtocol, 38 | Output: OutputProtocol, 39 | Error: OutputProtocol 40 | >( 41 | _ executable: Executable, 42 | arguments: Arguments = [], 43 | environment: Environment = .inherit, 44 | workingDirectory: FilePath? = nil, 45 | platformOptions: PlatformOptions = PlatformOptions(), 46 | input: Input = .none, 47 | output: Output = .string, 48 | error: Error = .discarded 49 | ) async throws -> CollectedResult { 50 | let configuration = Configuration( 51 | executable: executable, 52 | arguments: arguments, 53 | environment: environment, 54 | workingDirectory: workingDirectory, 55 | platformOptions: platformOptions 56 | ) 57 | return try await run( 58 | configuration, 59 | input: input, 60 | output: output, 61 | error: error 62 | ) 63 | } 64 | 65 | /// Run a executable with given parameters asynchrously and returns 66 | /// a `CollectedResult` containing the output of the child process. 67 | /// - Parameters: 68 | /// - executable: The executable to run. 69 | /// - arguments: The arguments to pass to the executable. 70 | /// - environment: The environment in which to run the executable. 71 | /// - workingDirectory: The working directory in which to run the executable. 72 | /// - platformOptions: The platform specific options to use 73 | /// when running the executable. 74 | /// - input: span to write to subprocess' standard input. 75 | /// - output: The method to use for redirecting the standard output. 76 | /// - error: The method to use for redirecting the standard error. 77 | /// - Returns a CollectedResult containing the result of the run. 78 | #if SubprocessSpan 79 | @available(SubprocessSpan, *) 80 | public func run< 81 | InputElement: BitwiseCopyable, 82 | Output: OutputProtocol, 83 | Error: OutputProtocol 84 | >( 85 | _ executable: Executable, 86 | arguments: Arguments = [], 87 | environment: Environment = .inherit, 88 | workingDirectory: FilePath? = nil, 89 | platformOptions: PlatformOptions = PlatformOptions(), 90 | input: borrowing Span, 91 | output: Output = .string, 92 | error: Error = .discarded 93 | ) async throws -> CollectedResult { 94 | return try await Configuration( 95 | executable: executable, 96 | arguments: arguments, 97 | environment: environment, 98 | workingDirectory: workingDirectory, 99 | platformOptions: platformOptions 100 | ).run(input: input, output: output, error: error) 101 | } 102 | #endif // SubprocessSpan 103 | 104 | // MARK: - Custom Execution Body 105 | 106 | /// Run a executable with given parameters and a custom closure 107 | /// to manage the running subprocess' lifetime and its IOs. 108 | /// - Parameters: 109 | /// - executable: The executable to run. 110 | /// - arguments: The arguments to pass to the executable. 111 | /// - environment: The environment in which to run the executable. 112 | /// - workingDirectory: The working directory in which to run the executable. 113 | /// - platformOptions: The platform specific options to use 114 | /// when running the executable. 115 | /// - input: The input to send to the executable. 116 | /// - output: How to manage the executable standard ouput. 117 | /// - error: How to manager executable standard error. 118 | /// - isolation: the isolation context to run the body closure. 119 | /// - body: The custom execution body to manually control the running process 120 | /// - Returns a ExecutableResult type containing the return value 121 | /// of the closure. 122 | #if SubprocessSpan 123 | @available(SubprocessSpan, *) 124 | #endif 125 | public func run( 126 | _ executable: Executable, 127 | arguments: Arguments = [], 128 | environment: Environment = .inherit, 129 | workingDirectory: FilePath? = nil, 130 | platformOptions: PlatformOptions = PlatformOptions(), 131 | input: Input = .none, 132 | output: Output, 133 | error: Error, 134 | isolation: isolated (any Actor)? = #isolation, 135 | body: ((Execution) async throws -> Result) 136 | ) async throws -> ExecutionResult where Output.OutputType == Void, Error.OutputType == Void { 137 | return try await Configuration( 138 | executable: executable, 139 | arguments: arguments, 140 | environment: environment, 141 | workingDirectory: workingDirectory, 142 | platformOptions: platformOptions 143 | ) 144 | .run(input: input, output: output, error: error, body) 145 | } 146 | 147 | /// Run a executable with given parameters and a custom closure 148 | /// to manage the running subprocess' lifetime and write to its 149 | /// standard input via `StandardInputWriter` 150 | /// - Parameters: 151 | /// - executable: The executable to run. 152 | /// - arguments: The arguments to pass to the executable. 153 | /// - environment: The environment in which to run the executable. 154 | /// - workingDirectory: The working directory in which to run the executable. 155 | /// - platformOptions: The platform specific options to use 156 | /// when running the executable. 157 | /// - output:How to handle executable's standard output 158 | /// - error: How to handle executable's standard error 159 | /// - isolation: the isolation context to run the body closure. 160 | /// - body: The custom execution body to manually control the running process 161 | /// - Returns a ExecutableResult type containing the return value 162 | /// of the closure. 163 | #if SubprocessSpan 164 | @available(SubprocessSpan, *) 165 | #endif 166 | public func run( 167 | _ executable: Executable, 168 | arguments: Arguments = [], 169 | environment: Environment = .inherit, 170 | workingDirectory: FilePath? = nil, 171 | platformOptions: PlatformOptions = PlatformOptions(), 172 | output: Output, 173 | error: Error, 174 | isolation: isolated (any Actor)? = #isolation, 175 | body: ((Execution, StandardInputWriter) async throws -> Result) 176 | ) async throws -> ExecutionResult where Output.OutputType == Void, Error.OutputType == Void { 177 | return try await Configuration( 178 | executable: executable, 179 | arguments: arguments, 180 | environment: environment, 181 | workingDirectory: workingDirectory, 182 | platformOptions: platformOptions 183 | ) 184 | .run(output: output, error: error, body) 185 | } 186 | 187 | // MARK: - Configuration Based 188 | 189 | /// Run a `Configuration` asynchrously and returns 190 | /// a `CollectedResult` containing the output of the child process. 191 | /// - Parameters: 192 | /// - configuration: The `Subprocess` configuration to run. 193 | /// - input: The input to send to the executable. 194 | /// - output: The method to use for redirecting the standard output. 195 | /// - error: The method to use for redirecting the standard error. 196 | /// - Returns a CollectedResult containing the result of the run. 197 | #if SubprocessSpan 198 | @available(SubprocessSpan, *) 199 | #endif 200 | public func run< 201 | Input: InputProtocol, 202 | Output: OutputProtocol, 203 | Error: OutputProtocol 204 | >( 205 | _ configuration: Configuration, 206 | input: Input = .none, 207 | output: Output = .string, 208 | error: Error = .discarded 209 | ) async throws -> CollectedResult { 210 | let result = try await configuration.run( 211 | input: input, 212 | output: output, 213 | error: error 214 | ) { execution in 215 | let ( 216 | standardOutput, 217 | standardError 218 | ) = try await execution.captureIOs() 219 | return ( 220 | processIdentifier: execution.processIdentifier, 221 | standardOutput: standardOutput, 222 | standardError: standardError 223 | ) 224 | } 225 | return CollectedResult( 226 | processIdentifier: result.value.processIdentifier, 227 | terminationStatus: result.terminationStatus, 228 | standardOutput: result.value.standardOutput, 229 | standardError: result.value.standardError 230 | ) 231 | } 232 | 233 | /// Run a executable with given parameters specified by a `Configuration` 234 | /// - Parameters: 235 | /// - configuration: The `Subprocess` configuration to run. 236 | /// - output: The method to use for redirecting the standard output. 237 | /// - error: The method to use for redirecting the standard error. 238 | /// - isolation: the isolation context to run the body closure. 239 | /// - body: The custom configuration body to manually control 240 | /// the running process and write to its standard input. 241 | /// - Returns a ExecutableResult type containing the return value 242 | /// of the closure. 243 | #if SubprocessSpan 244 | @available(SubprocessSpan, *) 245 | #endif 246 | public func run( 247 | _ configuration: Configuration, 248 | output: Output, 249 | error: Error, 250 | isolation: isolated (any Actor)? = #isolation, 251 | body: ((Execution, StandardInputWriter) async throws -> Result) 252 | ) async throws -> ExecutionResult where Output.OutputType == Void, Error.OutputType == Void { 253 | return try await configuration.run(output: output, error: error, body) 254 | } 255 | 256 | // MARK: - Detached 257 | 258 | /// Run a executable with given parameters and return its process 259 | /// identifier immediately without monitoring the state of the 260 | /// subprocess nor waiting until it exits. 261 | /// 262 | /// This method is useful for launching subprocesses that outlive their 263 | /// parents (for example, daemons and trampolines). 264 | /// 265 | /// - Parameters: 266 | /// - executable: The executable to run. 267 | /// - arguments: The arguments to pass to the executable. 268 | /// - environment: The environment to use for the process. 269 | /// - workingDirectory: The working directory for the process. 270 | /// - platformOptions: The platform specific options to use for the process. 271 | /// - input: A file descriptor to bind to the subprocess' standard input. 272 | /// - output: A file descriptor to bind to the subprocess' standard output. 273 | /// - error: A file descriptor to bind to the subprocess' standard error. 274 | /// - Returns: the process identifier for the subprocess. 275 | #if SubprocessSpan 276 | @available(SubprocessSpan, *) 277 | #endif 278 | public func runDetached( 279 | _ executable: Executable, 280 | arguments: Arguments = [], 281 | environment: Environment = .inherit, 282 | workingDirectory: FilePath? = nil, 283 | platformOptions: PlatformOptions = PlatformOptions(), 284 | input: FileDescriptor? = nil, 285 | output: FileDescriptor? = nil, 286 | error: FileDescriptor? = nil 287 | ) throws -> ProcessIdentifier { 288 | let config: Configuration = Configuration( 289 | executable: executable, 290 | arguments: arguments, 291 | environment: environment, 292 | workingDirectory: workingDirectory, 293 | platformOptions: platformOptions 294 | ) 295 | return try runDetached(config, input: input, output: output, error: error) 296 | } 297 | 298 | /// Run a executable with given configuration and return its process 299 | /// identifier immediately without monitoring the state of the 300 | /// subprocess nor waiting until it exits. 301 | /// 302 | /// This method is useful for launching subprocesses that outlive their 303 | /// parents (for example, daemons and trampolines). 304 | /// 305 | /// - Parameters: 306 | /// - configuration: The `Subprocess` configuration to run. 307 | /// - input: A file descriptor to bind to the subprocess' standard input. 308 | /// - output: A file descriptor to bind to the subprocess' standard output. 309 | /// - error: A file descriptor to bind to the subprocess' standard error. 310 | /// - Returns: the process identifier for the subprocess. 311 | #if SubprocessSpan 312 | @available(SubprocessSpan, *) 313 | #endif 314 | public func runDetached( 315 | _ configuration: Configuration, 316 | input: FileDescriptor? = nil, 317 | output: FileDescriptor? = nil, 318 | error: FileDescriptor? = nil 319 | ) throws -> ProcessIdentifier { 320 | switch (input, output, error) { 321 | case (.none, .none, .none): 322 | let processOutput = DiscardedOutput() 323 | let processError = DiscardedOutput() 324 | return try configuration.spawn( 325 | withInput: NoInput().createPipe(), 326 | output: processOutput, 327 | outputPipe: try processOutput.createPipe(), 328 | error: processError, 329 | errorPipe: try processError.createPipe() 330 | ).processIdentifier 331 | case (.none, .none, .some(let errorFd)): 332 | let processOutput = DiscardedOutput() 333 | let processError = FileDescriptorOutput(fileDescriptor: errorFd, closeAfterSpawningProcess: false) 334 | return try configuration.spawn( 335 | withInput: NoInput().createPipe(), 336 | output: processOutput, 337 | outputPipe: try processOutput.createPipe(), 338 | error: processError, 339 | errorPipe: try processError.createPipe() 340 | ).processIdentifier 341 | case (.none, .some(let outputFd), .none): 342 | let processOutput = FileDescriptorOutput(fileDescriptor: outputFd, closeAfterSpawningProcess: false) 343 | let processError = DiscardedOutput() 344 | return try configuration.spawn( 345 | withInput: NoInput().createPipe(), 346 | output: processOutput, 347 | outputPipe: try processOutput.createPipe(), 348 | error: processError, 349 | errorPipe: try processError.createPipe() 350 | ).processIdentifier 351 | case (.none, .some(let outputFd), .some(let errorFd)): 352 | let processOutput = FileDescriptorOutput( 353 | fileDescriptor: outputFd, 354 | closeAfterSpawningProcess: false 355 | ) 356 | let processError = FileDescriptorOutput( 357 | fileDescriptor: errorFd, 358 | closeAfterSpawningProcess: false 359 | ) 360 | return try configuration.spawn( 361 | withInput: NoInput().createPipe(), 362 | output: processOutput, 363 | outputPipe: try processOutput.createPipe(), 364 | error: processError, 365 | errorPipe: try processError.createPipe() 366 | ).processIdentifier 367 | case (.some(let inputFd), .none, .none): 368 | let processOutput = DiscardedOutput() 369 | let processError = DiscardedOutput() 370 | return try configuration.spawn( 371 | withInput: FileDescriptorInput( 372 | fileDescriptor: inputFd, 373 | closeAfterSpawningProcess: false 374 | ).createPipe(), 375 | output: processOutput, 376 | outputPipe: try processOutput.createPipe(), 377 | error: processError, 378 | errorPipe: try processError.createPipe() 379 | ).processIdentifier 380 | case (.some(let inputFd), .none, .some(let errorFd)): 381 | let processOutput = DiscardedOutput() 382 | let processError = FileDescriptorOutput( 383 | fileDescriptor: errorFd, 384 | closeAfterSpawningProcess: false 385 | ) 386 | return try configuration.spawn( 387 | withInput: FileDescriptorInput(fileDescriptor: inputFd, closeAfterSpawningProcess: false).createPipe(), 388 | output: processOutput, 389 | outputPipe: try processOutput.createPipe(), 390 | error: processError, 391 | errorPipe: try processError.createPipe() 392 | ).processIdentifier 393 | case (.some(let inputFd), .some(let outputFd), .none): 394 | let processOutput = FileDescriptorOutput( 395 | fileDescriptor: outputFd, 396 | closeAfterSpawningProcess: false 397 | ) 398 | let processError = DiscardedOutput() 399 | return try configuration.spawn( 400 | withInput: FileDescriptorInput(fileDescriptor: inputFd, closeAfterSpawningProcess: false).createPipe(), 401 | output: processOutput, 402 | outputPipe: try processOutput.createPipe(), 403 | error: processError, 404 | errorPipe: try processError.createPipe() 405 | ).processIdentifier 406 | case (.some(let inputFd), .some(let outputFd), .some(let errorFd)): 407 | let processOutput = FileDescriptorOutput( 408 | fileDescriptor: outputFd, 409 | closeAfterSpawningProcess: false 410 | ) 411 | let processError = FileDescriptorOutput( 412 | fileDescriptor: errorFd, 413 | closeAfterSpawningProcess: false 414 | ) 415 | return try configuration.spawn( 416 | withInput: FileDescriptorInput(fileDescriptor: inputFd, closeAfterSpawningProcess: false).createPipe(), 417 | output: processOutput, 418 | outputPipe: try processOutput.createPipe(), 419 | error: processError, 420 | errorPipe: try processError.createPipe() 421 | ).processIdentifier 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /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 | import System 14 | #else 15 | @preconcurrency import SystemPackage 16 | #endif 17 | 18 | #if SubprocessSpan 19 | @available(SubprocessSpan, *) 20 | #endif 21 | internal struct AsyncBufferSequence: AsyncSequence, Sendable { 22 | internal typealias Failure = any Swift.Error 23 | 24 | internal typealias Element = SequenceOutput.Buffer 25 | 26 | @_nonSendable 27 | internal struct Iterator: AsyncIteratorProtocol { 28 | internal typealias Element = SequenceOutput.Buffer 29 | 30 | private let fileDescriptor: TrackedFileDescriptor 31 | private var buffer: [UInt8] 32 | private var currentPosition: Int 33 | private var finished: Bool 34 | 35 | internal init(fileDescriptor: TrackedFileDescriptor) { 36 | self.fileDescriptor = fileDescriptor 37 | self.buffer = [] 38 | self.currentPosition = 0 39 | self.finished = false 40 | } 41 | 42 | internal mutating func next() async throws -> SequenceOutput.Buffer? { 43 | let data = try await self.fileDescriptor.wrapped.readChunk( 44 | upToLength: readBufferSize 45 | ) 46 | if data == nil { 47 | // We finished reading. Close the file descriptor now 48 | try self.fileDescriptor.safelyClose() 49 | return nil 50 | } 51 | return data 52 | } 53 | } 54 | 55 | private let fileDescriptor: TrackedFileDescriptor 56 | 57 | init(fileDescriptor: TrackedFileDescriptor) { 58 | self.fileDescriptor = fileDescriptor 59 | } 60 | 61 | internal func makeAsyncIterator() -> Iterator { 62 | return Iterator(fileDescriptor: self.fileDescriptor) 63 | } 64 | } 65 | 66 | // MARK: - Page Size 67 | import _SubprocessCShims 68 | 69 | #if canImport(Darwin) 70 | import Darwin 71 | internal import MachO.dyld 72 | 73 | private let _pageSize: Int = { 74 | Int(_subprocess_vm_size()) 75 | }() 76 | #elseif canImport(WinSDK) 77 | import WinSDK 78 | private let _pageSize: Int = { 79 | var sysInfo: SYSTEM_INFO = SYSTEM_INFO() 80 | GetSystemInfo(&sysInfo) 81 | return Int(sysInfo.dwPageSize) 82 | }() 83 | #elseif os(WASI) 84 | // WebAssembly defines a fixed page size 85 | private let _pageSize: Int = 65_536 86 | #elseif canImport(Android) 87 | @preconcurrency import Android 88 | private let _pageSize: Int = Int(getpagesize()) 89 | #elseif canImport(Glibc) 90 | @preconcurrency import Glibc 91 | private let _pageSize: Int = Int(getpagesize()) 92 | #elseif canImport(Musl) 93 | @preconcurrency import Musl 94 | private let _pageSize: Int = Int(getpagesize()) 95 | #elseif canImport(C) 96 | private let _pageSize: Int = Int(getpagesize()) 97 | #endif // canImport(Darwin) 98 | 99 | @inline(__always) 100 | internal var readBufferSize: Int { 101 | return _pageSize 102 | } 103 | -------------------------------------------------------------------------------- /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 SequenceOutput { 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 SequenceOutput.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 SequenceOutput.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 SequenceOutput.Buffer: Equatable, Hashable { 144 | #if os(Windows) 145 | // Compiler generated conformances 146 | #else 147 | public static func == (lhs: SequenceOutput.Buffer, rhs: SequenceOutput.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." 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 | 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 | import Synchronization 31 | 32 | /// An object that repersents a subprocess that has been 33 | /// executed. You can use this object to send signals to the 34 | /// child process as well as stream its output and error. 35 | #if SubprocessSpan 36 | @available(SubprocessSpan, *) 37 | #endif 38 | public final class Execution< 39 | Output: OutputProtocol, 40 | Error: OutputProtocol 41 | >: Sendable { 42 | /// The process identifier of the current execution 43 | public let processIdentifier: ProcessIdentifier 44 | 45 | internal let output: Output 46 | internal let error: Error 47 | internal let outputPipe: CreatedPipe 48 | internal let errorPipe: CreatedPipe 49 | internal let outputConsumptionState: Atomic 50 | #if os(Windows) 51 | internal let consoleBehavior: PlatformOptions.ConsoleBehavior 52 | 53 | init( 54 | processIdentifier: ProcessIdentifier, 55 | output: Output, 56 | error: Error, 57 | outputPipe: CreatedPipe, 58 | errorPipe: CreatedPipe, 59 | consoleBehavior: PlatformOptions.ConsoleBehavior 60 | ) { 61 | self.processIdentifier = processIdentifier 62 | self.output = output 63 | self.error = error 64 | self.outputPipe = outputPipe 65 | self.errorPipe = errorPipe 66 | self.outputConsumptionState = Atomic(0) 67 | self.consoleBehavior = consoleBehavior 68 | } 69 | #else 70 | init( 71 | processIdentifier: ProcessIdentifier, 72 | output: Output, 73 | error: Error, 74 | outputPipe: CreatedPipe, 75 | errorPipe: CreatedPipe 76 | ) { 77 | self.processIdentifier = processIdentifier 78 | self.output = output 79 | self.error = error 80 | self.outputPipe = outputPipe 81 | self.errorPipe = errorPipe 82 | self.outputConsumptionState = Atomic(0) 83 | } 84 | #endif // os(Windows) 85 | } 86 | 87 | #if SubprocessSpan 88 | @available(SubprocessSpan, *) 89 | #endif 90 | extension Execution where Output == SequenceOutput { 91 | /// The standard output of the subprocess. 92 | /// 93 | /// Accessing this property will **fatalError** if this property was 94 | /// accessed multiple times. Subprocess communicates with parent process 95 | /// via pipe under the hood and each pipe can only be consumed once. 96 | public var standardOutput: some AsyncSequence { 97 | let consumptionState = self.outputConsumptionState.bitwiseXor( 98 | OutputConsumptionState.standardOutputConsumed.rawValue, 99 | ordering: .relaxed 100 | ).newValue 101 | 102 | guard OutputConsumptionState(rawValue: consumptionState).contains(.standardOutputConsumed), 103 | let fd = self.outputPipe.readFileDescriptor 104 | else { 105 | fatalError("The standard output has already been consumed") 106 | } 107 | return AsyncBufferSequence(fileDescriptor: fd) 108 | } 109 | } 110 | 111 | #if SubprocessSpan 112 | @available(SubprocessSpan, *) 113 | #endif 114 | extension Execution where Error == SequenceOutput { 115 | /// The standard error of the subprocess. 116 | /// 117 | /// Accessing this property will **fatalError** if this property was 118 | /// accessed multiple times. Subprocess communicates with parent process 119 | /// via pipe under the hood and each pipe can only be consumed once. 120 | public var standardError: some AsyncSequence { 121 | let consumptionState = self.outputConsumptionState.bitwiseXor( 122 | OutputConsumptionState.standardErrorConsumed.rawValue, 123 | ordering: .relaxed 124 | ).newValue 125 | 126 | guard OutputConsumptionState(rawValue: consumptionState).contains(.standardErrorConsumed), 127 | let fd = self.errorPipe.readFileDescriptor 128 | else { 129 | fatalError("The standard output has already been consumed") 130 | } 131 | return AsyncBufferSequence(fileDescriptor: fd) 132 | } 133 | } 134 | 135 | // MARK: - Output Capture 136 | internal enum OutputCapturingState: Sendable { 137 | case standardOutputCaptured(Output) 138 | case standardErrorCaptured(Error) 139 | } 140 | 141 | internal struct OutputConsumptionState: OptionSet { 142 | typealias RawValue = UInt8 143 | 144 | internal let rawValue: UInt8 145 | 146 | internal init(rawValue: UInt8) { 147 | self.rawValue = rawValue 148 | } 149 | 150 | static let standardOutputConsumed: Self = .init(rawValue: 0b0001) 151 | static let standardErrorConsumed: Self = .init(rawValue: 0b0010) 152 | } 153 | 154 | internal typealias CapturedIOs< 155 | Output: Sendable, 156 | Error: Sendable 157 | > = (standardOutput: Output, standardError: Error) 158 | 159 | #if SubprocessSpan 160 | @available(SubprocessSpan, *) 161 | #endif 162 | extension Execution { 163 | internal func captureIOs() async throws -> CapturedIOs< 164 | Output.OutputType, Error.OutputType 165 | > { 166 | return try await withThrowingTaskGroup( 167 | of: OutputCapturingState.self 168 | ) { group in 169 | group.addTask { 170 | let stdout = try await self.output.captureOutput( 171 | from: self.outputPipe.readFileDescriptor 172 | ) 173 | return .standardOutputCaptured(stdout) 174 | } 175 | group.addTask { 176 | let stderr = try await self.error.captureOutput( 177 | from: self.errorPipe.readFileDescriptor 178 | ) 179 | return .standardErrorCaptured(stderr) 180 | } 181 | 182 | var stdout: Output.OutputType! 183 | var stderror: Error.OutputType! 184 | while let state = try await group.next() { 185 | switch state { 186 | case .standardOutputCaptured(let output): 187 | stdout = output 188 | case .standardErrorCaptured(let error): 189 | stderror = error 190 | } 191 | } 192 | return ( 193 | standardOutput: stdout, 194 | standardError: stderror 195 | ) 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /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 | 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 | public 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 let fileDescriptor: TrackedFileDescriptor 216 | 217 | init(fileDescriptor: TrackedFileDescriptor) { 218 | self.fileDescriptor = fileDescriptor 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.fileDescriptor.wrapped.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.fileDescriptor.wrapped.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.fileDescriptor.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 | 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: .init(devnull, closeWhenDone: true), 77 | writeFileDescriptor: nil 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 | private let encoding: Encoding.Type 129 | 130 | #if SubprocessSpan 131 | public func output(from span: RawSpan) throws -> String? { 132 | // FIXME: Span to String 133 | var array: [UInt8] = [] 134 | for index in 0..) throws -> String? { 141 | // FIXME: Span to String 142 | let array = Array(buffer) 143 | return String(decodingBytes: array, as: Encoding.self) 144 | } 145 | #endif 146 | 147 | internal init(limit: Int, encoding: Encoding.Type) { 148 | self.maxSize = limit 149 | self.encoding = encoding 150 | } 151 | } 152 | 153 | /// A concrete `Output` type for subprocesses that collects output 154 | /// from the subprocess as `[UInt8]`. This option must be used with 155 | /// the `run()` method that returns a `CollectedResult` 156 | #if SubprocessSpan 157 | @available(SubprocessSpan, *) 158 | #endif 159 | public struct BytesOutput: OutputProtocol { 160 | public typealias OutputType = [UInt8] 161 | public let maxSize: Int 162 | 163 | internal func captureOutput(from fileDescriptor: TrackedFileDescriptor?) async throws -> [UInt8] { 164 | return try await withCheckedThrowingContinuation { continuation in 165 | guard let fileDescriptor = fileDescriptor else { 166 | // Show not happen due to type system constraints 167 | fatalError("Trying to capture output without file descriptor") 168 | } 169 | fileDescriptor.wrapped.readUntilEOF(upToLength: self.maxSize) { result in 170 | switch result { 171 | case .success(let data): 172 | // FIXME: remove workaround for 173 | // rdar://143992296 174 | // https://github.com/swiftlang/swift-subprocess/issues/3 175 | #if os(Windows) 176 | continuation.resume(returning: data) 177 | #else 178 | continuation.resume(returning: data.array()) 179 | #endif 180 | case .failure(let error): 181 | continuation.resume(throwing: error) 182 | } 183 | } 184 | } 185 | } 186 | 187 | #if SubprocessSpan 188 | public func output(from span: RawSpan) throws -> [UInt8] { 189 | fatalError("Not implemented") 190 | } 191 | #else 192 | public func output(from buffer: some Sequence) throws -> [UInt8] { 193 | fatalError("Not implemented") 194 | } 195 | #endif 196 | 197 | internal init(limit: Int) { 198 | self.maxSize = limit 199 | } 200 | } 201 | 202 | /// A concrete `Output` type for subprocesses that redirects 203 | /// the child output to the `.standardOutput` (a sequence) or `.standardError` 204 | /// property of `Execution`. This output type is 205 | /// only applicable to the `run()` family that takes a custom closure. 206 | #if SubprocessSpan 207 | @available(SubprocessSpan, *) 208 | #endif 209 | public struct SequenceOutput: OutputProtocol { 210 | public typealias OutputType = Void 211 | 212 | internal init() {} 213 | } 214 | 215 | #if SubprocessSpan 216 | @available(SubprocessSpan, *) 217 | #endif 218 | extension OutputProtocol where Self == DiscardedOutput { 219 | /// Create a Subprocess output that discards the output 220 | public static var discarded: Self { .init() } 221 | } 222 | 223 | #if SubprocessSpan 224 | @available(SubprocessSpan, *) 225 | #endif 226 | extension OutputProtocol where Self == FileDescriptorOutput { 227 | /// Create a Subprocess output that writes output to a `FileDescriptor` 228 | /// and optionally close the `FileDescriptor` once process spawned. 229 | public static func fileDescriptor( 230 | _ fd: FileDescriptor, 231 | closeAfterSpawningProcess: Bool 232 | ) -> Self { 233 | return .init(fileDescriptor: fd, closeAfterSpawningProcess: closeAfterSpawningProcess) 234 | } 235 | } 236 | 237 | #if SubprocessSpan 238 | @available(SubprocessSpan, *) 239 | #endif 240 | extension OutputProtocol where Self == StringOutput { 241 | /// Create a `Subprocess` output that collects output as 242 | /// UTF8 String with 128kb limit. 243 | public static var string: Self { 244 | .init(limit: 128 * 1024, encoding: UTF8.self) 245 | } 246 | } 247 | 248 | #if SubprocessSpan 249 | @available(SubprocessSpan, *) 250 | #endif 251 | extension OutputProtocol { 252 | /// Create a `Subprocess` output that collects output as 253 | /// `String` using the given encoding up to limit it bytes. 254 | public static func string( 255 | limit: Int, 256 | encoding: Encoding.Type 257 | ) -> Self where Self == StringOutput { 258 | return .init(limit: limit, encoding: encoding) 259 | } 260 | } 261 | 262 | #if SubprocessSpan 263 | @available(SubprocessSpan, *) 264 | #endif 265 | extension OutputProtocol where Self == BytesOutput { 266 | /// Create a `Subprocess` output that collects output as 267 | /// `Buffer` with 128kb limit. 268 | public static var bytes: Self { .init(limit: 128 * 1024) } 269 | 270 | /// Create a `Subprocess` output that collects output as 271 | /// `Buffer` up to limit it bytes. 272 | public static func bytes(limit: Int) -> Self { 273 | return .init(limit: limit) 274 | } 275 | } 276 | 277 | #if SubprocessSpan 278 | @available(SubprocessSpan, *) 279 | #endif 280 | extension OutputProtocol where Self == SequenceOutput { 281 | /// Create a `Subprocess` output that redirects the output 282 | /// to the `.standardOutput` (or `.standardError`) property 283 | /// of `Execution` as `AsyncSequence`. 284 | public static var sequence: Self { .init() } 285 | } 286 | 287 | // MARK: - Span Default Implementations 288 | #if SubprocessSpan 289 | @available(SubprocessSpan, *) 290 | extension OutputProtocol { 291 | public func output(from buffer: some Sequence) throws -> OutputType { 292 | guard let rawBytes: UnsafeRawBufferPointer = buffer as? UnsafeRawBufferPointer else { 293 | fatalError("Unexpected input type passed: \(type(of: buffer))") 294 | } 295 | let span = RawSpan(_unsafeBytes: rawBytes) 296 | return try self.output(from: span) 297 | } 298 | } 299 | #endif 300 | 301 | // MARK: - Default Implementations 302 | #if SubprocessSpan 303 | @available(SubprocessSpan, *) 304 | #endif 305 | extension OutputProtocol { 306 | @_disfavoredOverload 307 | internal func createPipe() throws -> CreatedPipe { 308 | if let discard = self as? DiscardedOutput { 309 | return try discard.createPipe() 310 | } else if let fdOutput = self as? FileDescriptorOutput { 311 | return try fdOutput.createPipe() 312 | } 313 | // Base pipe based implementation for everything else 314 | return try CreatedPipe(closeWhenDone: true) 315 | } 316 | 317 | /// Capture the output from the subprocess up to maxSize 318 | @_disfavoredOverload 319 | internal func captureOutput( 320 | from fileDescriptor: TrackedFileDescriptor? 321 | ) async throws -> OutputType { 322 | if let bytesOutput = self as? BytesOutput { 323 | return try await bytesOutput.captureOutput(from: fileDescriptor) as! Self.OutputType 324 | } 325 | return try await withCheckedThrowingContinuation { continuation in 326 | if OutputType.self == Void.self { 327 | continuation.resume(returning: () as! OutputType) 328 | return 329 | } 330 | guard let fileDescriptor = fileDescriptor else { 331 | // Show not happen due to type system constraints 332 | fatalError("Trying to capture output without file descriptor") 333 | } 334 | 335 | fileDescriptor.wrapped.readUntilEOF(upToLength: self.maxSize) { result in 336 | do { 337 | switch result { 338 | case .success(let data): 339 | // FIXME: remove workaround for 340 | // rdar://143992296 341 | // https://github.com/swiftlang/swift-subprocess/issues/3 342 | let output = try self.output(from: data) 343 | continuation.resume(returning: output) 344 | case .failure(let error): 345 | continuation.resume(throwing: error) 346 | } 347 | } catch { 348 | continuation.resume(throwing: error) 349 | } 350 | } 351 | } 352 | } 353 | } 354 | 355 | #if SubprocessSpan 356 | @available(SubprocessSpan, *) 357 | #endif 358 | extension OutputProtocol where OutputType == Void { 359 | internal func captureOutput(from fileDescriptor: TrackedFileDescriptor?) async throws {} 360 | 361 | #if SubprocessSpan 362 | /// Convert the output from Data to expected output type 363 | public func output(from span: RawSpan) throws { 364 | // noop 365 | } 366 | #else 367 | public func output(from buffer: some Sequence) throws { 368 | // noop 369 | } 370 | #endif // SubprocessSpan 371 | } 372 | 373 | #if SubprocessSpan 374 | @available(SubprocessSpan, *) 375 | extension OutputProtocol { 376 | internal func output(from data: DispatchData) throws -> OutputType { 377 | guard !data.isEmpty else { 378 | let empty = UnsafeRawBufferPointer(start: nil, count: 0) 379 | let span = RawSpan(_unsafeBytes: empty) 380 | return try self.output(from: span) 381 | } 382 | 383 | return try data.withUnsafeBytes { ptr in 384 | let bufferPtr = UnsafeRawBufferPointer(start: ptr, count: data.count) 385 | let span = RawSpan(_unsafeBytes: bufferPtr) 386 | return try self.output(from: span) 387 | } 388 | } 389 | } 390 | #endif 391 | 392 | extension DispatchData { 393 | internal func array() -> [UInt8] { 394 | var result: [UInt8]? 395 | self.enumerateBytes { buffer, byteIndex, stop in 396 | let currentChunk = Array(UnsafeRawBufferPointer(buffer)) 397 | if result == nil { 398 | result = currentChunk 399 | } else { 400 | result?.append(contentsOf: currentChunk) 401 | } 402 | } 403 | return result ?? [] 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /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 | 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 | Output: OutputProtocol, 161 | Error: OutputProtocol 162 | >( 163 | withInput inputPipe: CreatedPipe, 164 | output: Output, 165 | outputPipe: CreatedPipe, 166 | error: Error, 167 | errorPipe: CreatedPipe 168 | ) throws -> Execution { 169 | // Instead of checking if every possible executable path 170 | // is valid, spawn each directly and catch ENOENT 171 | let possiblePaths = self.executable.possibleExecutablePaths( 172 | withPathValue: self.environment.pathValue() 173 | ) 174 | return try self.preSpawn { args throws -> Execution in 175 | let (env, uidPtr, gidPtr, supplementaryGroups) = args 176 | for possibleExecutablePath in possiblePaths { 177 | var pid: pid_t = 0 178 | 179 | // Setup Arguments 180 | let argv: [UnsafeMutablePointer?] = self.arguments.createArgs( 181 | withExecutablePath: possibleExecutablePath 182 | ) 183 | defer { 184 | for ptr in argv { ptr?.deallocate() } 185 | } 186 | 187 | // Setup file actions and spawn attributes 188 | var fileActions: posix_spawn_file_actions_t? = nil 189 | var spawnAttributes: posix_spawnattr_t? = nil 190 | // Setup stdin, stdout, and stderr 191 | posix_spawn_file_actions_init(&fileActions) 192 | defer { 193 | posix_spawn_file_actions_destroy(&fileActions) 194 | } 195 | // Input 196 | var result: Int32 = -1 197 | if let inputRead = inputPipe.readFileDescriptor { 198 | result = posix_spawn_file_actions_adddup2(&fileActions, inputRead.wrapped.rawValue, 0) 199 | guard result == 0 else { 200 | try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) 201 | throw SubprocessError( 202 | code: .init(.spawnFailed), 203 | underlyingError: .init(rawValue: result) 204 | ) 205 | } 206 | } 207 | if let inputWrite = inputPipe.writeFileDescriptor { 208 | // Close parent side 209 | result = posix_spawn_file_actions_addclose(&fileActions, inputWrite.wrapped.rawValue) 210 | guard result == 0 else { 211 | try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) 212 | throw SubprocessError( 213 | code: .init(.spawnFailed), 214 | underlyingError: .init(rawValue: result) 215 | ) 216 | } 217 | } 218 | // Output 219 | if let outputWrite = outputPipe.writeFileDescriptor { 220 | result = posix_spawn_file_actions_adddup2(&fileActions, outputWrite.wrapped.rawValue, 1) 221 | guard result == 0 else { 222 | try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) 223 | throw SubprocessError( 224 | code: .init(.spawnFailed), 225 | underlyingError: .init(rawValue: result) 226 | ) 227 | } 228 | } 229 | if let outputRead = outputPipe.readFileDescriptor { 230 | // Close parent side 231 | result = posix_spawn_file_actions_addclose(&fileActions, outputRead.wrapped.rawValue) 232 | guard result == 0 else { 233 | try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) 234 | throw SubprocessError( 235 | code: .init(.spawnFailed), 236 | underlyingError: .init(rawValue: result) 237 | ) 238 | } 239 | } 240 | // Error 241 | if let errorWrite = errorPipe.writeFileDescriptor { 242 | result = posix_spawn_file_actions_adddup2(&fileActions, errorWrite.wrapped.rawValue, 2) 243 | guard result == 0 else { 244 | try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) 245 | throw SubprocessError( 246 | code: .init(.spawnFailed), 247 | underlyingError: .init(rawValue: result) 248 | ) 249 | } 250 | } 251 | if let errorRead = errorPipe.readFileDescriptor { 252 | // Close parent side 253 | result = posix_spawn_file_actions_addclose(&fileActions, errorRead.wrapped.rawValue) 254 | guard result == 0 else { 255 | try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) 256 | throw SubprocessError( 257 | code: .init(.spawnFailed), 258 | underlyingError: .init(rawValue: result) 259 | ) 260 | } 261 | } 262 | // Setup spawnAttributes 263 | posix_spawnattr_init(&spawnAttributes) 264 | defer { 265 | posix_spawnattr_destroy(&spawnAttributes) 266 | } 267 | var noSignals = sigset_t() 268 | var allSignals = sigset_t() 269 | sigemptyset(&noSignals) 270 | sigfillset(&allSignals) 271 | posix_spawnattr_setsigmask(&spawnAttributes, &noSignals) 272 | posix_spawnattr_setsigdefault(&spawnAttributes, &allSignals) 273 | // Configure spawnattr 274 | var spawnAttributeError: Int32 = 0 275 | var flags: Int32 = POSIX_SPAWN_CLOEXEC_DEFAULT | POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF 276 | if let pgid = self.platformOptions.processGroupID { 277 | flags |= POSIX_SPAWN_SETPGROUP 278 | spawnAttributeError = posix_spawnattr_setpgroup(&spawnAttributes, pid_t(pgid)) 279 | } 280 | spawnAttributeError = posix_spawnattr_setflags(&spawnAttributes, Int16(flags)) 281 | // Set QualityOfService 282 | // spanattr_qos seems to only accept `QOS_CLASS_UTILITY` or `QOS_CLASS_BACKGROUND` 283 | // and returns an error of `EINVAL` if anything else is provided 284 | if spawnAttributeError == 0 && self.platformOptions.qualityOfService == .utility { 285 | spawnAttributeError = posix_spawnattr_set_qos_class_np(&spawnAttributes, QOS_CLASS_UTILITY) 286 | } else if spawnAttributeError == 0 && self.platformOptions.qualityOfService == .background { 287 | spawnAttributeError = posix_spawnattr_set_qos_class_np(&spawnAttributes, QOS_CLASS_BACKGROUND) 288 | } 289 | 290 | // Setup cwd 291 | let intendedWorkingDir = self.workingDirectory.string 292 | let chdirError: Int32 = intendedWorkingDir.withPlatformString { workDir in 293 | return posix_spawn_file_actions_addchdir_np(&fileActions, workDir) 294 | } 295 | 296 | // Error handling 297 | if chdirError != 0 || spawnAttributeError != 0 { 298 | try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) 299 | if spawnAttributeError != 0 { 300 | throw SubprocessError( 301 | code: .init(.spawnFailed), 302 | underlyingError: .init(rawValue: spawnAttributeError) 303 | ) 304 | } 305 | 306 | if chdirError != 0 { 307 | throw SubprocessError( 308 | code: .init(.spawnFailed), 309 | underlyingError: .init(rawValue: spawnAttributeError) 310 | ) 311 | } 312 | } 313 | // Run additional config 314 | if let spawnConfig = self.platformOptions.preSpawnProcessConfigurator { 315 | try spawnConfig(&spawnAttributes, &fileActions) 316 | } 317 | 318 | // Spawn 319 | let spawnError: CInt = possibleExecutablePath.withCString { exePath in 320 | return supplementaryGroups.withOptionalUnsafeBufferPointer { sgroups in 321 | return _subprocess_spawn( 322 | &pid, 323 | exePath, 324 | &fileActions, 325 | &spawnAttributes, 326 | argv, 327 | env, 328 | uidPtr, 329 | gidPtr, 330 | Int32(supplementaryGroups?.count ?? 0), 331 | sgroups?.baseAddress, 332 | self.platformOptions.createSession ? 1 : 0 333 | ) 334 | } 335 | } 336 | // Spawn error 337 | if spawnError != 0 { 338 | if spawnError == ENOENT { 339 | // Move on to another possible path 340 | continue 341 | } 342 | // Throw all other errors 343 | try self.cleanupPreSpawn( 344 | input: inputPipe, 345 | output: outputPipe, 346 | error: errorPipe 347 | ) 348 | throw SubprocessError( 349 | code: .init(.spawnFailed), 350 | underlyingError: .init(rawValue: spawnError) 351 | ) 352 | } 353 | return Execution( 354 | processIdentifier: .init(value: pid), 355 | output: output, 356 | error: error, 357 | outputPipe: outputPipe, 358 | errorPipe: errorPipe 359 | ) 360 | } 361 | 362 | // If we reach this point, it means either the executable path 363 | // or working directory is not valid. Since posix_spawn does not 364 | // provide which one is not valid, here we make a best effort guess 365 | // by checking whether the working directory is valid. This technically 366 | // still causes TOUTOC issue, but it's the best we can do for error recovery. 367 | try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) 368 | let workingDirectory = self.workingDirectory.string 369 | guard Configuration.pathAccessible(workingDirectory, mode: F_OK) else { 370 | throw SubprocessError( 371 | code: .init(.failedToChangeWorkingDirectory(workingDirectory)), 372 | underlyingError: .init(rawValue: ENOENT) 373 | ) 374 | } 375 | throw SubprocessError( 376 | code: .init(.executableNotFound(self.executable.description)), 377 | underlyingError: .init(rawValue: ENOENT) 378 | ) 379 | } 380 | } 381 | } 382 | 383 | // Special keys used in Error's user dictionary 384 | extension String { 385 | static let debugDescriptionErrorKey = "NSDebugDescription" 386 | } 387 | 388 | // MARK: - Process Monitoring 389 | @Sendable 390 | internal func monitorProcessTermination( 391 | forProcessWithIdentifier pid: ProcessIdentifier 392 | ) async throws -> TerminationStatus { 393 | return try await withCheckedThrowingContinuation { continuation in 394 | let source = DispatchSource.makeProcessSource( 395 | identifier: pid.value, 396 | eventMask: [.exit], 397 | queue: .global() 398 | ) 399 | source.setEventHandler { 400 | source.cancel() 401 | var siginfo = siginfo_t() 402 | let rc = waitid(P_PID, id_t(pid.value), &siginfo, WEXITED) 403 | guard rc == 0 else { 404 | continuation.resume( 405 | throwing: SubprocessError( 406 | code: .init(.failedToMonitorProcess), 407 | underlyingError: .init(rawValue: errno) 408 | ) 409 | ) 410 | return 411 | } 412 | switch siginfo.si_code { 413 | case .init(CLD_EXITED): 414 | continuation.resume(returning: .exited(siginfo.si_status)) 415 | return 416 | case .init(CLD_KILLED), .init(CLD_DUMPED): 417 | continuation.resume(returning: .unhandledException(siginfo.si_status)) 418 | case .init(CLD_TRAPPED), .init(CLD_STOPPED), .init(CLD_CONTINUED), .init(CLD_NOOP): 419 | // Ignore these signals because they are not related to 420 | // process exiting 421 | break 422 | default: 423 | fatalError("Unexpected exit status: \(siginfo.si_code)") 424 | } 425 | } 426 | source.resume() 427 | } 428 | } 429 | 430 | #endif // canImport(Darwin) 431 | -------------------------------------------------------------------------------- /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(Bionic) || canImport(Musl) 13 | 14 | #if canImport(System) 15 | import System 16 | #else 17 | @preconcurrency import SystemPackage 18 | #endif 19 | 20 | #if canImport(Glibc) 21 | import Glibc 22 | #elseif canImport(Bionic) 23 | import Bionic 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 | Output: OutputProtocol, 40 | Error: OutputProtocol 41 | >( 42 | withInput inputPipe: CreatedPipe, 43 | output: Output, 44 | outputPipe: CreatedPipe, 45 | error: Error, 46 | errorPipe: CreatedPipe 47 | ) throws -> Execution { 48 | _setupMonitorSignalHandler() 49 | 50 | // Instead of checking if every possible executable path 51 | // is valid, spawn each directly and catch ENOENT 52 | let possiblePaths = self.executable.possibleExecutablePaths( 53 | withPathValue: self.environment.pathValue() 54 | ) 55 | 56 | return try self.preSpawn { args throws -> Execution in 57 | let (env, uidPtr, gidPtr, supplementaryGroups) = args 58 | 59 | for possibleExecutablePath in possiblePaths { 60 | var processGroupIDPtr: UnsafeMutablePointer? = nil 61 | if let processGroupID = self.platformOptions.processGroupID { 62 | processGroupIDPtr = .allocate(capacity: 1) 63 | processGroupIDPtr?.pointee = gid_t(processGroupID) 64 | } 65 | // Setup Arguments 66 | let argv: [UnsafeMutablePointer?] = self.arguments.createArgs( 67 | withExecutablePath: possibleExecutablePath 68 | ) 69 | defer { 70 | for ptr in argv { ptr?.deallocate() } 71 | } 72 | // Setup input 73 | let fileDescriptors: [CInt] = [ 74 | inputPipe.readFileDescriptor?.wrapped.rawValue ?? -1, 75 | inputPipe.writeFileDescriptor?.wrapped.rawValue ?? -1, 76 | outputPipe.writeFileDescriptor?.wrapped.rawValue ?? -1, 77 | outputPipe.readFileDescriptor?.wrapped.rawValue ?? -1, 78 | errorPipe.writeFileDescriptor?.wrapped.rawValue ?? -1, 79 | errorPipe.readFileDescriptor?.wrapped.rawValue ?? -1, 80 | ] 81 | 82 | let workingDirectory: String = self.workingDirectory.string 83 | // Spawn 84 | var pid: pid_t = 0 85 | let spawnError: CInt = possibleExecutablePath.withCString { exePath in 86 | return workingDirectory.withCString { workingDir in 87 | return supplementaryGroups.withOptionalUnsafeBufferPointer { sgroups in 88 | return fileDescriptors.withUnsafeBufferPointer { fds in 89 | return _subprocess_fork_exec( 90 | &pid, 91 | exePath, 92 | workingDir, 93 | fds.baseAddress!, 94 | argv, 95 | env, 96 | uidPtr, 97 | gidPtr, 98 | processGroupIDPtr, 99 | CInt(supplementaryGroups?.count ?? 0), 100 | sgroups?.baseAddress, 101 | self.platformOptions.createSession ? 1 : 0, 102 | self.platformOptions.preSpawnProcessConfigurator 103 | ) 104 | } 105 | } 106 | } 107 | } 108 | // Spawn error 109 | if spawnError != 0 { 110 | if spawnError == ENOENT { 111 | // Move on to another possible path 112 | continue 113 | } 114 | // Throw all other errors 115 | try self.cleanupPreSpawn( 116 | input: inputPipe, 117 | output: outputPipe, 118 | error: errorPipe 119 | ) 120 | throw SubprocessError( 121 | code: .init(.spawnFailed), 122 | underlyingError: .init(rawValue: spawnError) 123 | ) 124 | } 125 | return Execution( 126 | processIdentifier: .init(value: pid), 127 | output: output, 128 | error: error, 129 | outputPipe: outputPipe, 130 | errorPipe: errorPipe 131 | ) 132 | } 133 | 134 | // If we reach this point, it means either the executable path 135 | // or working directory is not valid. Since posix_spawn does not 136 | // provide which one is not valid, here we make a best effort guess 137 | // by checking whether the working directory is valid. This technically 138 | // still causes TOUTOC issue, but it's the best we can do for error recovery. 139 | try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) 140 | let workingDirectory = self.workingDirectory.string 141 | guard Configuration.pathAccessible(workingDirectory, mode: F_OK) else { 142 | throw SubprocessError( 143 | code: .init(.failedToChangeWorkingDirectory(workingDirectory)), 144 | underlyingError: .init(rawValue: ENOENT) 145 | ) 146 | } 147 | throw SubprocessError( 148 | code: .init(.executableNotFound(self.executable.description)), 149 | underlyingError: .init(rawValue: ENOENT) 150 | ) 151 | } 152 | } 153 | } 154 | 155 | // MARK: - Platform Specific Options 156 | 157 | /// The collection of platform-specific settings 158 | /// to configure the subprocess when running 159 | public struct PlatformOptions: Sendable { 160 | // Set user ID for the subprocess 161 | public var userID: uid_t? = nil 162 | /// Set the real and effective group ID and the saved 163 | /// set-group-ID of the subprocess, equivalent to calling 164 | /// `setgid()` on the child process. 165 | /// Group ID is used to control permissions, particularly 166 | /// for file access. 167 | public var groupID: gid_t? = nil 168 | // Set list of supplementary group IDs for the subprocess 169 | public var supplementaryGroups: [gid_t]? = nil 170 | /// Set the process group for the subprocess, equivalent to 171 | /// calling `setpgid()` on the child process. 172 | /// Process group ID is used to group related processes for 173 | /// controlling signals. 174 | public var processGroupID: pid_t? = nil 175 | // Creates a session and sets the process group ID 176 | // i.e. Detach from the terminal. 177 | public var createSession: Bool = false 178 | /// An ordered list of steps in order to tear down the child 179 | /// process in case the parent task is cancelled before 180 | /// the child proces terminates. 181 | /// Always ends in sending a `.kill` signal at the end. 182 | public var teardownSequence: [TeardownStep] = [] 183 | /// A closure to configure platform-specific 184 | /// spawning constructs. This closure enables direct 185 | /// configuration or override of underlying platform-specific 186 | /// spawn settings that `Subprocess` utilizes internally, 187 | /// in cases where Subprocess does not provide higher-level 188 | /// APIs for such modifications. 189 | /// 190 | /// On Linux, Subprocess uses `fork/exec` as the 191 | /// underlying spawning mechanism. This closure is called 192 | /// after `fork()` but before `exec()`. You may use it to 193 | /// call any necessary process setup functions. 194 | public var preSpawnProcessConfigurator: (@convention(c) @Sendable () -> Void)? = nil 195 | 196 | public init() {} 197 | } 198 | 199 | extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible { 200 | internal func description(withIndent indent: Int) -> String { 201 | let indent = String(repeating: " ", count: indent * 4) 202 | return """ 203 | PlatformOptions( 204 | \(indent) userID: \(String(describing: userID)), 205 | \(indent) groupID: \(String(describing: groupID)), 206 | \(indent) supplementaryGroups: \(String(describing: supplementaryGroups)), 207 | \(indent) processGroupID: \(String(describing: processGroupID)), 208 | \(indent) createSession: \(createSession), 209 | \(indent) preSpawnProcessConfigurator: \(self.preSpawnProcessConfigurator == nil ? "not set" : "set") 210 | \(indent)) 211 | """ 212 | } 213 | 214 | public var description: String { 215 | return self.description(withIndent: 0) 216 | } 217 | 218 | public var debugDescription: String { 219 | return self.description(withIndent: 0) 220 | } 221 | } 222 | 223 | // Special keys used in Error's user dictionary 224 | extension String { 225 | static let debugDescriptionErrorKey = "DebugDescription" 226 | } 227 | 228 | // MARK: - Process Monitoring 229 | @Sendable 230 | internal func monitorProcessTermination( 231 | forProcessWithIdentifier pid: ProcessIdentifier 232 | ) async throws -> TerminationStatus { 233 | return try await withCheckedThrowingContinuation { continuation in 234 | _childProcessContinuations.withLock { continuations in 235 | if let existing = continuations.removeValue(forKey: pid.value), 236 | case .status(let existingStatus) = existing 237 | { 238 | // We already have existing status to report 239 | continuation.resume(returning: existingStatus) 240 | } else { 241 | // Save the continuation for handler 242 | continuations[pid.value] = .continuation(continuation) 243 | } 244 | } 245 | } 246 | } 247 | 248 | private enum ContinuationOrStatus { 249 | case continuation(CheckedContinuation) 250 | case status(TerminationStatus) 251 | } 252 | 253 | private let _childProcessContinuations: 254 | Mutex< 255 | [pid_t: ContinuationOrStatus] 256 | > = Mutex([:]) 257 | 258 | private let signalSource: SendableSourceSignal = SendableSourceSignal() 259 | 260 | private let setup: () = { 261 | signalSource.setEventHandler { 262 | _childProcessContinuations.withLock { continuations in 263 | while true { 264 | var siginfo = siginfo_t() 265 | guard waitid(P_ALL, id_t(0), &siginfo, WEXITED) == 0 else { 266 | return 267 | } 268 | var status: TerminationStatus? = nil 269 | switch siginfo.si_code { 270 | case .init(CLD_EXITED): 271 | status = .exited(siginfo._sifields._sigchld.si_status) 272 | case .init(CLD_KILLED), .init(CLD_DUMPED): 273 | status = .unhandledException(siginfo._sifields._sigchld.si_status) 274 | case .init(CLD_TRAPPED), .init(CLD_STOPPED), .init(CLD_CONTINUED): 275 | // Ignore these signals because they are not related to 276 | // process exiting 277 | break 278 | default: 279 | fatalError("Unexpected exit status: \(siginfo.si_code)") 280 | } 281 | if let status = status { 282 | let pid = siginfo._sifields._sigchld.si_pid 283 | if let existing = continuations.removeValue(forKey: pid), 284 | case .continuation(let c) = existing 285 | { 286 | c.resume(returning: status) 287 | } else { 288 | // We don't have continuation yet, just state status 289 | continuations[pid] = .status(status) 290 | } 291 | } 292 | } 293 | } 294 | } 295 | signalSource.resume() 296 | }() 297 | 298 | /// Unchecked Sendable here since this class is only explicitly 299 | /// initialzied once during the lifetime of the process 300 | final class SendableSourceSignal: @unchecked Sendable { 301 | private let signalSource: DispatchSourceSignal 302 | 303 | func setEventHandler(handler: @escaping DispatchSourceHandler) { 304 | self.signalSource.setEventHandler(handler: handler) 305 | } 306 | 307 | func resume() { 308 | self.signalSource.resume() 309 | } 310 | 311 | init() { 312 | self.signalSource = DispatchSource.makeSignalSource( 313 | signal: SIGCHLD, 314 | queue: .global() 315 | ) 316 | } 317 | } 318 | 319 | private func _setupMonitorSignalHandler() { 320 | // Only executed once 321 | setup 322 | } 323 | 324 | #endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl) 325 | -------------------------------------------------------------------------------- /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(Bionic) || canImport(Musl) 13 | 14 | #if canImport(System) 15 | 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(Bionic) 25 | import Bionic 26 | #elseif canImport(Glibc) 27 | import Glibc 28 | #elseif canImport(Musl) 29 | import Musl 30 | #endif 31 | 32 | package 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 | let pid = shouldSendToProcessGroup ? -(self.processIdentifier.value) : self.processIdentifier.value 125 | guard kill(pid, signal.rawValue) == 0 else { 126 | throw SubprocessError( 127 | code: .init(.failedToSendSignal(signal.rawValue)), 128 | underlyingError: .init(rawValue: errno) 129 | ) 130 | } 131 | } 132 | 133 | internal func tryTerminate() -> Swift.Error? { 134 | do { 135 | try self.send(signal: .kill) 136 | } catch { 137 | guard let posixError: SubprocessError = error as? SubprocessError else { 138 | return error 139 | } 140 | // Ignore ESRCH (no such process) 141 | if let underlyingError = posixError.underlyingError, 142 | underlyingError.rawValue != ESRCH 143 | { 144 | return error 145 | } 146 | } 147 | return nil 148 | } 149 | } 150 | 151 | // MARK: - Environment Resolution 152 | extension Environment { 153 | internal static let pathVariableName = "PATH" 154 | 155 | internal func pathValue() -> String? { 156 | switch self.config { 157 | case .inherit(let overrides): 158 | // If PATH value exists in overrides, use it 159 | if let value = overrides[Self.pathVariableName] { 160 | return value 161 | } 162 | // Fall back to current process 163 | return Self.currentEnvironmentValues()[Self.pathVariableName] 164 | case .custom(let fullEnvironment): 165 | if let value = fullEnvironment[Self.pathVariableName] { 166 | return value 167 | } 168 | return nil 169 | case .rawBytes(let rawBytesArray): 170 | let needle: [UInt8] = Array("\(Self.pathVariableName)=".utf8) 171 | for row in rawBytesArray { 172 | guard row.starts(with: needle) else { 173 | continue 174 | } 175 | // Attempt to 176 | let pathValue = row.dropFirst(needle.count) 177 | return String(decoding: pathValue, as: UTF8.self) 178 | } 179 | return nil 180 | } 181 | } 182 | 183 | // This method follows the standard "create" rule: `env` needs to be 184 | // manually deallocated 185 | internal func createEnv() -> [UnsafeMutablePointer?] { 186 | func createFullCString( 187 | fromKey keyContainer: StringOrRawBytes, 188 | value valueContainer: StringOrRawBytes 189 | ) -> UnsafeMutablePointer { 190 | let rawByteKey: UnsafeMutablePointer = keyContainer.createRawBytes() 191 | let rawByteValue: UnsafeMutablePointer = valueContainer.createRawBytes() 192 | defer { 193 | rawByteKey.deallocate() 194 | rawByteValue.deallocate() 195 | } 196 | /// length = `key` + `=` + `value` + `\null` 197 | let totalLength = keyContainer.count + 1 + valueContainer.count + 1 198 | let fullString: UnsafeMutablePointer = .allocate(capacity: totalLength) 199 | #if canImport(Darwin) 200 | _ = snprintf(ptr: fullString, totalLength, "%s=%s", rawByteKey, rawByteValue) 201 | #else 202 | _ = _shims_snprintf(fullString, CInt(totalLength), "%s=%s", rawByteKey, rawByteValue) 203 | #endif 204 | return fullString 205 | } 206 | 207 | var env: [UnsafeMutablePointer?] = [] 208 | switch self.config { 209 | case .inherit(let updates): 210 | var current = Self.currentEnvironmentValues() 211 | for (key, value) in updates { 212 | // Remove the value from current to override it 213 | current.removeValue(forKey: key) 214 | let fullString = "\(key)=\(value)" 215 | env.append(strdup(fullString)) 216 | } 217 | // Add the rest of `current` to env 218 | for (key, value) in current { 219 | let fullString = "\(key)=\(value)" 220 | env.append(strdup(fullString)) 221 | } 222 | case .custom(let customValues): 223 | for (key, value) in customValues { 224 | let fullString = "\(key)=\(value)" 225 | env.append(strdup(fullString)) 226 | } 227 | case .rawBytes(let rawBytesArray): 228 | for rawBytes in rawBytesArray { 229 | env.append(strdup(rawBytes)) 230 | } 231 | } 232 | env.append(nil) 233 | return env 234 | } 235 | 236 | internal static func withCopiedEnv(_ body: ([UnsafeMutablePointer]) -> R) -> R { 237 | var values: [UnsafeMutablePointer] = [] 238 | // This lock is taken by calls to getenv, so we want as few callouts to other code as possible here. 239 | _subprocess_lock_environ() 240 | guard 241 | let environments: UnsafeMutablePointer?> = 242 | _subprocess_get_environ() 243 | else { 244 | _subprocess_unlock_environ() 245 | return body([]) 246 | } 247 | var curr = environments 248 | while let value = curr.pointee { 249 | values.append(strdup(value)) 250 | curr = curr.advanced(by: 1) 251 | } 252 | _subprocess_unlock_environ() 253 | defer { values.forEach { free($0) } } 254 | return body(values) 255 | } 256 | } 257 | 258 | // MARK: Args Creation 259 | extension Arguments { 260 | // This method follows the standard "create" rule: `args` needs to be 261 | // manually deallocated 262 | internal func createArgs(withExecutablePath executablePath: String) -> [UnsafeMutablePointer?] { 263 | var argv: [UnsafeMutablePointer?] = self.storage.map { $0.createRawBytes() } 264 | // argv[0] = executable path 265 | if let override = self.executablePathOverride { 266 | argv.insert(override.createRawBytes(), at: 0) 267 | } else { 268 | argv.insert(strdup(executablePath), at: 0) 269 | } 270 | argv.append(nil) 271 | return argv 272 | } 273 | } 274 | 275 | // MARK: - Executable Searching 276 | extension Executable { 277 | internal static var defaultSearchPaths: Set { 278 | return Set([ 279 | "/usr/bin", 280 | "/bin", 281 | "/usr/sbin", 282 | "/sbin", 283 | "/usr/local/bin", 284 | ]) 285 | } 286 | 287 | internal func resolveExecutablePath(withPathValue pathValue: String?) throws -> String { 288 | switch self.storage { 289 | case .executable(let executableName): 290 | // If the executableName in is already a full path, return it directly 291 | if Configuration.pathAccessible(executableName, mode: X_OK) { 292 | return executableName 293 | } 294 | // Get $PATH from environment 295 | let searchPaths: Set 296 | if let pathValue = pathValue { 297 | let localSearchPaths = pathValue.split(separator: ":").map { String($0) } 298 | searchPaths = Set(localSearchPaths).union(Self.defaultSearchPaths) 299 | } else { 300 | searchPaths = Self.defaultSearchPaths 301 | } 302 | 303 | for path in searchPaths { 304 | let fullPath = "\(path)/\(executableName)" 305 | let fileExists = Configuration.pathAccessible(fullPath, mode: X_OK) 306 | if fileExists { 307 | return fullPath 308 | } 309 | } 310 | throw SubprocessError( 311 | code: .init(.executableNotFound(executableName)), 312 | underlyingError: nil 313 | ) 314 | case .path(let executablePath): 315 | // Use path directly 316 | return executablePath.string 317 | } 318 | } 319 | } 320 | 321 | // MARK: - PreSpawn 322 | extension Configuration { 323 | internal typealias PreSpawnArgs = ( 324 | env: [UnsafeMutablePointer?], 325 | uidPtr: UnsafeMutablePointer?, 326 | gidPtr: UnsafeMutablePointer?, 327 | supplementaryGroups: [gid_t]? 328 | ) 329 | 330 | internal func preSpawn( 331 | _ work: (PreSpawnArgs) throws -> Result 332 | ) throws -> Result { 333 | // Prepare environment 334 | let env = self.environment.createEnv() 335 | defer { 336 | for ptr in env { ptr?.deallocate() } 337 | } 338 | 339 | var uidPtr: UnsafeMutablePointer? = nil 340 | if let userID = self.platformOptions.userID { 341 | uidPtr = .allocate(capacity: 1) 342 | uidPtr?.pointee = userID 343 | } 344 | defer { 345 | uidPtr?.deallocate() 346 | } 347 | var gidPtr: UnsafeMutablePointer? = nil 348 | if let groupID = self.platformOptions.groupID { 349 | gidPtr = .allocate(capacity: 1) 350 | gidPtr?.pointee = groupID 351 | } 352 | defer { 353 | gidPtr?.deallocate() 354 | } 355 | var supplementaryGroups: [gid_t]? 356 | if let groupsValue = self.platformOptions.supplementaryGroups { 357 | supplementaryGroups = groupsValue 358 | } 359 | return try work( 360 | ( 361 | env: env, 362 | uidPtr: uidPtr, 363 | gidPtr: gidPtr, 364 | supplementaryGroups: supplementaryGroups 365 | ) 366 | ) 367 | } 368 | 369 | internal static func pathAccessible(_ path: String, mode: Int32) -> Bool { 370 | return path.withCString { 371 | return access($0, mode) == 0 372 | } 373 | } 374 | } 375 | 376 | // MARK: - FileDescriptor extensions 377 | extension FileDescriptor { 378 | internal static func openDevNull( 379 | withAcessMode mode: FileDescriptor.AccessMode 380 | ) throws -> FileDescriptor { 381 | let devnull: FileDescriptor = try .open("/dev/null", mode) 382 | return devnull 383 | } 384 | 385 | internal var platformDescriptor: PlatformFileDescriptor { 386 | return self 387 | } 388 | 389 | #if SubprocessSpan 390 | @available(SubprocessSpan, *) 391 | #endif 392 | package func readChunk(upToLength maxLength: Int) async throws -> SequenceOutput.Buffer? { 393 | return try await withCheckedThrowingContinuation { continuation in 394 | DispatchIO.read( 395 | fromFileDescriptor: self.rawValue, 396 | maxLength: maxLength, 397 | runningHandlerOn: .global() 398 | ) { data, error in 399 | if error != 0 { 400 | continuation.resume( 401 | throwing: SubprocessError( 402 | code: .init(.failedToReadFromSubprocess), 403 | underlyingError: .init(rawValue: error) 404 | ) 405 | ) 406 | return 407 | } 408 | if data.isEmpty { 409 | continuation.resume(returning: nil) 410 | } else { 411 | continuation.resume(returning: SequenceOutput.Buffer(data: data)) 412 | } 413 | } 414 | } 415 | } 416 | 417 | internal func readUntilEOF( 418 | upToLength maxLength: Int, 419 | resultHandler: sending @escaping (Swift.Result) -> Void 420 | ) { 421 | let dispatchIO = DispatchIO( 422 | type: .stream, 423 | fileDescriptor: self.rawValue, 424 | queue: .global() 425 | ) { error in } 426 | var buffer: DispatchData? 427 | dispatchIO.read( 428 | offset: 0, 429 | length: maxLength, 430 | queue: .global() 431 | ) { done, data, error in 432 | guard error == 0, let chunkData = data else { 433 | dispatchIO.close() 434 | resultHandler( 435 | .failure( 436 | SubprocessError( 437 | code: .init(.failedToReadFromSubprocess), 438 | underlyingError: .init(rawValue: error) 439 | ) 440 | ) 441 | ) 442 | return 443 | } 444 | // Easy case: if we are done and buffer is nil, this means 445 | // there is only one chunk of data 446 | if done && buffer == nil { 447 | dispatchIO.close() 448 | buffer = chunkData 449 | resultHandler(.success(chunkData)) 450 | return 451 | } 452 | 453 | if buffer == nil { 454 | buffer = chunkData 455 | } else { 456 | buffer?.append(chunkData) 457 | } 458 | 459 | if done { 460 | dispatchIO.close() 461 | resultHandler(.success(buffer!)) 462 | return 463 | } 464 | } 465 | } 466 | 467 | #if SubprocessSpan 468 | @available(SubprocessSpan, *) 469 | package func write( 470 | _ span: borrowing RawSpan 471 | ) async throws -> Int { 472 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 473 | let dispatchData = span.withUnsafeBytes { 474 | return DispatchData( 475 | bytesNoCopy: $0, 476 | deallocator: .custom( 477 | nil, 478 | { 479 | // noop 480 | } 481 | ) 482 | ) 483 | } 484 | self.write(dispatchData) { writtenLength, error in 485 | if let error = error { 486 | continuation.resume(throwing: error) 487 | } else { 488 | continuation.resume(returning: writtenLength) 489 | } 490 | } 491 | } 492 | } 493 | #endif // SubprocessSpan 494 | 495 | package func write( 496 | _ array: [UInt8] 497 | ) async throws -> Int { 498 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 499 | let dispatchData = array.withUnsafeBytes { 500 | return DispatchData( 501 | bytesNoCopy: $0, 502 | deallocator: .custom( 503 | nil, 504 | { 505 | // noop 506 | } 507 | ) 508 | ) 509 | } 510 | self.write(dispatchData) { writtenLength, error in 511 | if let error = error { 512 | continuation.resume(throwing: error) 513 | } else { 514 | continuation.resume(returning: writtenLength) 515 | } 516 | } 517 | } 518 | } 519 | 520 | package func write( 521 | _ dispatchData: DispatchData, 522 | queue: DispatchQueue = .global(), 523 | completion: @escaping (Int, Error?) -> Void 524 | ) { 525 | DispatchIO.write( 526 | toFileDescriptor: self.rawValue, 527 | data: dispatchData, 528 | runningHandlerOn: queue 529 | ) { unwritten, error in 530 | let unwrittenLength = unwritten?.count ?? 0 531 | let writtenLength = dispatchData.count - unwrittenLength 532 | guard error != 0 else { 533 | completion(writtenLength, nil) 534 | return 535 | } 536 | completion( 537 | writtenLength, 538 | SubprocessError( 539 | code: .init(.failedToWriteToSubprocess), 540 | underlyingError: .init(rawValue: error) 541 | ) 542 | ) 543 | } 544 | } 545 | } 546 | 547 | internal typealias PlatformFileDescriptor = FileDescriptor 548 | 549 | #endif // canImport(Darwin) || canImport(Glibc) || canImport(Bionic) || canImport(Musl) 550 | -------------------------------------------------------------------------------- /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 | 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 | #if canImport(Glibc) || canImport(Bionic) || canImport(Musl) 43 | internal import Dispatch 44 | 45 | @available(SubprocessSpan, *) 46 | extension DispatchData { 47 | var bytes: RawSpan { 48 | _read { 49 | if self.count == 0 { 50 | let empty = UnsafeRawBufferPointer(start: nil, count: 0) 51 | let span = RawSpan(_unsafeBytes: empty) 52 | yield _overrideLifetime(of: span, to: self) 53 | } else { 54 | // FIXME: We cannot get a stable ptr out of DispatchData. 55 | // For now revert back to copy 56 | let array = Array(self) 57 | let ptr = array.withUnsafeBytes { return $0 } 58 | let span = RawSpan(_unsafeBytes: ptr) 59 | yield _overrideLifetime(of: span, to: self) 60 | } 61 | } 62 | } 63 | } 64 | #endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl) 65 | 66 | #endif // SubprocessSpan 67 | -------------------------------------------------------------------------------- /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 | 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.fileDescriptor.wrapped.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 | extension FileDescriptor { 132 | #if os(Windows) 133 | internal func write( 134 | _ data: Data 135 | ) async throws -> Int { 136 | try await withCheckedThrowingContinuation { continuation in 137 | // TODO: Figure out a better way to asynchornously write 138 | DispatchQueue.global(qos: .userInitiated).async { 139 | data.withUnsafeBytes { 140 | self.write($0) { writtenLength, error in 141 | if let error = error { 142 | continuation.resume(throwing: error) 143 | } else { 144 | continuation.resume(returning: writtenLength) 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } 151 | #else 152 | internal func write( 153 | _ data: Data 154 | ) async throws -> Int { 155 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 156 | let dispatchData = data.withUnsafeBytes { 157 | return DispatchData( 158 | bytesNoCopy: $0, 159 | deallocator: .custom( 160 | nil, 161 | { 162 | // noop 163 | } 164 | ) 165 | ) 166 | } 167 | self.write(dispatchData) { writtenLength, error in 168 | if let error = error { 169 | continuation.resume(throwing: error) 170 | } else { 171 | continuation.resume(returning: writtenLength) 172 | } 173 | } 174 | } 175 | } 176 | #endif 177 | } 178 | 179 | #endif // SubprocessFoundation 180 | -------------------------------------------------------------------------------- /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 withUncancelledTask { 85 | await self.runTeardownSequence(sequence) 86 | } 87 | } 88 | } 89 | 90 | internal enum TeardownStepCompletion { 91 | case processHasExited 92 | case processStillAlive 93 | case killedTheProcess 94 | } 95 | 96 | #if SubprocessSpan 97 | @available(SubprocessSpan, *) 98 | #endif 99 | extension Execution { 100 | internal func gracefulShutDown( 101 | allowedDurationToNextStep duration: Duration 102 | ) async { 103 | #if os(Windows) 104 | guard 105 | let processHandle = OpenProcess( 106 | DWORD(PROCESS_QUERY_INFORMATION | SYNCHRONIZE), 107 | false, 108 | self.processIdentifier.value 109 | ) 110 | else { 111 | // Nothing more we can do 112 | return 113 | } 114 | defer { 115 | CloseHandle(processHandle) 116 | } 117 | 118 | // 1. Attempt to send WM_CLOSE to the main window 119 | if _subprocess_windows_send_vm_close( 120 | self.processIdentifier.value 121 | ) { 122 | try? await Task.sleep(for: duration) 123 | } 124 | 125 | // 2. Attempt to attach to the console and send CTRL_C_EVENT 126 | if AttachConsole(self.processIdentifier.value) { 127 | // Disable Ctrl-C handling in this process 128 | if SetConsoleCtrlHandler(nil, true) { 129 | if GenerateConsoleCtrlEvent(DWORD(CTRL_C_EVENT), 0) { 130 | // We successfully sent the event. wait for the process to exit 131 | try? await Task.sleep(for: duration) 132 | } 133 | // Re-enable Ctrl-C handling 134 | SetConsoleCtrlHandler(nil, false) 135 | } 136 | // Detach console 137 | FreeConsole() 138 | } 139 | 140 | // 3. Attempt to send CTRL_BREAK_EVENT to the process group 141 | if GenerateConsoleCtrlEvent(DWORD(CTRL_BREAK_EVENT), self.processIdentifier.value) { 142 | // Wait for process to exit 143 | try? await Task.sleep(for: duration) 144 | } 145 | #else 146 | // Send SIGTERM 147 | try? self.send(signal: .terminate) 148 | #endif 149 | } 150 | 151 | internal func runTeardownSequence(_ sequence: some Sequence & Sendable) async { 152 | // First insert the `.kill` step 153 | let finalSequence = sequence + [TeardownStep(storage: .kill)] 154 | for step in finalSequence { 155 | let stepCompletion: TeardownStepCompletion 156 | 157 | switch step.storage { 158 | case .gracefulShutDown(let allowedDuration): 159 | stepCompletion = await withTaskGroup(of: TeardownStepCompletion.self) { group in 160 | group.addTask { 161 | do { 162 | try await Task.sleep(for: allowedDuration) 163 | return .processStillAlive 164 | } catch { 165 | // teardown(using:) cancells this task 166 | // when process has exited 167 | return .processHasExited 168 | } 169 | } 170 | await self.gracefulShutDown(allowedDurationToNextStep: allowedDuration) 171 | return await group.next()! 172 | } 173 | #if !os(Windows) 174 | case .sendSignal(let signal, let allowedDuration): 175 | stepCompletion = await withTaskGroup(of: TeardownStepCompletion.self) { group in 176 | group.addTask { 177 | do { 178 | try await Task.sleep(for: allowedDuration) 179 | return .processStillAlive 180 | } catch { 181 | // teardown(using:) cancells this task 182 | // when process has exited 183 | return .processHasExited 184 | } 185 | } 186 | try? self.send(signal: signal) 187 | return await group.next()! 188 | } 189 | #endif // !os(Windows) 190 | case .kill: 191 | #if os(Windows) 192 | try? self.terminate(withExitCode: 0) 193 | #else 194 | try? self.send(signal: .kill) 195 | #endif 196 | stepCompletion = .killedTheProcess 197 | } 198 | 199 | switch stepCompletion { 200 | case .killedTheProcess, .processHasExited: 201 | return 202 | case .processStillAlive: 203 | // Continue to next step 204 | break 205 | } 206 | } 207 | } 208 | } 209 | 210 | func withUncancelledTask( 211 | returning: Result.Type = Result.self, 212 | _ body: @Sendable @escaping () async -> Result 213 | ) async -> Result { 214 | // This looks unstructured but it isn't, please note that we `await` `.value` of this task. 215 | // The reason we need this separate `Task` is that in general, we cannot assume that code performs to our 216 | // expectations if the task we run it on is already cancelled. However, in some cases we need the code to 217 | // run regardless -- even if our task is already cancelled. Therefore, we create a new, uncancelled task here. 218 | await Task { 219 | await body() 220 | }.value 221 | } 222 | -------------------------------------------------------------------------------- /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 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors 10 | // 11 | //===----------------------------------------------------------------------===// 12 | 13 | #ifndef _SHIMS_TARGET_CONDITIONALS_H 14 | #define _SHIMS_TARGET_CONDITIONALS_H 15 | 16 | #if __has_include() 17 | #include 18 | #endif 19 | 20 | #if (defined(__APPLE__) && defined(__MACH__)) 21 | #define TARGET_OS_MAC 1 22 | #else 23 | #define TARGET_OS_MAC 0 24 | #endif 25 | 26 | #if defined(__linux__) 27 | #define TARGET_OS_LINUX 1 28 | #else 29 | #define TARGET_OS_LINUX 0 30 | #endif 31 | 32 | #if defined(__unix__) 33 | #define TARGET_OS_BSD 1 34 | #else 35 | #define TARGET_OS_BSD 0 36 | #endif 37 | 38 | #if defined(_WIN32) 39 | #define TARGET_OS_WINDOWS 1 40 | #else 41 | #define TARGET_OS_WINDOWS 0 42 | #endif 43 | 44 | #if defined(__wasi__) 45 | #define TARGET_OS_WASI 1 46 | #else 47 | #define TARGET_OS_WASI 0 48 | #endif 49 | 50 | #endif // _SHIMS_TARGET_CONDITIONALS_H 51 | -------------------------------------------------------------------------------- /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 | 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 | .name("/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 | output: .discarded, 89 | error: .discarded 90 | ) { subprocess in 91 | // First suspend the procss 92 | try subprocess.send(signal: .suspend) 93 | var suspendedStatus: Int32 = 0 94 | waitpid(subprocess.processIdentifier.value, &suspendedStatus, WNOHANG | WUNTRACED) 95 | #expect(_was_process_suspended(suspendedStatus) > 0) 96 | // Now resume the process 97 | try subprocess.send(signal: .resume) 98 | var resumedStatus: Int32 = 0 99 | waitpid(subprocess.processIdentifier.value, &resumedStatus, WNOHANG | WUNTRACED) 100 | #expect(_was_process_suspended(resumedStatus) == 0) 101 | 102 | // Now kill the process 103 | try subprocess.send(signal: .terminate) 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 | #elseif os(Linux) || os(Windows) 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: enableLintingTest(), 44 | "Could not determine source path" 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 | #elseif os(Linux) || os(Windows) 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 | .name("/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 | output: .discarded, 79 | error: .discarded 80 | ) { subprocess in 81 | // First suspend the procss 82 | try subprocess.send(signal: .suspend) 83 | #expect( 84 | try isProcessSuspended(subprocess.processIdentifier.value) 85 | ) 86 | // Now resume the process 87 | try subprocess.send(signal: .resume) 88 | #expect( 89 | try isProcessSuspended(subprocess.processIdentifier.value) == false 90 | ) 91 | // Now kill the process 92 | try subprocess.send(signal: .terminate) 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 | 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 | --------------------------------------------------------------------------------