use std::ffi::{OsStr, OsString};
use std::fmt::{self, Display, Formatter};
use std::iter::once;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::os::windows::io::RawHandle;
use std::path::{Path, PathBuf};
use std::ptr::{null, null_mut};
use std::result::Result;
use std::slice;
use bitflags::bitflags;
use winpty_sys::*;
#[derive(Copy, Clone, Debug)]
pub enum ErrorCode {
OutOfMemory,
SpawnCreateProcessFailed,
LostConnection,
AgentExeMissing,
Unspecified,
AgentDied,
AgentTimeout,
AgentCreationFailed,
UnknownError(u32),
}
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 Error {
code: ErrorCode,
message: String,
}
fn check_err(e: *mut winpty_error_t) -> Result<(), Error> {
unsafe {
let code = winpty_error_code(e);
let raw = winpty_error_msg(e);
let message = String::from_utf16_lossy(slice::from_raw_parts(raw, wcslen(raw) as usize));
winpty_error_free(e);
let code = match code {
WINPTY_ERROR_SUCCESS => return Ok(()),
WINPTY_ERROR_OUT_OF_MEMORY => ErrorCode::OutOfMemory,
WINPTY_ERROR_SPAWN_CREATE_PROCESS_FAILED => ErrorCode::SpawnCreateProcessFailed,
WINPTY_ERROR_LOST_CONNECTION => ErrorCode::LostConnection,
WINPTY_ERROR_AGENT_EXE_MISSING => ErrorCode::AgentExeMissing,
WINPTY_ERROR_UNSPECIFIED => ErrorCode::Unspecified,
WINPTY_ERROR_AGENT_DIED => ErrorCode::AgentDied,
WINPTY_ERROR_AGENT_TIMEOUT => ErrorCode::AgentTimeout,
WINPTY_ERROR_AGENT_CREATION_FAILED => ErrorCode::AgentCreationFailed,
code => ErrorCode::UnknownError(code),
};
Err(Error { code, message })
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
write!(f, "Code: {:?}, Message: {}", self.code, self.message)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
#[derive(Debug)]
pub struct Config(*mut winpty_config_t);
impl Config {
pub fn new(flags: ConfigFlags) -> Result<Self, Error> {
let mut err = null_mut() as *mut winpty_error_t;
let config = unsafe { winpty_config_new(flags.bits(), &mut err) };
check_err(err)?;
Ok(Self(config))
}
pub fn set_initial_size(&mut self, cols: i32, rows: i32) {
unsafe {
winpty_config_set_initial_size(self.0, cols, rows);
}
}
pub fn set_mouse_mode(&mut self, mode: &MouseMode) {
let m = match mode {
MouseMode::None => WINPTY_MOUSE_MODE_NONE,
MouseMode::Auto => WINPTY_MOUSE_MODE_AUTO,
MouseMode::Force => WINPTY_MOUSE_MODE_FORCE,
};
unsafe {
winpty_config_set_mouse_mode(self.0, m as i32);
}
}
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)]
pub struct Winpty(*mut winpty_t);
pub struct ChildHandles {
pub process: HANDLE,
pub thread: HANDLE,
}
impl Winpty {
pub fn open(cfg: &Config) -> Result<Self, Error> {
let mut err = null_mut() as *mut winpty_error_t;
let winpty = unsafe { winpty_open(cfg.0, &mut err) };
check_err(err)?;
Ok(Self(winpty))
}
pub fn raw_handle(&mut self) -> RawHandle {
unsafe { winpty_agent_process(self.0) }
}
pub fn conin_name(&mut self) -> PathBuf {
unsafe {
let raw = winpty_conin_name(self.0);
OsString::from_wide(slice::from_raw_parts(raw, wcslen(raw) as usize)).into()
}
}
pub fn conout_name(&mut self) -> PathBuf {
unsafe {
let raw = winpty_conout_name(self.0);
OsString::from_wide(slice::from_raw_parts(raw, wcslen(raw) as usize)).into()
}
}
pub fn conerr_name(&mut self) -> PathBuf {
unsafe {
let raw = winpty_conerr_name(self.0);
OsString::from_wide(slice::from_raw_parts(raw, wcslen(raw) as usize)).into()
}
}
pub fn set_size(&mut self, cols: u16, rows: u16) -> Result<(), Error> {
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)
}
pub fn console_process_list(&mut self, count: usize) -> Result<Vec<i32>, Error> {
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)?;
Ok(process_list)
}
pub fn spawn(&mut self, cfg: &SpawnConfig) -> Result<ChildHandles, Error> {
let mut handles =
ChildHandles { process: std::ptr::null_mut(), thread: std::ptr::null_mut() };
let mut create_process_error: DWORD = 0;
let mut err = null_mut() as *mut winpty_error_t;
unsafe {
winpty_spawn(
self.0,
cfg.0 as *const winpty_spawn_config_s,
&mut handles.process as *mut _,
&mut handles.thread as *mut _,
&mut create_process_error as *mut _,
&mut err,
);
}
let mut result = check_err(err);
if let Err(Error { code: ErrorCode::SpawnCreateProcessFailed, message }) = &mut result {
*message = format!("{} (error code {})", message, create_process_error);
}
result.map(|_| handles)
}
}
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)]
pub struct SpawnConfig(*mut winpty_spawn_config_t);
impl SpawnConfig {
pub fn new(
spawnflags: SpawnFlags,
appname: Option<&str>,
cmdline: Option<&str>,
cwd: Option<&Path>,
env: Option<&str>,
) -> Result<Self, Error> {
let mut err = null_mut() as *mut winpty_error_t;
fn to_wstring<S: AsRef<OsStr> + ?Sized>(s: &S) -> Vec<u16> {
OsStr::new(s).encode_wide().chain(once(0)).collect()
}
let appname = appname.map(to_wstring);
let cmdline = cmdline.map(to_wstring);
let cwd = cwd.map(to_wstring);
let env = env.map(to_wstring);
let wstring_ptr = |opt: &Option<Vec<u16>>| opt.as_ref().map_or(null(), |ws| ws.as_ptr());
let spawn_config = unsafe {
winpty_spawn_config_new(
spawnflags.bits(),
wstring_ptr(&appname),
wstring_ptr(&cmdline),
wstring_ptr(&cwd),
wstring_ptr(&env),
&mut err,
)
};
check_err(err)?;
Ok(Self(spawn_config))
}
}
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]
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]
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");
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]
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();
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]
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");
processes.iter().for_each(|id| {
let handle = unsafe {
OpenProcess(
READ_CONTROL, false as i32, *id as u32,
)
};
assert!(!handle.is_null());
});
}
}