#![allow(non_snake_case)]
pub mod console;
pub mod error;
pub mod io;
pub mod util;
use error::Error;
use std::collections::HashMap;
use std::ffi::c_void;
use std::fmt;
use std::time::Duration;
use std::{mem::size_of, ptr::null_mut};
use windows::core::{self as win, IntoParam, Param, HRESULT};
use windows::Win32::Foundation::{CloseHandle, HANDLE, PWSTR, WAIT_TIMEOUT};
use windows::Win32::Storage::FileSystem::{
CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ,
FILE_SHARE_WRITE, OPEN_EXISTING,
};
use windows::Win32::System::Console::{
ClosePseudoConsole, CreatePseudoConsole, GetConsoleMode, GetConsoleScreenBufferInfo,
ResizePseudoConsole, SetConsoleMode, CONSOLE_MODE, CONSOLE_SCREEN_BUFFER_INFO, COORD,
ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, HPCON,
};
use windows::Win32::System::Pipes::CreatePipe;
use windows::Win32::System::Threading::{
CreateProcessW, DeleteProcThreadAttributeList, GetExitCodeProcess, GetProcessId,
InitializeProcThreadAttributeList, TerminateProcess, UpdateProcThreadAttribute,
WaitForSingleObject, CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT,
LPPROC_THREAD_ATTRIBUTE_LIST, PROCESS_INFORMATION, STARTUPINFOEXW,
};
use windows::Win32::System::WindowsProgramming::INFINITE;
pub fn spawn(cmd: impl Into<String>) -> Result<Process, Error> {
Process::spawn(ProcAttr::cmd(cmd.into()))
}
pub struct Process {
pty_input: HANDLE,
pty_output: HANDLE,
_proc: PROCESS_INFORMATION,
_proc_info: STARTUPINFOEXW,
_console: HPCON,
}
impl Process {
fn spawn(attr: ProcAttr) -> Result<Self, Error> {
enableVirtualTerminalSequenceProcessing()?;
let (mut console, pty_reader, pty_writer) = createPseudoConsole()?;
let startup_info = initializeStartupInfoAttachedToConPTY(&mut console)?;
let proc = execProc(startup_info, attr)?;
Ok(Self {
pty_input: pty_writer,
pty_output: pty_reader,
_console: console,
_proc: proc,
_proc_info: startup_info,
})
}
pub fn resize(&self, x: i16, y: i16) -> Result<(), Error> {
unsafe { ResizePseudoConsole(self._console, COORD { X: x, Y: y }) }?;
Ok(())
}
pub fn pid(&self) -> u32 {
unsafe { GetProcessId(self._proc.hProcess) }
}
pub fn exit(&self, code: u32) -> Result<(), Error> {
unsafe { TerminateProcess(self._proc.hProcess, code).ok() }?;
Ok(())
}
pub fn wait(&self, timeout_millis: Option<u32>) -> Result<u32, Error> {
unsafe {
match timeout_millis {
Some(timeout) => {
if WaitForSingleObject(self._proc.hProcess, timeout) == WAIT_TIMEOUT {
return Err(Error::Timeout(Duration::from_millis(timeout as u64)));
}
}
None => {
WaitForSingleObject(self._proc.hProcess, INFINITE);
}
}
let mut code = 0;
GetExitCodeProcess(self._proc.hProcess, &mut code).ok()?;
Ok(code)
}
}
pub fn is_alive(&self) -> bool {
unsafe { WaitForSingleObject(self._proc.hProcess, 0) == WAIT_TIMEOUT }
}
pub fn set_echo(&self, on: bool) -> Result<(), Error> {
let stdout_h = stdout_handle()?;
unsafe {
let mut mode = CONSOLE_MODE::default();
GetConsoleMode(stdout_h, &mut mode).ok()?;
match on {
true => mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT,
false => mode &= !ENABLE_ECHO_INPUT,
};
SetConsoleMode(stdout_h, mode).ok()?;
CloseHandle(stdout_h).ok()?;
}
Ok(())
}
pub fn input(&self) -> Result<io::PipeWriter, Error> {
let handle = util::clone_handle(self.pty_input)?;
Ok(io::PipeWriter::new(handle))
}
pub fn output(&self) -> Result<io::PipeReader, Error> {
let handle = util::clone_handle(self.pty_output)?;
Ok(io::PipeReader::new(handle))
}
}
impl Drop for Process {
fn drop(&mut self) {
unsafe {
ClosePseudoConsole(self._console);
let _ = CloseHandle(self._proc.hProcess);
let _ = CloseHandle(self._proc.hThread);
DeleteProcThreadAttributeList(self._proc_info.lpAttributeList);
let _ = Box::from_raw(self._proc_info.lpAttributeList as _);
let _ = CloseHandle(self.pty_input);
let _ = CloseHandle(self.pty_output);
}
}
}
impl fmt::Debug for Process {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PipeReader")
.field("pty_output", &(self.pty_output.0))
.field("pty_output(ptr)", &(self.pty_output.0 as *const c_void))
.field("pty_input", &(self.pty_input.0))
.field("pty_input(ptr)", &(self.pty_input.0 as *const c_void))
.finish_non_exhaustive()
}
}
#[derive(Default, Debug)]
pub struct ProcAttr {
application: Option<String>,
commandline: Option<String>,
current_dir: Option<String>,
args: Vec<String>,
env: Option<HashMap<String, String>>,
}
impl ProcAttr {
pub fn batch(file: impl AsRef<str>) -> Self {
let inter = std::env::var("COMSPEC").unwrap_or_else(|_| "cmd".to_string());
let args = format!("/C {:?}", file.as_ref());
Self::default().application(inter).commandline(args)
}
pub fn cmd(commandline: impl AsRef<str>) -> Self {
let args = format!("cmd /C {}", commandline.as_ref());
Self::default().commandline(args)
}
pub fn commandline(mut self, cmd: impl Into<String>) -> Self {
self.commandline = Some(cmd.into());
self
}
pub fn application(mut self, application: impl Into<String>) -> Self {
self.application = Some(application.into());
self
}
pub fn current_dir(mut self, dir: impl Into<String>) -> Self {
self.current_dir = Some(dir.into());
self
}
pub fn args(mut self, args: Vec<String>) -> Self {
self.args = args;
self
}
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
pub fn envs(mut self, env: HashMap<String, String>) -> Self {
self.env = Some(env);
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
match &mut self.env {
Some(env) => {
env.insert(key.into(), value.into());
self
}
None => self.envs(HashMap::new()).env(key.into(), value.into()),
}
}
pub fn spawn(self) -> Result<Process, Error> {
Process::spawn(self)
}
}
fn enableVirtualTerminalSequenceProcessing() -> win::Result<()> {
let stdout_h = stdout_handle()?;
unsafe {
let mut mode = CONSOLE_MODE::default();
GetConsoleMode(stdout_h, &mut mode).ok()?;
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; SetConsoleMode(stdout_h, mode).ok()?;
CloseHandle(stdout_h);
}
Ok(())
}
fn createPseudoConsole() -> win::Result<(HPCON, HANDLE, HANDLE)> {
let (pty_in, con_writer) = pipe()?;
let (con_reader, pty_out) = pipe()?;
let size = inhirentConsoleSize()?;
let console = unsafe { CreatePseudoConsole(size, pty_in, pty_out, 0)? };
unsafe {
CloseHandle(pty_in);
}
unsafe {
CloseHandle(pty_out);
}
Ok((console, con_reader, con_writer))
}
fn inhirentConsoleSize() -> win::Result<COORD> {
let stdout_h = stdout_handle()?;
let mut info = CONSOLE_SCREEN_BUFFER_INFO::default();
unsafe {
GetConsoleScreenBufferInfo(stdout_h, &mut info).ok()?;
CloseHandle(stdout_h);
};
let mut size = COORD { X: 24, Y: 80 };
size.X = info.srWindow.Right - info.srWindow.Left + 1;
size.Y = info.srWindow.Bottom - info.srWindow.Top + 1;
Ok(size)
}
const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE: usize = 0x00020016;
fn initializeStartupInfoAttachedToConPTY(hPC: &mut HPCON) -> win::Result<STARTUPINFOEXW> {
let mut siEx = STARTUPINFOEXW::default();
siEx.StartupInfo.cb = size_of::<STARTUPINFOEXW>() as u32;
let mut size: usize = 0;
let res = unsafe { InitializeProcThreadAttributeList(null_mut() as _, 1, 0, &mut size) };
if res.as_bool() || size == 0 {
return Err(win::Error::new(
HRESULT::default(),
"failed initialize proc attribute list".into(),
));
}
let lpAttributeList = vec![0u8; size].into_boxed_slice();
let lpAttributeList = Box::leak(lpAttributeList);
siEx.lpAttributeList = lpAttributeList.as_mut_ptr().cast() as LPPROC_THREAD_ATTRIBUTE_LIST;
unsafe {
InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &mut size).ok()?;
UpdateProcThreadAttribute(
siEx.lpAttributeList,
0,
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
*hPC as _,
size_of::<HPCON>(),
null_mut(),
null_mut(),
)
.ok()?;
}
Ok(siEx)
}
fn execProc(mut startup_info: STARTUPINFOEXW, attr: ProcAttr) -> win::Result<PROCESS_INFORMATION> {
if attr.commandline.is_none() && attr.application.is_none() {
panic!("")
}
let commandline = pwstr_param(attr.commandline);
let application = pwstr_param(attr.application);
let current_dir = pwstr_param(attr.current_dir);
let env = match attr.env {
Some(env) => Box::<[u16]>::into_raw(environment_block_unicode(env).into_boxed_slice()) as _,
None => null_mut(),
};
let mut proc_info = PROCESS_INFORMATION::default();
let result = unsafe {
CreateProcessW(
application.abi(),
commandline.abi(),
null_mut(),
null_mut(),
false,
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, env,
current_dir.abi(),
&mut startup_info.StartupInfo,
&mut proc_info,
)
.ok()
};
if !env.is_null() {
unsafe {
::std::boxed::Box::from_raw(env);
}
}
result?;
Ok(proc_info)
}
fn pipe() -> win::Result<(HANDLE, HANDLE)> {
let mut p_in = HANDLE::default();
let mut p_out = HANDLE::default();
unsafe { CreatePipe(&mut p_in, &mut p_out, std::ptr::null_mut(), 0).ok()? };
Ok((p_in, p_out))
}
fn stdout_handle() -> win::Result<HANDLE> {
let hConsole = unsafe {
CreateFileW(
"CONOUT$",
FILE_GENERIC_READ | FILE_GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null_mut(),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
HANDLE::default(),
)
.ok()?
};
Ok(hConsole)
}
fn environment_block_unicode(env: HashMap<String, String>) -> Vec<u16> {
if env.is_empty() {
return vec![0, 0];
}
let mut b = Vec::new();
for (key, value) in env {
let part = format!("{}={}\0", key, value);
b.extend(part.encode_utf16());
}
b.push(0);
b
}
fn pwstr_param(s: Option<String>) -> Param<'static, PWSTR> {
match s {
Some(s) => {
s.into_param()
}
None => {
Param::None
}
}
}
#[cfg(test)]
mod tests {
use std::iter::FromIterator;
use super::*;
#[test]
fn env_block_test() {
assert_eq!(
environment_block_unicode(HashMap::from_iter([("asd".to_string(), "qwe".to_string())])),
str_to_utf16("asd=qwe\0\0")
);
assert!(matches!(environment_block_unicode(HashMap::from_iter([
("asd".to_string(), "qwe".to_string()),
("zxc".to_string(), "123".to_string())
])), s if s == str_to_utf16("asd=qwe\0zxc=123\0\0") || s == str_to_utf16("zxc=123\0asd=qwe\0\0")));
assert_eq!(
environment_block_unicode(HashMap::from_iter([])),
str_to_utf16("\0\0")
);
}
fn str_to_utf16(s: impl AsRef<str>) -> Vec<u16> {
s.as_ref().encode_utf16().collect()
}
}