blob: 27780d4f0d805687b94d0eb1409b6c0842fdb219 [file] [log] [blame]
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::os::windows::io::RawHandle;
use std::path::PathBuf;
use std::ptr::{null, null_mut};
use std::result::Result;
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 {
code: u32,
message: String,
}
// Check to see whether winpty gave us an error, and perform the necessary memory freeing
fn check_err(e: *mut winpty_error_t) -> Option<Err> {
unsafe {
let code = winpty_error_code(e);
let raw = winpty_error_msg(e);
let message = String::from_utf16_lossy(std::slice::from_raw_parts(raw, wcslen(raw)));
winpty_error_free(e);
match code {
0 => None,
_ => Some(Err { code, message }),
}
}
}
impl Display for Err {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
write!(f, "Code: {}, Message: {}", self.code, self.message)
}
}
impl Error for Err {
fn description(&self) -> &str {
&self.message
}
}
#[derive(Debug)]
/// Winpty agent config
pub struct Config(*mut winpty_config_t);
impl Config {
pub fn new(flags: ConfigFlags) -> Result<Self, Err> {
let mut err = null_mut() as *mut winpty_error_t;
let config = unsafe { winpty_config_new(flags.bits(), &mut err) };
check_err(err).map_or(Ok(Self(config)), Result::Err)
}
/// 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 Drop for Config {
fn drop(&mut self) {
unsafe {
winpty_config_free(self.0);
}
}
}
#[derive(Debug)]
/// A struct representing the winpty agent process
pub struct Winpty(*mut winpty_t);
impl Winpty {
/// 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> {
let mut err = null_mut() as *mut winpty_error_t;
let winpty = unsafe { winpty_open(cfg.0, &mut err) };
check_err(err).map_or(Ok(Self(winpty)), Result::Err)
}
/// 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);
}
check_err(err).map_or(Ok(()), Result::Err)
}
/// Get the list of processes 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);
}
check_err(err).map_or(Ok(process_list), Result::Err)
}
/// 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(());
}
}
check_err(err).map_or(Ok(()), Result::Err)
}
}
// winpty_t is thread-safe
unsafe impl Sync for Winpty {}
unsafe impl Send for Winpty {}
impl Drop for Winpty {
fn drop(&mut self) {
unsafe {
winpty_free(self.0);
}
}
}
#[derive(Debug)]
/// Information about a process for winpty to spawn
pub struct SpawnConfig(*mut winpty_spawn_config_t);
impl SpawnConfig {
/// Creates a new spawnconfig
pub fn new(
spawnflags: SpawnFlags,
appname: Option<&str>,
cmdline: Option<&str>,
cwd: Option<&str>,
end: Option<&str>,
) -> Result<Self, Err> {
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);
}
}
check_err(err).map_or(Ok(Self(spawn_config)), Result::Err)
}
}
impl Drop for SpawnConfig {
fn drop(&mut self) {
unsafe {
winpty_spawn_config_free(self.0);
}
}
}
#[cfg(test)]
mod tests {
use named_pipe::PipeClient;
use winapi::um::processthreadsapi::OpenProcess;
use 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());
});
}
}