/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/util/lib/pty.rs
1 238 строк
35 KB
em
refactor: replace winapi usages with windows-sys (#34732)
02 июн 2026, 20:28
Не верифицирован
02 июн 2026, 20:28
bf0b47a
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::collections::HashMap; use std::collections::HashSet; use std::io::Read; use std::io::Write; use std::path::Path; use std::time::Duration; use std::time::Instant; use crate::IS_CI; use crate::eprintln; use crate::strip_ansi_codes; const PTY_ROWS_COLS: (u16, u16) = (500, 800); /// Result of running a command in a PTY #[derive(Debug)] pub struct PtyOutput { pub exit_code: Option<i32>, pub output: Vec<u8>, } /// Runs a command in a PTY and waits for it to complete. /// Returns the exit code and captured output. pub fn run_in_pty( program: &Path, args: &[&str], cwd: &Path, env_vars: Option<HashMap<String, String>>, timeout: Duration, ) -> PtyOutput { run_in_pty_impl(program, args, cwd, env_vars, timeout) } /// Points to know about when writing pty tests: /// /// - Consecutive writes cause issues where you might write while a prompt /// is not showing. So when you write, always `.expect(...)` on the output. /// - Similar to the last point, using `.expect(...)` can help make the test /// more deterministic. If the test is flaky, try adding more `.expect(...)`s pub struct Pty { pty: Box<dyn SystemPty>, read_bytes: Vec<u8>, last_index: usize, } impl Pty { pub fn new( program: &Path, args: &[&str], cwd: &Path, env_vars: Option<HashMap<String, String>>, ) -> Self { let pty = create_pty(program, args, cwd, env_vars); let mut pty = Self { pty, read_bytes: Vec::new(), last_index: 0, }; if args.is_empty() || args[0] == "repl" && !args.contains(&"--quiet") { // wait for the repl to start up before writing to it pty.read_until_condition_with_timeout( |pty| { pty .all_output() .contains("exit using ctrl+d, ctrl+c, or close()") }, // it sometimes takes a while to startup on the CI, so use a longer timeout Duration::from_secs(60), ); } pty } pub fn is_supported() -> bool { let is_windows = cfg!(windows); if is_windows && *IS_CI { // the pty tests don't really start up on the windows CI for some reason // so ignore them for now eprintln!("Ignoring windows CI."); false } else { true } } #[track_caller] pub fn write_raw(&mut self, line: impl AsRef<str>) { let line = if cfg!(windows) { line.as_ref().replace("\r\n", "\n").replace('\n', "\r\n") } else { line.as_ref().to_string() }; if let Err(err) = self.pty.write(line.as_bytes()) { panic!("{:#}", err) } self.pty.flush().unwrap(); } /// Pause for a human-like delay to read or react to something (human responses are ~100ms). #[track_caller] pub fn human_delay(&mut self) { std::thread::sleep(Duration::from_millis(250)); } #[track_caller] pub fn write_line(&mut self, line: impl AsRef<str>) { self.write_line_raw(&line); // expect what was written to show up in the output // due to "pty echo" for line in line.as_ref().lines() { self.expect(line); } } /// Writes a line without checking if it's in the output. #[track_caller] pub fn write_line_raw(&mut self, line: impl AsRef<str>) { self.write_raw(format!("{}\n", line.as_ref())); } #[track_caller] pub fn read_until(&mut self, end_text: impl AsRef<str>) -> String { self.read_until_with_advancing(|text| { text .find(end_text.as_ref()) .map(|index| index + end_text.as_ref().len()) }) } #[track_caller] pub fn expect(&mut self, text: impl AsRef<str>) { self.read_until(text.as_ref()); } #[track_caller] pub fn expect_any(&mut self, texts: &[&str]) { self.read_until_with_advancing(|text| { for find_text in texts { if let Some(index) = text.find(find_text) { return Some(index); } } None }); } /// Consumes and expects to find all the text until a timeout is hit. #[track_caller] pub fn expect_all(&mut self, texts: &[&str]) { let mut pending_texts: HashSet<&&str> = HashSet::from_iter(texts); let mut max_index: Option<usize> = None; self.read_until_with_advancing(|text| { for pending_text in pending_texts.clone() { if let Some(index) = text.find(pending_text) { let index = index + pending_text.len(); match &max_index { Some(current) => { if *current < index { max_index = Some(index); } } None => { max_index = Some(index); } } pending_texts.remove(pending_text); } } if pending_texts.is_empty() { max_index } else { None } }); } /// Expects the raw text to be found, which may include ANSI codes. /// Note: this expects the raw bytes in any output that has already /// occurred or may occur within the next few seconds. #[track_caller] pub fn expect_raw_in_current_output(&mut self, text: impl AsRef<str>) { self.read_until_condition(|pty| { let data = String::from_utf8_lossy(&pty.read_bytes); data.contains(text.as_ref()) }); } /// Expects the raw text to be found next. #[track_caller] pub fn expect_raw_next(&mut self, text: impl AsRef<str>) { let expected = text.as_ref(); let last_index = self.read_bytes.len(); self.read_until_condition(|pty| { if pty.read_bytes.len() >= last_index + expected.len() { let data = String::from_utf8_lossy( &pty.read_bytes[last_index..last_index + expected.len()], ); data == expected } else { false } }); } pub fn all_output(&self) -> Cow<'_, str> { String::from_utf8_lossy(&self.read_bytes) } #[track_caller] fn read_until_with_advancing( &mut self, mut condition: impl FnMut(&str) -> Option<usize>, ) -> String { let mut final_text = String::new(); self.read_until_condition(|pty| { let text = pty.next_text(); if let Some(end_index) = condition(&text) { pty.last_index += end_index; final_text = text[..end_index].to_string(); true } else { false } }); final_text } #[track_caller] fn read_until_condition(&mut self, condition: impl FnMut(&mut Self) -> bool) { let duration = if *IS_CI { Duration::from_secs(30) } else { Duration::from_secs(15) }; self.read_until_condition_with_timeout(condition, duration); } #[track_caller] fn read_until_condition_with_timeout( &mut self, condition: impl FnMut(&mut Self) -> bool, timeout_duration: Duration, ) { if self.try_read_until_condition_with_timeout(condition, timeout_duration) { return; } panic!("Timed out.") } /// Reads until the specified condition with a timeout duration returning /// `true` on success or `false` on timeout. fn try_read_until_condition_with_timeout( &mut self, mut condition: impl FnMut(&mut Self) -> bool, timeout_duration: Duration, ) -> bool { let timeout_time = Instant::now().checked_add(timeout_duration).unwrap(); while Instant::now() < timeout_time { self.fill_more_bytes(); if condition(self) { return true; } } let text = self.next_text(); eprintln!( "------ Start Full Text ------\n{:?}\n------- End Full Text -------", String::from_utf8_lossy(&self.read_bytes) ); eprintln!("Next text: {:?}", text); false } fn next_text(&self) -> String { let text = String::from_utf8_lossy(&self.read_bytes).to_string(); let text = strip_ansi_codes(&text); text[self.last_index..].to_string() } fn fill_more_bytes(&mut self) { let mut buf = [0; 256]; match self.pty.read(&mut buf) { Ok(count) if count > 0 => { self.read_bytes.extend(&buf[..count]); } _ => { // be a bit easier on the CI std::thread::sleep(Duration::from_millis(if *IS_CI { 100 } else { 20 })); } } } } trait SystemPty: Read + Write {} impl SystemPty for std::fs::File {} #[cfg(unix)] fn setup_pty(fd: i32) { use nix::fcntl::FcntlArg; use nix::fcntl::OFlag; use nix::fcntl::fcntl; use nix::sys::termios; use nix::sys::termios::SetArg; use nix::sys::termios::tcgetattr; use nix::sys::termios::tcsetattr; // SAFETY: Nix crate requires value to implement the AsFd trait let as_fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) }; let mut term = tcgetattr(as_fd).unwrap(); // disable cooked mode term.local_flags.remove(termios::LocalFlags::ICANON); tcsetattr(as_fd, SetArg::TCSANOW, &term).unwrap(); // turn on non-blocking mode so we get timeouts // SAFETY: fd is a valid open file descriptor for the pty let borrowed_fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) }; let flags = fcntl(borrowed_fd, FcntlArg::F_GETFL).unwrap(); let new_flags = OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK; fcntl(borrowed_fd, FcntlArg::F_SETFL(new_flags)).unwrap(); } #[cfg(unix)] fn set_winsize( fd: std::os::fd::RawFd, rows: u16, cols: u16, ) -> std::io::Result<()> { let ws = libc::winsize { ws_row: rows, ws_col: cols, ws_xpixel: 0, ws_ypixel: 0, }; // SAFETY: set windows size if unsafe { libc::ioctl(fd, libc::TIOCSWINSZ, &ws) == -1 } { return Err(std::io::Error::last_os_error()); } Ok(()) } #[cfg(unix)] fn run_in_pty_impl( program: &Path, args: &[&str], cwd: &Path, env_vars: Option<HashMap<String, String>>, timeout: Duration, ) -> PtyOutput { use std::os::unix::process::CommandExt; // RAII guard for file descriptors to prevent leaks on panic struct FdGuard(i32); impl Drop for FdGuard { fn drop(&mut self) { // SAFETY: Closing an owned file descriptor unsafe { libc::close(self.0) }; } } // SAFETY: Posix APIs let (fdm_guard, fds_guard) = unsafe { let fdm = libc::posix_openpt(libc::O_RDWR); if fdm < 0 { panic!("posix_openpt failed"); } let fdm_guard = FdGuard(fdm); let res = libc::grantpt(fdm); if res != 0 { panic!("grantpt failed"); } let res = libc::unlockpt(fdm); if res != 0 { panic!("unlockpt failed"); } let fds = libc::open(libc::ptsname(fdm), libc::O_RDWR); if fds < 0 { panic!("open(ptsname) failed"); } let fds_guard = FdGuard(fds); (fdm_guard, fds_guard) }; let fdm = fdm_guard.0; let fds = fds_guard.0; // SAFETY: Posix APIs let mut child = unsafe { std::process::Command::new(program) .current_dir(cwd) .args(args) .envs(env_vars.unwrap_or_default()) .pre_exec(move || { // Create a new session if libc::setsid() == -1 { return Err(std::io::Error::last_os_error()); } // Set the controlling terminal if libc::ioctl(fds, libc::TIOCSCTTY as libc::c_ulong, 0) == -1 { return Err(std::io::Error::last_os_error()); } set_winsize(fds, PTY_ROWS_COLS.0, PTY_ROWS_COLS.1)?; // Close parent's main handle libc::close(fdm); // Redirect stdin/stdout/stderr to the PTY if libc::dup2(fds, 0) == -1 { return Err(std::io::Error::last_os_error()); } if libc::dup2(fds, 1) == -1 { return Err(std::io::Error::last_os_error()); } if libc::dup2(fds, 2) == -1 { return Err(std::io::Error::last_os_error()); } libc::close(fds); Ok(()) }) .spawn() .unwrap() }; // Child spawned successfully, close fds in parent (fdm will be used for reading) drop(fds_guard); // Forget fdm_guard since we'll transfer ownership to File below std::mem::forget(fdm_guard); // Set non-blocking mode for reading use nix::fcntl::FcntlArg; use nix::fcntl::OFlag; use nix::fcntl::fcntl; // SAFETY: fdm is a valid open file descriptor for the pty master let borrowed_fdm = unsafe { std::os::fd::BorrowedFd::borrow_raw(fdm) }; let flags = fcntl(borrowed_fdm, FcntlArg::F_GETFL).unwrap(); let new_flags = OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK; fcntl(borrowed_fdm, FcntlArg::F_SETFL(new_flags)).unwrap(); let start_time = Instant::now(); let mut output = Vec::new(); let mut buf = [0u8; 1024]; use std::os::fd::FromRawFd; // SAFETY: fdm is a valid file descriptor for the master side of the PTY, // obtained from posix_openpt. We take ownership of it here. let mut file = unsafe { std::fs::File::from_raw_fd(fdm) }; loop { // Check if process has exited match child.try_wait() { Ok(Some(status)) => { // Process exited, read any remaining output loop { match file.read(&mut buf) { Ok(0) => break, Ok(n) => output.extend_from_slice(&buf[..n]), Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break, Err(_) => break, } } return PtyOutput { exit_code: status.code(), output, }; } Ok(None) => { // Process still running } Err(_) => { return PtyOutput { exit_code: None, output, }; } } // Check timeout if start_time.elapsed() > timeout { let _ = child.kill(); let _ = child.wait(); return PtyOutput { exit_code: None, output, }; } // Try to read output match file.read(&mut buf) { Ok(n) if n > 0 => { output.extend_from_slice(&buf[..n]); } _ => { std::thread::sleep(Duration::from_millis(10)); } } } } #[cfg(unix)] fn create_pty( program: &Path, args: &[&str], cwd: &Path, env_vars: Option<HashMap<String, String>>, ) -> Box<dyn SystemPty> { use std::os::unix::process::CommandExt; use crate::pty::unix::UnixPty; // Manually open pty main/secondary sides in the test process. Since we're not actually // changing uid/gid here, this is the easiest way to do it. // SAFETY: Posix APIs let (fdm, fds) = unsafe { let fdm = libc::posix_openpt(libc::O_RDWR); if fdm < 0 { panic!("posix_openpt failed"); } let res = libc::grantpt(fdm); if res != 0 { panic!("grantpt failed"); } let res = libc::unlockpt(fdm); if res != 0 { panic!("unlockpt failed"); } let fds = libc::open(libc::ptsname(fdm), libc::O_RDWR); if fdm < 0 { panic!("open(ptsname) failed"); } (fdm, fds) }; // SAFETY: Posix APIs unsafe { #[allow(clippy::zombie_processes, reason = "necessary for pty")] let cmd = std::process::Command::new(program) .current_dir(cwd) .args(args) .envs(env_vars.unwrap_or_default()) .pre_exec(move || { set_winsize(fds, PTY_ROWS_COLS.0, PTY_ROWS_COLS.1)?; // Close parent's main handle libc::close(fdm); libc::dup2(fds, 0); libc::dup2(fds, 1); libc::dup2(fds, 2); // Note that we could close `fds` here as well, but this is a short-lived process and // we're just not going to worry about "leaking" it Ok(()) }) .spawn() .unwrap(); // Close child's secondary handle libc::close(fds); setup_pty(fdm); use std::os::fd::FromRawFd; let pid = nix::unistd::Pid::from_raw(cmd.id() as _); let file = std::fs::File::from_raw_fd(fdm); Box::new(UnixPty { pid, file }) } } #[cfg(unix)] mod unix { use std::io::Read; use std::io::Write; use super::SystemPty; pub struct UnixPty { pub pid: nix::unistd::Pid, pub file: std::fs::File, } impl Drop for UnixPty { fn drop(&mut self) { use nix::sys::signal::Signal; use nix::sys::signal::kill; kill(self.pid, Signal::SIGTERM).unwrap() } } impl SystemPty for UnixPty {} impl Read for UnixPty { fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { self.file.read(buf) } } impl Write for UnixPty { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { self.file.write(buf) } fn flush(&mut self) -> std::io::Result<()> { self.file.flush() } } } #[cfg(target_os = "windows")] fn run_in_pty_impl( program: &Path, args: &[&str], cwd: &Path, env_vars: Option<HashMap<String, String>>, timeout: Duration, ) -> PtyOutput { use std::io::Read; let mut pty = windows::WinPseudoConsoleWithWait::new(program, args, cwd, env_vars); let start_time = Instant::now(); let mut output = Vec::new(); let mut buf = [0u8; 1024]; let mut consecutive_empty_reads = 0; loop { // Check timeout if start_time.elapsed() > timeout { return PtyOutput { exit_code: None, output, }; } // Check if process has exited if let Some(exit_code) = pty.try_get_exit_code() { // Read any remaining output loop { match pty.read(&mut buf) { Ok(n) if n > 0 => output.extend_from_slice(&buf[..n]), _ => break, } } return PtyOutput { exit_code: Some(exit_code), output, }; } // Try to read output match pty.read(&mut buf) { Ok(n) if n > 0 => { output.extend_from_slice(&buf[..n]); consecutive_empty_reads = 0; } _ => { consecutive_empty_reads += 1; if consecutive_empty_reads > 100 { // Likely process has exited if let Some(exit_code) = pty.try_get_exit_code() { return PtyOutput { exit_code: Some(exit_code), output, }; } } std::thread::sleep(Duration::from_millis(10)); } } } } #[cfg(target_os = "windows")] fn create_pty( program: &Path, args: &[&str], cwd: &Path, env_vars: Option<HashMap<String, String>>, ) -> Box<dyn SystemPty> { let pty = windows::WinPseudoConsole::new(program, args, cwd, env_vars); Box::new(pty) } #[cfg(target_os = "windows")] mod windows { use std::collections::HashMap; use std::io::ErrorKind; use std::io::Read; use std::path::Path; use std::ptr; use std::time::Duration; use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::DUPLICATE_SAME_ACCESS; use windows_sys::Win32::Foundation::DuplicateHandle; use windows_sys::Win32::Foundation::FALSE; use windows_sys::Win32::Foundation::HANDLE; use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; use windows_sys::Win32::Foundation::S_OK; use windows_sys::Win32::Foundation::TRUE; use windows_sys::Win32::Storage::FileSystem::FlushFileBuffers; use windows_sys::Win32::Storage::FileSystem::ReadFile; use windows_sys::Win32::Storage::FileSystem::WriteFile; use windows_sys::Win32::System::Console::COORD; use windows_sys::Win32::System::Console::ClosePseudoConsole; use windows_sys::Win32::System::Console::CreatePseudoConsole; use windows_sys::Win32::System::Console::HPCON; use windows_sys::Win32::System::Pipes::CreatePipe; use windows_sys::Win32::System::Pipes::PeekNamedPipe; use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT; use windows_sys::Win32::System::Threading::CreateProcessW; use windows_sys::Win32::System::Threading::DeleteProcThreadAttributeList; use windows_sys::Win32::System::Threading::EXTENDED_STARTUPINFO_PRESENT; use windows_sys::Win32::System::Threading::GetCurrentProcess; use windows_sys::Win32::System::Threading::INFINITE; use windows_sys::Win32::System::Threading::InitializeProcThreadAttributeList; use windows_sys::Win32::System::Threading::LPPROC_THREAD_ATTRIBUTE_LIST; use windows_sys::Win32::System::Threading::PROCESS_INFORMATION; use windows_sys::Win32::System::Threading::STARTUPINFOEXW; use windows_sys::Win32::System::Threading::UpdateProcThreadAttribute; use windows_sys::Win32::System::Threading::WaitForSingleObject; use super::PTY_ROWS_COLS; use super::SystemPty; use crate::print::spawn_thread; macro_rules! assert_win_success { ($expression:expr) => { let success = $expression; if success != TRUE { panic!("{}", std::io::Error::last_os_error().to_string()) } }; } macro_rules! handle_err { ($expression:expr) => { let success = $expression; if success != TRUE { return Err(std::io::Error::last_os_error()); } }; } pub struct WinPseudoConsole { stdin_write_handle: WinHandle, stdout_read_handle: WinHandle, // keep these alive for the duration of the pseudo console _process_handle: WinHandle, _thread_handle: WinHandle, _attribute_list: ProcThreadAttributeList, } impl WinPseudoConsole { pub fn new( program: &Path, args: &[&str], cwd: &Path, maybe_env_vars: Option<HashMap<String, String>>, ) -> Self { // https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session // SAFETY: // Generous use of Win32 to create a PTY (thus large unsafe block). unsafe { let mut size: COORD = std::mem::zeroed(); size.Y = PTY_ROWS_COLS.0 as i16; size.X = PTY_ROWS_COLS.1 as i16; let mut console_handle: HPCON = 0; let (stdin_read_handle, stdin_write_handle) = create_pipe(); let (stdout_read_handle, stdout_write_handle) = create_pipe(); let result = CreatePseudoConsole( size, stdin_read_handle.as_raw_handle(), stdout_write_handle.as_raw_handle(), 0, &mut console_handle, ); assert_eq!(result, S_OK); let mut environment_vars = maybe_env_vars.map(get_env_vars); let mut attribute_list = ProcThreadAttributeList::new(console_handle); let mut startup_info: STARTUPINFOEXW = std::mem::zeroed(); startup_info.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32; startup_info.lpAttributeList = attribute_list.as_mut_ptr(); let mut proc_info: PROCESS_INFORMATION = std::mem::zeroed(); let command = format!( "\"{}\" {}", program.to_string_lossy(), args .iter() .map(|a| format!("\"{}\"", a)) .collect::<Vec<_>>() .join(" ") ) .trim() .to_string(); let mut application_str = to_windows_str(&program.to_string_lossy()); let mut command_str = to_windows_str(&command); let cwd = cwd.to_string_lossy().replace('/', "\\"); let mut cwd = to_windows_str(&cwd); assert_win_success!(CreateProcessW( application_str.as_mut_ptr(), command_str.as_mut_ptr(), ptr::null_mut(), ptr::null_mut(), FALSE, EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, environment_vars .as_mut() .map(|v| v.as_mut_ptr() as *mut core::ffi::c_void) .unwrap_or(ptr::null_mut()), cwd.as_mut_ptr(), &startup_info.StartupInfo, &mut proc_info, )); // close the handles that the pseudoconsole now has drop(stdin_read_handle); drop(stdout_write_handle); // start a thread that will close the pseudoconsole on process exit let thread_handle = WinHandle::new(proc_info.hThread); spawn_thread({ let thread_handle = thread_handle.duplicate(); let console_handle = WinHandle::new(console_handle as HANDLE); move || { WaitForSingleObject(thread_handle.as_raw_handle(), INFINITE); // wait for the reading thread to catch up std::thread::sleep(Duration::from_millis(200)); // close the console handle which will close the // stdout pipe for the reader ClosePseudoConsole(console_handle.into_raw_handle() as HPCON); } }); Self { stdin_write_handle, stdout_read_handle, _process_handle: WinHandle::new(proc_info.hProcess), _thread_handle: thread_handle, _attribute_list: attribute_list, } } } } impl Read for WinPseudoConsole { fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { // don't do a blocking read in order to support timing out let mut bytes_available = 0; // SAFETY: Win32 call handle_err!(unsafe { PeekNamedPipe( self.stdout_read_handle.as_raw_handle(), ptr::null_mut(), 0, ptr::null_mut(), &mut bytes_available, ptr::null_mut(), ) }); if bytes_available == 0 { return Err(std::io::Error::new(ErrorKind::WouldBlock, "Would block.")); } let mut bytes_read = 0; // SAFETY: Win32 call handle_err!(unsafe { ReadFile( self.stdout_read_handle.as_raw_handle(), buf.as_mut_ptr() as _, buf.len() as u32, &mut bytes_read, ptr::null_mut(), ) }); Ok(bytes_read as usize) } } impl SystemPty for WinPseudoConsole {} /// A variant of WinPseudoConsole that allows waiting for the process to exit /// and getting its exit code. pub struct WinPseudoConsoleWithWait { _stdin_write_handle: WinHandle, stdout_read_handle: WinHandle, process_handle: WinHandle, _thread_handle: WinHandle, _attribute_list: ProcThreadAttributeList, console_handle: WinHandle, } impl WinPseudoConsoleWithWait { pub fn new( program: &Path, args: &[&str], cwd: &Path, maybe_env_vars: Option<HashMap<String, String>>, ) -> Self { // SAFETY: Generous use of Win32 to create a PTY unsafe { let mut size: COORD = std::mem::zeroed(); size.Y = super::PTY_ROWS_COLS.0 as i16; size.X = super::PTY_ROWS_COLS.1 as i16; let mut console_handle: HPCON = 0; let (stdin_read_handle, stdin_write_handle) = create_pipe(); let (stdout_read_handle, stdout_write_handle) = create_pipe(); let result = CreatePseudoConsole( size, stdin_read_handle.as_raw_handle(), stdout_write_handle.as_raw_handle(), 0, &mut console_handle, ); assert_eq!(result, S_OK); let mut environment_vars = maybe_env_vars.map(get_env_vars); let mut attribute_list = ProcThreadAttributeList::new(console_handle); let mut startup_info: STARTUPINFOEXW = std::mem::zeroed(); startup_info.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32; startup_info.lpAttributeList = attribute_list.as_mut_ptr(); let mut proc_info: PROCESS_INFORMATION = std::mem::zeroed(); let command = format!( "\"{}\" {}", program.to_string_lossy(), args .iter() .map(|a| format!("\"{}\"", a)) .collect::<Vec<_>>() .join(" ") ) .trim() .to_string(); let mut application_str = to_windows_str(&program.to_string_lossy()); let mut command_str = to_windows_str(&command); let cwd_str = cwd.to_string_lossy().replace('/', "\\"); let mut cwd_vec = to_windows_str(&cwd_str); assert_win_success!(CreateProcessW( application_str.as_mut_ptr(), command_str.as_mut_ptr(), ptr::null_mut(), ptr::null_mut(), FALSE, EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, environment_vars .as_mut() .map(|v| v.as_mut_ptr() as *mut core::ffi::c_void) .unwrap_or(ptr::null_mut()), cwd_vec.as_mut_ptr(), &startup_info.StartupInfo, &mut proc_info, )); // close the handles that the pseudoconsole now has drop(stdin_read_handle); drop(stdout_write_handle); Self { _stdin_write_handle: stdin_write_handle, stdout_read_handle, process_handle: WinHandle::new(proc_info.hProcess), _thread_handle: WinHandle::new(proc_info.hThread), _attribute_list: attribute_list, console_handle: WinHandle::new(console_handle as HANDLE), } } } /// Try to get the exit code if the process has finished. /// Returns None if the process is still running. pub fn try_get_exit_code(&self) -> Option<i32> { use windows_sys::Win32::Foundation::STILL_ACTIVE; use windows_sys::Win32::Foundation::WAIT_OBJECT_0; use windows_sys::Win32::System::Threading::GetExitCodeProcess; // SAFETY: Win32 call unsafe { let result = WaitForSingleObject(self.process_handle.as_raw_handle(), 0); if result == WAIT_OBJECT_0 { let mut exit_code: u32 = 0; if GetExitCodeProcess( self.process_handle.as_raw_handle(), &mut exit_code, ) != 0 && exit_code != STILL_ACTIVE as u32 { return Some(exit_code as i32); } } None } } } impl Drop for WinPseudoConsoleWithWait { fn drop(&mut self) { // SAFETY: Win32 call unsafe { ClosePseudoConsole(self.console_handle.as_raw_handle() as HPCON); } } } impl Read for WinPseudoConsoleWithWait { fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { let mut bytes_available = 0; // SAFETY: Win32 call handle_err!(unsafe { PeekNamedPipe( self.stdout_read_handle.as_raw_handle(), ptr::null_mut(), 0, ptr::null_mut(), &mut bytes_available, ptr::null_mut(), ) }); if bytes_available == 0 { return Err(std::io::Error::new(ErrorKind::WouldBlock, "Would block.")); } let mut bytes_read = 0; // SAFETY: Win32 call handle_err!(unsafe { ReadFile( self.stdout_read_handle.as_raw_handle(), buf.as_mut_ptr() as _, buf.len() as u32, &mut bytes_read, ptr::null_mut(), ) }); Ok(bytes_read as usize) } } impl std::io::Write for WinPseudoConsole { fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> { let mut bytes_written = 0; // SAFETY: // Win32 call handle_err!(unsafe { WriteFile( self.stdin_write_handle.as_raw_handle(), buffer.as_ptr() as *const _, buffer.len() as u32, &mut bytes_written, ptr::null_mut(), ) }); Ok(bytes_written as usize) } fn flush(&mut self) -> std::io::Result<()> { // SAFETY: Win32 call handle_err!(unsafe { FlushFileBuffers(self.stdin_write_handle.as_raw_handle()) }); Ok(()) } } struct WinHandle { inner: HANDLE, } impl WinHandle { pub fn new(handle: HANDLE) -> Self { WinHandle { inner: handle } } pub fn duplicate(&self) -> WinHandle { // SAFETY: Win32 call let process_handle = unsafe { GetCurrentProcess() }; let mut duplicate_handle = ptr::null_mut(); // SAFETY: Win32 call assert_win_success!(unsafe { DuplicateHandle( process_handle, self.inner, process_handle, &mut duplicate_handle, 0, 0, DUPLICATE_SAME_ACCESS, ) }); WinHandle::new(duplicate_handle) } pub fn as_raw_handle(&self) -> HANDLE { self.inner } pub fn into_raw_handle(self) -> HANDLE { let handle = self.inner; // skip the drop implementation in order to not close the handle std::mem::forget(self); handle } } // SAFETY: These handles are ok to send across threads. unsafe impl Send for WinHandle {} // SAFETY: These handles are ok to send across threads. unsafe impl Sync for WinHandle {} impl Drop for WinHandle { fn drop(&mut self) { if !self.inner.is_null() && self.inner != INVALID_HANDLE_VALUE { // SAFETY: Win32 call unsafe { CloseHandle(self.inner); } } } } struct ProcThreadAttributeList { buffer: Vec<u8>, } impl ProcThreadAttributeList { pub fn new(console_handle: HPCON) -> Self { // SAFETY: // Generous use of unsafe Win32 calls to create a ProcThreadAttributeList. unsafe { // discover size required for the list let mut size = 0; let attribute_count = 1; assert_eq!( InitializeProcThreadAttributeList( ptr::null_mut(), attribute_count, 0, &mut size ), FALSE ); let mut buffer = vec![0u8; size]; let attribute_list_ptr = buffer.as_mut_ptr() as _; assert_win_success!(InitializeProcThreadAttributeList( attribute_list_ptr, attribute_count, 0, &mut size, )); const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE: usize = 0x00020016; assert_win_success!(UpdateProcThreadAttribute( attribute_list_ptr, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, console_handle as *const core::ffi::c_void, std::mem::size_of::<HPCON>(), ptr::null_mut(), ptr::null_mut(), )); ProcThreadAttributeList { buffer } } } pub fn as_mut_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST { self.buffer.as_mut_slice().as_mut_ptr() as *mut _ } } impl Drop for ProcThreadAttributeList { fn drop(&mut self) { // SAFETY: Win32 call unsafe { DeleteProcThreadAttributeList(self.as_mut_ptr()) }; } } fn create_pipe() -> (WinHandle, WinHandle) { let mut read_handle = std::ptr::null_mut(); let mut write_handle = std::ptr::null_mut(); // SAFETY: Creating an anonymous pipe with Win32. assert_win_success!(unsafe { CreatePipe(&mut read_handle, &mut write_handle, ptr::null_mut(), 0) }); (WinHandle::new(read_handle), WinHandle::new(write_handle)) } fn to_windows_str(str: &str) -> Vec<u16> { use std::os::windows::prelude::OsStrExt; std::ffi::OsStr::new(str) .encode_wide() .chain(Some(0)) .collect() } fn get_env_vars(env_vars: HashMap<String, String>) -> Vec<u16> { // See lpEnvironment: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw let mut parts = env_vars .into_iter() // each environment variable is in the form `name=value\0` .map(|(key, value)| format!("{key}={value}\0")) .collect::<Vec<_>>(); // all strings in an environment block must be case insensitively // sorted alphabetically by name // https://docs.microsoft.com/en-us/windows/win32/procthread/changing-environment-variables parts.sort_by_key(|part| part.to_lowercase()); // the entire block is terminated by NULL (\0) format!("{}\0", parts.join("")) .encode_utf16() .collect::<Vec<_>>() } }