diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ad0fabe..81f812d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: - name: Checkout source uses: actions/checkout@v2 - - uses: Swatinem/rust-cargo@v1 + - uses: Swatinem/rust-cache@v1 - name: cargo test uses: actions-rs/cargo@v1 diff --git a/Cargo.lock b/Cargo.lock index 60e8adf..95cb5c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,9 +6,65 @@ version = 3 name = "CreateProcessW" version = "0.1.0" dependencies = [ + "thiserror", "windows", ] +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + [[package]] name = "windows" version = "0.24.0" diff --git a/Cargo.toml b/Cargo.toml index 44f61d0..bd2368e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +thiserror = "1.0.30" + [dependencies.windows] version = "0.24.0" features = [ diff --git a/src/lib.rs b/src/lib.rs index 44d8a23..b1fa0d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,199 @@ +// Disable warning for the crate name, not a really good way to do this but.. +// (https://github.com/rust-lang/rust/issues/45127) #![allow(non_snake_case)] + +use std::ffi::{c_void, OsStr, OsString}; +use std::mem::size_of; +use std::path::{Path, PathBuf}; +use thiserror::Error; +use windows::Win32::Foundation::{CloseHandle, GetLastError, PWSTR, STATUS_PENDING}; +use windows::Win32::Security::SECURITY_ATTRIBUTES; +use windows::Win32::System::Threading::{ + GetExitCodeProcess, TerminateProcess, WaitForSingleObject, PROCESS_CREATION_FLAGS, + PROCESS_INFORMATION, STARTUPINFOW, WAIT_OBJECT_0, +}; +use windows::Win32::System::WindowsProgramming::INFINITE; + +#[derive(Error, Debug)] +pub enum Error { + #[error("cannot create process: {0}")] + CreationFailed(u32), + #[error("cannot get exit status: {0}")] + GetExitCodeFailed(u32), + #[error("cannot kill process: {0}")] + KillFailed(u32), +} + +type Result = std::result::Result; + +#[derive(Debug)] +pub struct Command { + command: OsString, + inherit_handles: bool, + current_directory: Option, +} + +impl Command { + pub fn new(command: impl Into) -> Self { + Self { + command: command.into(), + inherit_handles: true, + current_directory: None, + } + } + + pub fn inherit_handles(&mut self, inherit: bool) -> &mut Self { + self.inherit_handles = inherit; + self + } + + pub fn current_dir(&mut self, dir: impl Into) -> &mut Self { + self.current_directory = Some(dir.into()); + self + } + + pub fn spawn(&mut self) -> Result { + Child::new( + &self.command, + self.inherit_handles, + self.current_directory.as_deref(), + ) + } + + pub fn status(&mut self) -> Result { + self.spawn()?.wait() + } +} + +#[derive(Debug)] +pub struct Child { + process_information: PROCESS_INFORMATION, +} + +impl Child { + fn new( + command: &OsStr, + inherit_handles: bool, + current_directory: Option<&Path>, + ) -> Result { + let mut startup_info = STARTUPINFOW::default(); + let mut process_info = PROCESS_INFORMATION::default(); + + startup_info.cb = size_of::() as u32; + + let process_creation_flags = PROCESS_CREATION_FLAGS(0); + + let res = unsafe { + if let Some(directory) = current_directory { + let directory = directory.as_os_str(); + windows::Win32::System::Threading::CreateProcessW( + PWSTR::default(), + command, + std::ptr::null() as *const SECURITY_ATTRIBUTES, + std::ptr::null() as *const SECURITY_ATTRIBUTES, + inherit_handles, + process_creation_flags, + std::ptr::null() as *const c_void, + directory, + &startup_info, + &mut process_info as *mut PROCESS_INFORMATION, + ) + } else { + windows::Win32::System::Threading::CreateProcessW( + PWSTR::default(), + command, + std::ptr::null() as *const SECURITY_ATTRIBUTES, + std::ptr::null() as *const SECURITY_ATTRIBUTES, + inherit_handles, + process_creation_flags, + std::ptr::null() as *const c_void, + PWSTR::default(), + &startup_info, + &mut process_info as *mut PROCESS_INFORMATION, + ) + } + }; + + if res.as_bool() { + Ok(Self { + process_information: process_info, + }) + } else { + Err(Error::CreationFailed(unsafe { GetLastError().0 })) + } + } + + pub fn wait(&self) -> Result { + unsafe { + let mut exit_code: u32 = 0; + let res = WaitForSingleObject(self.process_information.hProcess, INFINITE); + + if res == WAIT_OBJECT_0 { + if GetExitCodeProcess( + self.process_information.hProcess, + &mut exit_code as *mut u32, + ) + .as_bool() + { + close_handles(&self.process_information); + Ok(ExitStatus(exit_code)) + } else { + Err(Error::GetExitCodeFailed(GetLastError().0)) + } + } else { + Err(Error::GetExitCodeFailed(GetLastError().0)) + } + } + } + + pub fn try_wait(&self) -> Result> { + unsafe { + let mut exit_code: u32 = 0; + + let res = GetExitCodeProcess( + self.process_information.hProcess, + &mut exit_code as *mut u32, + ); + + if res.as_bool() { + if exit_code == STATUS_PENDING.0 { + Ok(None) + } else { + close_handles(&self.process_information); + Ok(Some(ExitStatus(exit_code))) + } + } else { + Err(Error::GetExitCodeFailed(GetLastError().0)) + } + } + } + + pub fn kill(&self) -> Result<()> { + unsafe { + let res = TerminateProcess(self.process_information.hProcess, 0); + + if res.as_bool() { + Ok(()) + } else { + Err(Error::KillFailed(GetLastError().0)) + } + } + } +} + +pub struct ExitStatus(u32); + +impl ExitStatus { + pub fn success(&self) -> bool { + self.0 == 0 + } + + pub fn code(&self) -> u32 { + self.0 + } +} + +unsafe fn close_handles(process_info: &PROCESS_INFORMATION) { + CloseHandle(process_info.hProcess); + CloseHandle(process_info.hThread); +}