Skip to content
93 changes: 93 additions & 0 deletions text/0000-process-stdio-redirection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
- Feature Name: process-stdio-redirection
- Start Date: 2015-04-10
- RFC PR:
- Rust Issue:

# Summary

Update the `std::process` API with the ability to redirect stdio of child
processes to any opened file handle.

# Motivation

The current API in `std::process` allows to either pipe stdio between parent and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this motivation section may want to be updated with the current state of the standard library because with the FromRaw{Fd,Handle} implementations on Stdio it's actually possible to do this. It looks like this RFC gets us to a point of a slightly-more-ergonomic version of what we have today, but I think it would be worth it to explore the motivation to make sure the gains are worth the additions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I hadn't realized those changes had landed! I'll update the RFC to just focus on a high-level design

child process or redirect stdio to `/dev/null`. It would also be largely useful
to allow stdio redirection to any currently opened `std::fs::File` (henceforth
`File`) handle. This would allow redirecting stdio with a physical file or even
another process (via OS pipe) without forcing the parent process to buffer the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When directing to another process, the pipe actually already exist in a sense where Stdio::piped() means that the child will be created with a stdin/stdout/stderr handle, under which is just a file descriptor. That may end up being more useful in the long run to hook up processes together perhaps?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's true that the pipes exist internally, but they only pipe between the parent and child only. Although an API for hooking up several processes directly may be useful, I think that extending the current API with this functionality may be too troublesome to make it truly flexible, besides simply allowing the caller to pass in some sort of file descriptor and let them worry about hooking things up properly.

data itself.

For example, one may wish to spawn a process which prints gigabytes
of data (e.g. logs) and use another process to filter through it, and save the
result to a file. The current API would force the parent process to dedicate a
thread just to buffer all data between child processes and disk, which is
impractical given that the OS can stream the data for us via pipes and file
redirection.

# Detailed design

First, the standard library should provide an OS agnostic way of creating OS
in-memory pipes, i.e. a `Pipe`, providing reader and writer handles as `File`s.
This would avoid the need for users to write OS specific (and `unsafe`) code
while retaining all the benefits offered by `File`. This proposal considers
making the `Pipe`'s reader and writer fields public to allow easily moving
ownership of the two handles, but any other appropriate interface is acceptable.

```rust
pub struct Pipe {
pub reader: File,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some details on how this structure is going to be created as well? (the more detailed an RFC is the better!)

Also, I think that File may not be the best type to use here, or at least it may depend on the implementation. I would guess that on unix this would use the pipe system call, but File has methods like set_len which may not work out too well for pipes.

As a final point, could you add some words as to how this will be implemented on Windows as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexcrichton I think you are right that File isn't the correct type to use here. I was thinking as a File as an abstraction over a file descriptor/HANDLE, but I didn't really think about the distinctions between pipes and files. I'll update the design with a separate wrapper for pipes to maintain those distinctions!

pub writer: File,
}
```

Next, `std::process::Stdio` should provide a `redirect` method which accepts and
stores a `&File`, ensuring the underlying handle will not go out of scope
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On unix at least I know that you can use basically any file descriptor for stdout/stdin/stderr of a child process, but I'm less familiar with the situation on Windows. It may be worth exploring if the same set of primitives that can be used on Unix can be used on Windows, and perhaps they could all be accepted as part of this functions?

For example we may want at least (some day) an extension trait along the lines of:

pub trait StdioExt {
    fn redirect_fd<T: AsRawFd>(t: &T) -> Self;
}

That way you could do Stdio::redirect_fd(&tcp_stream) or Stdio::redirect_fd(&unix_socket) and it should all "just work". I don't think this wants to totally dive into the territory of "accept anything that's a file descriptor" just yet but it's at least worth considering.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any potential problems you see with "accepting anything that's a file descriptor" at the time? If this is too broad what would you think about methods that only allow redirection with "blessed" types like a File or some sort of Pipe offered by the standard library?

unexpectedly before the child is spawned. The spawning implementation can then
extract and use the `File`'s OS specific handle when creating the child
process.

This `File` reference should be an immutable one, to allow "reuse" of the handle
across several `Command`s simultaneously. This, however, can allow code to
indirectly mutate a `File` through an immutable reference by passing it on to a
child process, although retrieving and mutating through the underlying OS handle
(via `AsRaw{Fd, Socket, Handle}`) is already possible through a `&File`. Thus
this API would not introduce any "mutability leaks" of `File`s that were not
already present.

This design also offers benefits when the user may wish to close (drop) a
`File`, for example, closing the read end of a pipe and sending EOF to its
child. The compiler can infer the lifetimes of all references to the original
`File`, eliminating some guesswork on whether all ends of a pipe have been
closed. This would not be possible in a design which duplicates the OS handle
internally which could more easily lead to a deadlock (e.g. waiting on a child
to exit while the same scope holds an open pipe to the child's stdin).

Reclaiming ownership of the borrowed `File` may require locally scoping the
creation of `Stdio` or `Command` instances to force their lifetimes to end,
however, this is minimally intrusive compared to alternative designs.

# Drawbacks

Implementing a design based on `File` borrows will require adding lifetime
parameters on stabilized `Stdio` and `Command` close to the 1.0 release.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's probably unlikely for a breaking change to add a lifetime parameter to Command to be accepted at this point, so this RFC may want to be worded with respect to keeping the same structure as-is today.

While not implemented today, I think that this could benefit from a "default lifetime" where we could define something like:

pub struct Command<'a = 'static> { ... }

That way it would be backwards-compatible for us to add a lifetime parameter. Unfortunately though I don't believe it's implemented.


# Alternatives

Do nothing now and choose a stability compatible design, possibly being stuck
with less ergonomic APIs.

One alternative strategy is to duplicate the underlying OS handle and have the
`std::process` APIs take ownership of the copy. When working with OS pipes,
however, the user would have to manually keep track where the duplicates have
gone if they wish to close them all; failing to do so may cause deadlocks.

Another strategy would be for `Stdio` to take ownership of a `File` and wrap it
as a `Rc<File>`, allowing it to be "reused" in any number of redirections by
cloning the `Stdio` wrapper. A caller could try to regain ownership (via
`try_unwrap` on the internal wrapper), but they would have to (manually) ensure
all other `Stdio` clones are dropped. This design would also suffer from
potential deadlocks, making it by far the least ergonomic option.

# Unresolved questions

None at the moment.