diff options
Diffstat (limited to 'test_util/src/pty.rs')
-rw-r--r-- | test_util/src/pty.rs | 770 |
1 files changed, 0 insertions, 770 deletions
diff --git a/test_util/src/pty.rs b/test_util/src/pty.rs deleted file mode 100644 index 3e3331b84..000000000 --- a/test_util/src/pty.rs +++ /dev/null @@ -1,770 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. 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::strip_ansi_codes; - -/// 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 && std::env::var("CI").is_ok() { - // 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('\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(); - } - - #[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()) - }); - } - - 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) { - self.read_until_condition_with_timeout(condition, Duration::from_secs(15)); - } - - #[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]); - } - _ => { - std::thread::sleep(Duration::from_millis(15)); - } - } - } -} - -trait SystemPty: Read + Write {} - -impl SystemPty for std::fs::File {} - -#[cfg(unix)] -fn setup_pty(fd: i32) { - use nix::fcntl::fcntl; - use nix::fcntl::FcntlArg; - use nix::fcntl::OFlag; - use nix::sys::termios; - use nix::sys::termios::tcgetattr; - use nix::sys::termios::tcsetattr; - use nix::sys::termios::SetArg; - - let mut term = tcgetattr(fd).unwrap(); - // disable cooked mode - term.local_flags.remove(termios::LocalFlags::ICANON); - tcsetattr(fd, SetArg::TCSANOW, &term).unwrap(); - - // turn on non-blocking mode so we get timeouts - let flags = fcntl(fd, FcntlArg::F_GETFL).unwrap(); - let new_flags = OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK; - fcntl(fd, FcntlArg::F_SETFL(new_flags)).unwrap(); -} - -#[cfg(unix)] -fn create_pty( - program: &Path, - args: &[&str], - cwd: &Path, - env_vars: Option<HashMap<String, String>>, -) -> Box<dyn SystemPty> { - use crate::pty::unix::UnixPty; - use std::os::unix::process::CommandExt; - - // 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 { - let cmd = std::process::Command::new(program) - .current_dir(cwd) - .args(args) - .envs(env_vars.unwrap_or_default()) - .pre_exec(move || { - // 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::kill; - use nix::sys::signal::Signal; - 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 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 winapi::shared::minwindef::FALSE; - use winapi::shared::minwindef::LPVOID; - use winapi::shared::minwindef::TRUE; - use winapi::shared::winerror::S_OK; - use winapi::um::consoleapi::ClosePseudoConsole; - use winapi::um::consoleapi::CreatePseudoConsole; - use winapi::um::fileapi::FlushFileBuffers; - use winapi::um::fileapi::ReadFile; - use winapi::um::fileapi::WriteFile; - use winapi::um::handleapi::DuplicateHandle; - use winapi::um::handleapi::INVALID_HANDLE_VALUE; - use winapi::um::namedpipeapi::CreatePipe; - use winapi::um::namedpipeapi::PeekNamedPipe; - use winapi::um::processthreadsapi::CreateProcessW; - use winapi::um::processthreadsapi::DeleteProcThreadAttributeList; - use winapi::um::processthreadsapi::GetCurrentProcess; - use winapi::um::processthreadsapi::InitializeProcThreadAttributeList; - use winapi::um::processthreadsapi::UpdateProcThreadAttribute; - use winapi::um::processthreadsapi::LPPROC_THREAD_ATTRIBUTE_LIST; - use winapi::um::processthreadsapi::PROCESS_INFORMATION; - use winapi::um::synchapi::WaitForSingleObject; - use winapi::um::winbase::CREATE_UNICODE_ENVIRONMENT; - use winapi::um::winbase::EXTENDED_STARTUPINFO_PRESENT; - use winapi::um::winbase::INFINITE; - use winapi::um::winbase::STARTUPINFOEXW; - use winapi::um::wincontypes::COORD; - use winapi::um::wincontypes::HPCON; - use winapi::um::winnt::DUPLICATE_SAME_ACCESS; - use winapi::um::winnt::HANDLE; - - use super::SystemPty; - - 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 winapi to create a PTY (thus large unsafe block). - unsafe { - let mut size: COORD = std::mem::zeroed(); - size.X = 800; - size.Y = 500; - let mut console_handle = std::ptr::null_mut(); - 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 LPVOID) - .unwrap_or(ptr::null_mut()), - cwd.as_mut_ptr(), - &mut 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); - std::thread::spawn({ - let thread_handle = thread_handle.duplicate(); - let console_handle = WinHandle::new(console_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()); - } - }); - - 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: winapi 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: winapi 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 {} - - impl std::io::Write for WinPseudoConsole { - fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> { - let mut bytes_written = 0; - // SAFETY: - // winapi 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: winapi 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: winapi call - let process_handle = unsafe { GetCurrentProcess() }; - let mut duplicate_handle = ptr::null_mut(); - // SAFETY: winapi 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: winapi call - unsafe { - winapi::um::handleapi::CloseHandle(self.inner); - } - } - } - } - - struct ProcThreadAttributeList { - buffer: Vec<u8>, - } - - impl ProcThreadAttributeList { - pub fn new(console_handle: HPCON) -> Self { - // SAFETY: - // Generous use of unsafe winapi 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, - 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: winapi 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 winapi. - 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<_>>() - } -} |