Skip to content

Commit 233af5e

Browse files
committed
Subprocess proposal v2 update
1 parent cf58128 commit 233af5e

File tree

1 file changed

+39
-10
lines changed

1 file changed

+39
-10
lines changed

Proposals/NNNN-swift-subprocess.md

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
## Revision History
1111

1212
* **v1**: Initial draft
13+
* **v2**: Minor Updates:
14+
- Switched `AsyncBytes` to be backed by `DispatchIO`.
15+
- Introduced `resolveExecutablePath(withEnvironment:)` to enable explicit lookup of the executable path.
16+
- Added a new option, `closeWhenDone`, to automatically close the file descriptors passed in via `.readFrom` and friends.
17+
- Introduced a new parameter, `shouldSendToProcessGroup`, in the `sendSignal` function to control whether the signal should be sent to the process or the process group.
18+
- Introduced a section on "Future Directions."
1319

1420
## Introduction
1521

@@ -280,8 +286,9 @@ public struct Subprocess: Sendable {
280286
// The standard error of the child process, expressed as AsyncSequence<UInt8>
281287
// This property is `nil` if the standard error is discarded or written to disk
282288
public var standardError: AsyncBytes? { get }
283-
284-
public func sendSignal(_ signal: Signal) throws
289+
// If `shouldSendToProcessGroup` is `true`, the signal will be send to the entire process
290+
// group instead of the current process.
291+
public func sendSignal(_ signal: Signal, toProcessGroup shouldSendToProcessGroup: Bool) throws
285292
}
286293

