diff --git a/Package.swift b/Package.swift index 2c56a2870..be77a785c 100644 --- a/Package.swift +++ b/Package.swift @@ -210,10 +210,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-system", from: "1.4.0"), .package(url: "https://github.com/apple/swift-log", from: "1.2.0"), .package(url: "https://github.com/apple/swift-collections", .upToNextMinor(from: "1.3.0")), // primarily for ordered collections - -// // FIXME: swift-subprocess stopped supporting 6.0 when it moved into a package; -// // we'll need to drop 6.0 as well, but currently blocked on doing so by swiftpm plugin pending design questions -// .package(url: "https://github.com/swiftlang/swift-subprocess.git", revision: "de15b67f7871c8a039ef7f4813eb39a8878f61a6"), + .package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.2.1", traits: ["SubprocessFoundation"]), // Benchmarking .package(url: "https://github.com/ordo-one/package-benchmark", .upToNextMajor(from: "1.4.0")), @@ -415,8 +412,7 @@ let package = Package( "JavaTypes", "SwiftJavaShared", "SwiftJavaConfigurationShared", - // .product(name: "Subprocess", package: "swift-subprocess") - "_Subprocess", + .product(name: "Subprocess", package: "swift-subprocess") ], swiftSettings: [ .swiftLanguageMode(.v5), @@ -541,28 +537,6 @@ let package = Package( .swiftLanguageMode(.v5), .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) ] - ), - - // Experimental Foundation Subprocess Copy - .target( - name: "_SubprocessCShims", - swiftSettings: [ - .swiftLanguageMode(.v5) - ] - ), - .target( - name: "_Subprocess", - dependencies: [ - "_SubprocessCShims", - .product(name: "SystemPackage", package: "swift-system"), - ], - swiftSettings: [ - .swiftLanguageMode(.v5), - .define( - "SYSTEM_PACKAGE_DARWIN", - .when(platforms: [.macOS, .macCatalyst, .iOS, .watchOS, .tvOS, .visionOS])), - .define("SYSTEM_PACKAGE"), - ] - ), + ) ] ) diff --git a/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift b/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift index 1c9477f11..9ccbc164b 100644 --- a/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift +++ b/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift @@ -23,6 +23,8 @@ import Glibc import CRT #elseif canImport(Darwin) import Darwin.C +#elseif canImport(Android) +import Android #endif public func helloWorld() { diff --git a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift index 7cc4c477d..de67c9176 100644 --- a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift +++ b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift @@ -21,7 +21,7 @@ import JavaUtilJar import SwiftJavaToolLib import SwiftJavaConfigurationShared import SwiftJavaShared -import _Subprocess +import Subprocess #if canImport(System) import System #else @@ -146,7 +146,7 @@ extension SwiftJava.ResolveCommand { try! printGradleProject(directory: resolverDir, dependencies: dependencies) if #available(macOS 15, *) { - let process = try! await _Subprocess.run( + let process = try! await Subprocess.run( .path(FilePath(resolverDir.appendingPathComponent("gradlew").path)), arguments: [ "--no-daemon", diff --git a/Sources/_Subprocess/API.swift b/Sources/_Subprocess/API.swift deleted file mode 100644 index 9673d3e11..000000000 --- a/Sources/_Subprocess/API.swift +++ /dev/null @@ -1,370 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif - -// MARK: - Collected Result - -/// Run a executable with given parameters asynchrously and returns -/// a `CollectedResult` containing the output of the child process. -/// - Parameters: -/// - executable: The executable to run. -/// - arguments: The arguments to pass to the executable. -/// - environment: The environment in which to run the executable. -/// - workingDirectory: The working directory in which to run the executable. -/// - platformOptions: The platform specific options to use -/// when running the executable. -/// - input: The input to send to the executable. -/// - output: The method to use for redirecting the standard output. -/// - error: The method to use for redirecting the standard error. -/// - Returns a CollectedResult containing the result of the run. -@available(macOS 15.0, *) // FIXME: manually added availability -public func run< - Input: InputProtocol, - Output: OutputProtocol, - Error: OutputProtocol ->( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - input: Input = .none, - output: Output = .string, - error: Error = .discarded -) async throws -> CollectedResult { - let configuration = Configuration( - executable: executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions - ) - return try await run( - configuration, - input: input, - output: output, - error: error - ) -} - -// MARK: - Custom Execution Body - -/// Run a executable with given parameters and a custom closure -/// to manage the running subprocess' lifetime and its IOs. -/// - Parameters: -/// - executable: The executable to run. -/// - arguments: The arguments to pass to the executable. -/// - environment: The environment in which to run the executable. -/// - workingDirectory: The working directory in which to run the executable. -/// - platformOptions: The platform specific options to use -/// when running the executable. -/// - input: The input to send to the executable. -/// - output: How to manage the executable standard ouput. -/// - error: How to manager executable standard error. -/// - isolation: the isolation context to run the body closure. -/// - body: The custom execution body to manually control the running process -/// - Returns a ExecutableResult type containing the return value -/// of the closure. -@available(macOS 15.0, *) // FIXME: manually added availability -public func run( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - input: Input = .none, - output: Output, - error: Error, - isolation: isolated (any Actor)? = #isolation, - body: ((Execution) async throws -> Result) -) async throws -> ExecutionResult where Output.OutputType == Void, Error.OutputType == Void { - return try await Configuration( - executable: executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions - ) - .run(input: input, output: output, error: error, body) -} - -/// Run a executable with given parameters and a custom closure -/// to manage the running subprocess' lifetime and write to its -/// standard input via `StandardInputWriter` -/// - Parameters: -/// - executable: The executable to run. -/// - arguments: The arguments to pass to the executable. -/// - environment: The environment in which to run the executable. -/// - workingDirectory: The working directory in which to run the executable. -/// - platformOptions: The platform specific options to use -/// when running the executable. -/// - output:How to handle executable's standard output -/// - error: How to handle executable's standard error -/// - isolation: the isolation context to run the body closure. -/// - body: The custom execution body to manually control the running process -/// - Returns a ExecutableResult type containing the return value -/// of the closure. -@available(macOS 15.0, *) // FIXME: manually added availability -public func run( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - output: Output, - error: Error, - isolation: isolated (any Actor)? = #isolation, - body: ((Execution, StandardInputWriter) async throws -> Result) -) async throws -> ExecutionResult where Output.OutputType == Void, Error.OutputType == Void { - return try await Configuration( - executable: executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions - ) - .run(output: output, error: error, body) -} - -// MARK: - Configuration Based - -/// Run a `Configuration` asynchrously and returns -/// a `CollectedResult` containing the output of the child process. -/// - Parameters: -/// - configuration: The `Subprocess` configuration to run. -/// - input: The input to send to the executable. -/// - output: The method to use for redirecting the standard output. -/// - error: The method to use for redirecting the standard error. -/// - Returns a CollectedResult containing the result of the run. -@available(macOS 15.0, *) // FIXME: manually added availability -public func run< - Input: InputProtocol, - Output: OutputProtocol, - Error: OutputProtocol ->( - _ configuration: Configuration, - input: Input = .none, - output: Output = .string, - error: Error = .discarded -) async throws -> CollectedResult { - let result = try await configuration.run( - input: input, - output: output, - error: error - ) { execution in - let ( - standardOutput, - standardError - ) = try await execution.captureIOs() - return ( - processIdentifier: execution.processIdentifier, - standardOutput: standardOutput, - standardError: standardError - ) - } - return CollectedResult( - processIdentifier: result.value.processIdentifier, - terminationStatus: result.terminationStatus, - standardOutput: result.value.standardOutput, - standardError: result.value.standardError - ) -} - -/// Run a executable with given parameters specified by a `Configuration` -/// - Parameters: -/// - configuration: The `Subprocess` configuration to run. -/// - output: The method to use for redirecting the standard output. -/// - error: The method to use for redirecting the standard error. -/// - isolation: the isolation context to run the body closure. -/// - body: The custom configuration body to manually control -/// the running process and write to its standard input. -/// - Returns a ExecutableResult type containing the return value -/// of the closure. -@available(macOS 15.0, *) // FIXME: manually added availability -public func run( - _ configuration: Configuration, - output: Output, - error: Error, - isolation: isolated (any Actor)? = #isolation, - body: ((Execution, StandardInputWriter) async throws -> Result) -) async throws -> ExecutionResult where Output.OutputType == Void, Error.OutputType == Void { - return try await configuration.run(output: output, error: error, body) -} - -// MARK: - Detached - -/// Run a executable with given parameters and return its process -/// identifier immediately without monitoring the state of the -/// subprocess nor waiting until it exits. -/// -/// This method is useful for launching subprocesses that outlive their -/// parents (for example, daemons and trampolines). -/// -/// - Parameters: -/// - executable: The executable to run. -/// - arguments: The arguments to pass to the executable. -/// - environment: The environment to use for the process. -/// - workingDirectory: The working directory for the process. -/// - platformOptions: The platform specific options to use for the process. -/// - input: A file descriptor to bind to the subprocess' standard input. -/// - output: A file descriptor to bind to the subprocess' standard output. -/// - error: A file descriptor to bind to the subprocess' standard error. -/// - Returns: the process identifier for the subprocess. -@available(macOS 15.0, *) // FIXME: manually added availability -public func runDetached( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - input: FileDescriptor? = nil, - output: FileDescriptor? = nil, - error: FileDescriptor? = nil -) throws -> ProcessIdentifier { - let config: Configuration = Configuration( - executable: executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions - ) - return try runDetached(config, input: input, output: output, error: error) -} - -/// Run a executable with given configuration and return its process -/// identifier immediately without monitoring the state of the -/// subprocess nor waiting until it exits. -/// -/// This method is useful for launching subprocesses that outlive their -/// parents (for example, daemons and trampolines). -/// -/// - Parameters: -/// - configuration: The `Subprocess` configuration to run. -/// - input: A file descriptor to bind to the subprocess' standard input. -/// - output: A file descriptor to bind to the subprocess' standard output. -/// - error: A file descriptor to bind to the subprocess' standard error. -/// - Returns: the process identifier for the subprocess. -@available(macOS 15.0, *) // FIXME: manually added availability -public func runDetached( - _ configuration: Configuration, - input: FileDescriptor? = nil, - output: FileDescriptor? = nil, - error: FileDescriptor? = nil -) throws -> ProcessIdentifier { - switch (input, output, error) { - case (.none, .none, .none): - let processOutput = DiscardedOutput() - let processError = DiscardedOutput() - return try configuration.spawn( - withInput: NoInput().createPipe(), - output: processOutput, - outputPipe: try processOutput.createPipe(), - error: processError, - errorPipe: try processError.createPipe() - ).processIdentifier - case (.none, .none, .some(let errorFd)): - let processOutput = DiscardedOutput() - let processError = FileDescriptorOutput(fileDescriptor: errorFd, closeAfterSpawningProcess: false) - return try configuration.spawn( - withInput: NoInput().createPipe(), - output: processOutput, - outputPipe: try processOutput.createPipe(), - error: processError, - errorPipe: try processError.createPipe() - ).processIdentifier - case (.none, .some(let outputFd), .none): - let processOutput = FileDescriptorOutput(fileDescriptor: outputFd, closeAfterSpawningProcess: false) - let processError = DiscardedOutput() - return try configuration.spawn( - withInput: NoInput().createPipe(), - output: processOutput, - outputPipe: try processOutput.createPipe(), - error: processError, - errorPipe: try processError.createPipe() - ).processIdentifier - case (.none, .some(let outputFd), .some(let errorFd)): - let processOutput = FileDescriptorOutput( - fileDescriptor: outputFd, - closeAfterSpawningProcess: false - ) - let processError = FileDescriptorOutput( - fileDescriptor: errorFd, - closeAfterSpawningProcess: false - ) - return try configuration.spawn( - withInput: NoInput().createPipe(), - output: processOutput, - outputPipe: try processOutput.createPipe(), - error: processError, - errorPipe: try processError.createPipe() - ).processIdentifier - case (.some(let inputFd), .none, .none): - let processOutput = DiscardedOutput() - let processError = DiscardedOutput() - return try configuration.spawn( - withInput: FileDescriptorInput( - fileDescriptor: inputFd, - closeAfterSpawningProcess: false - ).createPipe(), - output: processOutput, - outputPipe: try processOutput.createPipe(), - error: processError, - errorPipe: try processError.createPipe() - ).processIdentifier - case (.some(let inputFd), .none, .some(let errorFd)): - let processOutput = DiscardedOutput() - let processError = FileDescriptorOutput( - fileDescriptor: errorFd, - closeAfterSpawningProcess: false - ) - return try configuration.spawn( - withInput: FileDescriptorInput(fileDescriptor: inputFd, closeAfterSpawningProcess: false).createPipe(), - output: processOutput, - outputPipe: try processOutput.createPipe(), - error: processError, - errorPipe: try processError.createPipe() - ).processIdentifier - case (.some(let inputFd), .some(let outputFd), .none): - let processOutput = FileDescriptorOutput( - fileDescriptor: outputFd, - closeAfterSpawningProcess: false - ) - let processError = DiscardedOutput() - return try configuration.spawn( - withInput: FileDescriptorInput(fileDescriptor: inputFd, closeAfterSpawningProcess: false).createPipe(), - output: processOutput, - outputPipe: try processOutput.createPipe(), - error: processError, - errorPipe: try processError.createPipe() - ).processIdentifier - case (.some(let inputFd), .some(let outputFd), .some(let errorFd)): - let processOutput = FileDescriptorOutput( - fileDescriptor: outputFd, - closeAfterSpawningProcess: false - ) - let processError = FileDescriptorOutput( - fileDescriptor: errorFd, - closeAfterSpawningProcess: false - ) - return try configuration.spawn( - withInput: FileDescriptorInput(fileDescriptor: inputFd, closeAfterSpawningProcess: false).createPipe(), - output: processOutput, - outputPipe: try processOutput.createPipe(), - error: processError, - errorPipe: try processError.createPipe() - ).processIdentifier - } -} diff --git a/Sources/_Subprocess/AsyncBufferSequence.swift b/Sources/_Subprocess/AsyncBufferSequence.swift deleted file mode 100644 index 4316f7ebb..000000000 --- a/Sources/_Subprocess/AsyncBufferSequence.swift +++ /dev/null @@ -1,99 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif - -internal struct AsyncBufferSequence: AsyncSequence, Sendable { - internal typealias Failure = any Swift.Error - - internal typealias Element = SequenceOutput.Buffer - - @_nonSendable - internal struct Iterator: AsyncIteratorProtocol { - internal typealias Element = SequenceOutput.Buffer - - private let fileDescriptor: TrackedFileDescriptor - private var buffer: [UInt8] - private var currentPosition: Int - private var finished: Bool - - internal init(fileDescriptor: TrackedFileDescriptor) { - self.fileDescriptor = fileDescriptor - self.buffer = [] - self.currentPosition = 0 - self.finished = false - } - - internal mutating func next() async throws -> SequenceOutput.Buffer? { - let data = try await self.fileDescriptor.wrapped.readChunk( - upToLength: readBufferSize - ) - if data == nil { - // We finished reading. Close the file descriptor now - try self.fileDescriptor.safelyClose() - return nil - } - return data - } - } - - private let fileDescriptor: TrackedFileDescriptor - - init(fileDescriptor: TrackedFileDescriptor) { - self.fileDescriptor = fileDescriptor - } - - internal func makeAsyncIterator() -> Iterator { - return Iterator(fileDescriptor: self.fileDescriptor) - } -} - -// MARK: - Page Size -import _SubprocessCShims - -#if canImport(Darwin) -import Darwin -internal import MachO.dyld - -private let _pageSize: Int = { - Int(_subprocess_vm_size()) -}() -#elseif canImport(WinSDK) -import WinSDK -private let _pageSize: Int = { - var sysInfo: SYSTEM_INFO = SYSTEM_INFO() - GetSystemInfo(&sysInfo) - return Int(sysInfo.dwPageSize) -}() -#elseif os(WASI) -// WebAssembly defines a fixed page size -private let _pageSize: Int = 65_536 -#elseif canImport(Android) -@preconcurrency import Android -private let _pageSize: Int = Int(getpagesize()) -#elseif canImport(Glibc) -@preconcurrency import Glibc -private let _pageSize: Int = Int(getpagesize()) -#elseif canImport(Musl) -@preconcurrency import Musl -private let _pageSize: Int = Int(getpagesize()) -#elseif canImport(C) -private let _pageSize: Int = Int(getpagesize()) -#endif // canImport(Darwin) - -@inline(__always) -internal var readBufferSize: Int { - return _pageSize -} diff --git a/Sources/_Subprocess/Buffer.swift b/Sources/_Subprocess/Buffer.swift deleted file mode 100644 index 3ce73d7ea..000000000 --- a/Sources/_Subprocess/Buffer.swift +++ /dev/null @@ -1,104 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -@preconcurrency internal import Dispatch - -extension SequenceOutput { - /// A immutable collection of bytes - public struct Buffer: Sendable { - #if os(Windows) - private var data: [UInt8] - - internal init(data: [UInt8]) { - self.data = data - } - #else - private var data: DispatchData - - internal init(data: DispatchData) { - self.data = data - } - #endif - } -} - -// MARK: - Properties -extension SequenceOutput.Buffer { - /// Number of bytes stored in the buffer - public var count: Int { - return self.data.count - } - - /// A Boolean value indicating whether the collection is empty. - public var isEmpty: Bool { - return self.data.isEmpty - } -} - -// MARK: - Accessors -extension SequenceOutput.Buffer { - #if !SubprocessSpan - /// Access the raw bytes stored in this buffer - /// - Parameter body: A closure with an `UnsafeRawBufferPointer` parameter that - /// points to the contiguous storage for the type. If no such storage exists, - /// the method creates it. If body has a return value, this method also returns - /// that value. The argument is valid only for the duration of the - /// closure’s SequenceOutput. - /// - Returns: The return value, if any, of the body closure parameter. - public func withUnsafeBytes( - _ body: (UnsafeRawBufferPointer) throws -> ResultType - ) rethrows -> ResultType { - return try self._withUnsafeBytes(body) - } - #endif // !SubprocessSpan - - internal func _withUnsafeBytes( - _ body: (UnsafeRawBufferPointer) throws -> ResultType - ) rethrows -> ResultType { - #if os(Windows) - return try self.data.withUnsafeBytes(body) - #else - // Although DispatchData was designed to be uncontiguous, in practice - // we found that almost all DispatchData are contiguous. Therefore - // we can access this body in O(1) most of the time. - return try self.data.withUnsafeBytes { ptr in - let bytes = UnsafeRawBufferPointer(start: ptr, count: self.data.count) - return try body(bytes) - } - #endif - } - - private enum SpanBacking { - case pointer(UnsafeBufferPointer) - case array([UInt8]) - } -} - -// MARK: - Hashable, Equatable -extension SequenceOutput.Buffer: Equatable, Hashable { - #if os(Windows) - // Compiler generated conformances - #else - public static func == (lhs: SequenceOutput.Buffer, rhs: SequenceOutput.Buffer) -> Bool { - return lhs.data.elementsEqual(rhs.data) - } - - public func hash(into hasher: inout Hasher) { - self.data.withUnsafeBytes { ptr in - let bytes = UnsafeRawBufferPointer( - start: ptr, - count: self.data.count - ) - hasher.combine(bytes: bytes) - } - } - #endif -} diff --git a/Sources/_Subprocess/Configuration.swift b/Sources/_Subprocess/Configuration.swift deleted file mode 100644 index ba6f15bab..000000000 --- a/Sources/_Subprocess/Configuration.swift +++ /dev/null @@ -1,851 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif - -#if canImport(Darwin) -import Darwin -#elseif canImport(Bionic) -import Bionic -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#elseif canImport(WinSDK) -import WinSDK -#endif - -internal import Dispatch - -/// A collection of configurations parameters to use when -/// spawning a subprocess. -public struct Configuration: Sendable { - /// The executable to run. - public var executable: Executable - /// The arguments to pass to the executable. - public var arguments: Arguments - /// The environment to use when running the executable. - public var environment: Environment - /// The working directory to use when running the executable. - public var workingDirectory: FilePath - /// The platform specific options to use when - /// running the subprocess. - public var platformOptions: PlatformOptions - - public init( - executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions() - ) { - self.executable = executable - self.arguments = arguments - self.environment = environment - self.workingDirectory = workingDirectory ?? .currentWorkingDirectory - self.platformOptions = platformOptions - } - - @available(macOS 15.0, *) // FIXME: manually added availability - internal func run< - Result, - Output: OutputProtocol, - Error: OutputProtocol - >( - output: Output, - error: Error, - isolation: isolated (any Actor)? = #isolation, - _ body: ( - Execution, - StandardInputWriter - ) async throws -> Result - ) async throws -> ExecutionResult { - let input = CustomWriteInput() - - let inputPipe = try input.createPipe() - let outputPipe = try output.createPipe() - let errorPipe = try error.createPipe() - - let execution = try self.spawn( - withInput: inputPipe, - output: output, - outputPipe: outputPipe, - error: error, - errorPipe: errorPipe - ) - // After spawn, cleanup child side fds - try await self.cleanup( - execution: execution, - inputPipe: inputPipe, - outputPipe: outputPipe, - errorPipe: errorPipe, - childSide: true, - parentSide: false, - attemptToTerminateSubProcess: false - ) - return try await withAsyncTaskCleanupHandler { - async let waitingStatus = try await monitorProcessTermination( - forProcessWithIdentifier: execution.processIdentifier - ) - // Body runs in the same isolation - let result = try await body( - execution, - .init(fileDescriptor: inputPipe.writeFileDescriptor!) - ) - return ExecutionResult( - terminationStatus: try await waitingStatus, - value: result - ) - } onCleanup: { - // Attempt to terminate the child process - // Since the task has already been cancelled, - // this is the best we can do - try? await self.cleanup( - execution: execution, - inputPipe: inputPipe, - outputPipe: outputPipe, - errorPipe: errorPipe, - childSide: false, - parentSide: true, - attemptToTerminateSubProcess: true - ) - } - } - - @available(macOS 15.0, *) // FIXME: manually added availability - internal func run< - Result, - Input: InputProtocol, - Output: OutputProtocol, - Error: OutputProtocol - >( - input: Input, - output: Output, - error: Error, - isolation: isolated (any Actor)? = #isolation, - _ body: ((Execution) async throws -> Result) - ) async throws -> ExecutionResult { - - let inputPipe = try input.createPipe() - let outputPipe = try output.createPipe() - let errorPipe = try error.createPipe() - - let execution = try self.spawn( - withInput: inputPipe, - output: output, - outputPipe: outputPipe, - error: error, - errorPipe: errorPipe - ) - // After spawn, clean up child side - try await self.cleanup( - execution: execution, - inputPipe: inputPipe, - outputPipe: outputPipe, - errorPipe: errorPipe, - childSide: true, - parentSide: false, - attemptToTerminateSubProcess: false - ) - - return try await withAsyncTaskCleanupHandler { - return try await withThrowingTaskGroup( - of: TerminationStatus?.self, - returning: ExecutionResult.self - ) { group in - group.addTask { - if let writeFd = inputPipe.writeFileDescriptor { - let writer = StandardInputWriter(fileDescriptor: writeFd) - try await input.write(with: writer) - try await writer.finish() - } - return nil - } - group.addTask { - return try await monitorProcessTermination( - forProcessWithIdentifier: execution.processIdentifier - ) - } - - // Body runs in the same isolation - let result = try await body(execution) - var status: TerminationStatus? = nil - while let monitorResult = try await group.next() { - if let monitorResult = monitorResult { - status = monitorResult - } - } - return ExecutionResult(terminationStatus: status!, value: result) - } - } onCleanup: { - // Attempt to terminate the child process - // Since the task has already been cancelled, - // this is the best we can do - try? await self.cleanup( - execution: execution, - inputPipe: inputPipe, - outputPipe: outputPipe, - errorPipe: errorPipe, - childSide: false, - parentSide: true, - attemptToTerminateSubProcess: true - ) - } - } -} - -extension Configuration: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - return """ - Configuration( - executable: \(self.executable.description), - arguments: \(self.arguments.description), - environment: \(self.environment.description), - workingDirectory: \(self.workingDirectory), - platformOptions: \(self.platformOptions.description(withIndent: 1)) - ) - """ - } - - public var debugDescription: String { - return """ - Configuration( - executable: \(self.executable.debugDescription), - arguments: \(self.arguments.debugDescription), - environment: \(self.environment.debugDescription), - workingDirectory: \(self.workingDirectory), - platformOptions: \(self.platformOptions.description(withIndent: 1)) - ) - """ - } -} - -// MARK: - Cleanup -extension Configuration { - /// Close each input individually, and throw the first error if there's multiple errors thrown - @Sendable - @available(macOS 15.0, *) // FIXME: manually added availability - private func cleanup< - Output: OutputProtocol, - Error: OutputProtocol - >( - execution: Execution, - inputPipe: CreatedPipe, - outputPipe: CreatedPipe, - errorPipe: CreatedPipe, - childSide: Bool, - parentSide: Bool, - attemptToTerminateSubProcess: Bool - ) async throws { - func captureError(_ work: () throws -> Void) -> Swift.Error? { - do { - try work() - return nil - } catch { - // Ignore badFileDescriptor for double close - return error - } - } - - guard childSide || parentSide || attemptToTerminateSubProcess else { - return - } - - // Attempt to teardown the subprocess - if attemptToTerminateSubProcess { - await execution.teardown( - using: self.platformOptions.teardownSequence - ) - } - - var inputError: Swift.Error? - var outputError: Swift.Error? - var errorError: Swift.Error? // lol - - if childSide { - inputError = captureError { - try inputPipe.readFileDescriptor?.safelyClose() - } - outputError = captureError { - try outputPipe.writeFileDescriptor?.safelyClose() - } - errorError = captureError { - try errorPipe.writeFileDescriptor?.safelyClose() - } - } - - if parentSide { - inputError = captureError { - try inputPipe.writeFileDescriptor?.safelyClose() - } - outputError = captureError { - try outputPipe.readFileDescriptor?.safelyClose() - } - errorError = captureError { - try errorPipe.readFileDescriptor?.safelyClose() - } - } - - if let inputError = inputError { - throw inputError - } - - if let outputError = outputError { - throw outputError - } - - if let errorError = errorError { - throw errorError - } - } - - /// Close each input individually, and throw the first error if there's multiple errors thrown - @Sendable - internal func cleanupPreSpawn( - input: CreatedPipe, - output: CreatedPipe, - error: CreatedPipe - ) throws { - var inputError: Swift.Error? - var outputError: Swift.Error? - var errorError: Swift.Error? - - do { - try input.readFileDescriptor?.safelyClose() - try input.writeFileDescriptor?.safelyClose() - } catch { - inputError = error - } - - do { - try output.readFileDescriptor?.safelyClose() - try output.writeFileDescriptor?.safelyClose() - } catch { - outputError = error - } - - do { - try error.readFileDescriptor?.safelyClose() - try error.writeFileDescriptor?.safelyClose() - } catch { - errorError = error - } - - if let inputError = inputError { - throw inputError - } - if let outputError = outputError { - throw outputError - } - if let errorError = errorError { - throw errorError - } - } -} - -// MARK: - Executable - -/// `Executable` defines how the executable should -/// be looked up for execution. -public struct Executable: Sendable, Hashable { - internal enum Storage: Sendable, Hashable { - case executable(String) - case path(FilePath) - } - - internal let storage: Storage - - private init(_config: Storage) { - self.storage = _config - } - - /// Locate the executable by its name. - /// `Subprocess` will use `PATH` value to - /// determine the full path to the executable. - public static func name(_ executableName: String) -> Self { - return .init(_config: .executable(executableName)) - } - /// Locate the executable by its full path. - /// `Subprocess` will use this path directly. - public static func path(_ filePath: FilePath) -> Self { - return .init(_config: .path(filePath)) - } - /// Returns the full executable path given the environment value. - public func resolveExecutablePath(in environment: Environment) throws -> FilePath { - let path = try self.resolveExecutablePath(withPathValue: environment.pathValue()) - return FilePath(path) - } -} - -extension Executable: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - switch storage { - case .executable(let executableName): - return executableName - case .path(let filePath): - return filePath.string - } - } - - public var debugDescription: String { - switch storage { - case .executable(let string): - return "executable(\(string))" - case .path(let filePath): - return "path(\(filePath.string))" - } - } -} - -extension Executable { - internal func possibleExecutablePaths( - withPathValue pathValue: String? - ) -> Set { - switch self.storage { - case .executable(let executableName): - #if os(Windows) - // Windows CreateProcessW accepts executable name directly - return Set([executableName]) - #else - var results: Set = [] - // executableName could be a full path - results.insert(executableName) - // Get $PATH from environment - let searchPaths: Set - if let pathValue = pathValue { - let localSearchPaths = pathValue.split(separator: ":").map { String($0) } - searchPaths = Set(localSearchPaths).union(Self.defaultSearchPaths) - } else { - searchPaths = Self.defaultSearchPaths - } - for path in searchPaths { - results.insert( - FilePath(path).appending(executableName).string - ) - } - return results - #endif - case .path(let executablePath): - return Set([executablePath.string]) - } - } -} - -// MARK: - Arguments - -/// A collection of arguments to pass to the subprocess. -public struct Arguments: Sendable, ExpressibleByArrayLiteral, Hashable { - public typealias ArrayLiteralElement = String - - internal let storage: [StringOrRawBytes] - internal let executablePathOverride: StringOrRawBytes? - - /// Create an Arguments object using the given literal values - public init(arrayLiteral elements: String...) { - self.storage = elements.map { .string($0) } - self.executablePathOverride = nil - } - /// Create an Arguments object using the given array - public init(_ array: [String]) { - self.storage = array.map { .string($0) } - self.executablePathOverride = nil - } - - #if !os(Windows) // Windows does NOT support arg0 override - /// Create an `Argument` object using the given values, but - /// override the first Argument value to `executablePathOverride`. - /// If `executablePathOverride` is nil, - /// `Arguments` will automatically use the executable path - /// as the first argument. - /// - Parameters: - /// - executablePathOverride: the value to override the first argument. - /// - remainingValues: the rest of the argument value - public init(executablePathOverride: String?, remainingValues: [String]) { - self.storage = remainingValues.map { .string($0) } - if let executablePathOverride = executablePathOverride { - self.executablePathOverride = .string(executablePathOverride) - } else { - self.executablePathOverride = nil - } - } - - /// Create an `Argument` object using the given values, but - /// override the first Argument value to `executablePathOverride`. - /// If `executablePathOverride` is nil, - /// `Arguments` will automatically use the executable path - /// as the first argument. - /// - Parameters: - /// - executablePathOverride: the value to override the first argument. - /// - remainingValues: the rest of the argument value - public init(executablePathOverride: [UInt8]?, remainingValues: [[UInt8]]) { - self.storage = remainingValues.map { .rawBytes($0) } - if let override = executablePathOverride { - self.executablePathOverride = .rawBytes(override) - } else { - self.executablePathOverride = nil - } - } - - public init(_ array: [[UInt8]]) { - self.storage = array.map { .rawBytes($0) } - self.executablePathOverride = nil - } - #endif -} - -extension Arguments: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - var result: [String] = self.storage.map(\.description) - - if let override = self.executablePathOverride { - result.insert("override\(override.description)", at: 0) - } - return result.description - } - - public var debugDescription: String { return self.description } -} - -// MARK: - Environment - -/// A set of environment variables to use when executing the subprocess. -public struct Environment: Sendable, Hashable { - internal enum Configuration: Sendable, Hashable { - case inherit([String: String]) - case custom([String: String]) - #if !os(Windows) - case rawBytes([[UInt8]]) - #endif - } - - internal let config: Configuration - - init(config: Configuration) { - self.config = config - } - /// Child process should inherit the same environment - /// values from its parent process. - public static var inherit: Self { - return .init(config: .inherit([:])) - } - /// Override the provided `newValue` in the existing `Environment` - public func updating(_ newValue: [String: String]) -> Self { - return .init(config: .inherit(newValue)) - } - /// Use custom environment variables - public static func custom(_ newValue: [String: String]) -> Self { - return .init(config: .custom(newValue)) - } - - #if !os(Windows) - /// Use custom environment variables of raw bytes - public static func custom(_ newValue: [[UInt8]]) -> Self { - return .init(config: .rawBytes(newValue)) - } - #endif -} - -extension Environment: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - switch self.config { - case .custom(let customDictionary): - return """ - Custom environment: - \(customDictionary) - """ - case .inherit(let updateValue): - return """ - Inherting current environment with updates: - \(updateValue) - """ - #if !os(Windows) - case .rawBytes(let rawBytes): - return """ - Raw bytes: - \(rawBytes) - """ - #endif - } - } - - public var debugDescription: String { - return self.description - } - - internal static func currentEnvironmentValues() -> [String: String] { - return self.withCopiedEnv { environments in - var results: [String: String] = [:] - for env in environments { - let environmentString = String(cString: env) - - #if os(Windows) - // Windows GetEnvironmentStringsW API can return - // magic environment variables set by the cmd shell - // that starts with `=` - // We should exclude these values - if environmentString.utf8.first == Character("=").utf8.first { - continue - } - #endif // os(Windows) - - guard let delimiter = environmentString.firstIndex(of: "=") else { - continue - } - - let key = String(environmentString[environmentString.startIndex.. UnsafeMutablePointer { - switch self { - case .string(let string): - return strdup(string) - case .rawBytes(let rawBytes): - return strdup(rawBytes) - } - } - - var stringValue: String? { - switch self { - case .string(let string): - return string - case .rawBytes(let rawBytes): - return String(decoding: rawBytes, as: UTF8.self) - } - } - - var description: String { - switch self { - case .string(let string): - return string - case .rawBytes(let bytes): - return bytes.description - } - } - - var count: Int { - switch self { - case .string(let string): - return string.count - case .rawBytes(let rawBytes): - return strnlen(rawBytes, Int.max) - } - } - - func hash(into hasher: inout Hasher) { - // If Raw bytes is valid UTF8, hash it as so - switch self { - case .string(let string): - hasher.combine(string) - case .rawBytes(let bytes): - if let stringValue = self.stringValue { - hasher.combine(stringValue) - } else { - hasher.combine(bytes) - } - } - } -} - -/// A simple wrapper on `FileDescriptor` plus a flag indicating -/// whether it should be closed automactially when done. -internal struct TrackedFileDescriptor: Hashable { - internal let closeWhenDone: Bool - internal let wrapped: FileDescriptor - - internal init( - _ wrapped: FileDescriptor, - closeWhenDone: Bool - ) { - self.wrapped = wrapped - self.closeWhenDone = closeWhenDone - } - - internal func safelyClose() throws { - guard self.closeWhenDone else { - return - } - - do { - try self.wrapped.close() - } catch { - guard let errno: Errno = error as? Errno else { - throw error - } - if errno != .badFileDescriptor { - throw errno - } - } - } - - internal var platformDescriptor: PlatformFileDescriptor { - return self.wrapped.platformDescriptor - } -} - -internal struct CreatedPipe { - internal let readFileDescriptor: TrackedFileDescriptor? - internal let writeFileDescriptor: TrackedFileDescriptor? - - internal init( - readFileDescriptor: TrackedFileDescriptor?, - writeFileDescriptor: TrackedFileDescriptor? - ) { - self.readFileDescriptor = readFileDescriptor - self.writeFileDescriptor = writeFileDescriptor - } - - internal init(closeWhenDone: Bool) throws { - let pipe = try FileDescriptor.pipe() - - self.readFileDescriptor = .init( - pipe.readEnd, - closeWhenDone: closeWhenDone - ) - self.writeFileDescriptor = .init( - pipe.writeEnd, - closeWhenDone: closeWhenDone - ) - } -} - -extension FilePath { - static var currentWorkingDirectory: Self { - let path = getcwd(nil, 0)! - defer { free(path) } - return .init(String(cString: path)) - } -} - -extension Optional where Wrapped: Collection { - func withOptionalUnsafeBufferPointer( - _ body: ((UnsafeBufferPointer)?) throws -> Result - ) rethrows -> Result { - switch self { - case .some(let wrapped): - guard let array: [Wrapped.Element] = wrapped as? Array else { - return try body(nil) - } - return try array.withUnsafeBufferPointer { ptr in - return try body(ptr) - } - case .none: - return try body(nil) - } - } -} - -extension Optional where Wrapped == String { - func withOptionalCString( - _ body: ((UnsafePointer)?) throws -> Result - ) rethrows -> Result { - switch self { - case .none: - return try body(nil) - case .some(let wrapped): - return try wrapped.withCString { - return try body($0) - } - } - } - - var stringValue: String { - return self ?? "nil" - } -} - -internal func withAsyncTaskCleanupHandler( - _ body: () async throws -> Result, - onCleanup handler: @Sendable @escaping () async -> Void, - isolation: isolated (any Actor)? = #isolation -) async rethrows -> Result { - return try await withThrowingTaskGroup( - of: Void.self, - returning: Result.self - ) { group in - group.addTask { - // Keep this task sleep indefinitely until the parent task is cancelled. - // `Task.sleep` throws `CancellationError` when the task is canceled - // before the time ends. We then run the cancel handler. - do { while true { try await Task.sleep(nanoseconds: 1_000_000_000) } } catch {} - // Run task cancel handler - await handler() - } - - do { - let result = try await body() - group.cancelAll() - return result - } catch { - await handler() - throw error - } - } -} diff --git a/Sources/_Subprocess/Error.swift b/Sources/_Subprocess/Error.swift deleted file mode 100644 index bf1c91147..000000000 --- a/Sources/_Subprocess/Error.swift +++ /dev/null @@ -1,141 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if canImport(Darwin) -import Darwin -#elseif canImport(Bionic) -import Bionic -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#elseif canImport(WinSDK) -import WinSDK -#endif - -/// Error thrown from Subprocess -public struct SubprocessError: Swift.Error, Hashable, Sendable { - /// The error code of this error - public let code: SubprocessError.Code - /// The underlying error that caused this error, if any - public let underlyingError: UnderlyingError? -} - -// MARK: - Error Codes -extension SubprocessError { - /// A SubprocessError Code - public struct Code: Hashable, Sendable { - internal enum Storage: Hashable, Sendable { - case spawnFailed - case executableNotFound(String) - case failedToChangeWorkingDirectory(String) - case failedToReadFromSubprocess - case failedToWriteToSubprocess - case failedToMonitorProcess - // Signal - case failedToSendSignal(Int32) - // Windows Only - case failedToTerminate - case failedToSuspend - case failedToResume - case failedToCreatePipe - case invalidWindowsPath(String) - } - - public var value: Int { - switch self.storage { - case .spawnFailed: - return 0 - case .executableNotFound(_): - return 1 - case .failedToChangeWorkingDirectory(_): - return 2 - case .failedToReadFromSubprocess: - return 3 - case .failedToWriteToSubprocess: - return 4 - case .failedToMonitorProcess: - return 5 - case .failedToSendSignal(_): - return 6 - case .failedToTerminate: - return 7 - case .failedToSuspend: - return 8 - case .failedToResume: - return 9 - case .failedToCreatePipe: - return 10 - case .invalidWindowsPath(_): - return 11 - } - } - - internal let storage: Storage - - internal init(_ storage: Storage) { - self.storage = storage - } - } -} - -// MARK: - Description -extension SubprocessError: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - switch self.code.storage { - case .spawnFailed: - return "Failed to spawn the new process." - case .executableNotFound(let executableName): - return "Executable \"\(executableName)\" is not found or cannot be executed." - case .failedToChangeWorkingDirectory(let workingDirectory): - return "Failed to set working directory to \"\(workingDirectory)\"." - case .failedToReadFromSubprocess: - return "Failed to read bytes from the child process with underlying error: \(self.underlyingError!)" - case .failedToWriteToSubprocess: - return "Failed to write bytes to the child process." - case .failedToMonitorProcess: - return "Failed to monitor the state of child process with underlying error: \(self.underlyingError!)" - case .failedToSendSignal(let signal): - return "Failed to send signal \(signal) to the child process." - case .failedToTerminate: - return "Failed to terminate the child process." - case .failedToSuspend: - return "Failed to suspend the child process." - case .failedToResume: - return "Failed to resume the child process." - case .failedToCreatePipe: - return "Failed to create a pipe to communicate to child process." - case .invalidWindowsPath(let badPath): - return "\"\(badPath)\" is not a valid Windows path." - } - } - - public var debugDescription: String { self.description } -} - -extension SubprocessError { - /// The underlying error that caused this SubprocessError. - /// - On Unix-like systems, `UnderlyingError` wraps `errno` from libc; - /// - On Windows, `UnderlyingError` wraps Windows Error code - public struct UnderlyingError: Swift.Error, RawRepresentable, Hashable, Sendable { - #if os(Windows) - public typealias RawValue = DWORD - #else - public typealias RawValue = Int32 - #endif - - public let rawValue: RawValue - - public init(rawValue: RawValue) { - self.rawValue = rawValue - } - } -} diff --git a/Sources/_Subprocess/Execution.swift b/Sources/_Subprocess/Execution.swift deleted file mode 100644 index 8da9b4924..000000000 --- a/Sources/_Subprocess/Execution.swift +++ /dev/null @@ -1,192 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif - -#if canImport(Darwin) -import Darwin -#elseif canImport(Bionic) -import Bionic -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#elseif canImport(WinSDK) -import WinSDK -#endif - -import Synchronization - -/// An object that repersents a subprocess that has been -/// executed. You can use this object to send signals to the -/// child process as well as stream its output and error. -@available(macOS 15.0, *) // FIXME: manually added availability -public final class Execution< - Output: OutputProtocol, - Error: OutputProtocol ->: Sendable { - /// The process identifier of the current execution - public let processIdentifier: ProcessIdentifier - - internal let output: Output - internal let error: Error - internal let outputPipe: CreatedPipe - internal let errorPipe: CreatedPipe - internal let outputConsumptionState: Atomic - #if os(Windows) - internal let consoleBehavior: PlatformOptions.ConsoleBehavior - - init( - processIdentifier: ProcessIdentifier, - output: Output, - error: Error, - outputPipe: CreatedPipe, - errorPipe: CreatedPipe, - consoleBehavior: PlatformOptions.ConsoleBehavior - ) { - self.processIdentifier = processIdentifier - self.output = output - self.error = error - self.outputPipe = outputPipe - self.errorPipe = errorPipe - self.outputConsumptionState = Atomic(0) - self.consoleBehavior = consoleBehavior - } - #else - init( - processIdentifier: ProcessIdentifier, - output: Output, - error: Error, - outputPipe: CreatedPipe, - errorPipe: CreatedPipe - ) { - self.processIdentifier = processIdentifier - self.output = output - self.error = error - self.outputPipe = outputPipe - self.errorPipe = errorPipe - self.outputConsumptionState = Atomic(0) - } - #endif // os(Windows) -} - -@available(macOS 15.0, *) // FIXME: manually added availability -extension Execution where Output == SequenceOutput { - /// The standard output of the subprocess. - /// - /// Accessing this property will **fatalError** if this property was - /// accessed multiple times. Subprocess communicates with parent process - /// via pipe under the hood and each pipe can only be consumed once. - @available(macOS 15.0, *) // FIXME: manually added availability - public var standardOutput: some AsyncSequence { - let consumptionState = self.outputConsumptionState.bitwiseXor( - OutputConsumptionState.standardOutputConsumed.rawValue, - ordering: .relaxed - ).newValue - - guard OutputConsumptionState(rawValue: consumptionState).contains(.standardOutputConsumed), - let fd = self.outputPipe.readFileDescriptor - else { - fatalError("The standard output has already been consumed") - } - return AsyncBufferSequence(fileDescriptor: fd) - } -} - -@available(macOS 15.0, *) // FIXME: manually added availability -extension Execution where Error == SequenceOutput { - /// The standard error of the subprocess. - /// - /// Accessing this property will **fatalError** if this property was - /// accessed multiple times. Subprocess communicates with parent process - /// via pipe under the hood and each pipe can only be consumed once. - @available(macOS 15.0, *) // FIXME: manually added availability - public var standardError: some AsyncSequence { - let consumptionState = self.outputConsumptionState.bitwiseXor( - OutputConsumptionState.standardErrorConsumed.rawValue, - ordering: .relaxed - ).newValue - - guard OutputConsumptionState(rawValue: consumptionState).contains(.standardErrorConsumed), - let fd = self.errorPipe.readFileDescriptor - else { - fatalError("The standard output has already been consumed") - } - return AsyncBufferSequence(fileDescriptor: fd) - } -} - -// MARK: - Output Capture -internal enum OutputCapturingState: Sendable { - case standardOutputCaptured(Output) - case standardErrorCaptured(Error) -} - -internal struct OutputConsumptionState: OptionSet { - typealias RawValue = UInt8 - - internal let rawValue: UInt8 - - internal init(rawValue: UInt8) { - self.rawValue = rawValue - } - - static let standardOutputConsumed: Self = .init(rawValue: 0b0001) - static let standardErrorConsumed: Self = .init(rawValue: 0b0010) -} - -internal typealias CapturedIOs< - Output: Sendable, - Error: Sendable -> = (standardOutput: Output, standardError: Error) - -@available(macOS 15.0, *) // FIXME: manually added availability -extension Execution { - internal func captureIOs() async throws -> CapturedIOs< - Output.OutputType, Error.OutputType - > { - return try await withThrowingTaskGroup( - of: OutputCapturingState.self - ) { group in - group.addTask { - let stdout = try await self.output.captureOutput( - from: self.outputPipe.readFileDescriptor - ) - return .standardOutputCaptured(stdout) - } - group.addTask { - let stderr = try await self.error.captureOutput( - from: self.errorPipe.readFileDescriptor - ) - return .standardErrorCaptured(stderr) - } - - var stdout: Output.OutputType! - var stderror: Error.OutputType! - while let state = try await group.next() { - switch state { - case .standardOutputCaptured(let output): - stdout = output - case .standardErrorCaptured(let error): - stderror = error - } - } - return ( - standardOutput: stdout, - standardError: stderror - ) - } - } -} diff --git a/Sources/_Subprocess/IO/Input.swift b/Sources/_Subprocess/IO/Input.swift deleted file mode 100644 index 5aad5d94e..000000000 --- a/Sources/_Subprocess/IO/Input.swift +++ /dev/null @@ -1,315 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif - -#if SubprocessFoundation - -#if canImport(Darwin) -// On Darwin always prefer system Foundation -import Foundation -#else -// On other platforms prefer FoundationEssentials -import FoundationEssentials -#endif - -#endif // SubprocessFoundation - -// MARK: - Input - -/// `InputProtocol` defines the `write(with:)` method that a type -/// must implement to serve as the input source for a subprocess. -public protocol InputProtocol: Sendable, ~Copyable { - /// Asynchronously write the input to the subprocess using the - /// write file descriptor - func write(with writer: StandardInputWriter) async throws -} - -/// A concrete `Input` type for subprocesses that indicates -/// the absence of input to the subprocess. On Unix-like systems, -/// `NoInput` redirects the standard input of the subprocess -/// to `/dev/null`, while on Windows, it does not bind any -/// file handle to the subprocess standard input handle. -public struct NoInput: InputProtocol { - internal func createPipe() throws -> CreatedPipe { - #if os(Windows) - // On Windows, instead of binding to dev null, - // we don't set the input handle in the `STARTUPINFOW` - // to signal no input - return CreatedPipe( - readFileDescriptor: nil, - writeFileDescriptor: nil - ) - #else - let devnull: FileDescriptor = try .openDevNull(withAcessMode: .readOnly) - return CreatedPipe( - readFileDescriptor: .init(devnull, closeWhenDone: true), - writeFileDescriptor: nil - ) - #endif - } - - public func write(with writer: StandardInputWriter) async throws { - // noop - } - - internal init() {} -} - -/// A concrete `Input` type for subprocesses that -/// reads input from a specified `FileDescriptor`. -/// Developers have the option to instruct the `Subprocess` to -/// automatically close the provided `FileDescriptor` -/// after the subprocess is spawned. -public struct FileDescriptorInput: InputProtocol { - private let fileDescriptor: FileDescriptor - private let closeAfterSpawningProcess: Bool - - internal func createPipe() throws -> CreatedPipe { - return CreatedPipe( - readFileDescriptor: .init( - self.fileDescriptor, - closeWhenDone: self.closeAfterSpawningProcess - ), - writeFileDescriptor: nil - ) - } - - public func write(with writer: StandardInputWriter) async throws { - // noop - } - - internal init( - fileDescriptor: FileDescriptor, - closeAfterSpawningProcess: Bool - ) { - self.fileDescriptor = fileDescriptor - self.closeAfterSpawningProcess = closeAfterSpawningProcess - } -} - -/// A concrete `Input` type for subprocesses that reads input -/// from a given type conforming to `StringProtocol`. -/// Developers can specify the string encoding to use when -/// encoding the string to data, which defaults to UTF-8. -public struct StringInput< - InputString: StringProtocol & Sendable, - Encoding: Unicode.Encoding ->: InputProtocol { - private let string: InputString - - public func write(with writer: StandardInputWriter) async throws { - guard let array = self.string.byteArray(using: Encoding.self) else { - return - } - _ = try await writer.write(array) - } - - internal init(string: InputString, encoding: Encoding.Type) { - self.string = string - } -} - -/// A concrete `Input` type for subprocesses that reads input -/// from a given `UInt8` Array. -public struct ArrayInput: InputProtocol { - private let array: [UInt8] - - public func write(with writer: StandardInputWriter) async throws { - _ = try await writer.write(self.array) - } - - internal init(array: [UInt8]) { - self.array = array - } -} - -/// A concrete `Input` type for subprocess that indicates that -/// the Subprocess should read its input from `StandardInputWriter`. -public struct CustomWriteInput: InputProtocol { - public func write(with writer: StandardInputWriter) async throws { - // noop - } - - internal init() {} -} - -extension InputProtocol where Self == NoInput { - /// Create a Subprocess input that specfies there is no input - public static var none: Self { .init() } -} - -extension InputProtocol where Self == FileDescriptorInput { - /// Create a Subprocess input from a `FileDescriptor` and - /// specify whether the `FileDescriptor` should be closed - /// after the process is spawned. - public static func fileDescriptor( - _ fd: FileDescriptor, - closeAfterSpawningProcess: Bool - ) -> Self { - return .init( - fileDescriptor: fd, - closeAfterSpawningProcess: closeAfterSpawningProcess - ) - } -} - -extension InputProtocol { - /// Create a Subprocess input from a `Array` of `UInt8`. - public static func array( - _ array: [UInt8] - ) -> Self where Self == ArrayInput { - return ArrayInput(array: array) - } - - /// Create a Subprocess input from a type that conforms to `StringProtocol` - public static func string< - InputString: StringProtocol & Sendable - >( - _ string: InputString - ) -> Self where Self == StringInput { - return .init(string: string, encoding: UTF8.self) - } - - /// Create a Subprocess input from a type that conforms to `StringProtocol` - public static func string< - InputString: StringProtocol & Sendable, - Encoding: Unicode.Encoding - >( - _ string: InputString, - using encoding: Encoding.Type - ) -> Self where Self == StringInput { - return .init(string: string, encoding: encoding) - } -} - -extension InputProtocol { - internal func createPipe() throws -> CreatedPipe { - if let noInput = self as? NoInput { - return try noInput.createPipe() - } else if let fdInput = self as? FileDescriptorInput { - return try fdInput.createPipe() - } - // Base implementation - return try CreatedPipe(closeWhenDone: true) - } -} - -// MARK: - StandardInputWriter - -/// A writer that writes to the standard input of the subprocess. -public final actor StandardInputWriter: Sendable { - - internal let fileDescriptor: TrackedFileDescriptor - - init(fileDescriptor: TrackedFileDescriptor) { - self.fileDescriptor = fileDescriptor - } - - /// Write an array of UInt8 to the standard input of the subprocess. - /// - Parameter array: The sequence of bytes to write. - /// - Returns number of bytes written. - public func write( - _ array: [UInt8] - ) async throws -> Int { - return try await self.fileDescriptor.wrapped.write(array) - } - - /// Write a StringProtocol to the standard input of the subprocess. - /// - Parameters: - /// - string: The string to write. - /// - encoding: The encoding to use when converting string to bytes - /// - Returns number of bytes written. - public func write( - _ string: some StringProtocol, - using encoding: Encoding.Type = UTF8.self - ) async throws -> Int { - if let array = string.byteArray(using: encoding) { - return try await self.write(array) - } - return 0 - } - - /// Signal all writes are finished - public func finish() async throws { - try self.fileDescriptor.safelyClose() - } -} - -extension StringProtocol { - #if SubprocessFoundation - private func convertEncoding( - _ encoding: Encoding.Type - ) -> String.Encoding? { - switch encoding { - case is UTF8.Type: - return .utf8 - case is UTF16.Type: - return .utf16 - case is UTF32.Type: - return .utf32 - default: - return nil - } - } - #endif - package func byteArray(using encoding: Encoding.Type) -> [UInt8]? { - if Encoding.self == Unicode.ASCII.self { - let isASCII = self.utf8.allSatisfy { - return Character(Unicode.Scalar($0)).isASCII - } - - guard isASCII else { - return nil - } - return Array(self.utf8) - } - if Encoding.self == UTF8.self { - return Array(self.utf8) - } - if Encoding.self == UTF16.self { - return Array(self.utf16).flatMap { input in - var uint16: UInt16 = input - return withUnsafeBytes(of: &uint16) { ptr in - Array(ptr) - } - } - } - #if SubprocessFoundation - if let stringEncoding = self.convertEncoding(encoding), - let encoded = self.data(using: stringEncoding) - { - return Array(encoded) - } - return nil - #else - return nil - #endif - } -} - -extension String { - package init( - decodingBytes bytes: [T], - as encoding: Encoding.Type - ) { - self = bytes.withUnsafeBytes { raw in - String( - decoding: raw.bindMemory(to: Encoding.CodeUnit.self).lazy.map { $0 }, - as: encoding - ) - } - } -} diff --git a/Sources/_Subprocess/IO/Output.swift b/Sources/_Subprocess/IO/Output.swift deleted file mode 100644 index be186dd6b..000000000 --- a/Sources/_Subprocess/IO/Output.swift +++ /dev/null @@ -1,298 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif -internal import Dispatch - -// MARK: - Output - -/// `OutputProtocol` specifies the set of methods that a type -/// must implement to serve as the output target for a subprocess. -/// Instead of developing custom implementations of `OutputProtocol`, -/// it is recommended to utilize the default implementations provided -/// by the `Subprocess` library to specify the output handling requirements. -public protocol OutputProtocol: Sendable, ~Copyable { - associatedtype OutputType: Sendable - - /// Convert the output from buffer to expected output type - func output(from buffer: some Sequence) throws -> OutputType - - /// The max amount of data to collect for this output. - var maxSize: Int { get } -} - -extension OutputProtocol { - /// The max amount of data to collect for this output. - public var maxSize: Int { 128 * 1024 } -} - -/// A concrete `Output` type for subprocesses that indicates that -/// the `Subprocess` should not collect or redirect output -/// from the child process. On Unix-like systems, `DiscardedOutput` -/// redirects the standard output of the subprocess to `/dev/null`, -/// while on Windows, it does not bind any file handle to the -/// subprocess standard output handle. -public struct DiscardedOutput: OutputProtocol { - public typealias OutputType = Void - - internal func createPipe() throws -> CreatedPipe { - #if os(Windows) - // On Windows, instead of binding to dev null, - // we don't set the input handle in the `STARTUPINFOW` - // to signal no output - return CreatedPipe( - readFileDescriptor: nil, - writeFileDescriptor: nil - ) - #else - let devnull: FileDescriptor = try .openDevNull(withAcessMode: .readOnly) - return CreatedPipe( - readFileDescriptor: .init(devnull, closeWhenDone: true), - writeFileDescriptor: nil - ) - #endif - } - - internal init() {} -} - -/// A concrete `Output` type for subprocesses that -/// writes output to a specified `FileDescriptor`. -/// Developers have the option to instruct the `Subprocess` to -/// automatically close the provided `FileDescriptor` -/// after the subprocess is spawned. -public struct FileDescriptorOutput: OutputProtocol { - public typealias OutputType = Void - - private let closeAfterSpawningProcess: Bool - private let fileDescriptor: FileDescriptor - - internal func createPipe() throws -> CreatedPipe { - return CreatedPipe( - readFileDescriptor: nil, - writeFileDescriptor: .init( - self.fileDescriptor, - closeWhenDone: self.closeAfterSpawningProcess - ) - ) - } - - internal init( - fileDescriptor: FileDescriptor, - closeAfterSpawningProcess: Bool - ) { - self.fileDescriptor = fileDescriptor - self.closeAfterSpawningProcess = closeAfterSpawningProcess - } -} - -/// A concrete `Output` type for subprocesses that collects output -/// from the subprocess as `String` with the given encoding. -/// This option must be used with he `run()` method that -/// returns a `CollectedResult`. -public struct StringOutput: OutputProtocol { - public typealias OutputType = String? - public let maxSize: Int - private let encoding: Encoding.Type - - public func output(from buffer: some Sequence) throws -> String? { - // FIXME: Span to String - let array = Array(buffer) - return String(decodingBytes: array, as: Encoding.self) - } - - internal init(limit: Int, encoding: Encoding.Type) { - self.maxSize = limit - self.encoding = encoding - } -} - -/// A concrete `Output` type for subprocesses that collects output -/// from the subprocess as `[UInt8]`. This option must be used with -/// the `run()` method that returns a `CollectedResult` -public struct BytesOutput: OutputProtocol { - public typealias OutputType = [UInt8] - public let maxSize: Int - - internal func captureOutput(from fileDescriptor: TrackedFileDescriptor?) async throws -> [UInt8] { - return try await withCheckedThrowingContinuation { continuation in - guard let fileDescriptor = fileDescriptor else { - // Show not happen due to type system constraints - fatalError("Trying to capture output without file descriptor") - } - fileDescriptor.wrapped.readUntilEOF(upToLength: self.maxSize) { result in - switch result { - case .success(let data): - // FIXME: remove workaround for - // rdar://143992296 - // https://github.com/swiftlang/swift-subprocess/issues/3 - #if os(Windows) - continuation.resume(returning: data) - #else - continuation.resume(returning: data.array()) - #endif - case .failure(let error): - continuation.resume(throwing: error) - } - } - } - } - - public func output(from buffer: some Sequence) throws -> [UInt8] { - fatalError("Not implemented") - } - - internal init(limit: Int) { - self.maxSize = limit - } -} - -/// A concrete `Output` type for subprocesses that redirects -/// the child output to the `.standardOutput` (a sequence) or `.standardError` -/// property of `Execution`. This output type is -/// only applicable to the `run()` family that takes a custom closure. -public struct SequenceOutput: OutputProtocol { - public typealias OutputType = Void - - internal init() {} -} - -extension OutputProtocol where Self == DiscardedOutput { - /// Create a Subprocess output that discards the output - public static var discarded: Self { .init() } -} - -extension OutputProtocol where Self == FileDescriptorOutput { - /// Create a Subprocess output that writes output to a `FileDescriptor` - /// and optionally close the `FileDescriptor` once process spawned. - public static func fileDescriptor( - _ fd: FileDescriptor, - closeAfterSpawningProcess: Bool - ) -> Self { - return .init(fileDescriptor: fd, closeAfterSpawningProcess: closeAfterSpawningProcess) - } -} - -extension OutputProtocol where Self == StringOutput { - /// Create a `Subprocess` output that collects output as - /// UTF8 String with 128kb limit. - public static var string: Self { - .init(limit: 128 * 1024, encoding: UTF8.self) - } -} - -extension OutputProtocol { - /// Create a `Subprocess` output that collects output as - /// `String` using the given encoding up to limit it bytes. - public static func string( - limit: Int, - encoding: Encoding.Type - ) -> Self where Self == StringOutput { - return .init(limit: limit, encoding: encoding) - } -} - -extension OutputProtocol where Self == BytesOutput { - /// Create a `Subprocess` output that collects output as - /// `Buffer` with 128kb limit. - public static var bytes: Self { .init(limit: 128 * 1024) } - - /// Create a `Subprocess` output that collects output as - /// `Buffer` up to limit it bytes. - public static func bytes(limit: Int) -> Self { - return .init(limit: limit) - } -} - -extension OutputProtocol where Self == SequenceOutput { - /// Create a `Subprocess` output that redirects the output - /// to the `.standardOutput` (or `.standardError`) property - /// of `Execution` as `AsyncSequence`. - public static var sequence: Self { .init() } -} - -// MARK: - Default Implementations -extension OutputProtocol { - @_disfavoredOverload - internal func createPipe() throws -> CreatedPipe { - if let discard = self as? DiscardedOutput { - return try discard.createPipe() - } else if let fdOutput = self as? FileDescriptorOutput { - return try fdOutput.createPipe() - } - // Base pipe based implementation for everything else - return try CreatedPipe(closeWhenDone: true) - } - - /// Capture the output from the subprocess up to maxSize - @_disfavoredOverload - internal func captureOutput( - from fileDescriptor: TrackedFileDescriptor? - ) async throws -> OutputType { - if let bytesOutput = self as? BytesOutput { - return try await bytesOutput.captureOutput(from: fileDescriptor) as! Self.OutputType - } - return try await withCheckedThrowingContinuation { continuation in - if OutputType.self == Void.self { - continuation.resume(returning: () as! OutputType) - return - } - guard let fileDescriptor = fileDescriptor else { - // Show not happen due to type system constraints - fatalError("Trying to capture output without file descriptor") - } - - fileDescriptor.wrapped.readUntilEOF(upToLength: self.maxSize) { result in - do { - switch result { - case .success(let data): - // FIXME: remove workaround for - // rdar://143992296 - // https://github.com/swiftlang/swift-subprocess/issues/3 - let output = try self.output(from: data) - continuation.resume(returning: output) - case .failure(let error): - continuation.resume(throwing: error) - } - } catch { - continuation.resume(throwing: error) - } - } - } - } -} - -extension OutputProtocol where OutputType == Void { - internal func captureOutput(from fileDescriptor: TrackedFileDescriptor?) async throws {} - - public func output(from buffer: some Sequence) throws { - // noop - } -} - -extension DispatchData { - internal func array() -> [UInt8] { - var result: [UInt8]? - self.enumerateBytes { buffer, byteIndex, stop in - let currentChunk = Array(UnsafeRawBufferPointer(buffer)) - if result == nil { - result = currentChunk - } else { - result?.append(contentsOf: currentChunk) - } - } - return result ?? [] - } -} diff --git a/Sources/_Subprocess/Platforms/Subprocess+Darwin.swift b/Sources/_Subprocess/Platforms/Subprocess+Darwin.swift deleted file mode 100644 index cd5c310aa..000000000 --- a/Sources/_Subprocess/Platforms/Subprocess+Darwin.swift +++ /dev/null @@ -1,428 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if canImport(Darwin) - -import Darwin -internal import Dispatch -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif - -import _SubprocessCShims - -#if SubprocessFoundation - -#if canImport(Darwin) -// On Darwin always prefer system Foundation -import Foundation -#else -// On other platforms prefer FoundationEssentials -import FoundationEssentials -#endif - -#endif // SubprocessFoundation - -// MARK: - PlatformOptions - -/// The collection of platform-specific settings -/// to configure the subprocess when running -public struct PlatformOptions: Sendable { - public var qualityOfService: QualityOfService = .default - /// Set user ID for the subprocess - public var userID: uid_t? = nil - /// Set the real and effective group ID and the saved - /// set-group-ID of the subprocess, equivalent to calling - /// `setgid()` on the child process. - /// Group ID is used to control permissions, particularly - /// for file access. - public var groupID: gid_t? = nil - /// Set list of supplementary group IDs for the subprocess - public var supplementaryGroups: [gid_t]? = nil - /// Set the process group for the subprocess, equivalent to - /// calling `setpgid()` on the child process. - /// Process group ID is used to group related processes for - /// controlling signals. - public var processGroupID: pid_t? = nil - /// Creates a session and sets the process group ID - /// i.e. Detach from the terminal. - public var createSession: Bool = false - /// An ordered list of steps in order to tear down the child - /// process in case the parent task is cancelled before - /// the child proces terminates. - /// Always ends in sending a `.kill` signal at the end. - public var teardownSequence: [TeardownStep] = [] - /// A closure to configure platform-specific - /// spawning constructs. This closure enables direct - /// configuration or override of underlying platform-specific - /// spawn settings that `Subprocess` utilizes internally, - /// in cases where Subprocess does not provide higher-level - /// APIs for such modifications. - /// - /// On Darwin, Subprocess uses `posix_spawn()` as the - /// underlying spawning mechanism. This closure allows - /// modification of the `posix_spawnattr_t` spawn attribute - /// and file actions `posix_spawn_file_actions_t` before - /// they are sent to `posix_spawn()`. - public var preSpawnProcessConfigurator: - ( - @Sendable ( - inout posix_spawnattr_t?, - inout posix_spawn_file_actions_t? - ) throws -> Void - )? = nil - - public init() {} -} - -extension PlatformOptions { - #if SubprocessFoundation - public typealias QualityOfService = Foundation.QualityOfService - #else - /// Constants that indicate the nature and importance of work to the system. - /// - /// Work with higher quality of service classes receive more resources - /// than work with lower quality of service classes whenever - /// there’s resource contention. - public enum QualityOfService: Int, Sendable { - /// Used for work directly involved in providing an - /// interactive UI. For example, processing control - /// events or drawing to the screen. - case userInteractive = 0x21 - /// Used for performing work that has been explicitly requested - /// by the user, and for which results must be immediately - /// presented in order to allow for further user interaction. - /// For example, loading an email after a user has selected - /// it in a message list. - case userInitiated = 0x19 - /// Used for performing work which the user is unlikely to be - /// immediately waiting for the results. This work may have been - /// requested by the user or initiated automatically, and often - /// operates at user-visible timescales using a non-modal - /// progress indicator. For example, periodic content updates - /// or bulk file operations, such as media import. - case utility = 0x11 - /// Used for work that is not user initiated or visible. - /// In general, a user is unaware that this work is even happening. - /// For example, pre-fetching content, search indexing, backups, - /// or syncing of data with external systems. - case background = 0x09 - /// Indicates no explicit quality of service information. - /// Whenever possible, an appropriate quality of service is determined - /// from available sources. Otherwise, some quality of service level - /// between `.userInteractive` and `.utility` is used. - case `default` = -1 - } - #endif -} - -extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible { - internal func description(withIndent indent: Int) -> String { - let indent = String(repeating: " ", count: indent * 4) - return """ - PlatformOptions( - \(indent) qualityOfService: \(self.qualityOfService), - \(indent) userID: \(String(describing: userID)), - \(indent) groupID: \(String(describing: groupID)), - \(indent) supplementaryGroups: \(String(describing: supplementaryGroups)), - \(indent) processGroupID: \(String(describing: processGroupID)), - \(indent) createSession: \(createSession), - \(indent) preSpawnProcessConfigurator: \(self.preSpawnProcessConfigurator == nil ? "not set" : "set") - \(indent)) - """ - } - - public var description: String { - return self.description(withIndent: 0) - } - - public var debugDescription: String { - return self.description(withIndent: 0) - } -} - -// MARK: - Spawn -extension Configuration { - @available(macOS 15.0, *) // FIXME: manually added availability - internal func spawn< - Output: OutputProtocol, - Error: OutputProtocol - >( - withInput inputPipe: CreatedPipe, - output: Output, - outputPipe: CreatedPipe, - error: Error, - errorPipe: CreatedPipe - ) throws -> Execution { - // Instead of checking if every possible executable path - // is valid, spawn each directly and catch ENOENT - let possiblePaths = self.executable.possibleExecutablePaths( - withPathValue: self.environment.pathValue() - ) - return try self.preSpawn { args throws -> Execution in - let (env, uidPtr, gidPtr, supplementaryGroups) = args - for possibleExecutablePath in possiblePaths { - var pid: pid_t = 0 - - // Setup Arguments - let argv: [UnsafeMutablePointer?] = self.arguments.createArgs( - withExecutablePath: possibleExecutablePath - ) - defer { - for ptr in argv { ptr?.deallocate() } - } - - // Setup file actions and spawn attributes - var fileActions: posix_spawn_file_actions_t? = nil - var spawnAttributes: posix_spawnattr_t? = nil - // Setup stdin, stdout, and stderr - posix_spawn_file_actions_init(&fileActions) - defer { - posix_spawn_file_actions_destroy(&fileActions) - } - // Input - var result: Int32 = -1 - if let inputRead = inputPipe.readFileDescriptor { - result = posix_spawn_file_actions_adddup2(&fileActions, inputRead.wrapped.rawValue, 0) - guard result == 0 else { - try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: result) - ) - } - } - if let inputWrite = inputPipe.writeFileDescriptor { - // Close parent side - result = posix_spawn_file_actions_addclose(&fileActions, inputWrite.wrapped.rawValue) - guard result == 0 else { - try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: result) - ) - } - } - // Output - if let outputWrite = outputPipe.writeFileDescriptor { - result = posix_spawn_file_actions_adddup2(&fileActions, outputWrite.wrapped.rawValue, 1) - guard result == 0 else { - try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: result) - ) - } - } - if let outputRead = outputPipe.readFileDescriptor { - // Close parent side - result = posix_spawn_file_actions_addclose(&fileActions, outputRead.wrapped.rawValue) - guard result == 0 else { - try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: result) - ) - } - } - // Error - if let errorWrite = errorPipe.writeFileDescriptor { - result = posix_spawn_file_actions_adddup2(&fileActions, errorWrite.wrapped.rawValue, 2) - guard result == 0 else { - try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: result) - ) - } - } - if let errorRead = errorPipe.readFileDescriptor { - // Close parent side - result = posix_spawn_file_actions_addclose(&fileActions, errorRead.wrapped.rawValue) - guard result == 0 else { - try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: result) - ) - } - } - // Setup spawnAttributes - posix_spawnattr_init(&spawnAttributes) - defer { - posix_spawnattr_destroy(&spawnAttributes) - } - var noSignals = sigset_t() - var allSignals = sigset_t() - sigemptyset(&noSignals) - sigfillset(&allSignals) - posix_spawnattr_setsigmask(&spawnAttributes, &noSignals) - posix_spawnattr_setsigdefault(&spawnAttributes, &allSignals) - // Configure spawnattr - var spawnAttributeError: Int32 = 0 - var flags: Int32 = POSIX_SPAWN_CLOEXEC_DEFAULT | POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF - if let pgid = self.platformOptions.processGroupID { - flags |= POSIX_SPAWN_SETPGROUP - spawnAttributeError = posix_spawnattr_setpgroup(&spawnAttributes, pid_t(pgid)) - } - spawnAttributeError = posix_spawnattr_setflags(&spawnAttributes, Int16(flags)) - // Set QualityOfService - // spanattr_qos seems to only accept `QOS_CLASS_UTILITY` or `QOS_CLASS_BACKGROUND` - // and returns an error of `EINVAL` if anything else is provided - if spawnAttributeError == 0 && self.platformOptions.qualityOfService == .utility { - spawnAttributeError = posix_spawnattr_set_qos_class_np(&spawnAttributes, QOS_CLASS_UTILITY) - } else if spawnAttributeError == 0 && self.platformOptions.qualityOfService == .background { - spawnAttributeError = posix_spawnattr_set_qos_class_np(&spawnAttributes, QOS_CLASS_BACKGROUND) - } - - // Setup cwd - let intendedWorkingDir = self.workingDirectory.string - let chdirError: Int32 = intendedWorkingDir.withPlatformString { workDir in - return posix_spawn_file_actions_addchdir_np(&fileActions, workDir) - } - - // Error handling - if chdirError != 0 || spawnAttributeError != 0 { - try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) - if spawnAttributeError != 0 { - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: spawnAttributeError) - ) - } - - if chdirError != 0 { - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: spawnAttributeError) - ) - } - } - // Run additional config - if let spawnConfig = self.platformOptions.preSpawnProcessConfigurator { - try spawnConfig(&spawnAttributes, &fileActions) - } - - // Spawn - let spawnError: CInt = possibleExecutablePath.withCString { exePath in - return supplementaryGroups.withOptionalUnsafeBufferPointer { sgroups in - return _subprocess_spawn( - &pid, - exePath, - &fileActions, - &spawnAttributes, - argv, - env, - uidPtr, - gidPtr, - Int32(supplementaryGroups?.count ?? 0), - sgroups?.baseAddress, - self.platformOptions.createSession ? 1 : 0 - ) - } - } - // Spawn error - if spawnError != 0 { - if spawnError == ENOENT { - // Move on to another possible path - continue - } - // Throw all other errors - try self.cleanupPreSpawn( - input: inputPipe, - output: outputPipe, - error: errorPipe - ) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: spawnError) - ) - } - return Execution( - processIdentifier: .init(value: pid), - output: output, - error: error, - outputPipe: outputPipe, - errorPipe: errorPipe - ) - } - - // If we reach this point, it means either the executable path - // or working directory is not valid. Since posix_spawn does not - // provide which one is not valid, here we make a best effort guess - // by checking whether the working directory is valid. This technically - // still causes TOUTOC issue, but it's the best we can do for error recovery. - try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) - let workingDirectory = self.workingDirectory.string - guard Configuration.pathAccessible(workingDirectory, mode: F_OK) else { - throw SubprocessError( - code: .init(.failedToChangeWorkingDirectory(workingDirectory)), - underlyingError: .init(rawValue: ENOENT) - ) - } - throw SubprocessError( - code: .init(.executableNotFound(self.executable.description)), - underlyingError: .init(rawValue: ENOENT) - ) - } - } -} - -// Special keys used in Error's user dictionary -extension String { - static let debugDescriptionErrorKey = "NSDebugDescription" -} - -// MARK: - Process Monitoring -@Sendable -internal func monitorProcessTermination( - forProcessWithIdentifier pid: ProcessIdentifier -) async throws -> TerminationStatus { - return try await withCheckedThrowingContinuation { continuation in - let source = DispatchSource.makeProcessSource( - identifier: pid.value, - eventMask: [.exit], - queue: .global() - ) - source.setEventHandler { - source.cancel() - var siginfo = siginfo_t() - let rc = waitid(P_PID, id_t(pid.value), &siginfo, WEXITED) - guard rc == 0 else { - continuation.resume( - throwing: SubprocessError( - code: .init(.failedToMonitorProcess), - underlyingError: .init(rawValue: errno) - ) - ) - return - } - switch siginfo.si_code { - case .init(CLD_EXITED): - continuation.resume(returning: .exited(siginfo.si_status)) - return - case .init(CLD_KILLED), .init(CLD_DUMPED): - continuation.resume(returning: .unhandledException(siginfo.si_status)) - case .init(CLD_TRAPPED), .init(CLD_STOPPED), .init(CLD_CONTINUED), .init(CLD_NOOP): - // Ignore these signals because they are not related to - // process exiting - break - default: - fatalError("Unexpected exit status: \(siginfo.si_code)") - } - } - source.resume() - } -} - -#endif // canImport(Darwin) diff --git a/Sources/_Subprocess/Platforms/Subprocess+Linux.swift b/Sources/_Subprocess/Platforms/Subprocess+Linux.swift deleted file mode 100644 index 23c9b36e5..000000000 --- a/Sources/_Subprocess/Platforms/Subprocess+Linux.swift +++ /dev/null @@ -1,321 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if canImport(Glibc) || canImport(Bionic) || canImport(Musl) - -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif - -#if canImport(Glibc) -import Glibc -#elseif canImport(Bionic) -import Bionic -#elseif canImport(Musl) -import Musl -#endif - -internal import Dispatch - -import Synchronization -import _SubprocessCShims - -// Linux specific implementations -extension Configuration { - internal func spawn< - Output: OutputProtocol, - Error: OutputProtocol - >( - withInput inputPipe: CreatedPipe, - output: Output, - outputPipe: CreatedPipe, - error: Error, - errorPipe: CreatedPipe - ) throws -> Execution { - _setupMonitorSignalHandler() - - // Instead of checking if every possible executable path - // is valid, spawn each directly and catch ENOENT - let possiblePaths = self.executable.possibleExecutablePaths( - withPathValue: self.environment.pathValue() - ) - - return try self.preSpawn { args throws -> Execution in - let (env, uidPtr, gidPtr, supplementaryGroups) = args - - for possibleExecutablePath in possiblePaths { - var processGroupIDPtr: UnsafeMutablePointer? = nil - if let processGroupID = self.platformOptions.processGroupID { - processGroupIDPtr = .allocate(capacity: 1) - processGroupIDPtr?.pointee = gid_t(processGroupID) - } - // Setup Arguments - let argv: [UnsafeMutablePointer?] = self.arguments.createArgs( - withExecutablePath: possibleExecutablePath - ) - defer { - for ptr in argv { ptr?.deallocate() } - } - // Setup input - let fileDescriptors: [CInt] = [ - inputPipe.readFileDescriptor?.wrapped.rawValue ?? -1, - inputPipe.writeFileDescriptor?.wrapped.rawValue ?? -1, - outputPipe.writeFileDescriptor?.wrapped.rawValue ?? -1, - outputPipe.readFileDescriptor?.wrapped.rawValue ?? -1, - errorPipe.writeFileDescriptor?.wrapped.rawValue ?? -1, - errorPipe.readFileDescriptor?.wrapped.rawValue ?? -1, - ] - - let workingDirectory: String = self.workingDirectory.string - // Spawn - var pid: pid_t = 0 - let spawnError: CInt = possibleExecutablePath.withCString { exePath in - return workingDirectory.withCString { workingDir in - return supplementaryGroups.withOptionalUnsafeBufferPointer { sgroups in - return fileDescriptors.withUnsafeBufferPointer { fds in - return _subprocess_fork_exec( - &pid, - exePath, - workingDir, - fds.baseAddress!, - argv, - env, - uidPtr, - gidPtr, - processGroupIDPtr, - CInt(supplementaryGroups?.count ?? 0), - sgroups?.baseAddress, - self.platformOptions.createSession ? 1 : 0, - self.platformOptions.preSpawnProcessConfigurator - ) - } - } - } - } - // Spawn error - if spawnError != 0 { - if spawnError == ENOENT { - // Move on to another possible path - continue - } - // Throw all other errors - try self.cleanupPreSpawn( - input: inputPipe, - output: outputPipe, - error: errorPipe - ) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: spawnError) - ) - } - return Execution( - processIdentifier: .init(value: pid), - output: output, - error: error, - outputPipe: outputPipe, - errorPipe: errorPipe - ) - } - - // If we reach this point, it means either the executable path - // or working directory is not valid. Since posix_spawn does not - // provide which one is not valid, here we make a best effort guess - // by checking whether the working directory is valid. This technically - // still causes TOUTOC issue, but it's the best we can do for error recovery. - try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) - let workingDirectory = self.workingDirectory.string - guard Configuration.pathAccessible(workingDirectory, mode: F_OK) else { - throw SubprocessError( - code: .init(.failedToChangeWorkingDirectory(workingDirectory)), - underlyingError: .init(rawValue: ENOENT) - ) - } - throw SubprocessError( - code: .init(.executableNotFound(self.executable.description)), - underlyingError: .init(rawValue: ENOENT) - ) - } - } -} - -// MARK: - Platform Specific Options - -/// The collection of platform-specific settings -/// to configure the subprocess when running -public struct PlatformOptions: Sendable { - // Set user ID for the subprocess - public var userID: uid_t? = nil - /// Set the real and effective group ID and the saved - /// set-group-ID of the subprocess, equivalent to calling - /// `setgid()` on the child process. - /// Group ID is used to control permissions, particularly - /// for file access. - public var groupID: gid_t? = nil - // Set list of supplementary group IDs for the subprocess - public var supplementaryGroups: [gid_t]? = nil - /// Set the process group for the subprocess, equivalent to - /// calling `setpgid()` on the child process. - /// Process group ID is used to group related processes for - /// controlling signals. - public var processGroupID: pid_t? = nil - // Creates a session and sets the process group ID - // i.e. Detach from the terminal. - public var createSession: Bool = false - /// An ordered list of steps in order to tear down the child - /// process in case the parent task is cancelled before - /// the child proces terminates. - /// Always ends in sending a `.kill` signal at the end. - public var teardownSequence: [TeardownStep] = [] - /// A closure to configure platform-specific - /// spawning constructs. This closure enables direct - /// configuration or override of underlying platform-specific - /// spawn settings that `Subprocess` utilizes internally, - /// in cases where Subprocess does not provide higher-level - /// APIs for such modifications. - /// - /// On Linux, Subprocess uses `fork/exec` as the - /// underlying spawning mechanism. This closure is called - /// after `fork()` but before `exec()`. You may use it to - /// call any necessary process setup functions. - public var preSpawnProcessConfigurator: (@convention(c) @Sendable () -> Void)? = nil - - public init() {} -} - -extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible { - internal func description(withIndent indent: Int) -> String { - let indent = String(repeating: " ", count: indent * 4) - return """ - PlatformOptions( - \(indent) userID: \(String(describing: userID)), - \(indent) groupID: \(String(describing: groupID)), - \(indent) supplementaryGroups: \(String(describing: supplementaryGroups)), - \(indent) processGroupID: \(String(describing: processGroupID)), - \(indent) createSession: \(createSession), - \(indent) preSpawnProcessConfigurator: \(self.preSpawnProcessConfigurator == nil ? "not set" : "set") - \(indent)) - """ - } - - public var description: String { - return self.description(withIndent: 0) - } - - public var debugDescription: String { - return self.description(withIndent: 0) - } -} - -// Special keys used in Error's user dictionary -extension String { - static let debugDescriptionErrorKey = "DebugDescription" -} - -// MARK: - Process Monitoring -@Sendable -internal func monitorProcessTermination( - forProcessWithIdentifier pid: ProcessIdentifier -) async throws -> TerminationStatus { - return try await withCheckedThrowingContinuation { continuation in - _childProcessContinuations.withLock { continuations in - if let existing = continuations.removeValue(forKey: pid.value), - case .status(let existingStatus) = existing - { - // We already have existing status to report - continuation.resume(returning: existingStatus) - } else { - // Save the continuation for handler - continuations[pid.value] = .continuation(continuation) - } - } - } -} - -private enum ContinuationOrStatus { - case continuation(CheckedContinuation) - case status(TerminationStatus) -} - -private let _childProcessContinuations: - Mutex< - [pid_t: ContinuationOrStatus] - > = Mutex([:]) - -private let signalSource: SendableSourceSignal = SendableSourceSignal() - -private let setup: () = { - signalSource.setEventHandler { - _childProcessContinuations.withLock { continuations in - while true { - var siginfo = siginfo_t() - guard waitid(P_ALL, id_t(0), &siginfo, WEXITED) == 0 else { - return - } - var status: TerminationStatus? = nil - switch siginfo.si_code { - case .init(CLD_EXITED): - status = .exited(siginfo._sifields._sigchld.si_status) - case .init(CLD_KILLED), .init(CLD_DUMPED): - status = .unhandledException(siginfo._sifields._sigchld.si_status) - case .init(CLD_TRAPPED), .init(CLD_STOPPED), .init(CLD_CONTINUED): - // Ignore these signals because they are not related to - // process exiting - break - default: - fatalError("Unexpected exit status: \(siginfo.si_code)") - } - if let status = status { - let pid = siginfo._sifields._sigchld.si_pid - if let existing = continuations.removeValue(forKey: pid), - case .continuation(let c) = existing - { - c.resume(returning: status) - } else { - // We don't have continuation yet, just state status - continuations[pid] = .status(status) - } - } - } - } - } - signalSource.resume() -}() - -/// Unchecked Sendable here since this class is only explicitly -/// initialzied once during the lifetime of the process -final class SendableSourceSignal: @unchecked Sendable { - private let signalSource: DispatchSourceSignal - - func setEventHandler(handler: @escaping DispatchSourceHandler) { - self.signalSource.setEventHandler(handler: handler) - } - - func resume() { - self.signalSource.resume() - } - - init() { - self.signalSource = DispatchSource.makeSignalSource( - signal: SIGCHLD, - queue: .global() - ) - } -} - -private func _setupMonitorSignalHandler() { - // Only executed once - setup -} - -#endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl) diff --git a/Sources/_Subprocess/Platforms/Subprocess+Unix.swift b/Sources/_Subprocess/Platforms/Subprocess+Unix.swift deleted file mode 100644 index ae8fd639b..000000000 --- a/Sources/_Subprocess/Platforms/Subprocess+Unix.swift +++ /dev/null @@ -1,516 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if canImport(Darwin) || canImport(Glibc) || canImport(Bionic) || canImport(Musl) - -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif - -import _SubprocessCShims - -#if canImport(Darwin) -import Darwin -#elseif canImport(Bionic) -import Bionic -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#endif - -package import Dispatch - -// MARK: - Signals - -/// Signals are standardized messages sent to a running program -/// to trigger specific behavior, such as quitting or error handling. -public struct Signal: Hashable, Sendable { - /// The underlying platform specific value for the signal - public let rawValue: Int32 - - private init(rawValue: Int32) { - self.rawValue = rawValue - } - - /// The `.interrupt` signal is sent to a process by its - /// controlling terminal when a user wishes to interrupt - /// the process. - public static var interrupt: Self { .init(rawValue: SIGINT) } - /// The `.terminate` signal is sent to a process to request its - /// termination. Unlike the `.kill` signal, it can be caught - /// and interpreted or ignored by the process. This allows - /// the process to perform nice termination releasing resources - /// and saving state if appropriate. `.interrupt` is nearly - /// identical to `.terminate`. - public static var terminate: Self { .init(rawValue: SIGTERM) } - /// The `.suspend` signal instructs the operating system - /// to stop a process for later resumption. - public static var suspend: Self { .init(rawValue: SIGSTOP) } - /// The `resume` signal instructs the operating system to - /// continue (restart) a process previously paused by the - /// `.suspend` signal. - public static var resume: Self { .init(rawValue: SIGCONT) } - /// The `.kill` signal is sent to a process to cause it to - /// terminate immediately (kill). In contrast to `.terminate` - /// and `.interrupt`, this signal cannot be caught or ignored, - /// and the receiving process cannot perform any - /// clean-up upon receiving this signal. - public static var kill: Self { .init(rawValue: SIGKILL) } - /// The `.terminalClosed` signal is sent to a process when - /// its controlling terminal is closed. In modern systems, - /// this signal usually means that the controlling pseudo - /// or virtual terminal has been closed. - public static var terminalClosed: Self { .init(rawValue: SIGHUP) } - /// The `.quit` signal is sent to a process by its controlling - /// terminal when the user requests that the process quit - /// and perform a core dump. - public static var quit: Self { .init(rawValue: SIGQUIT) } - /// The `.userDefinedOne` signal is sent to a process to indicate - /// user-defined conditions. - public static var userDefinedOne: Self { .init(rawValue: SIGUSR1) } - /// The `.userDefinedTwo` signal is sent to a process to indicate - /// user-defined conditions. - public static var userDefinedTwo: Self { .init(rawValue: SIGUSR2) } - /// The `.alarm` signal is sent to a process when the corresponding - /// time limit is reached. - public static var alarm: Self { .init(rawValue: SIGALRM) } - /// The `.windowSizeChange` signal is sent to a process when - /// its controlling terminal changes its size (a window change). - public static var windowSizeChange: Self { .init(rawValue: SIGWINCH) } -} - -// MARK: - ProcessIdentifier - -/// A platform independent identifier for a Subprocess. -public struct ProcessIdentifier: Sendable, Hashable, Codable { - /// The platform specific process identifier value - public let value: pid_t - - public init(value: pid_t) { - self.value = value - } -} - -extension ProcessIdentifier: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { "\(self.value)" } - - public var debugDescription: String { "\(self.value)" } -} - -@available(macOS 15.0, *) // FIXME: manually added availability -extension Execution { - /// Send the given signal to the child process. - /// - Parameters: - /// - signal: The signal to send. - /// - shouldSendToProcessGroup: Whether this signal should be sent to - /// the entire process group. - public func send( - signal: Signal, - toProcessGroup shouldSendToProcessGroup: Bool = false - ) throws { - let pid = shouldSendToProcessGroup ? -(self.processIdentifier.value) : self.processIdentifier.value - guard kill(pid, signal.rawValue) == 0 else { - throw SubprocessError( - code: .init(.failedToSendSignal(signal.rawValue)), - underlyingError: .init(rawValue: errno) - ) - } - } - - internal func tryTerminate() -> Swift.Error? { - do { - try self.send(signal: .kill) - } catch { - guard let posixError: SubprocessError = error as? SubprocessError else { - return error - } - // Ignore ESRCH (no such process) - if let underlyingError = posixError.underlyingError, - underlyingError.rawValue != ESRCH - { - return error - } - } - return nil - } -} - -// MARK: - Environment Resolution -extension Environment { - internal static let pathVariableName = "PATH" - - internal func pathValue() -> String? { - switch self.config { - case .inherit(let overrides): - // If PATH value exists in overrides, use it - if let value = overrides[Self.pathVariableName] { - return value - } - // Fall back to current process - return Self.currentEnvironmentValues()[Self.pathVariableName] - case .custom(let fullEnvironment): - if let value = fullEnvironment[Self.pathVariableName] { - return value - } - return nil - case .rawBytes(let rawBytesArray): - let needle: [UInt8] = Array("\(Self.pathVariableName)=".utf8) - for row in rawBytesArray { - guard row.starts(with: needle) else { - continue - } - // Attempt to - let pathValue = row.dropFirst(needle.count) - return String(decoding: pathValue, as: UTF8.self) - } - return nil - } - } - - // This method follows the standard "create" rule: `env` needs to be - // manually deallocated - internal func createEnv() -> [UnsafeMutablePointer?] { - func createFullCString( - fromKey keyContainer: StringOrRawBytes, - value valueContainer: StringOrRawBytes - ) -> UnsafeMutablePointer { - let rawByteKey: UnsafeMutablePointer = keyContainer.createRawBytes() - let rawByteValue: UnsafeMutablePointer = valueContainer.createRawBytes() - defer { - rawByteKey.deallocate() - rawByteValue.deallocate() - } - /// length = `key` + `=` + `value` + `\null` - let totalLength = keyContainer.count + 1 + valueContainer.count + 1 - let fullString: UnsafeMutablePointer = .allocate(capacity: totalLength) - #if canImport(Darwin) - _ = snprintf(ptr: fullString, totalLength, "%s=%s", rawByteKey, rawByteValue) - #else - _ = _shims_snprintf(fullString, CInt(totalLength), "%s=%s", rawByteKey, rawByteValue) - #endif - return fullString - } - - var env: [UnsafeMutablePointer?] = [] - switch self.config { - case .inherit(let updates): - var current = Self.currentEnvironmentValues() - for (key, value) in updates { - // Remove the value from current to override it - current.removeValue(forKey: key) - let fullString = "\(key)=\(value)" - env.append(strdup(fullString)) - } - // Add the rest of `current` to env - for (key, value) in current { - let fullString = "\(key)=\(value)" - env.append(strdup(fullString)) - } - case .custom(let customValues): - for (key, value) in customValues { - let fullString = "\(key)=\(value)" - env.append(strdup(fullString)) - } - case .rawBytes(let rawBytesArray): - for rawBytes in rawBytesArray { - env.append(strdup(rawBytes)) - } - } - env.append(nil) - return env - } - - internal static func withCopiedEnv(_ body: ([UnsafeMutablePointer]) -> R) -> R { - var values: [UnsafeMutablePointer] = [] - // This lock is taken by calls to getenv, so we want as few callouts to other code as possible here. - _subprocess_lock_environ() - guard - let environments: UnsafeMutablePointer?> = - _subprocess_get_environ() - else { - _subprocess_unlock_environ() - return body([]) - } - var curr = environments - while let value = curr.pointee { - values.append(strdup(value)) - curr = curr.advanced(by: 1) - } - _subprocess_unlock_environ() - defer { values.forEach { free($0) } } - return body(values) - } -} - -// MARK: Args Creation -extension Arguments { - // This method follows the standard "create" rule: `args` needs to be - // manually deallocated - internal func createArgs(withExecutablePath executablePath: String) -> [UnsafeMutablePointer?] { - var argv: [UnsafeMutablePointer?] = self.storage.map { $0.createRawBytes() } - // argv[0] = executable path - if let override = self.executablePathOverride { - argv.insert(override.createRawBytes(), at: 0) - } else { - argv.insert(strdup(executablePath), at: 0) - } - argv.append(nil) - return argv - } -} - -// MARK: - Executable Searching -extension Executable { - internal static var defaultSearchPaths: Set { - return Set([ - "/usr/bin", - "/bin", - "/usr/sbin", - "/sbin", - "/usr/local/bin", - ]) - } - - internal func resolveExecutablePath(withPathValue pathValue: String?) throws -> String { - switch self.storage { - case .executable(let executableName): - // If the executableName in is already a full path, return it directly - if Configuration.pathAccessible(executableName, mode: X_OK) { - return executableName - } - // Get $PATH from environment - let searchPaths: Set - if let pathValue = pathValue { - let localSearchPaths = pathValue.split(separator: ":").map { String($0) } - searchPaths = Set(localSearchPaths).union(Self.defaultSearchPaths) - } else { - searchPaths = Self.defaultSearchPaths - } - - for path in searchPaths { - let fullPath = "\(path)/\(executableName)" - let fileExists = Configuration.pathAccessible(fullPath, mode: X_OK) - if fileExists { - return fullPath - } - } - throw SubprocessError( - code: .init(.executableNotFound(executableName)), - underlyingError: nil - ) - case .path(let executablePath): - // Use path directly - return executablePath.string - } - } -} - -// MARK: - PreSpawn -extension Configuration { - internal typealias PreSpawnArgs = ( - env: [UnsafeMutablePointer?], - uidPtr: UnsafeMutablePointer?, - gidPtr: UnsafeMutablePointer?, - supplementaryGroups: [gid_t]? - ) - - internal func preSpawn( - _ work: (PreSpawnArgs) throws -> Result - ) throws -> Result { - // Prepare environment - let env = self.environment.createEnv() - defer { - for ptr in env { ptr?.deallocate() } - } - - var uidPtr: UnsafeMutablePointer? = nil - if let userID = self.platformOptions.userID { - uidPtr = .allocate(capacity: 1) - uidPtr?.pointee = userID - } - defer { - uidPtr?.deallocate() - } - var gidPtr: UnsafeMutablePointer? = nil - if let groupID = self.platformOptions.groupID { - gidPtr = .allocate(capacity: 1) - gidPtr?.pointee = groupID - } - defer { - gidPtr?.deallocate() - } - var supplementaryGroups: [gid_t]? - if let groupsValue = self.platformOptions.supplementaryGroups { - supplementaryGroups = groupsValue - } - return try work( - ( - env: env, - uidPtr: uidPtr, - gidPtr: gidPtr, - supplementaryGroups: supplementaryGroups - ) - ) - } - - internal static func pathAccessible(_ path: String, mode: Int32) -> Bool { - return path.withCString { - return access($0, mode) == 0 - } - } -} - -// MARK: - FileDescriptor extensions -extension FileDescriptor { - internal static func openDevNull( - withAcessMode mode: FileDescriptor.AccessMode - ) throws -> FileDescriptor { - let devnull: FileDescriptor = try .open("/dev/null", mode) - return devnull - } - - internal var platformDescriptor: PlatformFileDescriptor { - return self - } - - package func readChunk(upToLength maxLength: Int) async throws -> SequenceOutput.Buffer? { - return try await withCheckedThrowingContinuation { continuation in - DispatchIO.read( - fromFileDescriptor: self.rawValue, - maxLength: maxLength, - runningHandlerOn: .global() - ) { data, error in - if error != 0 { - continuation.resume( - throwing: SubprocessError( - code: .init(.failedToReadFromSubprocess), - underlyingError: .init(rawValue: error) - ) - ) - return - } - if data.isEmpty { - continuation.resume(returning: nil) - } else { - continuation.resume(returning: SequenceOutput.Buffer(data: data)) - } - } - } - } - - internal func readUntilEOF( - upToLength maxLength: Int, - resultHandler: sending @escaping (Swift.Result) -> Void - ) { - let dispatchIO = DispatchIO( - type: .stream, - fileDescriptor: self.rawValue, - queue: .global() - ) { error in } - var buffer: DispatchData? - dispatchIO.read( - offset: 0, - length: maxLength, - queue: .global() - ) { done, data, error in - guard error == 0, let chunkData = data else { - dispatchIO.close() - resultHandler( - .failure( - SubprocessError( - code: .init(.failedToReadFromSubprocess), - underlyingError: .init(rawValue: error) - ) - ) - ) - return - } - // Easy case: if we are done and buffer is nil, this means - // there is only one chunk of data - if done && buffer == nil { - dispatchIO.close() - buffer = chunkData - resultHandler(.success(chunkData)) - return - } - - if buffer == nil { - buffer = chunkData - } else { - buffer?.append(chunkData) - } - - if done { - dispatchIO.close() - resultHandler(.success(buffer!)) - return - } - } - } - - package func write( - _ array: [UInt8] - ) async throws -> Int { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - let dispatchData = array.withUnsafeBytes { - return DispatchData( - bytesNoCopy: $0, - deallocator: .custom( - nil, - { - // noop - } - ) - ) - } - self.write(dispatchData) { writtenLength, error in - if let error = error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: writtenLength) - } - } - } - } - - package func write( - _ dispatchData: DispatchData, - queue: DispatchQueue = .global(), - completion: @escaping (Int, Error?) -> Void - ) { - DispatchIO.write( - toFileDescriptor: self.rawValue, - data: dispatchData, - runningHandlerOn: queue - ) { unwritten, error in - let unwrittenLength = unwritten?.count ?? 0 - let writtenLength = dispatchData.count - unwrittenLength - guard error != 0 else { - completion(writtenLength, nil) - return - } - completion( - writtenLength, - SubprocessError( - code: .init(.failedToWriteToSubprocess), - underlyingError: .init(rawValue: error) - ) - ) - } - } -} - -internal typealias PlatformFileDescriptor = FileDescriptor - -#endif // canImport(Darwin) || canImport(Glibc) || canImport(Bionic) || canImport(Musl) diff --git a/Sources/_Subprocess/Platforms/Subprocess+Windows.swift b/Sources/_Subprocess/Platforms/Subprocess+Windows.swift deleted file mode 100644 index e84d14148..000000000 --- a/Sources/_Subprocess/Platforms/Subprocess+Windows.swift +++ /dev/null @@ -1,1221 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if canImport(WinSDK) - -import WinSDK -internal import Dispatch -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif - -// Windows specific implementation -extension Configuration { - internal func spawn< - Output: OutputProtocol, - Error: OutputProtocol - >( - withInput inputPipe: CreatedPipe, - output: Output, - outputPipe: CreatedPipe, - error: Error, - errorPipe: CreatedPipe - ) throws -> Execution { - // Spawn differently depending on whether - // we need to spawn as a user - guard let userCredentials = self.platformOptions.userCredentials else { - return try self.spawnDirect( - withInput: inputPipe, - output: output, - outputPipe: outputPipe, - error: error, - errorPipe: errorPipe - ) - } - return try self.spawnAsUser( - withInput: inputPipe, - output: output, - outputPipe: outputPipe, - error: error, - errorPipe: errorPipe, - userCredentials: userCredentials - ) - } - - internal func spawnDirect< - Output: OutputProtocol, - Error: OutputProtocol - >( - withInput inputPipe: CreatedPipe, - output: Output, - outputPipe: CreatedPipe, - error: Error, - errorPipe: CreatedPipe - ) throws -> Execution { - let ( - applicationName, - commandAndArgs, - environment, - intendedWorkingDir - ) = try self.preSpawn() - var startupInfo = try self.generateStartupInfo( - withInput: inputPipe, - output: outputPipe, - error: errorPipe - ) - var processInfo: PROCESS_INFORMATION = PROCESS_INFORMATION() - var createProcessFlags = self.generateCreateProcessFlag() - // Give calling process a chance to modify flag and startup info - if let configurator = self.platformOptions.preSpawnProcessConfigurator { - try configurator(&createProcessFlags, &startupInfo) - } - // Spawn! - try applicationName.withOptionalNTPathRepresentation { applicationNameW in - try commandAndArgs.withCString( - encodedAs: UTF16.self - ) { commandAndArgsW in - try environment.withCString( - encodedAs: UTF16.self - ) { environmentW in - try intendedWorkingDir.withNTPathRepresentation { intendedWorkingDirW in - let created = CreateProcessW( - applicationNameW, - UnsafeMutablePointer(mutating: commandAndArgsW), - nil, // lpProcessAttributes - nil, // lpThreadAttributes - true, // bInheritHandles - createProcessFlags, - UnsafeMutableRawPointer(mutating: environmentW), - intendedWorkingDirW, - &startupInfo, - &processInfo - ) - guard created else { - let windowsError = GetLastError() - try self.cleanupPreSpawn( - input: inputPipe, - output: outputPipe, - error: errorPipe - ) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: windowsError) - ) - } - } - } - } - } - // We don't need the handle objects, so close it right away - guard CloseHandle(processInfo.hThread) else { - let windowsError = GetLastError() - try self.cleanupPreSpawn( - input: inputPipe, - output: outputPipe, - error: errorPipe - ) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: windowsError) - ) - } - guard CloseHandle(processInfo.hProcess) else { - let windowsError = GetLastError() - try self.cleanupPreSpawn( - input: inputPipe, - output: outputPipe, - error: errorPipe - ) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: windowsError) - ) - } - let pid = ProcessIdentifier( - value: processInfo.dwProcessId - ) - return Execution( - processIdentifier: pid, - output: output, - error: error, - outputPipe: outputPipe, - errorPipe: errorPipe, - consoleBehavior: self.platformOptions.consoleBehavior - ) - } - - internal func spawnAsUser< - Output: OutputProtocol, - Error: OutputProtocol - >( - withInput inputPipe: CreatedPipe, - output: Output, - outputPipe: CreatedPipe, - error: Error, - errorPipe: CreatedPipe, - userCredentials: PlatformOptions.UserCredentials - ) throws -> Execution { - let ( - applicationName, - commandAndArgs, - environment, - intendedWorkingDir - ) = try self.preSpawn() - var startupInfo = try self.generateStartupInfo( - withInput: inputPipe, - output: outputPipe, - error: errorPipe - ) - var processInfo: PROCESS_INFORMATION = PROCESS_INFORMATION() - var createProcessFlags = self.generateCreateProcessFlag() - // Give calling process a chance to modify flag and startup info - if let configurator = self.platformOptions.preSpawnProcessConfigurator { - try configurator(&createProcessFlags, &startupInfo) - } - // Spawn (featuring pyamid!) - try userCredentials.username.withCString( - encodedAs: UTF16.self - ) { usernameW in - try userCredentials.password.withCString( - encodedAs: UTF16.self - ) { passwordW in - try userCredentials.domain.withOptionalCString( - encodedAs: UTF16.self - ) { domainW in - try applicationName.withOptionalNTPathRepresentation { applicationNameW in - try commandAndArgs.withCString( - encodedAs: UTF16.self - ) { commandAndArgsW in - try environment.withCString( - encodedAs: UTF16.self - ) { environmentW in - try intendedWorkingDir.withNTPathRepresentation { intendedWorkingDirW in - let created = CreateProcessWithLogonW( - usernameW, - domainW, - passwordW, - DWORD(LOGON_WITH_PROFILE), - applicationNameW, - UnsafeMutablePointer(mutating: commandAndArgsW), - createProcessFlags, - UnsafeMutableRawPointer(mutating: environmentW), - intendedWorkingDirW, - &startupInfo, - &processInfo - ) - guard created else { - let windowsError = GetLastError() - try self.cleanupPreSpawn( - input: inputPipe, - output: outputPipe, - error: errorPipe - ) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: windowsError) - ) - } - } - } - } - } - } - } - } - // We don't need the handle objects, so close it right away - guard CloseHandle(processInfo.hThread) else { - let windowsError = GetLastError() - try self.cleanupPreSpawn( - input: inputPipe, - output: outputPipe, - error: errorPipe - ) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: windowsError) - ) - } - guard CloseHandle(processInfo.hProcess) else { - let windowsError = GetLastError() - try self.cleanupPreSpawn( - input: inputPipe, - output: outputPipe, - error: errorPipe - ) - throw SubprocessError( - code: .init(.spawnFailed), - underlyingError: .init(rawValue: windowsError) - ) - } - let pid = ProcessIdentifier( - value: processInfo.dwProcessId - ) - return Execution( - processIdentifier: pid, - output: output, - error: error, - outputPipe: outputPipe, - errorPipe: errorPipe, - consoleBehavior: self.platformOptions.consoleBehavior - ) - } -} - -// MARK: - Platform Specific Options - -/// The collection of platform-specific settings -/// to configure the subprocess when running -public struct PlatformOptions: Sendable { - /// A `UserCredentials` to use spawning the subprocess - /// as a different user - public struct UserCredentials: Sendable, Hashable { - // The name of the user. This is the name - // of the user account to run as. - public var username: String - // The clear-text password for the account. - public var password: String - // The name of the domain or server whose account database - // contains the account. - public var domain: String? - } - - /// `ConsoleBehavior` defines how should the console appear - /// when spawning a new process - public struct ConsoleBehavior: Sendable, Hashable { - internal enum Storage: Sendable, Hashable { - case createNew - case detatch - case inherit - } - - internal let storage: Storage - - private init(_ storage: Storage) { - self.storage = storage - } - - /// The subprocess has a new console, instead of - /// inheriting its parent's console (the default). - public static let createNew: Self = .init(.createNew) - /// For console processes, the new process does not - /// inherit its parent's console (the default). - /// The new process can call the `AllocConsole` - /// function at a later time to create a console. - public static let detatch: Self = .init(.detatch) - /// The subprocess inherits its parent's console. - public static let inherit: Self = .init(.inherit) - } - - /// `ConsoleBehavior` defines how should the window appear - /// when spawning a new process - public struct WindowStyle: Sendable, Hashable { - internal enum Storage: Sendable, Hashable { - case normal - case hidden - case maximized - case minimized - } - - internal let storage: Storage - - internal var platformStyle: WORD { - switch self.storage { - case .hidden: return WORD(SW_HIDE) - case .maximized: return WORD(SW_SHOWMAXIMIZED) - case .minimized: return WORD(SW_SHOWMINIMIZED) - default: return WORD(SW_SHOWNORMAL) - } - } - - private init(_ storage: Storage) { - self.storage = storage - } - - /// Activates and displays a window of normal size - public static let normal: Self = .init(.normal) - /// Does not activate a new window - public static let hidden: Self = .init(.hidden) - /// Activates the window and displays it as a maximized window. - public static let maximized: Self = .init(.maximized) - /// Activates the window and displays it as a minimized window. - public static let minimized: Self = .init(.minimized) - } - - /// Sets user credentials when starting the process as another user - public var userCredentials: UserCredentials? = nil - /// The console behavior of the new process, - /// default to inheriting the console from parent process - public var consoleBehavior: ConsoleBehavior = .inherit - /// Window style to use when the process is started - public var windowStyle: WindowStyle = .normal - /// Whether to create a new process group for the new - /// process. The process group includes all processes - /// that are descendants of this root process. - /// The process identifier of the new process group - /// is the same as the process identifier. - public var createProcessGroup: Bool = false - /// An ordered list of steps in order to tear down the child - /// process in case the parent task is cancelled before - /// the child proces terminates. - /// Always ends in forcefully terminate at the end. - public var teardownSequence: [TeardownStep] = [] - /// A closure to configure platform-specific - /// spawning constructs. This closure enables direct - /// configuration or override of underlying platform-specific - /// spawn settings that `Subprocess` utilizes internally, - /// in cases where Subprocess does not provide higher-level - /// APIs for such modifications. - /// - /// On Windows, Subprocess uses `CreateProcessW()` as the - /// underlying spawning mechanism. This closure allows - /// modification of the `dwCreationFlags` creation flag - /// and startup info `STARTUPINFOW` before - /// they are sent to `CreateProcessW()`. - public var preSpawnProcessConfigurator: - ( - @Sendable ( - inout DWORD, - inout STARTUPINFOW - ) throws -> Void - )? = nil - - public init() {} -} - -extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible { - internal func description(withIndent indent: Int) -> String { - let indent = String(repeating: " ", count: indent * 4) - return """ - PlatformOptions( - \(indent) userCredentials: \(String(describing: self.userCredentials)), - \(indent) consoleBehavior: \(String(describing: self.consoleBehavior)), - \(indent) windowStyle: \(String(describing: self.windowStyle)), - \(indent) createProcessGroup: \(self.createProcessGroup), - \(indent) preSpawnProcessConfigurator: \(self.preSpawnProcessConfigurator == nil ? "not set" : "set") - \(indent)) - """ - } - - public var description: String { - return self.description(withIndent: 0) - } - - public var debugDescription: String { - return self.description(withIndent: 0) - } -} - -// MARK: - Process Monitoring -@Sendable -internal func monitorProcessTermination( - forProcessWithIdentifier pid: ProcessIdentifier -) async throws -> TerminationStatus { - // Once the continuation resumes, it will need to unregister the wait, so - // yield the wait handle back to the calling scope. - var waitHandle: HANDLE? - defer { - if let waitHandle { - _ = UnregisterWait(waitHandle) - } - } - guard - let processHandle = OpenProcess( - DWORD(PROCESS_QUERY_INFORMATION | SYNCHRONIZE), - false, - pid.value - ) - else { - return .exited(1) - } - - try? await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - // Set up a callback that immediately resumes the continuation and does no - // other work. - let context = Unmanaged.passRetained(continuation as AnyObject).toOpaque() - let callback: WAITORTIMERCALLBACK = { context, _ in - let continuation = - Unmanaged.fromOpaque(context!).takeRetainedValue() as! CheckedContinuation - continuation.resume() - } - - // We only want the callback to fire once (and not be rescheduled.) Waiting - // may take an arbitrarily long time, so let the thread pool know that too. - let flags = ULONG(WT_EXECUTEONLYONCE | WT_EXECUTELONGFUNCTION) - guard - RegisterWaitForSingleObject( - &waitHandle, - processHandle, - callback, - context, - INFINITE, - flags - ) - else { - continuation.resume( - throwing: SubprocessError( - code: .init(.failedToMonitorProcess), - underlyingError: .init(rawValue: GetLastError()) - ) - ) - return - } - } - - var status: DWORD = 0 - guard GetExitCodeProcess(processHandle, &status) else { - // The child process terminated but we couldn't get its status back. - // Assume generic failure. - return .exited(1) - } - let exitCodeValue = CInt(bitPattern: .init(status)) - guard exitCodeValue >= 0 else { - return .unhandledException(status) - } - return .exited(status) -} - -// MARK: - Subprocess Control -@available(macOS 15.0, *) // FIXME: manually added availability -extension Execution { - /// Terminate the current subprocess with the given exit code - /// - Parameter exitCode: The exit code to use for the subprocess. - public func terminate(withExitCode exitCode: DWORD) throws { - guard - let processHandle = OpenProcess( - // PROCESS_ALL_ACCESS - DWORD(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF), - false, - self.processIdentifier.value - ) - else { - throw SubprocessError( - code: .init(.failedToTerminate), - underlyingError: .init(rawValue: GetLastError()) - ) - } - defer { - CloseHandle(processHandle) - } - guard TerminateProcess(processHandle, exitCode) else { - throw SubprocessError( - code: .init(.failedToTerminate), - underlyingError: .init(rawValue: GetLastError()) - ) - } - } - - /// Suspend the current subprocess - public func suspend() throws { - guard - let processHandle = OpenProcess( - // PROCESS_ALL_ACCESS - DWORD(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF), - false, - self.processIdentifier.value - ) - else { - throw SubprocessError( - code: .init(.failedToSuspend), - underlyingError: .init(rawValue: GetLastError()) - ) - } - defer { - CloseHandle(processHandle) - } - - let NTSuspendProcess: (@convention(c) (HANDLE) -> LONG)? = - unsafeBitCast( - GetProcAddress( - GetModuleHandleA("ntdll.dll"), - "NtSuspendProcess" - ), - to: Optional<(@convention(c) (HANDLE) -> LONG)>.self - ) - guard let NTSuspendProcess = NTSuspendProcess else { - throw SubprocessError( - code: .init(.failedToSuspend), - underlyingError: .init(rawValue: GetLastError()) - ) - } - guard NTSuspendProcess(processHandle) >= 0 else { - throw SubprocessError( - code: .init(.failedToSuspend), - underlyingError: .init(rawValue: GetLastError()) - ) - } - } - - /// Resume the current subprocess after suspension - public func resume() throws { - guard - let processHandle = OpenProcess( - // PROCESS_ALL_ACCESS - DWORD(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF), - false, - self.processIdentifier.value - ) - else { - throw SubprocessError( - code: .init(.failedToResume), - underlyingError: .init(rawValue: GetLastError()) - ) - } - defer { - CloseHandle(processHandle) - } - - let NTResumeProcess: (@convention(c) (HANDLE) -> LONG)? = - unsafeBitCast( - GetProcAddress( - GetModuleHandleA("ntdll.dll"), - "NtResumeProcess" - ), - to: Optional<(@convention(c) (HANDLE) -> LONG)>.self - ) - guard let NTResumeProcess = NTResumeProcess else { - throw SubprocessError( - code: .init(.failedToResume), - underlyingError: .init(rawValue: GetLastError()) - ) - } - guard NTResumeProcess(processHandle) >= 0 else { - throw SubprocessError( - code: .init(.failedToResume), - underlyingError: .init(rawValue: GetLastError()) - ) - } - } - - internal func tryTerminate() -> Swift.Error? { - do { - try self.terminate(withExitCode: 0) - } catch { - return error - } - return nil - } -} - -// MARK: - Executable Searching -extension Executable { - // Technically not needed for CreateProcess since - // it takes process name. It's here to support - // Executable.resolveExecutablePath - internal func resolveExecutablePath(withPathValue pathValue: String?) throws -> String { - switch self.storage { - case .executable(let executableName): - return try executableName.withCString( - encodedAs: UTF16.self - ) { exeName -> String in - return try pathValue.withOptionalCString( - encodedAs: UTF16.self - ) { path -> String in - let pathLenth = SearchPathW( - path, - exeName, - nil, - 0, - nil, - nil - ) - guard pathLenth > 0 else { - throw SubprocessError( - code: .init(.executableNotFound(executableName)), - underlyingError: .init(rawValue: GetLastError()) - ) - } - return withUnsafeTemporaryAllocation( - of: WCHAR.self, - capacity: Int(pathLenth) + 1 - ) { - _ = SearchPathW( - path, - exeName, - nil, - pathLenth + 1, - $0.baseAddress, - nil - ) - return String(decodingCString: $0.baseAddress!, as: UTF16.self) - } - } - } - case .path(let executablePath): - // Use path directly - return executablePath.string - } - } -} - -// MARK: - Environment Resolution -extension Environment { - internal static let pathVariableName = "Path" - - internal func pathValue() -> String? { - switch self.config { - case .inherit(let overrides): - // If PATH value exists in overrides, use it - if let value = overrides[Self.pathVariableName] { - return value - } - // Fall back to current process - return Self.currentEnvironmentValues()[Self.pathVariableName] - case .custom(let fullEnvironment): - if let value = fullEnvironment[Self.pathVariableName] { - return value - } - return nil - } - } - - internal static func withCopiedEnv(_ body: ([UnsafeMutablePointer]) -> R) -> R { - var values: [UnsafeMutablePointer] = [] - guard let pwszEnvironmentBlock = GetEnvironmentStringsW() else { - return body([]) - } - defer { FreeEnvironmentStringsW(pwszEnvironmentBlock) } - - var pwszEnvironmentEntry: LPWCH? = pwszEnvironmentBlock - while let value = pwszEnvironmentEntry { - let entry = String(decodingCString: value, as: UTF16.self) - if entry.isEmpty { break } - values.append(entry.withCString { _strdup($0)! }) - pwszEnvironmentEntry = pwszEnvironmentEntry?.advanced(by: wcslen(value) + 1) - } - defer { values.forEach { free($0) } } - return body(values) - } -} - -// MARK: - ProcessIdentifier - -/// A platform independent identifier for a subprocess. -public struct ProcessIdentifier: Sendable, Hashable, Codable { - /// Windows specifc process identifier value - public let value: DWORD - - internal init(value: DWORD) { - self.value = value - } -} - -extension ProcessIdentifier: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - return "(processID: \(self.value))" - } - - public var debugDescription: String { - return description - } -} - -// MARK: - Private Utils -extension Configuration { - private func preSpawn() throws -> ( - applicationName: String?, - commandAndArgs: String, - environment: String, - intendedWorkingDir: String - ) { - // Prepare environment - var env: [String: String] = [:] - switch self.environment.config { - case .custom(let customValues): - // Use the custom values directly - env = customValues - case .inherit(let updateValues): - // Combine current environment - env = Environment.currentEnvironmentValues() - for (key, value) in updateValues { - env.updateValue(value, forKey: key) - } - } - // On Windows, the PATH is required in order to locate dlls needed by - // the process so we should also pass that to the child - let pathVariableName = Environment.pathVariableName - if env[pathVariableName] == nil, - let parentPath = Environment.currentEnvironmentValues()[pathVariableName] - { - env[pathVariableName] = parentPath - } - // The environment string must be terminated by a double - // null-terminator. Otherwise, CreateProcess will fail with - // INVALID_PARMETER. - let environmentString = - env.map { - $0.key + "=" + $0.value - }.joined(separator: "\0") + "\0\0" - - // Prepare arguments - let ( - applicationName, - commandAndArgs - ) = try self.generateWindowsCommandAndAgruments() - // Validate workingDir - guard Self.pathAccessible(self.workingDirectory.string) else { - throw SubprocessError( - code: .init( - .failedToChangeWorkingDirectory(self.workingDirectory.string) - ), - underlyingError: nil - ) - } - return ( - applicationName: applicationName, - commandAndArgs: commandAndArgs, - environment: environmentString, - intendedWorkingDir: self.workingDirectory.string - ) - } - - private func generateCreateProcessFlag() -> DWORD { - var flags = CREATE_UNICODE_ENVIRONMENT - switch self.platformOptions.consoleBehavior.storage { - case .createNew: - flags |= CREATE_NEW_CONSOLE - case .detatch: - flags |= DETACHED_PROCESS - case .inherit: - break - } - if self.platformOptions.createProcessGroup { - flags |= CREATE_NEW_PROCESS_GROUP - } - return DWORD(flags) - } - - private func generateStartupInfo( - withInput input: CreatedPipe, - output: CreatedPipe, - error: CreatedPipe - ) throws -> STARTUPINFOW { - var info: STARTUPINFOW = STARTUPINFOW() - info.cb = DWORD(MemoryLayout.size) - info.dwFlags |= DWORD(STARTF_USESTDHANDLES) - - if self.platformOptions.windowStyle.storage != .normal { - info.wShowWindow = self.platformOptions.windowStyle.platformStyle - info.dwFlags |= DWORD(STARTF_USESHOWWINDOW) - } - // Bind IOs - // Input - if let inputRead = input.readFileDescriptor { - info.hStdInput = inputRead.platformDescriptor - } - if let inputWrite = input.writeFileDescriptor { - // Set parent side to be uninhertable - SetHandleInformation( - inputWrite.platformDescriptor, - DWORD(HANDLE_FLAG_INHERIT), - 0 - ) - } - // Output - if let outputWrite = output.writeFileDescriptor { - info.hStdOutput = outputWrite.platformDescriptor - } - if let outputRead = output.readFileDescriptor { - // Set parent side to be uninhertable - SetHandleInformation( - outputRead.platformDescriptor, - DWORD(HANDLE_FLAG_INHERIT), - 0 - ) - } - // Error - if let errorWrite = error.writeFileDescriptor { - info.hStdError = errorWrite.platformDescriptor - } - if let errorRead = error.readFileDescriptor { - // Set parent side to be uninhertable - SetHandleInformation( - errorRead.platformDescriptor, - DWORD(HANDLE_FLAG_INHERIT), - 0 - ) - } - return info - } - - private func generateWindowsCommandAndAgruments() throws -> ( - applicationName: String?, - commandAndArgs: String - ) { - // CreateProcess accepts partial names - let executableNameOrPath: String - switch self.executable.storage { - case .path(let path): - executableNameOrPath = path.string - case .executable(let name): - // Technically CreateProcessW accepts just the name - // of the executable, therefore we don't need to - // actually resolve the path. However, to maintain - // the same behavior as other platforms, still check - // here to make sure the executable actually exists - do { - _ = try self.executable.resolveExecutablePath( - withPathValue: self.environment.pathValue() - ) - } catch { - throw error - } - executableNameOrPath = name - } - var args = self.arguments.storage.map { - guard case .string(let stringValue) = $0 else { - // We should never get here since the API - // is guarded off - fatalError("Windows does not support non unicode String as arguments") - } - return stringValue - } - // The first parameter of CreateProcessW, `lpApplicationName` - // is optional. If it's nil, CreateProcessW uses argument[0] - // as the execuatble name. - // We should only set lpApplicationName if it's different from - // argument[0] (i.e. executablePathOverride) - var applicationName: String? = nil - if case .string(let overrideName) = self.arguments.executablePathOverride { - // Use the override as argument0 and set applicationName - args.insert(overrideName, at: 0) - applicationName = executableNameOrPath - } else { - // Set argument[0] to be executableNameOrPath - args.insert(executableNameOrPath, at: 0) - } - return ( - applicationName: applicationName, - commandAndArgs: self.quoteWindowsCommandLine(args) - ) - } - - // Taken from SCF - private func quoteWindowsCommandLine(_ commandLine: [String]) -> String { - func quoteWindowsCommandArg(arg: String) -> String { - // Windows escaping, adapted from Daniel Colascione's "Everyone quotes - // command line arguments the wrong way" - Microsoft Developer Blog - if !arg.contains(where: { " \t\n\"".contains($0) }) { - return arg - } - - // To escape the command line, we surround the argument with quotes. However - // the complication comes due to how the Windows command line parser treats - // backslashes (\) and quotes (") - // - // - \ is normally treated as a literal backslash - // - e.g. foo\bar\baz => foo\bar\baz - // - However, the sequence \" is treated as a literal " - // - e.g. foo\"bar => foo"bar - // - // But then what if we are given a path that ends with a \? Surrounding - // foo\bar\ with " would be "foo\bar\" which would be an unterminated string - - // since it ends on a literal quote. To allow this case the parser treats: - // - // - \\" as \ followed by the " metachar - // - \\\" as \ followed by a literal " - // - In general: - // - 2n \ followed by " => n \ followed by the " metachar - // - 2n+1 \ followed by " => n \ followed by a literal " - var quoted = "\"" - var unquoted = arg.unicodeScalars - - while !unquoted.isEmpty { - guard let firstNonBackslash = unquoted.firstIndex(where: { $0 != "\\" }) else { - // String ends with a backslash e.g. foo\bar\, escape all the backslashes - // then add the metachar " below - let backslashCount = unquoted.count - quoted.append(String(repeating: "\\", count: backslashCount * 2)) - break - } - let backslashCount = unquoted.distance(from: unquoted.startIndex, to: firstNonBackslash) - if unquoted[firstNonBackslash] == "\"" { - // This is a string of \ followed by a " e.g. foo\"bar. Escape the - // backslashes and the quote - quoted.append(String(repeating: "\\", count: backslashCount * 2 + 1)) - quoted.append(String(unquoted[firstNonBackslash])) - } else { - // These are just literal backslashes - quoted.append(String(repeating: "\\", count: backslashCount)) - quoted.append(String(unquoted[firstNonBackslash])) - } - // Drop the backslashes and the following character - unquoted.removeFirst(backslashCount + 1) - } - quoted.append("\"") - return quoted - } - return commandLine.map(quoteWindowsCommandArg).joined(separator: " ") - } - - private static func pathAccessible(_ path: String) -> Bool { - return path.withCString(encodedAs: UTF16.self) { - let attrs = GetFileAttributesW($0) - return attrs != INVALID_FILE_ATTRIBUTES - } - } -} - -// MARK: - PlatformFileDescriptor Type -internal typealias PlatformFileDescriptor = HANDLE - -// MARK: - Pipe Support -extension FileDescriptor { - internal static func pipe() throws -> ( - readEnd: FileDescriptor, - writeEnd: FileDescriptor - ) { - var saAttributes: SECURITY_ATTRIBUTES = SECURITY_ATTRIBUTES() - saAttributes.nLength = DWORD(MemoryLayout.size) - saAttributes.bInheritHandle = true - saAttributes.lpSecurityDescriptor = nil - - var readHandle: HANDLE? = nil - var writeHandle: HANDLE? = nil - guard CreatePipe(&readHandle, &writeHandle, &saAttributes, 0), - readHandle != INVALID_HANDLE_VALUE, - writeHandle != INVALID_HANDLE_VALUE, - let readHandle: HANDLE = readHandle, - let writeHandle: HANDLE = writeHandle - else { - throw SubprocessError( - code: .init(.failedToCreatePipe), - underlyingError: .init(rawValue: GetLastError()) - ) - } - let readFd = _open_osfhandle( - intptr_t(bitPattern: readHandle), - FileDescriptor.AccessMode.readOnly.rawValue - ) - let writeFd = _open_osfhandle( - intptr_t(bitPattern: writeHandle), - FileDescriptor.AccessMode.writeOnly.rawValue - ) - - return ( - readEnd: FileDescriptor(rawValue: readFd), - writeEnd: FileDescriptor(rawValue: writeFd) - ) - } - - var platformDescriptor: PlatformFileDescriptor { - return HANDLE(bitPattern: _get_osfhandle(self.rawValue))! - } - - internal func readChunk(upToLength maxLength: Int) async throws -> SequenceOutput.Buffer? { - return try await withCheckedThrowingContinuation { continuation in - self.readUntilEOF( - upToLength: maxLength - ) { result in - switch result { - case .failure(let error): - continuation.resume(throwing: error) - case .success(let bytes): - continuation.resume(returning: SequenceOutput.Buffer(data: bytes)) - } - } - } - } - - internal func readUntilEOF( - upToLength maxLength: Int, - resultHandler: @Sendable @escaping (Swift.Result<[UInt8], any (Error & Sendable)>) -> Void - ) { - DispatchQueue.global(qos: .userInitiated).async { - var totalBytesRead: Int = 0 - var lastError: DWORD? = nil - let values = [UInt8]( - unsafeUninitializedCapacity: maxLength - ) { buffer, initializedCount in - while true { - guard let baseAddress = buffer.baseAddress else { - initializedCount = 0 - break - } - let bufferPtr = baseAddress.advanced(by: totalBytesRead) - var bytesRead: DWORD = 0 - let readSucceed = ReadFile( - self.platformDescriptor, - UnsafeMutableRawPointer(mutating: bufferPtr), - DWORD(maxLength - totalBytesRead), - &bytesRead, - nil - ) - if !readSucceed { - // Windows throws ERROR_BROKEN_PIPE when the pipe is closed - let error = GetLastError() - if error == ERROR_BROKEN_PIPE { - // We are done reading - initializedCount = totalBytesRead - } else { - // We got some error - lastError = error - initializedCount = 0 - } - break - } else { - // We succesfully read the current round - totalBytesRead += Int(bytesRead) - } - - if totalBytesRead >= maxLength { - initializedCount = min(maxLength, totalBytesRead) - break - } - } - } - if let lastError = lastError { - let windowsError = SubprocessError( - code: .init(.failedToReadFromSubprocess), - underlyingError: .init(rawValue: lastError) - ) - resultHandler(.failure(windowsError)) - } else { - resultHandler(.success(values)) - } - } - } - - internal func write( - _ array: [UInt8] - ) async throws -> Int { - try await withCheckedThrowingContinuation { continuation in - // TODO: Figure out a better way to asynchornously write - DispatchQueue.global(qos: .userInitiated).async { - array.withUnsafeBytes { - self.write($0) { writtenLength, error in - if let error = error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: writtenLength) - } - } - } - } - } - } - - package func write( - _ ptr: UnsafeRawBufferPointer, - completion: @escaping (Int, Swift.Error?) -> Void - ) { - func _write( - _ ptr: UnsafeRawBufferPointer, - count: Int, - completion: @escaping (Int, Swift.Error?) -> Void - ) { - var writtenBytes: DWORD = 0 - let writeSucceed = WriteFile( - self.platformDescriptor, - ptr.baseAddress, - DWORD(count), - &writtenBytes, - nil - ) - if !writeSucceed { - let error = SubprocessError( - code: .init(.failedToWriteToSubprocess), - underlyingError: .init(rawValue: GetLastError()) - ) - completion(Int(writtenBytes), error) - } else { - completion(Int(writtenBytes), nil) - } - } - } -} - -extension Optional where Wrapped == String { - fileprivate func withOptionalCString( - encodedAs targetEncoding: Encoding.Type, - _ body: (UnsafePointer?) throws -> Result - ) rethrows -> Result where Encoding: _UnicodeEncoding { - switch self { - case .none: - return try body(nil) - case .some(let value): - return try value.withCString(encodedAs: targetEncoding, body) - } - } - - fileprivate func withOptionalNTPathRepresentation( - _ body: (UnsafePointer?) throws -> Result - ) throws -> Result { - switch self { - case .none: - return try body(nil) - case .some(let value): - return try value.withNTPathRepresentation(body) - } - } -} - -// MARK: - Remove these when merging back to SwiftFoundation -extension String { - internal func withNTPathRepresentation( - _ body: (UnsafePointer) throws -> Result - ) throws -> Result { - guard !isEmpty else { - throw SubprocessError( - code: .init(.invalidWindowsPath(self)), - underlyingError: nil - ) - } - - var iter = self.utf8.makeIterator() - let bLeadingSlash = - if [._slash, ._backslash].contains(iter.next()), iter.next()?.isLetter ?? false, iter.next() == ._colon { - true - } else { false } - - // Strip the leading `/` on a RFC8089 path (`/[drive-letter]:/...` ). A - // leading slash indicates a rooted path on the drive for the current - // working directory. - return try Substring(self.utf8.dropFirst(bLeadingSlash ? 1 : 0)).withCString(encodedAs: UTF16.self) { - pwszPath in - // 1. Normalize the path first. - let dwLength: DWORD = GetFullPathNameW(pwszPath, 0, nil, nil) - return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { - guard GetFullPathNameW(pwszPath, DWORD($0.count), $0.baseAddress, nil) > 0 else { - throw SubprocessError( - code: .init(.invalidWindowsPath(self)), - underlyingError: .init(rawValue: GetLastError()) - ) - } - - // 2. Perform the operation on the normalized path. - return try body($0.baseAddress!) - } - } - } -} - -extension UInt8 { - static var _slash: UInt8 { UInt8(ascii: "/") } - static var _backslash: UInt8 { UInt8(ascii: "\\") } - static var _colon: UInt8 { UInt8(ascii: ":") } - - var isLetter: Bool? { - return (0x41...0x5a) ~= self || (0x61...0x7a) ~= self - } -} - -extension OutputProtocol { - internal func output(from data: [UInt8]) throws -> OutputType { - return try data.withUnsafeBytes { - return try self.output(from: $0) - } - } -} - -#endif // canImport(WinSDK) diff --git a/Sources/_Subprocess/Result.swift b/Sources/_Subprocess/Result.swift deleted file mode 100644 index e1f798940..000000000 --- a/Sources/_Subprocess/Result.swift +++ /dev/null @@ -1,123 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif - -// MARK: - Result - -/// A simple wrapper around the generic result returned by the -/// `run` closures with the corresponding `TerminationStatus` -/// of the child process. -public struct ExecutionResult { - /// The termination status of the child process - public let terminationStatus: TerminationStatus - /// The result returned by the closure passed to `.run` methods - public let value: Result - - internal init(terminationStatus: TerminationStatus, value: Result) { - self.terminationStatus = terminationStatus - self.value = value - } -} - -/// The result of a subprocess execution with its collected -/// standard output and standard error. -public struct CollectedResult< - Output: OutputProtocol, - Error: OutputProtocol ->: Sendable { - /// The process identifier for the executed subprocess - public let processIdentifier: ProcessIdentifier - /// The termination status of the executed subprocess - public let terminationStatus: TerminationStatus - public let standardOutput: Output.OutputType - public let standardError: Error.OutputType - - internal init( - processIdentifier: ProcessIdentifier, - terminationStatus: TerminationStatus, - standardOutput: Output.OutputType, - standardError: Error.OutputType - ) { - self.processIdentifier = processIdentifier - self.terminationStatus = terminationStatus - self.standardOutput = standardOutput - self.standardError = standardError - } -} - -// MARK: - CollectedResult Conformances -extension CollectedResult: Equatable where Output.OutputType: Equatable, Error.OutputType: Equatable {} - -extension CollectedResult: Hashable where Output.OutputType: Hashable, Error.OutputType: Hashable {} - -extension CollectedResult: Codable where Output.OutputType: Codable, Error.OutputType: Codable {} - -extension CollectedResult: CustomStringConvertible -where Output.OutputType: CustomStringConvertible, Error.OutputType: CustomStringConvertible { - public var description: String { - return """ - CollectedResult( - processIdentifier: \(self.processIdentifier), - terminationStatus: \(self.terminationStatus.description), - standardOutput: \(self.standardOutput.description) - standardError: \(self.standardError.description) - ) - """ - } -} - -extension CollectedResult: CustomDebugStringConvertible -where Output.OutputType: CustomDebugStringConvertible, Error.OutputType: CustomDebugStringConvertible { - public var debugDescription: String { - return """ - CollectedResult( - processIdentifier: \(self.processIdentifier), - terminationStatus: \(self.terminationStatus.description), - standardOutput: \(self.standardOutput.debugDescription) - standardError: \(self.standardError.debugDescription) - ) - """ - } -} - -// MARK: - ExecutionResult Conformances -extension ExecutionResult: Equatable where Result: Equatable {} - -extension ExecutionResult: Hashable where Result: Hashable {} - -extension ExecutionResult: Codable where Result: Codable {} - -extension ExecutionResult: CustomStringConvertible where Result: CustomStringConvertible { - public var description: String { - return """ - ExecutionResult( - terminationStatus: \(self.terminationStatus.description), - value: \(self.value.description) - ) - """ - } -} - -extension ExecutionResult: CustomDebugStringConvertible where Result: CustomDebugStringConvertible { - public var debugDescription: String { - return """ - ExecutionResult( - terminationStatus: \(self.terminationStatus.debugDescription), - value: \(self.value.debugDescription) - ) - """ - } -} diff --git a/Sources/_Subprocess/SubprocessFoundation/Input+Foundation.swift b/Sources/_Subprocess/SubprocessFoundation/Input+Foundation.swift deleted file mode 100644 index 3ec421737..000000000 --- a/Sources/_Subprocess/SubprocessFoundation/Input+Foundation.swift +++ /dev/null @@ -1,179 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if SubprocessFoundation - -#if canImport(Darwin) -// On Darwin always prefer system Foundation -import Foundation -#else -// On other platforms prefer FoundationEssentials -import FoundationEssentials -#endif // canImport(Darwin) - -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif - -internal import Dispatch - -/// A concrete `Input` type for subprocesses that reads input -/// from a given `Data`. -public struct DataInput: InputProtocol { - private let data: Data - - public func write(with writer: StandardInputWriter) async throws { - _ = try await writer.write(self.data) - } - - internal init(data: Data) { - self.data = data - } -} - -/// A concrete `Input` type for subprocesses that accepts input -/// from a specified sequence of `Data`. -public struct DataSequenceInput< - InputSequence: Sequence & Sendable ->: InputProtocol where InputSequence.Element == Data { - private let sequence: InputSequence - - public func write(with writer: StandardInputWriter) async throws { - var buffer = Data() - for chunk in self.sequence { - buffer.append(chunk) - } - _ = try await writer.write(buffer) - } - - internal init(underlying: InputSequence) { - self.sequence = underlying - } -} - -/// A concrete `Input` type for subprocesses that reads input -/// from a given async sequence of `Data`. -public struct DataAsyncSequenceInput< - InputSequence: AsyncSequence & Sendable ->: InputProtocol where InputSequence.Element == Data { - private let sequence: InputSequence - - private func writeChunk(_ chunk: Data, with writer: StandardInputWriter) async throws { - _ = try await writer.write(chunk) - } - - public func write(with writer: StandardInputWriter) async throws { - for try await chunk in self.sequence { - try await self.writeChunk(chunk, with: writer) - } - } - - internal init(underlying: InputSequence) { - self.sequence = underlying - } -} - -extension InputProtocol { - /// Create a Subprocess input from a `Data` - public static func data(_ data: Data) -> Self where Self == DataInput { - return DataInput(data: data) - } - - /// Create a Subprocess input from a `Sequence` of `Data`. - public static func sequence( - _ sequence: InputSequence - ) -> Self where Self == DataSequenceInput { - return .init(underlying: sequence) - } - - /// Create a Subprocess input from a `AsyncSequence` of `Data`. - public static func sequence( - _ asyncSequence: InputSequence - ) -> Self where Self == DataAsyncSequenceInput { - return .init(underlying: asyncSequence) - } -} - -extension StandardInputWriter { - /// Write a `Data` to the standard input of the subprocess. - /// - Parameter data: The sequence of bytes to write. - /// - Returns number of bytes written. - public func write( - _ data: Data - ) async throws -> Int { - return try await self.fileDescriptor.wrapped.write(data) - } - - /// Write a AsyncSequence of Data to the standard input of the subprocess. - /// - Parameter sequence: The sequence of bytes to write. - /// - Returns number of bytes written. - public func write( - _ asyncSequence: AsyncSendableSequence - ) async throws -> Int where AsyncSendableSequence.Element == Data { - var buffer = Data() - for try await data in asyncSequence { - buffer.append(data) - } - return try await self.write(buffer) - } -} - -extension FileDescriptor { - #if os(Windows) - internal func write( - _ data: Data - ) async throws -> Int { - try await withCheckedThrowingContinuation { continuation in - // TODO: Figure out a better way to asynchornously write - DispatchQueue.global(qos: .userInitiated).async { - data.withUnsafeBytes { - self.write($0) { writtenLength, error in - if let error = error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: writtenLength) - } - } - } - } - } - } - #else - internal func write( - _ data: Data - ) async throws -> Int { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - let dispatchData = data.withUnsafeBytes { - return DispatchData( - bytesNoCopy: $0, - deallocator: .custom( - nil, - { - // noop - } - ) - ) - } - self.write(dispatchData) { writtenLength, error in - if let error = error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: writtenLength) - } - } - } - } - #endif -} - -#endif // SubprocessFoundation diff --git a/Sources/_Subprocess/SubprocessFoundation/Output+Foundation.swift b/Sources/_Subprocess/SubprocessFoundation/Output+Foundation.swift deleted file mode 100644 index fac0bb8f1..000000000 --- a/Sources/_Subprocess/SubprocessFoundation/Output+Foundation.swift +++ /dev/null @@ -1,53 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if SubprocessFoundation - -#if canImport(Darwin) -// On Darwin always prefer system Foundation -import Foundation -#else -// On other platforms prefer FoundationEssentials -import FoundationEssentials -#endif - -/// A concrete `Output` type for subprocesses that collects output -/// from the subprocess as `Data`. This option must be used with -/// the `run()` method that returns a `CollectedResult` -public struct DataOutput: OutputProtocol { - public typealias OutputType = Data - public let maxSize: Int - - public func output(from buffer: some Sequence) throws -> Data { - return Data(buffer) - } - - internal init(limit: Int) { - self.maxSize = limit - } -} - -extension OutputProtocol where Self == DataOutput { - /// Create a `Subprocess` output that collects output as `Data` - /// up to 128kb. - public static var data: Self { - return .data(limit: 128 * 1024) - } - - /// Create a `Subprocess` output that collects output as `Data` - /// with given max number of bytes to collect. - public static func data(limit: Int) -> Self { - return .init(limit: limit) - } -} - - -#endif // SubprocessFoundation diff --git a/Sources/_Subprocess/SubprocessFoundation/Span+SubprocessFoundation.swift b/Sources/_Subprocess/SubprocessFoundation/Span+SubprocessFoundation.swift deleted file mode 100644 index 10697091e..000000000 --- a/Sources/_Subprocess/SubprocessFoundation/Span+SubprocessFoundation.swift +++ /dev/null @@ -1,76 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if SubprocessFoundation && SubprocessSpan - -#if canImport(Darwin) -// On Darwin always prefer system Foundation -import Foundation -#else -// On other platforms prefer FoundationEssentials -import FoundationEssentials -#endif // canImport(Darwin) - -internal import Dispatch - - -extension Data { - init(_ s: borrowing RawSpan) { - self = s.withUnsafeBytes { Data($0) } - } - - public var bytes: RawSpan { - // FIXME: For demo purpose only - let ptr = self.withUnsafeBytes { ptr in - return ptr - } - let span = RawSpan(_unsafeBytes: ptr) - return _overrideLifetime(of: span, to: self) - } -} - - -extension DataProtocol { - var bytes: RawSpan { - _read { - if self.regions.isEmpty { - let empty = UnsafeRawBufferPointer(start: nil, count: 0) - let span = RawSpan(_unsafeBytes: empty) - yield _overrideLifetime(of: span, to: self) - } else if self.regions.count == 1 { - // Easy case: there is only one region in the data - let ptr = self.regions.first!.withUnsafeBytes { ptr in - return ptr - } - let span = RawSpan(_unsafeBytes: ptr) - yield _overrideLifetime(of: span, to: self) - } else { - // This data contains discontiguous chunks. We have to - // copy and make a contiguous chunk - var contiguous: ContiguousArray? - for region in self.regions { - if contiguous != nil { - contiguous?.append(contentsOf: region) - } else { - contiguous = .init(region) - } - } - let ptr = contiguous!.withUnsafeBytes { ptr in - return ptr - } - let span = RawSpan(_unsafeBytes: ptr) - yield _overrideLifetime(of: span, to: self) - } - } - } -} - -#endif // SubprocessFoundation diff --git a/Sources/_Subprocess/Teardown.swift b/Sources/_Subprocess/Teardown.swift deleted file mode 100644 index b2da85a5f..000000000 --- a/Sources/_Subprocess/Teardown.swift +++ /dev/null @@ -1,217 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -import _SubprocessCShims - -#if canImport(Darwin) -import Darwin -#elseif canImport(Bionic) -import Bionic -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#elseif canImport(WinSDK) -import WinSDK -#endif - -/// A step in the graceful shutdown teardown sequence. -/// It consists of an action to perform on the child process and the -/// duration allowed for the child process to exit before proceeding -/// to the next step. -public struct TeardownStep: Sendable, Hashable { - internal enum Storage: Sendable, Hashable { - #if !os(Windows) - case sendSignal(Signal, allowedDuration: Duration) - #endif - case gracefulShutDown(allowedDuration: Duration) - case kill - } - var storage: Storage - - #if !os(Windows) - /// Sends `signal` to the process and allows `allowedDurationToExit` - /// for the process to exit before proceeding to the next step. - /// The final step in the sequence will always send a `.kill` signal. - public static func send( - signal: Signal, - allowedDurationToNextStep: Duration - ) -> Self { - return Self( - storage: .sendSignal( - signal, - allowedDuration: allowedDurationToNextStep - ) - ) - } - #endif // !os(Windows) - - /// Attempt to perform a graceful shutdown and allows - /// `allowedDurationToNextStep` for the process to exit - /// before proceeding to the next step: - /// - On Unix: send `SIGTERM` - /// - On Windows: - /// 1. Attempt to send `VM_CLOSE` if the child process is a GUI process; - /// 2. Attempt to send `CTRL_C_EVENT` to console; - /// 3. Attempt to send `CTRL_BREAK_EVENT` to process group. - public static func gracefulShutDown( - allowedDurationToNextStep: Duration - ) -> Self { - return Self( - storage: .gracefulShutDown( - allowedDuration: allowedDurationToNextStep - ) - ) - } -} - -@available(macOS 15.0, *) // FIXME: manually added availability -extension Execution { - /// Performs a sequence of teardown steps on the Subprocess. - /// Teardown sequence always ends with a `.kill` signal - /// - Parameter sequence: The steps to perform. - public func teardown(using sequence: some Sequence & Sendable) async { - await withUncancelledTask { - await self.runTeardownSequence(sequence) - } - } -} - -internal enum TeardownStepCompletion { - case processHasExited - case processStillAlive - case killedTheProcess -} - -@available(macOS 15.0, *) // FIXME: manually added availability -extension Execution { - internal func gracefulShutDown( - allowedDurationToNextStep duration: Duration - ) async { - #if os(Windows) - guard - let processHandle = OpenProcess( - DWORD(PROCESS_QUERY_INFORMATION | SYNCHRONIZE), - false, - self.processIdentifier.value - ) - else { - // Nothing more we can do - return - } - defer { - CloseHandle(processHandle) - } - - // 1. Attempt to send WM_CLOSE to the main window - if _subprocess_windows_send_vm_close( - self.processIdentifier.value - ) { - try? await Task.sleep(for: duration) - } - - // 2. Attempt to attach to the console and send CTRL_C_EVENT - if AttachConsole(self.processIdentifier.value) { - // Disable Ctrl-C handling in this process - if SetConsoleCtrlHandler(nil, true) { - if GenerateConsoleCtrlEvent(DWORD(CTRL_C_EVENT), 0) { - // We successfully sent the event. wait for the process to exit - try? await Task.sleep(for: duration) - } - // Re-enable Ctrl-C handling - SetConsoleCtrlHandler(nil, false) - } - // Detach console - FreeConsole() - } - - // 3. Attempt to send CTRL_BREAK_EVENT to the process group - if GenerateConsoleCtrlEvent(DWORD(CTRL_BREAK_EVENT), self.processIdentifier.value) { - // Wait for process to exit - try? await Task.sleep(for: duration) - } - #else - // Send SIGTERM - try? self.send(signal: .terminate) - #endif - } - - internal func runTeardownSequence(_ sequence: some Sequence & Sendable) async { - // First insert the `.kill` step - let finalSequence = sequence + [TeardownStep(storage: .kill)] - for step in finalSequence { - let stepCompletion: TeardownStepCompletion - - switch step.storage { - case .gracefulShutDown(let allowedDuration): - stepCompletion = await withTaskGroup(of: TeardownStepCompletion.self) { group in - group.addTask { - do { - try await Task.sleep(for: allowedDuration) - return .processStillAlive - } catch { - // teardown(using:) cancells this task - // when process has exited - return .processHasExited - } - } - await self.gracefulShutDown(allowedDurationToNextStep: allowedDuration) - return await group.next()! - } - #if !os(Windows) - case .sendSignal(let signal, let allowedDuration): - stepCompletion = await withTaskGroup(of: TeardownStepCompletion.self) { group in - group.addTask { - do { - try await Task.sleep(for: allowedDuration) - return .processStillAlive - } catch { - // teardown(using:) cancells this task - // when process has exited - return .processHasExited - } - } - try? self.send(signal: signal) - return await group.next()! - } - #endif // !os(Windows) - case .kill: - #if os(Windows) - try? self.terminate(withExitCode: 0) - #else - try? self.send(signal: .kill) - #endif - stepCompletion = .killedTheProcess - } - - switch stepCompletion { - case .killedTheProcess, .processHasExited: - return - case .processStillAlive: - // Continue to next step - break - } - } - } -} - -func withUncancelledTask( - returning: Result.Type = Result.self, - _ body: @Sendable @escaping () async -> Result -) async -> Result { - // This looks unstructured but it isn't, please note that we `await` `.value` of this task. - // The reason we need this separate `Task` is that in general, we cannot assume that code performs to our - // expectations if the task we run it on is already cancelled. However, in some cases we need the code to - // run regardless -- even if our task is already cancelled. Therefore, we create a new, uncancelled task here. - await Task { - await body() - }.value -} diff --git a/Sources/_SubprocessCShims/include/process_shims.h b/Sources/_SubprocessCShims/include/process_shims.h deleted file mode 100644 index 35cbd2fe3..000000000 --- a/Sources/_SubprocessCShims/include/process_shims.h +++ /dev/null @@ -1,91 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#ifndef process_shims_h -#define process_shims_h - -#include "target_conditionals.h" - -#if !TARGET_OS_WINDOWS -#include - -#if _POSIX_SPAWN -#include -#endif - -#if __has_include() -vm_size_t _subprocess_vm_size(void); -#endif - -#if TARGET_OS_MAC -int _subprocess_spawn( - pid_t * _Nonnull pid, - const char * _Nonnull exec_path, - const posix_spawn_file_actions_t _Nullable * _Nonnull file_actions, - const posix_spawnattr_t _Nullable * _Nonnull spawn_attrs, - char * _Nullable const args[_Nonnull], - char * _Nullable const env[_Nullable], - uid_t * _Nullable uid, - gid_t * _Nullable gid, - int number_of_sgroups, const gid_t * _Nullable sgroups, - int create_session -); -#endif // TARGET_OS_MAC - -int _subprocess_fork_exec( - pid_t * _Nonnull pid, - const char * _Nonnull exec_path, - const char * _Nullable working_directory, - const int file_descriptors[_Nonnull], - char * _Nullable const args[_Nonnull], - char * _Nullable const env[_Nullable], - uid_t * _Nullable uid, - gid_t * _Nullable gid, - gid_t * _Nullable process_group_id, - int number_of_sgroups, const gid_t * _Nullable sgroups, - int create_session, - void (* _Nullable configurator)(void) -); - -int _was_process_exited(int status); -int _get_exit_code(int status); -int _was_process_signaled(int status); -int _get_signal_code(int status); -int _was_process_suspended(int status); - -void _subprocess_lock_environ(void); -void _subprocess_unlock_environ(void); -char * _Nullable * _Nullable _subprocess_get_environ(void); - -#if TARGET_OS_LINUX -int _shims_snprintf( - char * _Nonnull str, - int len, - const char * _Nonnull format, - char * _Nonnull str1, - char * _Nonnull str2 -); -#endif - -#endif // !TARGET_OS_WINDOWS - -#if TARGET_OS_WINDOWS - -#ifndef _WINDEF_ -typedef unsigned long DWORD; -typedef int BOOL; -#endif - -BOOL _subprocess_windows_send_vm_close(DWORD pid); - -#endif - -#endif /* process_shims_h */ diff --git a/Sources/_SubprocessCShims/include/target_conditionals.h b/Sources/_SubprocessCShims/include/target_conditionals.h deleted file mode 100644 index fef2eaf2e..000000000 --- a/Sources/_SubprocessCShims/include/target_conditionals.h +++ /dev/null @@ -1,50 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2021 - 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -#ifndef _SHIMS_TARGET_CONDITIONALS_H -#define _SHIMS_TARGET_CONDITIONALS_H - -#if __has_include() -#include -#endif - -#if (defined(__APPLE__) && defined(__MACH__)) -#define TARGET_OS_MAC 1 -#else -#define TARGET_OS_MAC 0 -#endif - -#if defined(__linux__) -#define TARGET_OS_LINUX 1 -#else -#define TARGET_OS_LINUX 0 -#endif - -#if defined(__unix__) -#define TARGET_OS_BSD 1 -#else -#define TARGET_OS_BSD 0 -#endif - -#if defined(_WIN32) -#define TARGET_OS_WINDOWS 1 -#else -#define TARGET_OS_WINDOWS 0 -#endif - -#if defined(__wasi__) -#define TARGET_OS_WASI 1 -#else -#define TARGET_OS_WASI 0 -#endif - -#endif // _SHIMS_TARGET_CONDITIONALS_H diff --git a/Sources/_SubprocessCShims/process_shims.c b/Sources/_SubprocessCShims/process_shims.c deleted file mode 100644 index 287e1a8d0..000000000 --- a/Sources/_SubprocessCShims/process_shims.c +++ /dev/null @@ -1,682 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#include "include/target_conditionals.h" - -#if TARGET_OS_LINUX -// For posix_spawn_file_actions_addchdir_np -#define _GNU_SOURCE 1 -#endif - -#include "include/process_shims.h" - -#if TARGET_OS_WINDOWS -#include -#else -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#if __has_include() -#include -#elif defined(_WIN32) -#include -#elif __has_include() -#include -extern char **environ; -#endif - -int _was_process_exited(int status) { - return WIFEXITED(status); -} - -int _get_exit_code(int status) { - return WEXITSTATUS(status); -} - -int _was_process_signaled(int status) { - return WIFSIGNALED(status); -} - -int _get_signal_code(int status) { - return WTERMSIG(status); -} - -int _was_process_suspended(int status) { - return WIFSTOPPED(status); -} - -#if TARGET_OS_LINUX -#include - -int _shims_snprintf( - char * _Nonnull str, - int len, - const char * _Nonnull format, - char * _Nonnull str1, - char * _Nonnull str2 -) { - return snprintf(str, len, format, str1, str2); -} -#endif - -#if __has_include() -vm_size_t _subprocess_vm_size(void) { - // This shim exists because vm_page_size is not marked const, and therefore looks like global mutable state to Swift. - return vm_page_size; -} -#endif - -// MARK: - Darwin (posix_spawn) -#if TARGET_OS_MAC -static int _subprocess_spawn_prefork( - pid_t * _Nonnull pid, - const char * _Nonnull exec_path, - const posix_spawn_file_actions_t _Nullable * _Nonnull file_actions, - const posix_spawnattr_t _Nullable * _Nonnull spawn_attrs, - char * _Nullable const args[_Nonnull], - char * _Nullable const env[_Nullable], - uid_t * _Nullable uid, - gid_t * _Nullable gid, - int number_of_sgroups, const gid_t * _Nullable sgroups, - int create_session -) { - // Set `POSIX_SPAWN_SETEXEC` flag since we are forking ourselves - short flags = 0; - int rc = posix_spawnattr_getflags(spawn_attrs, &flags); - if (rc != 0) { - return rc; - } - - rc = posix_spawnattr_setflags( - (posix_spawnattr_t *)spawn_attrs, flags | POSIX_SPAWN_SETEXEC - ); - if (rc != 0) { - return rc; - } - // Setup pipe to catch exec failures from child - int pipefd[2]; - if (pipe(pipefd) != 0) { - return errno; - } - // Set FD_CLOEXEC so the pipe is automatically closed when exec succeeds - flags = fcntl(pipefd[0], F_GETFD); - if (flags == -1) { - close(pipefd[0]); - close(pipefd[1]); - return errno; - } - flags |= FD_CLOEXEC; - if (fcntl(pipefd[0], F_SETFD, flags) == -1) { - close(pipefd[0]); - close(pipefd[1]); - return errno; - } - - flags = fcntl(pipefd[1], F_GETFD); - if (flags == -1) { - close(pipefd[0]); - close(pipefd[1]); - return errno; - } - flags |= FD_CLOEXEC; - if (fcntl(pipefd[1], F_SETFD, flags) == -1) { - close(pipefd[0]); - close(pipefd[1]); - return errno; - } - - // Finally, fork -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated" - pid_t childPid = fork(); -#pragma GCC diagnostic pop - if (childPid == -1) { - close(pipefd[0]); - close(pipefd[1]); - return errno; - } - - if (childPid == 0) { - // Child process - close(pipefd[0]); // Close unused read end - - // Perform setups - if (number_of_sgroups > 0 && sgroups != NULL) { - if (setgroups(number_of_sgroups, sgroups) != 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } - } - - if (uid != NULL) { - if (setuid(*uid) != 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } - } - - if (gid != NULL) { - if (setgid(*gid) != 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } - } - - if (create_session != 0) { - (void)setsid(); - } - - // Use posix_spawnas exec - int error = posix_spawn(pid, exec_path, file_actions, spawn_attrs, args, env); - // If we reached this point, something went wrong - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } else { - // Parent process - close(pipefd[1]); // Close unused write end - // Communicate child pid back - *pid = childPid; - // Read from the pipe until pipe is closed - // Eitehr due to exec succeeds or error is written - int childError = 0; - if (read(pipefd[0], &childError, sizeof(childError)) > 0) { - // We encountered error - close(pipefd[0]); - return childError; - } else { - // Child process exec was successful - close(pipefd[0]); - return 0; - } - } -} - -int _subprocess_spawn( - pid_t * _Nonnull pid, - const char * _Nonnull exec_path, - const posix_spawn_file_actions_t _Nullable * _Nonnull file_actions, - const posix_spawnattr_t _Nullable * _Nonnull spawn_attrs, - char * _Nullable const args[_Nonnull], - char * _Nullable const env[_Nullable], - uid_t * _Nullable uid, - gid_t * _Nullable gid, - int number_of_sgroups, const gid_t * _Nullable sgroups, - int create_session -) { - int require_pre_fork = uid != NULL || - gid != NULL || - number_of_sgroups > 0 || - create_session > 0; - - if (require_pre_fork != 0) { - int rc = _subprocess_spawn_prefork( - pid, - exec_path, - file_actions, spawn_attrs, - args, env, - uid, gid, number_of_sgroups, sgroups, create_session - ); - return rc; - } - - // Spawn - return posix_spawn(pid, exec_path, file_actions, spawn_attrs, args, env); -} - -#endif // TARGET_OS_MAC - -// MARK: - Linux (fork/exec + posix_spawn fallback) -#if TARGET_OS_LINUX - -#if _POSIX_SPAWN -static int _subprocess_is_addchdir_np_available() { -#if defined(__GLIBC__) && !__GLIBC_PREREQ(2, 29) - // Glibc versions prior to 2.29 don't support posix_spawn_file_actions_addchdir_np, impacting: - // - Amazon Linux 2 (EoL mid-2025) - return 0; -#elif defined(__OpenBSD__) || defined(__QNX__) - // Currently missing as of: - // - OpenBSD 7.5 (April 2024) - // - QNX 8 (December 2023) - return 0; -#elif defined(__GLIBC__) || TARGET_OS_DARWIN || defined(__FreeBSD__) || (defined(__ANDROID__) && __ANDROID_API__ >= 34) || defined(__musl__) - // Pre-standard posix_spawn_file_actions_addchdir_np version available in: - // - Solaris 11.3 (October 2015) - // - Glibc 2.29 (February 2019) - // - macOS 10.15 (October 2019) - // - musl 1.1.24 (October 2019) - // - FreeBSD 13.1 (May 2022) - // - Android 14 (October 2023) - return 1; -#else - // Standardized posix_spawn_file_actions_addchdir version (POSIX.1-2024, June 2024) available in: - // - Solaris 11.4 (August 2018) - // - NetBSD 10.0 (March 2024) - return 1; -#endif -} - -static int _subprocess_addchdir_np( - posix_spawn_file_actions_t *file_actions, - const char * __restrict path -) { -#if defined(__GLIBC__) && !__GLIBC_PREREQ(2, 29) - // Glibc versions prior to 2.29 don't support posix_spawn_file_actions_addchdir_np, impacting: - // - Amazon Linux 2 (EoL mid-2025) - // noop -#elif defined(__OpenBSD__) || defined(__QNX__) - // Currently missing as of: - // - OpenBSD 7.5 (April 2024) - // - QNX 8 (December 2023) - // noop -#elif defined(__GLIBC__) || TARGET_OS_DARWIN || defined(__FreeBSD__) || (defined(__ANDROID__) && __ANDROID_API__ >= 34) || defined(__musl__) - // Pre-standard posix_spawn_file_actions_addchdir_np version available in: - // - Solaris 11.3 (October 2015) - // - Glibc 2.29 (February 2019) - // - macOS 10.15 (October 2019) - // - musl 1.1.24 (October 2019) - // - FreeBSD 13.1 (May 2022) - // - Android 14 (October 2023) - return posix_spawn_file_actions_addchdir_np(file_actions, path); -#else - // Standardized posix_spawn_file_actions_addchdir version (POSIX.1-2024, June 2024) available in: - // - Solaris 11.4 (August 2018) - // - NetBSD 10.0 (March 2024) - return posix_spawn_file_actions_addchdir(file_actions, path); -#endif -} - -static int _subprocess_posix_spawn_fallback( - pid_t * _Nonnull pid, - const char * _Nonnull exec_path, - const char * _Nullable working_directory, - const int file_descriptors[_Nonnull], - char * _Nullable const args[_Nonnull], - char * _Nullable const env[_Nullable], - gid_t * _Nullable process_group_id -) { - // Setup stdin, stdout, and stderr - posix_spawn_file_actions_t file_actions; - - int rc = posix_spawn_file_actions_init(&file_actions); - if (rc != 0) { return rc; } - if (file_descriptors[0] >= 0) { - rc = posix_spawn_file_actions_adddup2( - &file_actions, file_descriptors[0], STDIN_FILENO - ); - if (rc != 0) { return rc; } - } - if (file_descriptors[2] >= 0) { - rc = posix_spawn_file_actions_adddup2( - &file_actions, file_descriptors[2], STDOUT_FILENO - ); - if (rc != 0) { return rc; } - } - if (file_descriptors[4] >= 0) { - rc = posix_spawn_file_actions_adddup2( - &file_actions, file_descriptors[4], STDERR_FILENO - ); - if (rc != 0) { return rc; } - } - // Setup working directory - rc = _subprocess_addchdir_np(&file_actions, working_directory); - if (rc != 0) { - return rc; - } - - // Close parent side - if (file_descriptors[1] >= 0) { - rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[1]); - if (rc != 0) { return rc; } - } - if (file_descriptors[3] >= 0) { - rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[3]); - if (rc != 0) { return rc; } - } - if (file_descriptors[5] >= 0) { - rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[5]); - if (rc != 0) { return rc; } - } - - // Setup spawnattr - posix_spawnattr_t spawn_attr; - rc = posix_spawnattr_init(&spawn_attr); - if (rc != 0) { return rc; } - // Masks - sigset_t no_signals; - sigset_t all_signals; - sigemptyset(&no_signals); - sigfillset(&all_signals); - rc = posix_spawnattr_setsigmask(&spawn_attr, &no_signals); - if (rc != 0) { return rc; } - rc = posix_spawnattr_setsigdefault(&spawn_attr, &all_signals); - if (rc != 0) { return rc; } - // Flags - short flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF; - if (process_group_id != NULL) { - flags |= POSIX_SPAWN_SETPGROUP; - rc = posix_spawnattr_setpgroup(&spawn_attr, *process_group_id); - if (rc != 0) { return rc; } - } - rc = posix_spawnattr_setflags(&spawn_attr, flags); - - // Spawn! - rc = posix_spawn( - pid, exec_path, - &file_actions, &spawn_attr, - args, env - ); - posix_spawn_file_actions_destroy(&file_actions); - posix_spawnattr_destroy(&spawn_attr); - return rc; -} -#endif // _POSIX_SPAWN - -int _subprocess_fork_exec( - pid_t * _Nonnull pid, - const char * _Nonnull exec_path, - const char * _Nullable working_directory, - const int file_descriptors[_Nonnull], - char * _Nullable const args[_Nonnull], - char * _Nullable const env[_Nullable], - uid_t * _Nullable uid, - gid_t * _Nullable gid, - gid_t * _Nullable process_group_id, - int number_of_sgroups, const gid_t * _Nullable sgroups, - int create_session, - void (* _Nullable configurator)(void) -) { - int require_pre_fork = _subprocess_is_addchdir_np_available() == 0 || - uid != NULL || - gid != NULL || - process_group_id != NULL || - (number_of_sgroups > 0 && sgroups != NULL) || - create_session || - configurator != NULL; - -#if _POSIX_SPAWN - // If posix_spawn is available on this platform and - // we do not require prefork, use posix_spawn if possible. - // - // (Glibc's posix_spawn does not support - // `POSIX_SPAWN_SETEXEC` therefore we have to keep - // using fork/exec if `require_pre_fork` is true. - if (require_pre_fork == 0) { - return _subprocess_posix_spawn_fallback( - pid, exec_path, - working_directory, - file_descriptors, - args, env, - process_group_id - ); - } -#endif - - // Setup pipe to catch exec failures from child - int pipefd[2]; - if (pipe(pipefd) != 0) { - return errno; - } - // Set FD_CLOEXEC so the pipe is automatically closed when exec succeeds - short flags = fcntl(pipefd[0], F_GETFD); - if (flags == -1) { - close(pipefd[0]); - close(pipefd[1]); - return errno; - } - flags |= FD_CLOEXEC; - if (fcntl(pipefd[0], F_SETFD, flags) == -1) { - close(pipefd[0]); - close(pipefd[1]); - return errno; - } - - flags = fcntl(pipefd[1], F_GETFD); - if (flags == -1) { - close(pipefd[0]); - close(pipefd[1]); - return errno; - } - flags |= FD_CLOEXEC; - if (fcntl(pipefd[1], F_SETFD, flags) == -1) { - close(pipefd[0]); - close(pipefd[1]); - return errno; - } - - // Finally, fork -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated" - pid_t childPid = fork(); -#pragma GCC diagnostic pop - if (childPid == -1) { - close(pipefd[0]); - close(pipefd[1]); - return errno; - } - - if (childPid == 0) { - // Child process - close(pipefd[0]); // Close unused read end - - // Perform setups - if (working_directory != NULL) { - if (chdir(working_directory) != 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } - } - - - if (uid != NULL) { - if (setuid(*uid) != 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } - } - - if (gid != NULL) { - if (setgid(*gid) != 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } - } - - if (number_of_sgroups > 0 && sgroups != NULL) { - if (setgroups(number_of_sgroups, sgroups) != 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } - } - - if (create_session != 0) { - (void)setsid(); - } - - if (process_group_id != NULL) { - (void)setpgid(0, *process_group_id); - } - - // Bind stdin, stdout, and stderr - int rc = 0; - if (file_descriptors[0] >= 0) { - rc = dup2(file_descriptors[0], STDIN_FILENO); - if (rc < 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } - } - if (file_descriptors[2] >= 0) { - rc = dup2(file_descriptors[2], STDOUT_FILENO); - if (rc < 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } - } - if (file_descriptors[4] >= 0) { - rc = dup2(file_descriptors[4], STDERR_FILENO); - if (rc < 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } - } - // Close parent side - if (file_descriptors[1] >= 0) { - rc = close(file_descriptors[1]); - } - if (file_descriptors[3] >= 0) { - rc = close(file_descriptors[3]); - } - if (file_descriptors[4] >= 0) { - rc = close(file_descriptors[5]); - } - if (rc != 0) { - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } - // Run custom configuratior - if (configurator != NULL) { - configurator(); - } - // Finally, exec - execve(exec_path, args, env); - // If we reached this point, something went wrong - int error = errno; - write(pipefd[1], &error, sizeof(error)); - close(pipefd[1]); - _exit(EXIT_FAILURE); - } else { - // Parent process - close(pipefd[1]); // Close unused write end - // Communicate child pid back - *pid = childPid; - // Read from the pipe until pipe is closed - // Eitehr due to exec succeeds or error is written - int childError = 0; - if (read(pipefd[0], &childError, sizeof(childError)) > 0) { - // We encountered error - close(pipefd[0]); - return childError; - } else { - // Child process exec was successful - close(pipefd[0]); - return 0; - } - } -} - -#endif // TARGET_OS_LINUX - -#endif // !TARGET_OS_WINDOWS - -#pragma mark - Environment Locking - -#if __has_include() -#import -void _subprocess_lock_environ(void) { - environ_lock_np(); -} - -void _subprocess_unlock_environ(void) { - environ_unlock_np(); -} -#else -void _subprocess_lock_environ(void) { /* noop */ } -void _subprocess_unlock_environ(void) { /* noop */ } -#endif - -char ** _subprocess_get_environ(void) { -#if __has_include() - return *_NSGetEnviron(); -#elif defined(_WIN32) -#include - return _environ; -#elif TARGET_OS_WASI - return __wasilibc_get_environ(); -#elif __has_include() - return environ; -#endif -} - - -#if TARGET_OS_WINDOWS - -typedef struct { - DWORD pid; - HWND mainWindow; -} CallbackContext; - -static BOOL CALLBACK enumWindowsCallback( - HWND hwnd, - LPARAM lParam -) { - CallbackContext *context = (CallbackContext *)lParam; - DWORD pid; - GetWindowThreadProcessId(hwnd, &pid); - if (pid == context->pid) { - context->mainWindow = hwnd; - return FALSE; // Stop enumeration - } - return TRUE; // Continue enumeration -} - -BOOL _subprocess_windows_send_vm_close( - DWORD pid -) { - // First attempt to find the Window associate - // with this process - CallbackContext context = {0}; - context.pid = pid; - EnumWindows(enumWindowsCallback, (LPARAM)&context); - - if (context.mainWindow != NULL) { - if (SendMessage(context.mainWindow, WM_CLOSE, 0, 0)) { - return TRUE; - } - } - - return FALSE; -} - -#endif - diff --git a/Tests/SwiftJavaToolLibTests/CompileJavaWrapTools.swift b/Tests/SwiftJavaToolLibTests/CompileJavaWrapTools.swift index 68702b621..0254703eb 100644 --- a/Tests/SwiftJavaToolLibTests/CompileJavaWrapTools.swift +++ b/Tests/SwiftJavaToolLibTests/CompileJavaWrapTools.swift @@ -18,7 +18,7 @@ import JavaUtilJar import JavaNet import SwiftJavaShared import SwiftJavaConfigurationShared -import _Subprocess +import Subprocess import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 import Foundation @@ -37,7 +37,7 @@ func compileJava(_ sourceText: String) async throws -> Foundation.URL { let classesDirectory = try createTemporaryDirectory(in: FileManager.default.temporaryDirectory) - let javacProcess = try await _Subprocess.run( + let javacProcess = try await Subprocess.run( .path(.init("\(javaHome)" + "/bin/javac")), arguments: [ "-d", classesDirectory.path, // output directory for .class files @@ -171,4 +171,4 @@ let failureMessage = "Expected chunk: \n" + XCTAssertTrue(checkAgainstText.contains(checkAgainstExpectedChunk), "\(failureMessage)") } -} \ No newline at end of file +} diff --git a/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift b/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift index 9983813d7..3063cc2c2 100644 --- a/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift +++ b/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift @@ -18,7 +18,7 @@ import SwiftJavaConfigurationShared import SwiftJavaShared import SwiftJavaToolLib import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 -import _Subprocess +import Subprocess class JavaTranslatorTests: XCTestCase { diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift index 74ed3ce28..fb45a69ac 100644 --- a/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift @@ -18,7 +18,7 @@ import JavaUtilJar import SwiftJavaShared import JavaNet import SwiftJavaConfigurationShared -import _Subprocess +import Subprocess import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 final class BasicWrapJavaTests: XCTestCase { @@ -88,4 +88,4 @@ final class BasicWrapJavaTests: XCTestCase { ) } -} \ No newline at end of file +} diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift index ceb05df97..636ef8dc3 100644 --- a/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift @@ -18,7 +18,7 @@ import JavaUtilJar import SwiftJavaShared import JavaNet import SwiftJavaConfigurationShared -import _Subprocess +import Subprocess import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 final class GenericsWrapJavaTests: XCTestCase {