-
Notifications
You must be signed in to change notification settings - Fork 14.1k
add std::os::unix::process::CommandExt::fd #145687
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Has this code been run? The implementation just duplicates the fd without interacting with self.
Please make sure that all new API, even unstable, always gets document0ation and examples, as well as tests if the functionality can't be covered in a simple example. This is tricky API, make sure to cover the FD_CLOEXEC behavior in tests and include something to the effect of https://rust-lang.zulipchat.com/#narrow/channel/327149-t-libs-api.2Fapi-changes/topic/Extra.20FDs.20in.20CommandExt/near/439547420 in docs.
The tests at https://github.com/google/command-fds/tree/main can probably serve as reference.
This comment has been minimized.
This comment has been minimized.
library/std/src/os/fd/process.rs
Outdated
| impl CommandExt for Command { | ||
| fn fd(&mut self, new_fd: RawFd, old_fd: impl Into<OwnedFd>) -> &mut Self { | ||
| let old = old_fd.into().as_raw_fd(); | ||
| unsafe { | ||
| self.as_inner_mut().pre_exec(Box::new(move || { | ||
| cvt_r(|| libc::dup2(old, new_fd))?; | ||
| let flags = cvt(libc::fcntl(new_fd, F_GETFD))?; | ||
| cvt(libc::fcntl(new_fd, F_SETFD, flags & !FD_CLOEXEC))?; | ||
| cvt_r(|| libc::close(old))?; | ||
| Ok(()) | ||
| })) | ||
| } | ||
|
|
||
| self | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using pre_exec for this can reduce the performance of starting the child process.
See my Zulip thread on this:
#t-libs-api/api-changes > Extra FDs in CommandExt @ 💬
also mentioned in this comment in the tracking issue:
#144989 (comment)
Using pre_exec switches the spawn implementation from posix_spawn to fork + execve syscalls. Forking is slower than posix_spawn, because it needs to copy the programs memory (in practice copy on write optimizations are used, but they are still somewhat costly - see my benchmarks also linked in the Zulip message). posix_spawn supports setting the passed FDs, so this feature should be used if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you happen to have a sketch of what a better interface would look like?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, but the issue I've described here is not a problem with the signature of fd. The extension should not use the public pre_exec method, the implementation needs to be modified instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this file is of interest:
rust/library/std/src/sys/process/unix/unix.rs
Lines 72 to 74 in 71289c3
| if let Some(ret) = self.posix_spawn(&theirs, envp.as_ref())? { | |
| return Ok((ret, ours)); | |
| } |
Note that there are multiple structs called Command because of how the target-specific functionality is implemented.
Command.spawn first tries to run the posix_spawn method, but its implementations return None if an attribute which the limited posix_spawn syscall cannot handle is set - see the implementations of the posix_spawn method.
Please keep this behavior in mind when adding new Command attributes and creating tests - cases when spawn uses the posix_spawn syscall and when it does not should both be tested. I'm not sure if there is a way to check which spawn variant was chosen in the tests, it is an implementation detail after all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The extension should not use the public
pre_execmethod, the implementation needs to be modified instead.
See how other methods in the CommandExt trait are implemented in the library/std/src/os/fd/process.rs file that you modified - they all call the actual implementation using .as.inner_mut()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be reasonably straightforward to add a Vec<(OwnedFd, RawFd)> to Command that could be used either with something based on posix_spawn_file_actions_adddup2 or with the exec fallback (also avoiding 1 closure per fd). But aside from being slow, is there any correctness problem with the current implementation?
As long as there isn't anything explicitly wrong, I think it would be reasonable to get the current implementation over the line first so we have some tests. Any chance you would be interested in submitting a followup changing the implementation, since you have been looking into this a lot?
I'm not sure if there is a way to check which spawn variant was chosen in the tests, it is an implementation detail after all.
Adding a last_spawn_was_posix_spawn field to Command for debugging would be fine. That will be accessible from tests once they're moved to within std/src.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But aside from being slow, is there any correctness problem with the current implementation?
No, I'm not claiming this implementation is incorrect - I also haven't reviewed it thoroughly though.
Any chance you would be interested in submitting a followup changing the implementation, since you have been looking into this a lot?
If I have the time, sure.
Please make sure to note this; a single test unfortunately doesn't cover the nuances that can show up here. https://github.com/google/command-fds/blob/38670577b393c5b6f4e17b4854128cf6120a3ca1/src/lib.rs#L206 has a few test cases, I think it would be completely reasonable to port each of these to a version that matches our API. Edit: realizing that repo is Apache-2.0 so we can't take anything directly. But tests from it that we should include are:
It would also be good to test that:
|
|
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
That's expected with the current implementation when both fds need to be alive at the same time; this is what we should check for. The goal is to make sure we don't quietly change this behavior by accident.
This one is unfortunately trickier. Linux aggressively reuses fds so it's getting mapped to something else after close, so I guess checking flags isn't enough. Instead, checking that the dev+ino are unequal should work: use std::{fs, io};
use std::os::fd::AsRawFd;
use std::os::unix::fs::MetadataExt;
#[test]
fn whatever() {
let (_pipe_reader, pipe_writer) = io::pipe().unwrap();
let fd = pipe_writer.as_raw_fd();
let fd_path = format!("/dev/fd/{fd}");
// let mut cmd = Command::new("cat") ...
// Get the identifier of the fd (metadata follows symlinks)
let fd_id = md_file_id(&fs::metadata(&fd_path).expect("fd should be open"));
// stand in for cmd.spawn().unwrap();
drop(pipe_writer);
// After the child is spawned, our fd should be closed
match fs::metadata(&fd_path) {
// Ok; fd exists but points to a different file
Ok(md) => assert_ne!(md_file_id(&md), fd_id),
// Ok; fd does not exist
Err(_) => ()
}
// ...
}
/// Use dev + ino to uniquely identify a file
fn md_file_id(md: &fs::Metadata) -> (u64, u64) {
(md.dev(), md.ino())
} |
|
Even when checking device/inode values, |
|
Ibraheem has a lot of reviews r? libs |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
@bors2 try |
This comment has been minimized.
This comment has been minimized.
add std::os::unix::process::CommandExt::fd try-job: aarch64-apple try-job: arm-android try-job: test-various
This comment has been minimized.
This comment has been minimized.
|
💔 Test for bb5247b failed: CI. Failed jobs:
|
|
It looks like the behavior of test_swap is platform-dependent. I don't have easy access to non-Linux machines for testing purposes, so I've removed the test. @rustbot ready |
| /// | ||
| /// [`posix_spawn`]: https://www.man7.org/linux/man-pages/man3/posix_spawn.3.html | ||
| #[unstable(feature = "command_pass_fds", issue = "144989")] | ||
| fn last_spawn_was_posix_spawn(&self) -> Option<bool>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem mentioned in the ACP or tracking issue, and seems like an odd place to put this. It's also not clear to me why this is on Command vs on Child?
I'm not convinced we want to make this a stable API -- posix_spawn feels like a low level detail rather than something callers should care about. If we do want this exposed we should explain what callers are supposed to do with the information.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is for testing purposes (see #145687 (comment)). Would it be better to mark it perma-unstable? (I'm not sure exactly how to do that)
I don't think it can be private because tests are treated as separate crates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be possible to make it private. Integration tests in library/std/tests are treated as separate crates, but these are in src/ so can use internal API.
|
This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
|
@rustbot ready |
One failing platform certainly isn't a reason to remove a completely valid test entirely. Mark it |
| pipe_writer.write_all(b"Hello, world!").unwrap(); | ||
| drop(pipe_writer); | ||
|
|
||
| child.wait().unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| child.wait().unwrap(); | |
| child.wait().unwrap().exit_ok().unwrap(); |
Verify the child didn't run into errors, applies a few places
ACP: rust-lang/libs-team#623
Tracking issue: #144989
try-job: aarch64-apple
try-job: arm-android
try-job: test-various