aboutsummaryrefslogtreecommitdiff
path: root/winpty/src/windows.rs
diff options
context:
space:
mode:
Diffstat (limited to 'winpty/src/windows.rs')
-rw-r--r--winpty/src/windows.rs467
1 files changed, 467 insertions, 0 deletions
diff --git a/winpty/src/windows.rs b/winpty/src/windows.rs
new file mode 100644
index 00000000..67370709
--- /dev/null
+++ b/winpty/src/windows.rs
@@ -0,0 +1,467 @@
+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<Err<'a>> {
+ 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<Self, Err<'b>> {
+ 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<Self, Err<'b>> {
+ 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: usize, rows: usize) -> Result<(), Err> {
+ assert!(cols > 0 && rows > 0);
+ let mut err = null_mut() as *mut winpty_error_t;
+
+ unsafe {
+ winpty_set_size(self.0, cols as i32, rows as i32, &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<Handle> instead of Vec<i32>
+ pub fn console_process_list(&mut self, count: usize) -> Result<Vec<i32>, 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<Self, Err<'b>> {
+ 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 {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());
+ });
+ }
+}