Skip to content

Commit 7dda1a6

Browse files
committed
add std::os::unix::process::CommandExt::fd
1 parent e8a792d commit 7dda1a6

File tree

2 files changed

+175
-1
lines changed

2 files changed

+175
-1
lines changed

library/std/src/os/unix/process.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::ffi::OsStr;
88
use crate::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
99
use crate::path::Path;
1010
use crate::sealed::Sealed;
11+
use crate::sys::{cvt, cvt_r};
1112
use crate::sys_common::{AsInner, AsInnerMut, FromInner, IntoInner};
1213
use crate::{io, process, sys};
1314

@@ -213,6 +214,84 @@ pub trait CommandExt: Sealed {
213214

214215
#[unstable(feature = "process_setsid", issue = "105376")]
215216
fn setsid(&mut self, setsid: bool) -> &mut process::Command;
217+
218+
/// Pass a file descriptor to a child process.
219+
///
220+
/// Getting this right is tricky. It is recommended to provide further information to the child
221+
/// process by some other mechanism. This could be an argument confirming file descriptors that
222+
/// the child can use, device/inode numbers to allow for sanity checks, or something similar.
223+
///
224+
/// If `new_fd` is an open file descriptor and closing it would produce one or more errors,
225+
/// those errors will be lost when this function is called. See
226+
/// [`man 2 dup`](https://www.man7.org/linux/man-pages/man2/dup.2.html#NOTES) for more information.
227+
///
228+
/// ```
229+
/// #![feature(command_pass_fds)]
230+
///
231+
/// use std::process::{Command, Stdio};
232+
/// use std::os::fd::process::CommandExt;
233+
/// use std::io::{self, Write};
234+
///
235+
/// # fn main() -> io::Result<()> {
236+
/// let (pipe_reader, mut pipe_writer) = io::pipe()?;
237+
///
238+
/// let fd_num = 123;
239+
///
240+
/// let mut cmd = Command::new("cat");
241+
/// cmd.arg(format!("/dev/fd/{fd_num}")).stdout(Stdio::piped()).fd(fd_num, pipe_reader);
242+
///
243+
/// let mut child = cmd.spawn()?;
244+
/// let mut stdout = child.stdout.take().unwrap();
245+
///
246+
/// pipe_writer.write_all(b"Hello, world!")?;
247+
/// drop(pipe_writer);
248+
///
249+
/// child.wait()?;
250+
/// assert_eq!(io::read_to_string(&mut stdout)?, "Hello, world!");
251+
///
252+
/// # Ok(())
253+
/// # }
254+
/// ```
255+
///
256+
/// If this method is called multiple times with the same `new_fd`, all but one file descriptor
257+
/// will be lost.
258+
///
259+
/// ```
260+
/// #![feature(command_pass_fds)]
261+
///
262+
/// use std::process::{Command, Stdio};
263+
/// use std::os::fd::process::CommandExt;
264+
/// use std::io::{self, Write};
265+
///
266+
/// # fn main() -> io::Result<()> {
267+
/// let (pipe_reader1, mut pipe_writer1) = io::pipe()?;
268+
/// let (pipe_reader2, mut pipe_writer2) = io::pipe()?;
269+
///
270+
/// let fd_num = 123;
271+
///
272+
/// let mut cmd = Command::new("cat");
273+
/// cmd.arg(format!("/dev/fd/{fd_num}"))
274+
/// .stdout(Stdio::piped())
275+
/// .fd(fd_num, pipe_reader1)
276+
/// .fd(fd_num, pipe_reader2);
277+
///
278+
/// let mut child = cmd.spawn()?;
279+
/// let mut stdout = child.stdout.take().unwrap();
280+
///
281+
/// pipe_writer1.write_all(b"Hello from pipe 1!")?;
282+
/// drop(pipe_writer1);
283+
///
284+
/// pipe_writer2.write_all(b"Hello from pipe 2!")?;
285+
/// drop(pipe_writer2);
286+
///
287+
/// child.wait()?;
288+
/// assert_eq!(io::read_to_string(&mut stdout)?, "Hello from pipe 2!");
289+
///
290+
/// # Ok(())
291+
/// # }
292+
/// ```
293+
#[unstable(feature = "command_pass_fds", issue = "144989")]
294+
fn fd(&mut self, new_fd: RawFd, old_fd: impl Into<OwnedFd>) -> &mut Self;
216295
}
217296

