use crate::error::{Error, Result};
use crate::handle::OwnedHandle;
use crate::string::{to_wide, WideString};
use std::borrow::Cow;
use std::time::Duration;
use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_OBJECT_0, WAIT_TIMEOUT};
use windows::Win32::System::Threading::{
CreateProcessW, GetExitCodeProcess, OpenProcess, TerminateProcess, WaitForSingleObject,
CREATE_NEW_CONSOLE, CREATE_NO_WINDOW, CREATE_UNICODE_ENVIRONMENT, PROCESS_CREATION_FLAGS,
PROCESS_INFORMATION, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, STARTUPINFOW,
};
pub struct Process {
handle: OwnedHandle,
pid: u32,
}
impl Process {
pub fn open(pid: u32, access: ProcessAccess) -> Result<Self> {
let handle = unsafe { OpenProcess(access.0, false, pid)? };
Ok(Self {
handle: OwnedHandle::new(handle)?,
pid,
})
}
pub fn pid(&self) -> u32 {
self.pid
}
pub fn handle(&self) -> HANDLE {
self.handle.as_raw()
}
pub fn wait(&self) -> Result<u32> {
self.wait_timeout(None)
}
pub fn wait_timeout(&self, timeout: Option<Duration>) -> Result<u32> {
let timeout_ms = timeout
.map(|d| d.as_millis() as u32)
.unwrap_or(windows::Win32::System::Threading::INFINITE);
let result = unsafe { WaitForSingleObject(self.handle.as_raw(), timeout_ms) };
match result {
WAIT_OBJECT_0 => self.exit_code(),
WAIT_TIMEOUT => Err(Error::custom("Wait timed out")),
_ => Err(Error::custom("Wait failed")),
}
}
pub fn try_wait(&self) -> Result<Option<u32>> {
let result = unsafe { WaitForSingleObject(self.handle.as_raw(), 0) };
match result {
WAIT_OBJECT_0 => Ok(Some(self.exit_code()?)),
WAIT_TIMEOUT => Ok(None),
_ => Err(Error::custom("Wait failed")),
}
}
pub fn exit_code(&self) -> Result<u32> {
let mut exit_code = 0u32;
unsafe {
GetExitCodeProcess(self.handle.as_raw(), &mut exit_code)?;
}
Ok(exit_code)
}
pub fn terminate(&self, exit_code: u32) -> Result<()> {
unsafe {
TerminateProcess(self.handle.as_raw(), exit_code)?;
}
Ok(())
}
pub fn is_running(&self) -> Result<bool> {
Ok(self.try_wait()?.is_none())
}
}
#[derive(Clone, Copy, Debug)]
pub struct ProcessAccess(pub windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS);
impl ProcessAccess {
pub const ALL: Self = Self(windows::Win32::System::Threading::PROCESS_ALL_ACCESS);
pub const QUERY: Self = Self(PROCESS_QUERY_INFORMATION);
pub const TERMINATE: Self = Self(PROCESS_TERMINATE);
pub const QUERY_AND_TERMINATE: Self =
Self(windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS(
PROCESS_QUERY_INFORMATION.0 | PROCESS_TERMINATE.0,
));
}
pub struct Command {
program: String,
args: Vec<String>,
current_dir: Option<String>,
creation_flags: PROCESS_CREATION_FLAGS,
env: Option<Vec<(String, String)>>,
}
impl Command {
pub fn new(program: impl Into<String>) -> Self {
Self {
program: program.into(),
args: Vec::new(),
current_dir: None,
creation_flags: PROCESS_CREATION_FLAGS(0),
env: None,
}
}
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.args.extend(args.into_iter().map(Into::into));
self
}
pub fn current_dir(mut self, dir: impl Into<String>) -> Self {
self.current_dir = Some(dir.into());
self
}
pub fn new_console(mut self) -> Self {
self.creation_flags.0 |= CREATE_NEW_CONSOLE.0;
self
}
pub fn no_window(mut self) -> Self {
self.creation_flags.0 |= CREATE_NO_WINDOW.0;
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env
.get_or_insert_with(Vec::new)
.push((key.into(), value.into()));
self
}
pub fn spawn(self) -> Result<Process> {
let command_line = self.build_command_line();
let mut command_line_wide = to_wide(&command_line);
let current_dir_wide = self.current_dir.as_ref().map(|d| WideString::new(d));
let env_block = self.build_env_block();
let startup_info = STARTUPINFOW {
cb: std::mem::size_of::<STARTUPINFOW>() as u32,
..Default::default()
};
let mut process_info = PROCESS_INFORMATION::default();
let creation_flags = if env_block.is_some() {
PROCESS_CREATION_FLAGS(self.creation_flags.0 | CREATE_UNICODE_ENVIRONMENT.0)
} else {
self.creation_flags
};
unsafe {
match ¤t_dir_wide {
Some(dir) => CreateProcessW(
None,
windows::core::PWSTR(command_line_wide.as_mut_ptr()),
None,
None,
false,
creation_flags,
env_block.as_ref().map(|e| e.as_ptr() as *const _),
dir.as_pcwstr(),
&startup_info,
&mut process_info,
)?,
None => CreateProcessW(
None,
windows::core::PWSTR(command_line_wide.as_mut_ptr()),
None,
None,
false,
creation_flags,
env_block.as_ref().map(|e| e.as_ptr() as *const _),
None,
&startup_info,
&mut process_info,
)?,
};
}
if !process_info.hThread.is_invalid() {
unsafe {
let _ = CloseHandle(process_info.hThread);
}
}
Ok(Process {
handle: OwnedHandle::new(process_info.hProcess)?,
pid: process_info.dwProcessId,
})
}
pub fn run(self) -> Result<u32> {
let process = self.spawn()?;
process.wait()
}
fn build_command_line(&self) -> String {
let total_len =
self.program.len() + 3 + self.args.iter().map(|a| a.len() * 2 + 3).sum::<usize>();
let mut cmd = String::with_capacity(total_len);
cmd.push_str("e_arg(&self.program));
for arg in &self.args {
cmd.push(' ');
cmd.push_str("e_arg(arg));
}
cmd
}
fn build_env_block(&self) -> Option<Vec<u16>> {
let env = self.env.as_ref()?;
let mut block = Vec::new();
for (key, value) in env {
let entry = format!("{}={}", key, value);
block.extend(entry.encode_utf16());
block.push(0);
}
block.push(0);
Some(block)
}
}
#[inline]
fn quote_arg(arg: &str) -> Cow<'_, str> {
let needs_quoting = arg.is_empty() || arg.bytes().any(|b| b == b' ' || b == b'\t' || b == b'"');
if needs_quoting {
let mut quoted = String::with_capacity(arg.len() + 2);
quoted.push('"');
let mut chars = arg.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
let mut backslash_count = 1;
while chars.peek() == Some(&'\\') {
chars.next();
backslash_count += 1;
}
if chars.peek() == Some(&'"') || chars.peek().is_none() {
for _ in 0..backslash_count * 2 {
quoted.push('\\');
}
} else {
for _ in 0..backslash_count {
quoted.push('\\');
}
}
} else if c == '"' {
quoted.push('\\');
quoted.push('"');
} else {
quoted.push(c);
}
}
quoted.push('"');
Cow::Owned(quoted)
} else {
Cow::Borrowed(arg)
}
}
#[inline]
pub fn current_pid() -> u32 {
unsafe { windows::Win32::System::Threading::GetCurrentProcessId() }
}
#[inline]
pub fn current_process() -> HANDLE {
unsafe { windows::Win32::System::Threading::GetCurrentProcess() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quote_arg() {
assert_eq!(quote_arg("simple"), "simple");
assert_eq!(quote_arg("with space"), "\"with space\"");
assert_eq!(quote_arg(""), "\"\"");
}
#[test]
fn test_current_pid() {
let pid = current_pid();
assert!(pid > 0);
}
#[test]
fn test_spawn_cmd_echo() {
let process = Command::new("cmd.exe")
.arg("/c")
.arg("echo hello")
.no_window()
.spawn();
assert!(process.is_ok(), "Failed to spawn cmd.exe");
let process = process.unwrap();
assert!(process.pid() > 0);
let exit_code = process.wait();
assert!(exit_code.is_ok());
assert_eq!(exit_code.unwrap(), 0);
}
#[test]
fn test_spawn_cmd_exit_code() {
let exit_code = Command::new("cmd.exe")
.arg("/c")
.arg("exit 42")
.no_window()
.run();
assert!(exit_code.is_ok());
assert_eq!(exit_code.unwrap(), 42);
}
#[test]
fn test_spawn_nonexistent_program() {
let result = Command::new("this_program_does_not_exist_12345.exe").spawn();
assert!(result.is_err());
}
#[test]
fn test_spawn_with_args() {
let exit_code = Command::new("cmd.exe")
.arg("/c")
.arg("echo")
.arg("hello world")
.no_window()
.run();
assert!(exit_code.is_ok());
assert_eq!(exit_code.unwrap(), 0);
}
#[test]
fn test_spawn_with_working_directory() {
let temp_dir = std::env::temp_dir();
let temp_str = temp_dir.to_string_lossy().into_owned();
let exit_code = Command::new("cmd.exe")
.arg("/c")
.arg("cd")
.current_dir(temp_str)
.no_window()
.run();
assert!(exit_code.is_ok());
assert_eq!(exit_code.unwrap(), 0);
}
#[test]
fn test_spawn_with_env() {
let exit_code = Command::new("cmd.exe")
.arg("/c")
.arg("echo %TEST_VAR%")
.env("TEST_VAR", "hello_test")
.no_window()
.run();
assert!(exit_code.is_ok());
assert_eq!(exit_code.unwrap(), 0);
}
#[test]
fn test_try_wait_running_process() {
let process = Command::new("cmd.exe")
.arg("/c")
.arg("timeout /t 1 /nobreak > nul")
.no_window()
.spawn();
assert!(process.is_ok());
let process = process.unwrap();
let result = process.try_wait();
assert!(result.is_ok());
}
#[test]
fn test_wait_timeout() {
let process = Command::new("cmd.exe")
.arg("/c")
.arg("timeout /t 10 /nobreak > nul")
.no_window()
.spawn();
assert!(process.is_ok());
let process = process.unwrap();
let result = process.wait_timeout(Some(Duration::from_millis(100)));
assert!(result.is_err());
let _ = process.terminate(1);
}
#[test]
fn test_is_running() {
let process = Command::new("cmd.exe")
.arg("/c")
.arg("exit 0")
.no_window()
.spawn();
assert!(process.is_ok());
let process = process.unwrap();
let _ = process.wait();
let is_running = process.is_running();
assert!(is_running.is_ok());
assert!(!is_running.unwrap());
}
#[test]
fn test_terminate_process() {
let process = Command::new("cmd.exe")
.arg("/c")
.arg("timeout /t 60 /nobreak > nul")
.no_window()
.spawn();
assert!(process.is_ok());
let process = process.unwrap();
let result = process.terminate(99);
assert!(result.is_ok());
let exit_code = process.wait();
assert!(exit_code.is_ok());
}
#[test]
fn test_open_existing_process() {
let pid = current_pid();
let result = Process::open(pid, ProcessAccess::QUERY);
assert!(result.is_ok());
let process = result.unwrap();
assert_eq!(process.pid(), pid);
}
#[test]
fn test_open_nonexistent_process() {
let result = Process::open(99999999, ProcessAccess::QUERY);
assert!(result.is_err());
}
#[test]
fn test_quote_arg_edge_cases() {
assert_eq!(quote_arg(""), "\"\"");
assert_eq!(quote_arg("a\tb"), "\"a\tb\"");
assert_eq!(quote_arg("a\"b"), "\"a\\\"b\"");
assert_eq!(quote_arg("a\\\"b"), "\"a\\\\\\\"b\"");
assert_eq!(quote_arg("path\\"), "path\\");
assert_eq!(quote_arg("path with space\\"), "\"path with space\\\\\"");
assert_eq!(quote_arg("simple-path.txt"), "simple-path.txt");
}
#[test]
fn test_command_line_building() {
let cmd = Command::new("program.exe")
.arg("arg1")
.arg("arg with space")
.arg("arg\"quote");
let cmd_line = cmd.build_command_line();
assert!(cmd_line.contains("program.exe"));
assert!(cmd_line.contains("arg1"));
assert!(cmd_line.contains("\"arg with space\""));
assert!(cmd_line.contains("\\\""));
}
#[test]
fn test_spawn_unicode_args() {
let exit_code = Command::new("cmd.exe")
.arg("/c")
.arg("echo")
.arg("日本語")
.no_window()
.run();
assert!(exit_code.is_ok());
assert_eq!(exit_code.unwrap(), 0);
}
}