use std::error::Error; use std::fmt::{self, Display, Formatter}; use std::path::PathBuf; use std::result::Result; use std::os::windows::io::RawHandle; use std::ptr::{null, null_mut}; use winpty_sys::*; use widestring::WideCString; pub enum ErrorCodes { Success, OutOfMemory, SpawnCreateProcessFailed, LostConnection, AgentExeMissing, Unspecified, AgentDied, AgentTimeout, AgentCreationFailed, } pub enum MouseMode { None, Auto, Force, } bitflags!( pub struct SpawnFlags: u64 { const AUTO_SHUTDOWN = 0x1; const EXIT_AFTER_SHUTDOWN = 0x2; } ); bitflags!( pub struct ConfigFlags: u64 { const CONERR = 0x1; const PLAIN_OUTPUT = 0x2; const COLOR_ESCAPES = 0x4; } ); #[derive(Debug)] pub struct Err<'a> { ptr: &'a mut winpty_error_t, code: u32, message: String, } // Check to see whether winpty gave us an error fn check_err<'a>(e: *mut winpty_error_t) -> Option> { let err = unsafe { let raw = winpty_error_msg(e); Err { ptr: &mut *e, code: winpty_error_code(e), message: String::from_utf16_lossy(std::slice::from_raw_parts(raw, wcslen(raw))), } }; if err.code == 0 { None } else { Some(err) } } impl<'a> Drop for Err<'a> { fn drop(&mut self) { unsafe { winpty_error_free(self.ptr); } } } impl<'a> Display for Err<'a> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { write!(f, "Code: {}, Message: {}", self.code, self.message) } } impl<'a> Error for Err<'a> { fn description(&self) -> &str { &self.message } } #[derive(Debug)] /// Winpty agent config pub struct Config<'a>(&'a mut winpty_config_t); impl<'a, 'b> Config<'a> { pub fn new(flags: ConfigFlags) -> Result> { let mut err = null_mut() as *mut winpty_error_t; let config = unsafe { winpty_config_new(flags.bits(), &mut err) }; if let Some(err) = check_err(err) { Result::Err(err) } else { unsafe { Ok(Config(&mut *config)) } } } /// Set the initial size of the console window pub fn set_initial_size(&mut self, cols: i32, rows: i32) { unsafe { winpty_config_set_initial_size(self.0, cols, rows); } } /// Set the mouse mode pub fn set_mouse_mode(&mut self, mode: &MouseMode) { let m = match mode { MouseMode::None => 0, MouseMode::Auto => 1, MouseMode::Force => 2, }; unsafe { winpty_config_set_mouse_mode(self.0, m); } } /// Amount of time to wait for the agent to startup and to wait for any given /// agent RPC request. Must be greater than 0. Can be INFINITE. // Might be a better way to represent this while still retaining infinite capability? // Enum? pub fn set_agent_timeout(&mut self, timeout: u32) { unsafe { winpty_config_set_agent_timeout(self.0, timeout); } } } impl<'a> Drop for Config<'a> { fn drop(&mut self) { unsafe { winpty_config_free(self.0); } } } #[derive(Debug)] /// A struct representing the winpty agent process pub struct Winpty<'a>(&'a mut winpty_t); impl<'a, 'b> Winpty<'a> { /// Starts the agent. This process will connect to the agent /// over a control pipe, and the agent will open data pipes /// (e.g. CONIN and CONOUT). pub fn open(cfg: &Config) -> Result> { let mut err = null_mut() as *mut winpty_error_t; unsafe { let winpty = winpty_open(cfg.0, &mut err); let err = check_err(err); if let Some(err) = err { Result::Err(err) } else { Ok(Winpty(&mut *winpty)) } } } /// Returns the handle to the winpty agent process pub fn raw_handle(&mut self) -> RawHandle { unsafe { winpty_agent_process(self.0) } } /// Returns the name of the input pipe. /// Pipe is half-duplex. pub fn conin_name(&mut self) -> PathBuf { unsafe { let raw = winpty_conin_name(self.0); PathBuf::from(&String::from_utf16_lossy(std::slice::from_raw_parts( raw, wcslen(raw), ))) } } /// Returns the name of the output pipe. /// Pipe is half-duplex. pub fn conout_name(&mut self) -> PathBuf { unsafe { let raw = winpty_conout_name(self.0); PathBuf::from(&String::from_utf16_lossy(std::slice::from_raw_parts( raw, wcslen(raw), ))) } } /// Returns the name of the error pipe. /// The name will only be valid if ConfigFlags::CONERR was specified. /// Pipe is half-duplex. pub fn conerr_name(&mut self) -> PathBuf { unsafe { let raw = winpty_conerr_name(self.0); PathBuf::from(&String::from_utf16_lossy(std::slice::from_raw_parts( raw, wcslen(raw), ))) } } /// Change the size of the Windows console window. /// /// cols & rows MUST be greater than 0 pub fn set_size(&mut self, cols: u16, rows: u16) -> Result<(), Err> { assert!(cols > 0 && rows > 0); let mut err = null_mut() as *mut winpty_error_t; unsafe { winpty_set_size(self.0, i32::from(cols), i32::from(rows), &mut err); } if let Some(err) = check_err(err) { Result::Err(err) } else { Ok(()) } } /// Get the list of processses running in the winpty agent. Returns <= count processes /// /// `count` must be greater than 0. Larger values cause a larger allocation. // TODO: This should return Vec instead of Vec pub fn console_process_list(&mut self, count: usize) -> Result, Err> { assert!(count > 0); let mut err = null_mut() as *mut winpty_error_t; let mut process_list = Vec::with_capacity(count); unsafe { let len = winpty_get_console_process_list(self.0, process_list.as_mut_ptr(), count as i32, &mut err) as usize; process_list.set_len(len); } if let Some(err) = check_err(err) { Result::Err(err) } else { Ok(process_list) } } /// Spawns the new process. /// /// spawn can only be called once per Winpty object. If it is called /// before the output data pipe(s) is/are connected, then collected output is /// buffered until the pipes are connected, rather than being discarded. /// (https://blogs.msdn.microsoft.com/oldnewthing/20110107-00/?p=11803) // TODO: Support getting the process and thread handle of the spawned process (Not the agent) // TODO: Support returning the error from CreateProcess pub fn spawn( &mut self, cfg: &SpawnConfig, ) -> Result<(), Err> { let mut err = null_mut() as *mut winpty_error_t; unsafe { let ok = winpty_spawn( self.0, cfg.0 as *const winpty_spawn_config_s, null_mut(), // Process handle null_mut(), // Thread handle null_mut(), // Create process error &mut err, ); if ok == 0 { return Ok(());} } if let Some(err) = check_err(err) { Result::Err(err) } else { Ok(()) } } } // winpty_t is thread-safe unsafe impl<'a> Sync for Winpty<'a> {} unsafe impl<'a> Send for Winpty<'a> {} impl<'a> Drop for Winpty<'a> { fn drop(&mut self) { unsafe { winpty_free(self.0); } } } #[derive(Debug)] /// Information about a process for winpty to spawn pub struct SpawnConfig<'a>(&'a mut winpty_spawn_config_t); impl<'a, 'b> SpawnConfig<'a> { /// Creates a new spawnconfig pub fn new( spawnflags: SpawnFlags, appname: Option<&str>, cmdline: Option<&str>, cwd: Option<&str>, end: Option<&str>, ) -> Result> { let mut err = null_mut() as *mut winpty_error_t; let (appname, cmdline, cwd, end) = ( appname.map_or(null(), |s| WideCString::from_str(s).unwrap().into_raw()), cmdline.map_or(null(), |s| WideCString::from_str(s).unwrap().into_raw()), cwd.map_or(null(), |s| WideCString::from_str(s).unwrap().into_raw()), end.map_or(null(), |s| WideCString::from_str(s).unwrap().into_raw()), ); let spawn_config = unsafe { winpty_spawn_config_new(spawnflags.bits(), appname, cmdline, cwd, end, &mut err) }; // Required to free the strings unsafe { if !appname.is_null() { WideCString::from_raw(appname as *mut u16); } if !cmdline.is_null() { WideCString::from_raw(cmdline as *mut u16); } if !cwd.is_null() { WideCString::from_raw(cwd as *mut u16); } if !end.is_null() { WideCString::from_raw(end as *mut u16); } } if let Some(err) = check_err(err) { Result::Err(err) } else { unsafe { Ok(SpawnConfig(&mut *spawn_config)) } } } } impl<'a> Drop for SpawnConfig<'a> { fn drop(&mut self) { unsafe { winpty_spawn_config_free(self.0); } } } #[cfg(test)] mod tests { extern crate named_pipe; extern crate winapi; use self::named_pipe::PipeClient; use self::winapi::um::processthreadsapi::OpenProcess; use self::winapi::um::winnt::READ_CONTROL; use crate::{Config, ConfigFlags, SpawnConfig, SpawnFlags, Winpty}; #[test] // Test that we can start a process in winpty fn spawn_process() { let mut winpty = Winpty::open( &Config::new(ConfigFlags::empty()).expect("failed to create config") ).expect("failed to create winpty instance"); winpty.spawn( &SpawnConfig::new( SpawnFlags::empty(), None, Some("cmd"), None, None ).expect("failed to create spawn config") ).unwrap(); } #[test] // Test that pipes connected before winpty is spawned can be connected to fn valid_pipe_connect_before() { let mut winpty = Winpty::open( &Config::new(ConfigFlags::empty()).expect("failed to create config") ).expect("failed to create winpty instance"); // Check we can connect to both pipes PipeClient::connect_ms(winpty.conout_name(), 1000).expect("failed to connect to conout pipe"); PipeClient::connect_ms(winpty.conin_name(), 1000).expect("failed to connect to conin pipe"); winpty.spawn( &SpawnConfig::new( SpawnFlags::empty(), None, Some("cmd"), None, None ).expect("failed to create spawn config") ).unwrap(); } #[test] // Test that pipes connected after winpty is spawned can be connected to fn valid_pipe_connect_after() { let mut winpty = Winpty::open( &Config::new(ConfigFlags::empty()).expect("failed to create config") ).expect("failed to create winpty instance"); winpty.spawn( &SpawnConfig::new( SpawnFlags::empty(), None, Some("cmd"), None, None ).expect("failed to create spawn config") ).unwrap(); // Check we can connect to both pipes PipeClient::connect_ms(winpty.conout_name(), 1000).expect("failed to connect to conout pipe"); PipeClient::connect_ms(winpty.conin_name(), 1000).expect("failed to connect to conin pipe"); } #[test] fn resize() { let mut winpty = Winpty::open( &Config::new(ConfigFlags::empty()).expect("failed to create config") ).expect("failed to create winpty instance"); winpty.spawn( &SpawnConfig::new( SpawnFlags::empty(), None, Some("cmd"), None, None ).expect("failed to create spawn config") ).unwrap(); winpty.set_size(1, 1).unwrap(); } #[test] #[ignore] // Test that each id returned by cosole_process_list points to an actual process fn console_process_list_valid() { let mut winpty = Winpty::open( &Config::new(ConfigFlags::empty()).expect("failed to create config") ).expect("failed to create winpty instance"); winpty.spawn( &SpawnConfig::new( SpawnFlags::empty(), None, Some("cmd"), None, None ).expect("failed to create spawn config") ).unwrap(); let processes = winpty.console_process_list(1000).expect("failed to get console process list"); // Check that each id is valid processes.iter().for_each(|id| { let handle = unsafe { OpenProcess( READ_CONTROL, // permissions false as i32, // inheret *id as u32 ) }; assert!(!handle.is_null()); }); } }