218297
#[stable(feature = "rust1", since = "1.0.0")]
@@ -268,6 +347,19 @@ impl CommandExt for process::Command {
268347
self.as_inner_mut().setsid(setsid);
269348
self
270349
}
350+
351+
fn fd(&mut self, new_fd: RawFd, old_fd: impl Into<OwnedFd>) -> &mut Self {
352+
let old = old_fd.into();
353+
unsafe {
354+
self.pre_exec(move || {
355+
cvt_r(|| libc::dup2(old.as_raw_fd(), new_fd))?;
356+
let flags = cvt(libc::fcntl(new_fd, libc::F_GETFD))?;
357+
cvt(libc::fcntl(new_fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC))?;
358+
cvt_r(|| libc::close(old.as_raw_fd()))?;
359+
Ok(())
360+
})
361+
}
362+
}
271363
}
272364

273365
/// Unix-specific extensions to [`process::ExitStatus`] and

library/std/src/sys/process/unix/unix/tests.rs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1+
use crate::fs;
2+
use crate::io::{self, Write};
3+
use crate::os::unix::fs::MetadataExt;
4+
use crate::os::unix::io::AsRawFd;
15
use crate::os::unix::process::{CommandExt, ExitStatusExt};
26
use crate::panic::catch_unwind;
3-
use crate::process::Command;
7+
use crate::process::{Command, Stdio};
48

59
// Many of the other aspects of this situation, including heap alloc concurrency
610
// safety etc., are tested in tests/ui/process/process-panic-after-fork.rs
711

12+
/// Use dev + ino to uniquely identify a file
13+
fn md_file_id(md: &fs::Metadata) -> (u64, u64) {
14+
(md.dev(), md.ino())
15+
}
16+
817
#[test]
918
fn exitstatus_display_tests() {
1019
// In practice this is the same on every Unix.
@@ -73,3 +82,76 @@ fn test_command_fork_no_unwind() {
7382
|| signal == libc::SIGSEGV
7483
);
7584
}
85+
86+
#[test]
87+
fn fd_test_stdin() {
88+
let (pipe_reader, mut pipe_writer) = io::pipe().unwrap();
89+
90+
let fd_num = libc::STDIN_FILENO;
91+
92+
let mut cmd = Command::new("cat");
93+
cmd.stdout(Stdio::piped()).fd(fd_num, pipe_reader);
94+
95+
let mut child = cmd.spawn().unwrap();
96+
let mut stdout = child.stdout.take().unwrap();
97+
98+
pipe_writer.write_all(b"Hello, world!").unwrap();
99+
drop(pipe_writer);
100+
101+
child.wait().unwrap();
102+
assert_eq!(io::read_to_string(&mut stdout).unwrap(), "Hello, world!");
103+
}
104+
105+
#[test]
106+
fn fd_test_swap() {
107+
let (pipe_reader1, mut pipe_writer1) = io::pipe().unwrap();
108+
let (pipe_reader2, mut pipe_writer2) = io::pipe().unwrap();
109+
110+
let num1 = pipe_reader1.as_raw_fd();
111+
let num2 = pipe_reader2.as_raw_fd();
112+
113+
let mut cmd = Command::new("cat");
114+
cmd.arg(format!("/dev/fd/{num1}"))
115+
.arg(format!("/dev/fd/{num2}"))
116+
.stdout(Stdio::piped())
117+
.fd(num2, pipe_reader1)
118+
.fd(num1, pipe_reader2);
119+
120+
let mut child = cmd.spawn().unwrap();
121+
let mut stdout = child.stdout.take().unwrap();
122+
123+
pipe_writer1.write_all(b"Hello from pipe 1!").unwrap();
124+
drop(pipe_writer1);
125+
126+
pipe_writer2.write_all(b"Hello from pipe 2!").unwrap();
127+
drop(pipe_writer2);
128+
129+
child.wait().unwrap();
130+
// the second pipe's output is clobbered; this is expected.
131+
assert_eq!(io::read_to_string(&mut stdout).unwrap(), "Hello from pipe 1!");
132+
}
133+
134+
// ensure that the fd is properly closed in the parent, but only after the child is spawned.
135+
#[test]
136+
fn fd_test_close_time() {
137+
let (_pipe_reader, pipe_writer) = io::pipe().unwrap();
138+
139+
let fd = pipe_writer.as_raw_fd();
140+
let fd_path = format!("/dev/fd/{fd}");
141+
142+
let mut cmd = Command::new("true");
143+
cmd.fd(123, pipe_writer);
144+
145+
// Get the identifier of the fd (metadata follows symlinks)
146+
let fd_id = md_file_id(&fs::metadata(&fd_path).expect("fd should be open"));
147+
148+
cmd.spawn().unwrap().wait().unwrap();
149+
150+
// After the child is spawned, our fd should be closed
151+
match fs::metadata(&fd_path) {
152+
// Ok; fd exists but points to a different file
153+
Ok(md) => assert_ne!(md_file_id(&md), fd_id),
154+
// Ok; fd does not exist
155+
Err(_) => (),
156+
}
157+
}

0 commit comments

Comments
 (0)