Skip to content

Commit 48c14e4

Browse files
committed
add std::os::unix::process::CommandExt::fd
1 parent e8a792d commit 48c14e4

2 files changed

Lines changed: 176 additions & 1 deletion

File tree

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: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1+
use crate::fs;
2+
use crate::io::{self, Write};
3+
use crate::os::raw;
4+
use crate::os::unix::fs::MetadataExt;
5+
use crate::os::unix::io::AsRawFd;
16
use crate::os::unix::process::{CommandExt, ExitStatusExt};
27
use crate::panic::catch_unwind;
3-
use crate::process::Command;
8+
use crate::process::{Command, Stdio};
49

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

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

0 commit comments

Comments
 (0)