287294
extension Subprocess {
@@ -491,7 +498,7 @@ _(We welcome community input on which Linux and Windows "escape hatches" we shou
491498

492499
In addition to supporting the direct passing of `Sequence<UInt8>` and `AsyncSequence<UInt8>` as the standard input to the child process, `Subprocess` also provides a `Subprocess.InputMethod` type that includes two additional input options:
493500
- `.noInput`: Specifies that the subprocess does not require any standard input. This is the default value.
494-
- `.readingFrom`: Specifies that the subprocess should read its standard input from a file descriptor provided by the developer.
501+
- `.readFrom`: Specifies that the subprocess should read its standard input from a file descriptor provided by the developer. Subprocess will automatically close the file descriptor after the process exits if `closeWhenDone` is set to `true`.
495502

496503
```swift
497504
extension Subprocess {
@@ -501,7 +508,7 @@ extension Subprocess {
501508
@available(watchOS, unavailable)
502509
public struct InputMethod: Sendable, Hashable {
503510
public static var noInput: Self
504-
public static func readFrom(_ fd: FileDescriptor) -> Self
511+
public static func readFrom(_ fd: FileDescriptor, closeWhenDone: Bool) -> Self
505512
}
506513
}
507514
```
@@ -514,7 +521,7 @@ let ls = try await Subprocess.run(executing: .named("ls"))
514521

515522
// Alteratively, developers could pass in a file descriptor
516523
let fd: FileDescriptor = ...
517-
let cat = try await Subprocess.run(executing: .named("cat"), input: .readingFrom(fd))
524+
let cat = try await Subprocess.run(executing: .named("cat"), input: .readFrom(fd, closeWhenDone: true))
518525

519526
// Pass in a async sequence directly
520527
let sequence: AsyncSequence = ...
@@ -526,7 +533,7 @@ let exe = try await Subprocess.run(executing: .at("/some/executable"), input: se
526533

527534
`Subprocess` uses two types to describe where the standard output and standard error of the child process should be redirected. These two types, `Subprocess.collectOutputMethod` and `Subprocess.redirectOutputMethod`, correspond to the two general categories of `run` methods mentioned above. Similar to `InputMethod`, both `OutputMethod`s add two general output destinations:
528535
- `.discard`: Specifies that the child process's output should be discarded, effectively written to `/dev/null`.
529-
- `.writeTo`: Specifies that the child process should write its output to a file descriptor provided by the developer. This file descriptor will be closed once the write operation is complete.
536+
- `.writeTo`: Specifies that the child process should write its output to a file descriptor provided by the developer. Subprocess will automatically close the file descriptor after the process exits if `closeWhenDone` is set to `true`.
530537

531538
`CollectedOutMethod` adds one more option to non-closure-based `run` methods that return a `CollectedResult`: `.collect` and its variation `.collect(limit:)`. This option specifies that `Subprocess` should collect the output as `Data`. Since the output of a child process could be arbitrarily large, `Subprocess` imposes a limit on how many bytes it will collect. By default, this limit is 16kb (when specifying `.collect`). Developers can override this limit by specifying `.collect(limit: newLimit)`:
532539

@@ -542,7 +549,7 @@ extension Subprocess {
542549
// Collect the output as Data with the default 16kb limit
543550
public static var collect: Self
544551
// Write the output directly to a FileDescriptor
545-
public static func writeTo(_ fd: FileDescriptor) -> Self
552+
public static func writeTo(_ fd: FileDescriptor, closeWhenDone: Bool) -> Self
546553
// Collect the output as Data with modified limit
547554
public static func collect(limit limit: Int) -> Self
548555
}
@@ -563,7 +570,7 @@ extension Subprocess {
563570
// Redirect the output as AsyncSequence
564571
public static var redirect: Self
565572
// Write the output directly to a FileDescriptor
566-
public static func writeTo(_ fd: FileDescriptor) -> Self
573+
public static func writeTo(_ fd: FileDescriptor, closeWhenDone: Bool) -> Self
567574
}
568575
}
569576
```
@@ -584,7 +591,8 @@ print("curl output: \(String(data: curl.standardOutput!, encoding: .utf8)!)")
584591

585592
// Write to a specific file descriptor
586593
let fd: FileDescriptor = try .open(...)
587-
let result = try await Subprocess.run(executing: .at("/some/script"), output: .writeTo(fd))
594+
let result = try await Subprocess.run(
595+
executing: .at("/some/script"), output: .writeTo(fd, closeWhenDone: true))
588596

589597
// Redirect the output as AsyncSequence
590598
let result2 = try await Subprocess.run(executing: .named("/some/script"), output: .redirect) { subprocess in
@@ -662,14 +670,16 @@ extension Subprocess {
662670
/// Create an `EnvironmentConfig` with an executable path
663671
/// such as `/bin/ls`
664672
public static func at(_ filePath: FilePath) -> Self
673+
// Resolves the executable path with the given `Environment` value
674+
public func resolveExecutablePath(withEnvironment environment: Environment) -> FilePath?
665675
}
666676
}
667677
```
668678

669679

670680
### `Subprocess.Environment`
671681

672-
`struct Environment` is used to configure how should the process being lunched receive its environment values:
682+
`struct Environment` is used to configure how should the process being launched receive its environment values:
673683

674684
```swift
675685
extension Subprocess {
@@ -826,6 +836,25 @@ extension Subprocess {
826836
No impact on existing code is anticipated. All introduced changes are additive.
827837

828838

839+
## Future Directions
840+
841+
### Automatic Splitting of `Arguments`
842+
843+
Ideally, the `Arguments` feature should automatically split a string, such as "-a -n 1024 -v 'abc'", into an array of arguments. This enhancement would enable `Arguments` to conform to `ExpressibleByStringLiteral`, allowing developers to conveniently pass either a `String` or `[String]` as `Arguments`.
844+
845+
I decided to defer this feature because it turned out to be a "hard problem" -- different platforms handle arguments differently, requiring careful consideration to ensure correctness.
846+
847+
For reference, Python uses [`shlex.split`](https://docs.python.org/3/library/shlex.html), which could serve as a valuable starting point for implementation.
848+
849+
## Combined `stdout` and `stderr`
850+
851+
In Python's `Subprocess`, developers can merge standard output and standard error into a single stream. This is particularly useful when an executable improperly utilizes standard error as standard output (or vice versa). We should explore the most effective way to achieve this enhancement without introducing confusion to existing parameters—perhaps by introducing a new property.
852+
853+
## Automatic `Subprocess` Cancellation
854+
855+
Currently, `Subprocess` does not terminate the spawned process, even if the `Subprocess` instance goes out of scope. It would be beneficial to investigate whether it is more sensible to attempt terminating the spawned process when the `Subprocess` itself goes out of scope or when the parent task is canceled.
856+
857+
829858
## Alternatives Considered
830859

831860
### Improving `Process` vs Creating New Type

0 commit comments

Comments
 (0)