Skip to content

Commit

Permalink
First pass windows
Browse files Browse the repository at this point in the history
  • Loading branch information
Jake-Shadle committed Dec 20, 2023
1 parent f225800 commit d5a639d
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 8 deletions.
2 changes: 2 additions & 0 deletions nextest-runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ winapi = { version = "0.3.9", features = ["std"] }
windows-sys = { version = "0.52.0", features = [
"Win32_Foundation",
"Win32_Globalization",
"Win32_Security",
"Win32_System_Console",
"Win32_System_JobObjects",
"Win32_System_Pipes",
] }
win32job = "1.0.2"
dunce = "1.0.4"
Expand Down
12 changes: 4 additions & 8 deletions nextest-runner/src/test_command/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub(super) fn spawn(
) -> std::io::Result<Child> {
cmd.stdin(Stdio::null());

let state = match strategy {
let state: Option<os::State> = match strategy {
CaptureStrategy::None => None,
CaptureStrategy::Split => {
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
Expand All @@ -67,13 +67,9 @@ pub(super) fn spawn(

Some(Output::Split { stdout, stderr })
}
CaptureStrategy::Combined => {
// It doesn't matter which stream we take since they are both the same
// handle, so we take the easy one
Some(Output::Combined(os::state_to_stdout(
state.expect("state was set"),
)?))
}
CaptureStrategy::Combined => Some(Output::Combined(os::state_to_stdout(
state.expect("state was set"),
)?)),
};

Ok(Child { child, output })
Expand Down
140 changes: 140 additions & 0 deletions nextest-runner/src/test_command/windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use super::Stdio;
use std::{
io,
os::windows::{ffi::OsStrExt as _, io::FromRawHandle as _, prelude::OwnedHandle},
ptr::null_mut,
};
use windows_sys::Win32::{
Foundation as fnd, Security::SECURITY_ATTRIBUTES, Storage::FileSystem as fs,
System::Pipes as pipe,
};

pub struct State {
ours: OwnedHandle,
}

pub(super) fn setup_io(cmd: &mut std::process::Command) -> io::Result<State> {
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};

static RANDOM_SEQ: once_cell::sync::OnceCell<AtomicUsize> = once_cell::sync::OnceCell::new();
let rand_seq = RANDOM_SEQ.get_or_init(|| {
use rand::{rngs::OsRng, RngCore};
AtomicUsize::new(OsRng.next_u32() as _)
});

// A 64kb pipe capacity is the same as a typical Linux default.
const PIPE_BUFFER_CAPACITY: u32 = 64 * 1024;
const FLAGS: u32 =
fs::FILE_FLAG_FIRST_PIPE_INSTANCE | fs::FILE_FLAG_OVERLAPPED | fs::PIPE_ACCESS_INBOUND;

unsafe {
let ours;
let mut wide_path;
let mut tries = 0;
let mut reject_remote_clients_flag = pipe::PIPE_REJECT_REMOTE_CLIENTS;
loop {
tries += 1;
let name = format!(
r"\\.\pipe\__nextest_pipe__.{}.{}",
std::process::id(),
rand_seq.fetch_add(1, SeqCst),
);
wide_path = std::ffi::OsStr::new(&name)
.encode_wide()
.chain(Some(0))
.collect::<Vec<_>>();

let handle = pipe::CreateNamedPipeW(
wide_path.as_ptr(),
FLAGS,
pipe::PIPE_TYPE_BYTE
| pipe::PIPE_READMODE_BYTE
| pipe::PIPE_WAIT
| reject_remote_clients_flag,
1,
PIPE_BUFFER_CAPACITY,
PIPE_BUFFER_CAPACITY,
0,
null_mut(),
);

// We pass the `FILE_FLAG_FIRST_PIPE_INSTANCE` flag above, and we're
// also just doing a best effort at selecting a unique name. If
// `ERROR_ACCESS_DENIED` is returned then it could mean that we
// accidentally conflicted with an already existing pipe, so we try
// again.
//
// Don't try again too much though as this could also perhaps be a
// legit error.
// If `ERROR_INVALID_PARAMETER` is returned, this probably means we're
// running on pre-Vista version where `PIPE_REJECT_REMOTE_CLIENTS` is
// not supported, so we continue retrying without it. This implies
// reduced security on Windows versions older than Vista by allowing
// connections to this pipe from remote machines.
// Proper fix would increase the number of FFI imports and introduce
// significant amount of Windows XP specific code with no clean
// testing strategy
// For more info, see https://github.com/rust-lang/rust/pull/37677.
if handle == fnd::INVALID_HANDLE_VALUE {
let err = io::Error::last_os_error();
let raw_os_err = err.raw_os_error();
if tries < 10 {
if raw_os_err == Some(fnd::ERROR_ACCESS_DENIED as i32) {
continue;
} else if reject_remote_clients_flag != 0
&& raw_os_err == Some(fnd::ERROR_INVALID_PARAMETER as i32)
{
reject_remote_clients_flag = 0;
tries -= 1;
continue;
}
}
return Err(err);
}
ours = OwnedHandle::from_raw_handle(handle as _);
break;
}

// Note we differ from rust here because it sets the SECURITY_ATTRIBUTES
// but that method is not exposed on OpenOptionsExt so we need to do the
// call manually

// Open the write end of the file that we'll pass to the child process
let handle = fs::CreateFileW(
wide_path.as_ptr(),
fnd::GENERIC_WRITE,
0,
&SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as _,
lpSecurityDescriptor: null_mut(),
bInheritHandle: 1,
},
fs::OPEN_EXISTING,
0,
0,
);

if handle == fnd::INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}

let handle = OwnedHandle::from_raw_handle(handle as _);

// Use the handle for stdout AND stderr
cmd.stdout(handle.try_clone()?).stderr(handle.try_clone()?);

Ok(State { ours })
}
}

#[inline]
pub(super) fn stderr_to_stdout(
stderr: tokio::process::ChildStderr,
) -> std::io::Result<super::Pipe> {
super::Pipe::from_std(stderr.into_owned_handle()?.into())
}

#[inline]
pub(super) fn state_to_stdout(state: State) -> io::Result<super::Pipe> {
super::Pipe::from_std(std::process::ChildStdout::from(state.ours))
}

0 comments on commit d5a639d

Please sign in to comment.