This is looking really great. Can't wait to finally use it! There were a few things that stood out while reviewing the proposal in depth again.
Sending closures
public static func run<R>(
...
_ body: (sending @escaping (Subprocess) async throws -> R)
) async throws -> ExecutionResult<R>
All of the run
methods that take a closure require the closure to be sending
. Why is that the case? Most with-style
methods in the ecosystem take non-sending/non-Sendable closures that makes them easily composable with most other code. Is there an implementation detail that requires this? If so can we restructure the implementation to not require it?
Isolation of closures
I believe the closure taking methods are missing a isolation: isolated (any Actor)? = #isolation
parameter here. Without that I assume most of those methods are almost impossible to use from an actor isolated context without data race errors. I talked about the need for the parameter in my recent talk here.
AsyncSequence of UInt8
I recall previous discussions around the usage of AsyncSequence<UInt8>
and the performance problems that it can bring. Byte wise async iteration is often a cause for a huge performance loss and from our server experience we often use ByteBuffer
/Array[Slice]<UInt8>
/Data
as the element of an async sequence here. @icharleshu you have done some performance benchmarking right? What was the outcome of that w.r.t. the type of the element used here? (I see further down that some async sequences use Data
so this looks a bit inconsistent throughout the proposal)
Usage of pid_t
and DWORD
I understand that the ProcessIdentifier APIs need to be platform specific though is it the right thing to expose APIs that use types like pid_t
and DWORD
directly or should we just expose them as Int
?
ProcessIdentifier.value vs ProcessIdentifier.processID
Why are we differing between the platforms here? Couldn't it also be .value
on Windows? Furthermore having a type called ProcessIdentifier
have a property called processID
s feels redundant.
Unmanaged Subprocess
This section tries to achieve multiple things at the same time:
- Allow to pass unmanaged file descriptors
- Unstructured concurrency to be able to outlive the current task
I think 1. is really valuable and something that we should expose for expert users. However, it doesn't follow to me why this is tied to 2. There might be use-cases where you pass in file descriptors directly but you still want to wait for the process to terminate. Furthermore, you can achieve 2. by just sticking it into an unstructured Task like this, right?
Task {
let ls = try await Subprocess.run(
.named("ls"),
output: .collect
)
}
Usage of FileDescriptor & FilePath
FileDescriptor
and FilePath
are types provided by swift-system
. Currently swift-system
is not part of the toolchain. How do you expect to solve using those types in a public Foundation
API?
Teardown
I got confused by this section for a second and assumed that there is only a teardown
method but later on saw that there is also a configuration option. Is my assumption correct here that the teardown sequence passed via platform options is used when the task calling run
is cancelled? If so could we spell this out more explicitly in the documentation?
ExecutionResult requires Sendable type
public struct ExecutionResult<T: Sendable>: Sendable
Why does this require a Sendable
type? I guess it is tied to my above question why the closure is sending
?
Minor: Generic parameter names
Can we use longer generic parameter names than R
or S
? So far the standard library tried to use names like Return
for such parameters. It makes reading the docs later on a lot easier.
Minor: Common signals
We are currently offering a similar type in swift-service-lifecycle
here. Over time we added more signals that were commonly used. We might want to extend the common values with the missing ones from there. Though this is not as important since there is a rawValue escape hatch.
Minor: send signal parameter label
public func send(_ signal: Signal, toProcessGroup shouldSendToProcessGroup: Bool)
Should the first external parameter label here really be underscored? I personally feel like subprocess.send(signal: .kill)
reads better. (Same applies to the sendSignal
on TeardownStep