use std::{
ffi::OsStr,
io, mem,
os::windows::{
ffi::OsStrExt,
io::{AsRawHandle, FromRawHandle, OwnedHandle},
},
path::Path,
ptr,
};
use windows_sys::Win32::{
Foundation::INVALID_HANDLE_VALUE,
System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next,
},
System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOBOBJECT_BASIC_LIMIT_INFORMATION, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JobObjectExtendedLimitInformation, SetInformationJobObject,
},
System::Threading::{CREATE_SUSPENDED, OpenThread, ResumeThread, THREAD_SUSPEND_RESUME},
UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOWNORMAL},
};
pub(crate) const USES_PATHEXT: bool = true;
pub(crate) fn is_executable(path: &Path) -> bool {
path.metadata().is_ok_and(|metadata| metadata.is_file())
}
pub const SYSTEM_VARIABLES: &[&str] = &[
"HOME",
"SystemRoot",
"ComSpec",
"PATHEXT",
"USERPROFILE",
"APPDATA",
"LOCALAPPDATA",
"TEMP",
"TMP",
];
pub(crate) struct Tree {
pub(crate) child: tokio::process::Child,
job: Option<OwnedHandle>,
}
pub(crate) fn spawn_tree(command: &mut tokio::process::Command) -> io::Result<Tree> {
let handle = unsafe { CreateJobObjectW(ptr::null(), ptr::null()) };
if handle.is_null() {
return Err(io::Error::last_os_error());
}
let job = unsafe { OwnedHandle::from_raw_handle(handle) };
let limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
BasicLimitInformation: JOBOBJECT_BASIC_LIMIT_INFORMATION {
LimitFlags: JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
..Default::default()
},
..Default::default()
};
if unsafe {
SetInformationJobObject(
job.as_raw_handle(),
JobObjectExtendedLimitInformation,
ptr::from_ref(&limits).cast(),
mem::size_of_val(&limits) as u32,
)
} == 0
{
return Err(io::Error::last_os_error());
}
command.creation_flags(CREATE_SUSPENDED);
let mut suspended = SuspendedChild(command.spawn()?);
let admitted = (|| {
let process = suspended.0.raw_handle().ok_or(io::ErrorKind::NotFound)?;
if unsafe { AssignProcessToJobObject(job.as_raw_handle(), process) } == 0 {
return Err(io::Error::last_os_error());
}
resume(&suspended)
})();
if let Err(error) = admitted {
let _ = suspended.0.start_kill();
return Err(error);
}
Ok(Tree {
child: suspended.0,
job: Some(job),
})
}
struct SuspendedChild(tokio::process::Child);
const RESUME_THREAD_FAILED: u32 = u32::MAX;
fn resume(child: &SuspendedChild) -> io::Result<()> {
let pid = child.0.id().ok_or(io::ErrorKind::NotFound)?;
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if snapshot == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
let snapshot = unsafe { OwnedHandle::from_raw_handle(snapshot) };
let mut entry = THREADENTRY32 {
dwSize: mem::size_of::<THREADENTRY32>() as u32,
..Default::default()
};
let mut resumed = false;
let mut more = unsafe { Thread32First(snapshot.as_raw_handle(), &mut entry) } != 0;
while more {
if entry.th32OwnerProcessID == pid {
let thread = unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID) };
if thread.is_null() {
return Err(io::Error::last_os_error());
}
let thread = unsafe { OwnedHandle::from_raw_handle(thread) };
if unsafe { ResumeThread(thread.as_raw_handle()) } == RESUME_THREAD_FAILED {
return Err(io::Error::last_os_error());
}
resumed = true;
}
more = unsafe { Thread32Next(snapshot.as_raw_handle(), &mut entry) } != 0;
}
if resumed {
Ok(())
} else {
Err(io::Error::other(
"the suspended child has no thread to resume",
))
}
}
impl Tree {
pub(crate) fn kill(&mut self) {
self.job.take();
}
}
impl Drop for Tree {
fn drop(&mut self) {
self.kill();
}
}
pub(crate) async fn open_desktop(target: &OsStr) -> io::Result<()> {
let target = target.to_owned();
match tokio::task::spawn_blocking(move || shell_execute(&target)).await {
Ok(result) => result,
Err(error) if error.is_panic() => std::panic::resume_unwind(error.into_panic()),
Err(error) => Err(io::Error::other(error)),
}
}
const SHELL_EXECUTE_ERROR_MAX: isize = 32;
fn shell_execute(target: &OsStr) -> io::Result<()> {
let mut target: Vec<_> = target.encode_wide().collect();
if target.contains(&0) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Open target contains NUL.",
));
}
target.push(0);
let result = unsafe {
ShellExecuteW(
ptr::null_mut(),
ptr::null(),
target.as_ptr(),
ptr::null(),
ptr::null(),
SW_SHOWNORMAL,
)
} as isize;
if result <= SHELL_EXECUTE_ERROR_MAX {
return Err(io::Error::other(format!(
"ShellExecute could not open the target (code {result})."
)));
}
Ok(())
}