I'm actually in the midst of writing a non-trivial case of subprocess execution, whereby I need to keep the subprocess alive indefinitely and interact with it at various times from various parts of my program.
I reviewed every relevant package I could find on the Swift Package Index, ultimately concluding that Foundation's Process
is my best bet. The only other interesting-looking option I found was SwiftCommand, which improves Process
's ergonomics slightly. Everything else out there is "old fashioned" (no async APIs) which is a non-starter for me given the nature of this problem domain.
Looking at the proposal for this Subprocess
, there's a few things which seem like they'd be significant problems for this use case. For example, it appears I can only interact with a subprocess through the run
variant that takes a closure, and only while that closure is live. Which means I have to keep a Task
somewhere running that closure indefinitely, and somehow communicate from inside that closure to the rest of my program. I also have to somehow make that work even when the subprocess dies and I need to restart it from scratch. This all seems technically doable, but a lot more work than what I'm currently doing with Process
, which is simply using a lock (for mutual exclusion) and interacting with the long-lived instance that's stored in a variable (inside an actor, or a class, or as a global). It's easy to handle errors such as subprocess termination by just re-setting the variable to a new Process
instance - this can be done automatically without callers having a clue.
The one potential saving grace is that I haven't actually been able to try writing real code against this Subprocess
API yet, so maybe I'm overlooking something.
There are definitely some awkward aspects to the existing Process
, like:
- A lack of convenience initialisers to succinctly set up the desired state.
- No way to search
PATH
for a command.
- The inability to send signals other than SIGTERM.
- An inability to read from the subprocess without blocking (contrary to its name and obviously intended purpose,
availableData
and similar block if there's no data available
).
- The ability to do pipeline-common operations like read up until a sentinel string.
- The ability to read from both stdout and stderr simultaneously.
- Incidentally, the SwiftCommand package tries to add this but fails to indicate which pipe a given piece of data came from, making it useless for most purposes (although at least it avoids deadlock).
- A lack of convenience methods for writing strings (it's all based on
FileHandle
s which only take DataProtocol
s).
- No [built-in] way to run the subprocess in a sandbox, or otherwise influence its environment for security purposes.
This proposal addresses some but not all of these. It'd be great to see it cover all of them.
I'm also wondering what happens if you don't immediately read from the subprocess when using the redirect
method, given there's no option to specify a buffer size. When does the subprocess end up blocked on its writes (to stdout or stderr)?
Also, why is stderr discarded by default with many of the run
methods? That seems like it's inviting bad behaviour and unwitting design flaws.
Why is there no inherit
option for stdin, stdout, & stderr?
Overall I think the proposed API looks alright for simplistic cases - mainly run-once-and-read-output situations, particularly for background processes (that aren't visible to the parent process's user). For interactive use of subprocesses, or shell-like usage where everything shares the same tty by default, I'm not sure it's a net improvement over Process
. Better in some ways, but worse in others.