use std::ffi::{OsStr, OsString};
use std::io;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle as StdOwnedHandle, RawHandle};
use std::process::{ChildStderr, ChildStdin, ChildStdout, Command};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE};
use winapi::um::fileapi::{CreateFileW, OPEN_EXISTING};
use winapi::um::handleapi::{CloseHandle, DuplicateHandle, INVALID_HANDLE_VALUE};
use winapi::um::jobapi2::{AssignProcessToJobObject, CreateJobObjectW, SetInformationJobObject};
use winapi::um::minwinbase::SECURITY_ATTRIBUTES;
use winapi::um::namedpipeapi::CreateNamedPipeW;
use winapi::um::processenv::GetStdHandle;
use winapi::um::processthreadsapi::{
CreateProcessW, DeleteProcThreadAttributeList, GetCurrentProcess, GetCurrentProcessId,
GetExitCodeProcess, InitializeProcThreadAttributeList, ResumeThread, TerminateProcess,
UpdateProcThreadAttribute, LPPROC_THREAD_ATTRIBUTE_LIST, PROCESS_INFORMATION,
};
use winapi::um::synchapi::WaitForSingleObject;
use winapi::um::winbase::{
CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, CREATE_SUSPENDED, CREATE_UNICODE_ENVIRONMENT,
EXTENDED_STARTUPINFO_PRESENT, FILE_FLAG_FIRST_PIPE_INSTANCE, FILE_FLAG_OVERLAPPED, INFINITE,
PIPE_ACCESS_INBOUND, PIPE_ACCESS_OUTBOUND, PIPE_READMODE_BYTE, PIPE_REJECT_REMOTE_CLIENTS,
PIPE_TYPE_BYTE, PIPE_WAIT, STARTF_USESTDHANDLES, STARTUPINFOEXW, STD_ERROR_HANDLE,
STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, WAIT_OBJECT_0,
};
use winapi::um::winnt::{
JobObjectExtendedLimitInformation, DUPLICATE_SAME_ACCESS, FILE_SHARE_READ, FILE_SHARE_WRITE,
GENERIC_READ, GENERIC_WRITE, HANDLE, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JOB_OBJECT_LIMIT_BREAKAWAY_OK, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
const PROC_THREAD_ATTRIBUTE_HANDLE_LIST: usize = 0x00020002;
const STILL_ACTIVE: u32 = 259;
pub struct OwnedHandle(HANDLE);
impl OwnedHandle {
pub fn as_raw(&self) -> HANDLE {
self.0
}
pub fn into_raw(self) -> HANDLE {
let h = self.0;
std::mem::forget(self);
h
}
}
impl Drop for OwnedHandle {
fn drop(&mut self) {
if !self.0.is_null() && self.0 != INVALID_HANDLE_VALUE {
unsafe {
CloseHandle(self.0);
}
}
}
}
unsafe impl Send for OwnedHandle {}
unsafe impl Sync for OwnedHandle {}
pub struct OverlappedHandle(OwnedHandle);
impl OverlappedHandle {
pub fn into_child_stdin(self) -> ChildStdin {
let raw = self.0.into_raw() as RawHandle;
let owned = unsafe { StdOwnedHandle::from_raw_handle(raw) };
ChildStdin::from(owned)
}
pub fn into_child_stdout(self) -> ChildStdout {
let raw = self.0.into_raw() as RawHandle;
let owned = unsafe { StdOwnedHandle::from_raw_handle(raw) };
ChildStdout::from(owned)
}
pub fn into_child_stderr(self) -> ChildStderr {
let raw = self.0.into_raw() as RawHandle;
let owned = unsafe { StdOwnedHandle::from_raw_handle(raw) };
ChildStderr::from(owned)
}
}
pub struct SyncHandle(OwnedHandle);
impl SyncHandle {
fn into_owned(self) -> OwnedHandle {
self.0
}
}
#[derive(Clone, Copy)]
enum PipeDir {
ParentWritesChildReads,
ChildWritesParentReads,
}
fn open_nul(write: bool) -> io::Result<OwnedHandle> {
let path: Vec<u16> = OsStr::new("NUL")
.encode_wide()
.chain(std::iter::once(0))
.collect();
let mut sa: SECURITY_ATTRIBUTES = unsafe { std::mem::zeroed() };
sa.nLength = std::mem::size_of::<SECURITY_ATTRIBUTES>() as DWORD;
sa.bInheritHandle = TRUE as BOOL;
let access = if write { GENERIC_WRITE } else { GENERIC_READ };
let h = unsafe {
CreateFileW(
path.as_ptr(),
access,
FILE_SHARE_READ | FILE_SHARE_WRITE,
&mut sa as *mut SECURITY_ATTRIBUTES,
OPEN_EXISTING,
0,
std::ptr::null_mut(),
)
};
if h.is_null() || h == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
Ok(OwnedHandle(h))
}
fn create_pipe_pair(dir: PipeDir) -> io::Result<(OverlappedHandle, SyncHandle)> {
use std::sync::atomic::{AtomicU64, Ordering};
static PIPE_COUNTER: AtomicU64 = AtomicU64::new(0);
let counter = PIPE_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = unsafe { GetCurrentProcessId() };
let name = format!(r"\\.\pipe\running-process-{pid}-{counter}");
let name_w: Vec<u16> = OsStr::new(&name)
.encode_wide()
.chain(std::iter::once(0))
.collect();
let (parent_open_mode, child_access) = match dir {
PipeDir::ParentWritesChildReads => (PIPE_ACCESS_OUTBOUND, GENERIC_READ),
PipeDir::ChildWritesParentReads => (PIPE_ACCESS_INBOUND, GENERIC_WRITE),
};
let parent = unsafe {
CreateNamedPipeW(
name_w.as_ptr(),
parent_open_mode | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
1, 64 * 1024, 64 * 1024, 0, std::ptr::null_mut(),
)
};
if parent.is_null() || parent == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
let parent = OverlappedHandle(OwnedHandle(parent));
let mut child_sa: SECURITY_ATTRIBUTES = unsafe { std::mem::zeroed() };
child_sa.nLength = std::mem::size_of::<SECURITY_ATTRIBUTES>() as DWORD;
child_sa.bInheritHandle = TRUE as BOOL;
child_sa.lpSecurityDescriptor = std::ptr::null_mut();
let child = unsafe {
CreateFileW(
name_w.as_ptr(),
child_access,
0, &mut child_sa as *mut SECURITY_ATTRIBUTES,
OPEN_EXISTING,
0, std::ptr::null_mut(),
)
};
if child.is_null() || child == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
let child = SyncHandle(OwnedHandle(child));
Ok((parent, child))
}
fn dup_inheritable(src: HANDLE) -> io::Result<OwnedHandle> {
let current = unsafe { GetCurrentProcess() };
let mut out: HANDLE = std::ptr::null_mut();
let ok = unsafe {
DuplicateHandle(
current,
src,
current,
&mut out as *mut HANDLE,
0,
TRUE as BOOL,
DUPLICATE_SAME_ACCESS,
)
};
if ok == FALSE {
return Err(io::Error::last_os_error());
}
Ok(OwnedHandle(out))
}
struct ResolvedSlot {
child_handle: OwnedHandle,
parent_end: Option<OverlappedHandle>,
}
enum SlotDir {
Stdin,
Stdout,
Stderr,
}
fn resolve_slot(slot: &super::StdioSource<'_>, dir: SlotDir) -> io::Result<ResolvedSlot> {
match slot {
super::StdioSource::Null => {
let write = !matches!(dir, SlotDir::Stdin);
Ok(ResolvedSlot {
child_handle: open_nul(write)?,
parent_end: None,
})
}
super::StdioSource::Parent => {
let std_handle = match dir {
SlotDir::Stdin => STD_INPUT_HANDLE,
SlotDir::Stdout => STD_OUTPUT_HANDLE,
SlotDir::Stderr => STD_ERROR_HANDLE,
};
let src = unsafe { GetStdHandle(std_handle) };
if src.is_null() || src == INVALID_HANDLE_VALUE {
let write = !matches!(dir, SlotDir::Stdin);
return Ok(ResolvedSlot {
child_handle: open_nul(write)?,
parent_end: None,
});
}
Ok(ResolvedSlot {
child_handle: dup_inheritable(src)?,
parent_end: None,
})
}
super::StdioSource::Handle(borrowed) => {
let raw = borrowed.as_raw_handle() as HANDLE;
Ok(ResolvedSlot {
child_handle: dup_inheritable(raw)?,
parent_end: None,
})
}
super::StdioSource::Pipe => {
let pipe_dir = match dir {
SlotDir::Stdin => PipeDir::ParentWritesChildReads,
SlotDir::Stdout | SlotDir::Stderr => PipeDir::ChildWritesParentReads,
};
let (parent_end, child_end) = create_pipe_pair(pipe_dir)?;
Ok(ResolvedSlot {
child_handle: child_end.into_owned(),
parent_end: Some(parent_end),
})
}
super::StdioSource::_Phantom(_) => unreachable!(),
}
}
pub struct SpawnedInner {
process: Option<OwnedHandle>,
job: Option<OwnedHandle>,
_drain_keepalive: Option<Arc<()>>,
}
impl SpawnedInner {
pub fn kill(&self) -> io::Result<()> {
if let Some(h) = self.process.as_ref() {
let ok = unsafe { TerminateProcess(h.as_raw(), 1) };
if ok == FALSE {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
pub fn wait(&self) -> io::Result<i32> {
let Some(h) = self.process.as_ref() else {
return Err(io::Error::other("child handle absent"));
};
wait_inner(h)
}
pub fn try_wait(&self) -> io::Result<Option<i32>> {
let Some(h) = self.process.as_ref() else {
return Ok(None);
};
try_wait_inner(h)
}
pub fn shutdown(&mut self) {
drop(self.job.take());
drop(self.process.take());
}
}
pub fn spawn_daemon(command: &mut Command, clear_env: bool) -> io::Result<super::DaemonChild> {
let stdin = open_nul(false)?;
let stdout = open_nul(true)?;
let stderr = open_nul(true)?;
let (handle, _thread, pid) = create_process_inner(
command,
&stdin,
&stdout,
&stderr,
CreateMode::Daemon,
clear_env,
)?;
Ok(super::DaemonChild {
pid,
handle: OwnedHandle(handle),
})
}
pub fn spawn(
command: &mut Command,
stdio: super::SpawnStdio<'_>,
) -> io::Result<super::SpawnedChild> {
let stdin_slot = resolve_slot(&stdio.stdin, SlotDir::Stdin)?;
let stdout_slot = resolve_slot(&stdio.stdout, SlotDir::Stdout)?;
let stderr_slot = resolve_slot(&stdio.stderr, SlotDir::Stderr)?;
let (process, thread, pid) = create_process_inner(
command,
&stdin_slot.child_handle,
&stdout_slot.child_handle,
&stderr_slot.child_handle,
CreateMode::Contained {
show_console: stdio.show_console,
},
false,
)?;
let job = create_job_object()?;
let ok = unsafe { AssignProcessToJobObject(job.as_raw(), process) };
if ok == FALSE {
let err = io::Error::last_os_error();
unsafe {
TerminateProcess(process, 1);
CloseHandle(thread);
CloseHandle(process);
}
return Err(err);
}
unsafe {
ResumeThread(thread);
CloseHandle(thread);
}
let stdin_pipe = stdin_slot
.parent_end
.map(OverlappedHandle::into_child_stdin);
let stdout_pipe = stdout_slot
.parent_end
.map(OverlappedHandle::into_child_stdout);
let stderr_pipe = stderr_slot
.parent_end
.map(OverlappedHandle::into_child_stderr);
let drain_keepalive = if let Some(timeout) = stdio.drain_timeout {
let process_handle = dup_inheritable(process)?;
let keep = Arc::new(());
let keep_watcher = Arc::clone(&keep);
thread::spawn(move || {
drain_watcher(process_handle, timeout, keep_watcher);
});
Some(keep)
} else {
None
};
Ok(super::SpawnedChild {
stdin: stdin_pipe,
stdout: stdout_pipe,
stderr: stderr_pipe,
pid,
inner: SpawnedInner {
process: Some(OwnedHandle(process)),
job: Some(job),
_drain_keepalive: drain_keepalive,
},
})
}
fn drain_watcher(process_handle: OwnedHandle, timeout: Duration, _keep: Arc<()>) {
unsafe {
WaitForSingleObject(process_handle.as_raw(), INFINITE);
}
thread::sleep(timeout);
}
enum CreateMode {
Daemon,
Contained { show_console: bool },
}
fn create_process_inner(
command: &mut Command,
stdin: &OwnedHandle,
stdout: &OwnedHandle,
stderr: &OwnedHandle,
mode: CreateMode,
clear_env: bool,
) -> io::Result<(HANDLE, HANDLE, u32)> {
let mut cmdline = build_command_line(command.get_program(), command.get_args());
let envs: Vec<(OsString, Option<OsString>)> = command
.get_envs()
.map(|(k, v)| (k.to_os_string(), v.map(|v| v.to_os_string())))
.collect();
let env_block = if envs.is_empty() && !clear_env {
None
} else {
Some(build_env_block(envs, clear_env))
};
let cwd_w: Option<Vec<u16>> = command.get_current_dir().map(|p| {
OsStr::new(p)
.encode_wide()
.chain(std::iter::once(0))
.collect()
});
let mut size: usize = 0;
unsafe {
InitializeProcThreadAttributeList(std::ptr::null_mut(), 1, 0, &mut size);
}
let mut attr_buf: Vec<u8> = vec![0; size];
let attr_list = attr_buf.as_mut_ptr() as LPPROC_THREAD_ATTRIBUTE_LIST;
let ok = unsafe { InitializeProcThreadAttributeList(attr_list, 1, 0, &mut size) };
if ok == FALSE {
return Err(io::Error::last_os_error());
}
let handle_list: [HANDLE; 3] = [stdin.as_raw(), stdout.as_raw(), stderr.as_raw()];
let ok = unsafe {
UpdateProcThreadAttribute(
attr_list,
0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
handle_list.as_ptr() as *mut _,
std::mem::size_of::<[HANDLE; 3]>(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
if ok == FALSE {
let err = io::Error::last_os_error();
unsafe { DeleteProcThreadAttributeList(attr_list) };
return Err(err);
}
let mut si: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
si.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as DWORD;
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
si.StartupInfo.hStdInput = stdin.as_raw();
si.StartupInfo.hStdOutput = stdout.as_raw();
si.StartupInfo.hStdError = stderr.as_raw();
si.lpAttributeList = attr_list;
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let mut flags: DWORD = EXTENDED_STARTUPINFO_PRESENT;
match mode {
CreateMode::Daemon => {
flags |= CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP;
}
CreateMode::Contained { show_console } => {
flags |= CREATE_SUSPENDED;
if !show_console {
flags |= CREATE_NO_WINDOW;
}
}
}
if env_block.is_some() {
flags |= CREATE_UNICODE_ENVIRONMENT;
}
let cwd_ptr = cwd_w
.as_ref()
.map(|v| v.as_ptr())
.unwrap_or(std::ptr::null());
let env_ptr = env_block
.as_ref()
.map(|v| v.as_ptr() as *mut winapi::ctypes::c_void)
.unwrap_or(std::ptr::null_mut());
let ok = unsafe {
CreateProcessW(
std::ptr::null(),
cmdline.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
TRUE as BOOL,
flags,
env_ptr,
cwd_ptr,
&mut si.StartupInfo,
&mut pi,
)
};
let err = if ok == FALSE {
Some(io::Error::last_os_error())
} else {
None
};
unsafe {
DeleteProcThreadAttributeList(attr_list);
}
if let Some(err) = err {
return Err(err);
}
if matches!(mode, CreateMode::Daemon) {
unsafe {
CloseHandle(pi.hThread);
}
Ok((pi.hProcess, std::ptr::null_mut(), pi.dwProcessId))
} else {
Ok((pi.hProcess, pi.hThread, pi.dwProcessId))
}
}
fn create_job_object() -> io::Result<OwnedHandle> {
let job = unsafe { CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()) };
if job.is_null() || job == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { std::mem::zeroed() };
info.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_BREAKAWAY_OK;
let ok = unsafe {
SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
(&mut info as *mut JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
};
if ok == FALSE {
let err = io::Error::last_os_error();
unsafe { CloseHandle(job) };
return Err(err);
}
Ok(OwnedHandle(job))
}
pub fn terminate(handle: &OwnedHandle) -> io::Result<()> {
let ok = unsafe { TerminateProcess(handle.as_raw(), 1) };
if ok == FALSE {
return Err(io::Error::last_os_error());
}
Ok(())
}
pub fn wait(handle: &OwnedHandle) -> io::Result<i32> {
wait_inner(handle)
}
pub fn try_wait(handle: &OwnedHandle) -> io::Result<Option<i32>> {
try_wait_inner(handle)
}
fn wait_inner(handle: &OwnedHandle) -> io::Result<i32> {
let rc = unsafe { WaitForSingleObject(handle.as_raw(), INFINITE) };
if rc != WAIT_OBJECT_0 {
return Err(io::Error::last_os_error());
}
let mut code: DWORD = 0;
let ok = unsafe { GetExitCodeProcess(handle.as_raw(), &mut code as *mut DWORD) };
if ok == FALSE {
return Err(io::Error::last_os_error());
}
Ok(code as i32)
}
fn try_wait_inner(handle: &OwnedHandle) -> io::Result<Option<i32>> {
let mut code: DWORD = 0;
let ok = unsafe { GetExitCodeProcess(handle.as_raw(), &mut code as *mut DWORD) };
if ok == FALSE {
return Err(io::Error::last_os_error());
}
if code == STILL_ACTIVE {
Ok(None)
} else {
Ok(Some(code as i32))
}
}
fn build_command_line<'a>(program: &OsStr, args: impl Iterator<Item = &'a OsStr>) -> Vec<u16> {
let program_str = program.to_string_lossy().into_owned();
let is_cmd = is_cmd_exe(&program_str);
let arg_strs: Vec<String> = args.map(|a| a.to_string_lossy().into_owned()).collect();
let mut s = String::new();
s.push_str("e(&program_str));
let mut i = 0;
while i < arg_strs.len() {
let a = &arg_strs[i];
s.push(' ');
if is_cmd && is_cmd_script_switch(a) {
s.push_str(a);
let script = arg_strs[i + 1..].join(" ");
if !script.is_empty() {
s.push(' ');
s.push('"');
s.push_str(&script);
s.push('"');
}
break;
}
s.push_str("e(a));
i += 1;
}
OsStr::new(&s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn is_cmd_exe(program: &str) -> bool {
let lower = program.to_ascii_lowercase();
let tail = std::path::Path::new(&lower)
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or(lower);
tail == "cmd" || tail == "cmd.exe"
}
fn is_cmd_script_switch(arg: &str) -> bool {
matches!(arg.to_ascii_lowercase().as_str(), "/c" | "/k")
}
fn quote(arg: &str) -> String {
if !arg.is_empty()
&& !arg
.chars()
.any(|c| matches!(c, ' ' | '\t' | '\n' | '\x0b' | '"'))
{
return arg.to_string();
}
let mut out = String::from("\"");
let chars: Vec<char> = arg.chars().collect();
let mut i = 0;
while i < chars.len() {
let mut nbs = 0;
while i < chars.len() && chars[i] == '\\' {
nbs += 1;
i += 1;
}
if i == chars.len() {
for _ in 0..(nbs * 2) {
out.push('\\');
}
break;
} else if chars[i] == '"' {
for _ in 0..(nbs * 2 + 1) {
out.push('\\');
}
out.push('"');
} else {
for _ in 0..nbs {
out.push('\\');
}
out.push(chars[i]);
}
i += 1;
}
out.push('"');
out
}
fn build_env_block(overrides: Vec<(OsString, Option<OsString>)>, clear_env: bool) -> Vec<u16> {
use std::collections::BTreeMap;
let upper_key = |k: &OsStr| -> Vec<u16> {
k.encode_wide()
.map(|c| {
if (b'a' as u16..=b'z' as u16).contains(&c) {
c - (b'a' as u16 - b'A' as u16)
} else {
c
}
})
.collect()
};
let mut env: BTreeMap<Vec<u16>, (OsString, OsString)> = BTreeMap::new();
if !clear_env {
for (k, v) in std::env::vars_os() {
env.insert(upper_key(&k), (k, v));
}
}
for (k, v) in overrides {
let ck = upper_key(&k);
match v {
Some(val) => {
env.insert(ck, (k, val));
}
None => {
env.remove(&ck);
}
}
}
let mut block: Vec<u16> = Vec::new();
for (_ck, (k, v)) in env {
block.extend(k.encode_wide());
block.push(b'=' as u16);
block.extend(v.encode_wide());
block.push(0);
}
block.push(0);
block
}