Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ jobs:
name: Test
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
with:
linux_os_versions: '["amazonlinux2", "bookworm", "noble", "jammy", "focal", "rhel-ubi9"]'
linux_os_versions: '["amazonlinux2", "bookworm", "noble", "jammy", "rhel-ubi9"]'
linux_swift_versions: '["6.1", "nightly-main"]'
linux_pre_build_command: |
if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy, focal
if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy
apt-get update -y

# Test dependencies
Expand Down
56 changes: 34 additions & 22 deletions Sources/Subprocess/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
public func run<
Input: InputProtocol,
Output: OutputProtocol,
Error: OutputProtocol
Error: ErrorOutputProtocol
>(
_ executable: Executable,
arguments: Arguments = [],
Expand Down Expand Up @@ -74,7 +74,7 @@ public func run<
public func run<
InputElement: BitwiseCopyable,
Output: OutputProtocol,
Error: OutputProtocol
Error: ErrorOutputProtocol
>(
_ executable: Executable,
arguments: Arguments = [],
Expand Down Expand Up @@ -117,7 +117,7 @@ public func run<
/// - isolation: the isolation context to run the body closure.
/// - body: The custom execution body to manually control the running process
/// - Returns: an `ExecutableResult` type containing the return value of the closure.
public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: OutputProtocol>(
public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: ErrorOutputProtocol>(
_ executable: Executable,
arguments: Arguments = [],
environment: Environment = .inherit,
Expand Down Expand Up @@ -164,7 +164,7 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: Out
/// - isolation: the isolation context to run the body closure.
/// - body: The custom execution body to manually control the running process.
/// - Returns: an `ExecutableResult` type containing the return value of the closure.
public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
public func run<Result, Input: InputProtocol, Error: ErrorOutputProtocol>(
_ executable: Executable,
arguments: Arguments = [],
environment: Environment = .inherit,
Expand Down Expand Up @@ -257,7 +257,7 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol>(
/// - isolation: the isolation context to run the body closure.
/// - body: The custom execution body to manually control the running process
/// - Returns: An `ExecutableResult` type containing the return value of the closure.
public func run<Result, Error: OutputProtocol>(
public func run<Result, Error: ErrorOutputProtocol>(
_ executable: Executable,
arguments: Arguments = [],
environment: Environment = .inherit,
Expand Down Expand Up @@ -391,7 +391,7 @@ public func run<Result>(
public func run<
InputElement: BitwiseCopyable,
Output: OutputProtocol,
Error: OutputProtocol
Error: ErrorOutputProtocol
>(
_ configuration: Configuration,
input: borrowing Span<InputElement>,
Expand Down Expand Up @@ -451,7 +451,7 @@ public func run<
public func run<
Input: InputProtocol,
Output: OutputProtocol,
Error: OutputProtocol
Error: ErrorOutputProtocol
>(
_ configuration: Configuration,
input: Input = .none,
Expand All @@ -463,10 +463,13 @@ public func run<
standardOutput: Output.OutputType,
standardError: Error.OutputType
)
let inputPipe = try input.createPipe()
let outputPipe = try output.createPipe()
let errorPipe = try error.createPipe(from: outputPipe)
let result = try await configuration.run(
input: try input.createPipe(),
output: try output.createPipe(),
error: try error.createPipe()
input: inputPipe,
output: outputPipe,
error: errorPipe
) { (execution, inputIO, outputIO, errorIO) -> RunResult in
// Write input, capture output and error in parallel
var inputIOBox: IOChannel? = consume inputIO
Expand Down Expand Up @@ -540,18 +543,21 @@ public func run<
/// - body: The custom execution body to manually control the running process
/// - Returns an executableResult type containing the return value
/// of the closure.
public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: OutputProtocol>(
public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: ErrorOutputProtocol>(
_ configuration: Configuration,
input: Input = .none,
output: Output = .discarded,
error: Error = .discarded,
isolation: isolated (any Actor)? = #isolation,
body: ((Execution) async throws -> Result)
) async throws -> ExecutionResult<Result> where Error.OutputType == Void {
let inputPipe = try input.createPipe()
let outputPipe = try output.createPipe()
let errorPipe = try error.createPipe(from: outputPipe)
return try await configuration.run(
input: try input.createPipe(),
output: try output.createPipe(),
error: try error.createPipe()
input: inputPipe,
output: outputPipe,
error: errorPipe
) { execution, inputIO, outputIO, errorIO in
var inputIOBox: IOChannel? = consume inputIO
return try await withThrowingTaskGroup(
Expand Down Expand Up @@ -590,7 +596,7 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: Out
/// - body: The custom execution body to manually control the running process
/// - Returns an executableResult type containing the return value
/// of the closure.
public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
public func run<Result, Input: InputProtocol, Error: ErrorOutputProtocol>(
_ configuration: Configuration,
input: Input = .none,
error: Error = .discarded,
Expand All @@ -599,10 +605,13 @@ public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
body: ((Execution, AsyncBufferSequence) async throws -> Result)
) async throws -> ExecutionResult<Result> where Error.OutputType == Void {
let output = SequenceOutput()
let inputPipe = try input.createPipe()
let outputPipe = try output.createPipe()
let errorPipe = try error.createPipe(from: outputPipe)
return try await configuration.run(
input: try input.createPipe(),
output: try output.createPipe(),
error: try error.createPipe()
input: inputPipe,
output: outputPipe,
error: errorPipe
) { execution, inputIO, outputIO, errorIO in
var inputIOBox: IOChannel? = consume inputIO
var outputIOBox: IOChannel? = consume outputIO
Expand Down Expand Up @@ -702,7 +711,7 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol>(
/// - body: The custom execution body to manually control the running process
/// - Returns an executableResult type containing the return value
/// of the closure.
public func run<Result, Error: OutputProtocol>(
public func run<Result, Error: ErrorOutputProtocol>(
_ configuration: Configuration,
error: Error = .discarded,
preferredBufferSize: Int? = nil,
Expand All @@ -711,10 +720,13 @@ public func run<Result, Error: OutputProtocol>(
) async throws -> ExecutionResult<Result> where Error.OutputType == Void {
let input = CustomWriteInput()
let output = SequenceOutput()
let inputPipe = try input.createPipe()
let outputPipe = try output.createPipe()
let errorPipe = try error.createPipe(from: outputPipe)
return try await configuration.run(
input: try input.createPipe(),
output: try output.createPipe(),
error: try error.createPipe()
input: inputPipe,
output: outputPipe,
error: errorPipe
) { execution, inputIO, outputIO, errorIO in
let writer = StandardInputWriter(diskIO: inputIO!)
let outputSequence = AsyncBufferSequence(
Expand Down
49 changes: 49 additions & 0 deletions Sources/Subprocess/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,19 @@ internal struct IODescriptor: ~Copyable {
self.closeWhenDone = closeWhenDone
}

internal init?(duplicating ioDescriptor: borrowing IODescriptor?) throws {
let descriptor = try ioDescriptor?.duplicate()
if let descriptor {
self = descriptor
} else {
return nil
}
}

func duplicate() throws -> IODescriptor {
return try IODescriptor(self.descriptor.duplicate(), closeWhenDone: self.closeWhenDone)
}

consuming func createIOChannel() -> IOChannel {
let shouldClose = self.closeWhenDone
self.closeWhenDone = false
Expand Down Expand Up @@ -962,6 +975,13 @@ internal struct CreatedPipe: ~Copyable {
return self._writeFileDescriptor.take()
}

internal init(duplicating createdPipe: borrowing CreatedPipe) throws {
self.init(
readFileDescriptor: try IODescriptor(duplicating: createdPipe._readFileDescriptor),
writeFileDescriptor: try IODescriptor(duplicating: createdPipe._writeFileDescriptor)
)
}

internal init(closeWhenDone: Bool, purpose: Purpose) throws {
#if canImport(WinSDK)
/// On Windows, we need to create a named pipe.
Expand Down Expand Up @@ -1213,3 +1233,32 @@ extension Set {
return self.remove(element) != nil
}
}

#if canImport(WinSDK)
extension HANDLE {
func duplicate() throws -> HANDLE {
var handle: HANDLE? = nil
guard
DuplicateHandle(
GetCurrentProcess(),
self,
GetCurrentProcess(),
&handle,
0, true, DWORD(DUPLICATE_SAME_ACCESS)
)
else {
throw SubprocessError(
code: .init(.failedToCreatePipe),
underlyingError: .init(rawValue: GetLastError())
)
}
guard let handle else {
throw SubprocessError(
code: .init(.failedToCreatePipe),
underlyingError: .init(rawValue: GetLastError())
)
}
return handle
}
}
#endif
59 changes: 55 additions & 4 deletions Sources/Subprocess/IO/Output.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ extension OutputProtocol {
/// On Unix-like systems, `DiscardedOutput` redirects the
/// standard output of the subprocess to `/dev/null`, while on Windows,
/// redirects the output to `NUL`.
public struct DiscardedOutput: OutputProtocol {
public struct DiscardedOutput: OutputProtocol, ErrorOutputProtocol {
/// The type for the output.
public typealias OutputType = Void

Expand All @@ -82,7 +82,7 @@ public struct DiscardedOutput: OutputProtocol {
///
/// Developers have the option to instruct the `Subprocess` to automatically
/// close the related `FileDescriptor` after the subprocess is spawned.
public struct FileDescriptorOutput: OutputProtocol {
public struct FileDescriptorOutput: OutputProtocol, ErrorOutputProtocol {
/// The type for this output.
public typealias OutputType = Void

Expand Down Expand Up @@ -115,7 +115,7 @@ public struct FileDescriptorOutput: OutputProtocol {

/// A concrete `Output` type for subprocesses that collects output
/// from the subprocess as `String` with the given encoding.
public struct StringOutput<Encoding: Unicode.Encoding>: OutputProtocol {
public struct StringOutput<Encoding: Unicode.Encoding>: OutputProtocol, ErrorOutputProtocol {
/// The type for this output.
public typealias OutputType = String?
/// The max number of bytes to collect.
Expand Down Expand Up @@ -147,7 +147,7 @@ public struct StringOutput<Encoding: Unicode.Encoding>: OutputProtocol {

/// A concrete `Output` type for subprocesses that collects output from
/// the subprocess as `[UInt8]`.
public struct BytesOutput: OutputProtocol {
public struct BytesOutput: OutputProtocol, ErrorOutputProtocol {
/// The output type for this output option
public typealias OutputType = [UInt8]
/// The max number of bytes to collect
Expand Down Expand Up @@ -286,6 +286,57 @@ extension OutputProtocol where Self == BytesOutput {
}
}

// MARK: - ErrorOutputProtocol

/// Error output protocol specifies the set of methods that a type must implement to
/// serve as the error output target for a subprocess.
///
/// Instead of developing custom implementations of `ErrorOutputProtocol`, use the
/// default implementations provided by the `Subprocess` library to specify the
/// output handling requirements.
public protocol ErrorOutputProtocol: OutputProtocol {}

/// A concrete error output type for subprocesses that combines the standard error
/// output with the standard output stream.
///
/// When `CombinedErrorOutput` is used as the error output for a subprocess, both
/// standard output and standard error from the child process are merged into a
/// single output stream. This is equivalent to using shell redirection like `2>&1`.
///
/// This output type is useful when you want to capture or redirect both output
/// streams together, making it possible to process all subprocess output as a unified
/// stream rather than handling standard output and standard error separately.
public struct CombinedErrorOutput: ErrorOutputProtocol {
public typealias OutputType = Void
}

extension ErrorOutputProtocol {
internal func createPipe(from outputPipe: borrowing CreatedPipe) throws -> CreatedPipe {
if self is CombinedErrorOutput {
return try CreatedPipe(duplicating: outputPipe)
}
return try createPipe()
}
}

extension ErrorOutputProtocol where Self == CombinedErrorOutput {
/// Creates an error output that combines standard error with standard output.
///
/// When using `combineWithOutput`, both standard output and standard error from
/// the child process are merged into a single output stream. This is equivalent
/// to using shell redirection like `2>&1`.
///
/// This is useful when you want to capture or redirect both output streams
/// together, making it possible to process all subprocess output as a unified
/// stream rather than handling standard output and standard error separately
///
/// - Returns: A `CombinedErrorOutput` instance that merges standard error
/// with standard output.
public static var combineWithOutput: Self {
return CombinedErrorOutput()
}
}

// MARK: - Span Default Implementations
#if SubprocessSpan
extension OutputProtocol {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import FoundationEssentials

/// A concrete `Output` type for subprocesses that collects output
/// from the subprocess as data.
public struct DataOutput: OutputProtocol {
public struct DataOutput: OutputProtocol, ErrorOutputProtocol {
/// The output type for this output option
public typealias OutputType = Data
/// The maximum number of bytes to collect.
Expand Down
Loading