-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
cargo-credential: reset stdin & stdout to the Console #12469
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
//! Provider used for testing redirection of stdout. | ||
|
||
use cargo_credential::{Action, Credential, CredentialResponse, Error, RegistryInfo}; | ||
|
||
struct MyCredential; | ||
|
||
impl Credential for MyCredential { | ||
fn perform( | ||
&self, | ||
_registry: &RegistryInfo, | ||
_action: &Action, | ||
_args: &[&str], | ||
) -> Result<CredentialResponse, Error> { | ||
// Informational messages should be sent on stderr. | ||
eprintln!("message on stderr should be sent the the parent process"); | ||
|
||
// Reading from stdin and writing to stdout will go to the attached console (tty). | ||
println!("message from test credential provider"); | ||
Err(Error::OperationNotSupported) | ||
} | ||
} | ||
|
||
fn main() { | ||
cargo_credential::main(MyCredential); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
use std::{fs::File, io::Error}; | ||
|
||
/// Reset stdin and stdout to the attached console / tty for the duration of the closure. | ||
/// If no console is available, stdin and stdout will be redirected to null. | ||
pub fn stdin_stdout_to_console<F, T>(f: F) -> Result<T, Error> | ||
where | ||
F: FnOnce() -> T, | ||
{ | ||
let open_write = |f| std::fs::OpenOptions::new().write(true).open(f); | ||
|
||
let mut stdin = File::open(imp::IN_DEVICE).or_else(|_| File::open(imp::NULL_DEVICE))?; | ||
let mut stdout = open_write(imp::OUT_DEVICE).or_else(|_| open_write(imp::NULL_DEVICE))?; | ||
|
||
let _stdin_guard = imp::ReplacementGuard::new(Stdio::Stdin, &mut stdin)?; | ||
let _stdout_guard = imp::ReplacementGuard::new(Stdio::Stdout, &mut stdout)?; | ||
Ok(f()) | ||
} | ||
|
||
enum Stdio { | ||
Stdin, | ||
Stdout, | ||
} | ||
|
||
#[cfg(windows)] | ||
mod imp { | ||
use super::Stdio; | ||
use std::{fs::File, io::Error, os::windows::prelude::AsRawHandle}; | ||
use windows_sys::Win32::{ | ||
Foundation::{HANDLE, INVALID_HANDLE_VALUE}, | ||
System::Console::{ | ||
GetStdHandle, SetStdHandle, STD_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, | ||
}, | ||
}; | ||
pub const OUT_DEVICE: &str = "CONOUT$"; | ||
pub const IN_DEVICE: &str = "CONIN$"; | ||
pub const NULL_DEVICE: &str = "NUL"; | ||
|
||
/// Restores previous stdio when dropped. | ||
pub struct ReplacementGuard { | ||
std_handle: STD_HANDLE, | ||
previous: HANDLE, | ||
} | ||
|
||
impl ReplacementGuard { | ||
pub(super) fn new(stdio: Stdio, replacement: &mut File) -> Result<ReplacementGuard, Error> { | ||
let std_handle = match stdio { | ||
Stdio::Stdin => STD_INPUT_HANDLE, | ||
Stdio::Stdout => STD_OUTPUT_HANDLE, | ||
}; | ||
|
||
let previous; | ||
unsafe { | ||
// Make a copy of the current handle | ||
previous = GetStdHandle(std_handle); | ||
if previous == INVALID_HANDLE_VALUE { | ||
return Err(std::io::Error::last_os_error()); | ||
} | ||
|
||
// Replace stdin with the replacement handle | ||
if SetStdHandle(std_handle, replacement.as_raw_handle() as HANDLE) == 0 { | ||
return Err(std::io::Error::last_os_error()); | ||
} | ||
} | ||
|
||
Ok(ReplacementGuard { | ||
previous, | ||
std_handle, | ||
}) | ||
} | ||
} | ||
|
||
impl Drop for ReplacementGuard { | ||
fn drop(&mut self) { | ||
unsafe { | ||
// Put previous handle back in to stdin | ||
SetStdHandle(self.std_handle, self.previous); | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[cfg(unix)] | ||
mod imp { | ||
use super::Stdio; | ||
use libc::{close, dup, dup2, STDIN_FILENO, STDOUT_FILENO}; | ||
use std::{fs::File, io::Error, os::fd::AsRawFd}; | ||
pub const IN_DEVICE: &str = "/dev/tty"; | ||
pub const OUT_DEVICE: &str = "/dev/tty"; | ||
pub const NULL_DEVICE: &str = "/dev/null"; | ||
|
||
/// Restores previous stdio when dropped. | ||
pub struct ReplacementGuard { | ||
std_fileno: i32, | ||
previous: i32, | ||
} | ||
|
||
impl ReplacementGuard { | ||
pub(super) fn new(stdio: Stdio, replacement: &mut File) -> Result<ReplacementGuard, Error> { | ||
let std_fileno = match stdio { | ||
Stdio::Stdin => STDIN_FILENO, | ||
Stdio::Stdout => STDOUT_FILENO, | ||
}; | ||
|
||
let previous; | ||
unsafe { | ||
// Duplicate the existing stdin file to a new descriptor | ||
previous = dup(std_fileno); | ||
if previous == -1 { | ||
return Err(std::io::Error::last_os_error()); | ||
} | ||
// Replace stdin with the replacement file | ||
if dup2(replacement.as_raw_fd(), std_fileno) == -1 { | ||
return Err(std::io::Error::last_os_error()); | ||
} | ||
} | ||
|
||
Ok(ReplacementGuard { | ||
previous, | ||
std_fileno, | ||
}) | ||
} | ||
} | ||
|
||
impl Drop for ReplacementGuard { | ||
fn drop(&mut self) { | ||
unsafe { | ||
// Put previous file back in to stdin | ||
dup2(self.previous, self.std_fileno); | ||
// Close the file descriptor we used as a backup | ||
close(self.previous); | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use std::fs::OpenOptions; | ||
use std::io::{Seek, Write}; | ||
|
||
use super::imp::ReplacementGuard; | ||
use super::Stdio; | ||
|
||
#[test] | ||
fn stdin() { | ||
let tempdir = snapbox::path::PathFixture::mutable_temp().unwrap(); | ||
let file = tempdir.path().unwrap().join("stdin"); | ||
let mut file = OpenOptions::new() | ||
.read(true) | ||
.write(true) | ||
.create(true) | ||
.open(file) | ||
.unwrap(); | ||
|
||
writeln!(&mut file, "hello").unwrap(); | ||
file.seek(std::io::SeekFrom::Start(0)).unwrap(); | ||
{ | ||
let _guard = ReplacementGuard::new(Stdio::Stdin, &mut file).unwrap(); | ||
let line = std::io::stdin().lines().next().unwrap().unwrap(); | ||
assert_eq!(line, "hello"); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to verify that the previous fd is really put back? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not in this test, since reading from stdin after it's put back will either block forever or fail. The integration tests in |
||
} | ||
} |
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.
If both
/dev/tty
and/dev/null
don't exist, you get something like this whereby/dev/null
is a file now and some previous output will now be interpreted and fail compilation (or worse).OR, maybe I got this wrong and the issue is somewhere else, in
rustc
, for example:https://github.com/rust-lang/rust/blob/4cf5723dbe471ef0a32857b968b91498551f5e38/library/std/src/sys/pal/unix/process/process_common.rs#L479-L486
EDIT: yep, I got it wrong, SORRY!, it's not your code here that affected the link I gave initially, it's the one in rustc mentioned
belowabove(moved up)!Either way, perhaps a better error reported(even here) would be better? you know, like related to the problem. For example a patch like this, for your code here (aka
cargo test stdout_redirected
inside./credential/cargo-credential/
subdir of a git clone of this cargo repo) but also for rustc, would look like this(assuming it already got compiled1, else the patch for rustc will trigger instead thus obsoleting the need for patching this in cargo2):(when
/dev/null
is a file becausesudo
always created an empty 0 byte one if none exists)and when
/dev/null
doesn't exist:instead of like this(without the patch, that is):
see? no one knows which file's missing (it's
/dev/null
but it doesn't say)Footnotes
and thus the test binary is just being re-run, without needing a re-compilation(because a re-compilation would make it fail in rustc first) ↩
well, except that if u ship that binary(in this case it's just the test itself) then that binary will fail when no
/dev/null
or it's not a char device, with that cryptic message not telling you that it's/dev/null
that's missing. ↩