//! Windows subprocess backend. Launch details follow libuv and Rust's //! standard library as adapted by Deno: CRT descriptor inheritance through //! `lpReserved2`, MSVC argument quoting, and verbatim path simplification. use std::ffi::{OsStr, OsString, c_void}; use std::io; use std::os::windows::ffi::{OsStrExt, OsStringExt}; use std::os::windows::fs::OpenOptionsExt; use std::os::windows::io::{AsRawHandle, BorrowedHandle, FromRawHandle, OwnedHandle}; use std::os::windows::process::ExitStatusExt; use std::path::{Component, Path, PathBuf, Prefix}; use std::process::ExitStatus; use std::sync::atomic::{AtomicU64, Ordering}; use crate::Launch; use futures::FutureExt; use tokio::net::windows::named_pipe::{NamedPipeServer, PipeMode, ServerOptions}; use windows_sys::Win32::Foundation::{ HANDLE, HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE, MAX_PATH, SetHandleInformation, WAIT_OBJECT_0, WAIT_TIMEOUT, }; use windows_sys::Win32::Storage::FileSystem::{ FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, FILE_TYPE_CHAR, FILE_TYPE_DISK, FILE_TYPE_PIPE, FILE_WRITE_ATTRIBUTES, GetFileType, GetShortPathNameW, }; use windows_sys::Win32::System::JobObjects::{ JOB_OBJECT_LIMIT_BREAKAWAY_OK, JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK, }; use windows_sys::Win32::System::Threading::{ CREATE_UNICODE_ENVIRONMENT, CreateProcessW, DeleteProcThreadAttributeList, EXTENDED_STARTUPINFO_PRESENT, GetExitCodeProcess, INFINITE, InitializeProcThreadAttributeList, LPPROC_THREAD_ATTRIBUTE_LIST, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, PROC_THREAD_ATTRIBUTE_JOB_LIST, PROCESS_INFORMATION, STARTF_USESTDHANDLES, STARTUPINFOEXW, TerminateProcess, UpdateProcThreadAttribute, WaitForSingleObject, }; /// A handle handed to the child at its declared CRT descriptor number. pub type Endpoint = OwnedHandle; /// The parent's end of a stdio pipe. pub type Pipe = NamedPipeServer; /// Terminals are not supported on Windows yet. pub enum Window {} /// The CRT's inherited descriptor table holds at most 255 entries. pub const MAX_DESCRIPTOR: u32 = 254; const SIGNALS: [&str; 16] = [ "SIGHUP", "SIGINT", "SIGQUIT", "SIGABRT", "SIGKILL", "SIGUSR1", "SIGUSR2", "SIGPIPE", "SIGALRM", "SIGTERM", "SIGCONT", "SIGSTOP", "SIGTSTP", "SIGTTIN", "SIGTTOU", "SIGWINCH", ]; /// Like libuv, the signals that end a process all terminate it immediately: /// Windows has no general way to ask a process to stop. pub fn validate_signal(name: &str) -> io::Result<()> { if matches!(name, "SIGKILL" | "SIGTERM" | "SIGINT" | "SIGQUIT") { Ok(()) } else if SIGNALS.contains(&name) { Err(io::Error::new( io::ErrorKind::Unsupported, "Windows subprocesses accept only SIGTERM, SIGKILL, SIGINT, and SIGQUIT", )) } else { Err(io::Error::new( io::ErrorKind::InvalidInput, "Unsupported subprocess signal", )) } } pub fn exit_signal(_: &ExitStatus) -> Option { None } fn last_error() -> io::Error { io::Error::last_os_error() } fn wide(value: &OsStr) -> Vec { value.encode_wide().chain([0]).collect() } /// Returns the parent's end and the child's end of a new pipe. The parent's /// end is overlapped for async IO; the child's end is synchronous, which is /// what most programs expect of their standard streams. pub fn pipe(parent_writes: bool) -> io::Result<(Pipe, Endpoint)> { static NEXT: AtomicU64 = AtomicU64::new(0); let name = format!( r"\\.\pipe\avril-{}-{}-{:016x}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed), getrandom::u64().map_err(io::Error::other)?, ); // first_pipe_instance refuses a squatted name; max_instances keeps out // a second client. let server = ServerOptions::new() .first_pipe_instance(true) .max_instances(1) .reject_remote_clients(true) .pipe_mode(PipeMode::Byte) .access_inbound(!parent_writes) .access_outbound(parent_writes) .create(&name)?; // libuv's access masks: the child may adjust its end's pipe state. let access = if parent_writes { FILE_GENERIC_READ | FILE_WRITE_ATTRIBUTES } else { FILE_GENERIC_WRITE | FILE_READ_ATTRIBUTES }; let client = std::fs::OpenOptions::new() .access_mode(access) .share_mode(0) .open(&name)?; match server.connect().now_or_never() { Some(result) => result?, None => return Err(io::Error::other("Subprocess pipe did not connect")), } Ok((server, client.into())) } pub fn null(read: bool) -> io::Result { Ok(std::fs::OpenOptions::new() .read(read) .write(!read) .open("NUL")? .into()) } pub fn inherit(fd: u32) -> io::Result { let handle = match fd { 0 => std::io::stdin().as_raw_handle(), 1 => std::io::stdout().as_raw_handle(), _ => std::io::stderr().as_raw_handle(), }; // Without a console or redirection there is nothing to inherit. if handle.is_null() || handle == INVALID_HANDLE_VALUE { return null(fd == 0); } unsafe { BorrowedHandle::borrow_raw(handle) }.try_clone_to_owned() } pub fn duplicate(endpoint: &Endpoint) -> io::Result { endpoint.try_clone() } pub struct Pty { pub reader: Pipe, pub writer: Pipe, pub endpoints: [Endpoint; 3], pub window: Window, } pub fn pty(_columns: u16, _rows: u16) -> io::Result { Err(io::Error::new( io::ErrorKind::Unsupported, "Subprocess terminals are not supported on Windows", )) } pub fn resize(window: &Window, _columns: u16, _rows: u16) -> io::Result<()> { match *window {} } pub fn is_executable(metadata: &std::fs::Metadata) -> bool { metadata.is_file() } /// Names without an executable extension also try `.com` and `.exe`, like /// libuv's search. Batch extensions are never added: they need a shell. pub fn expand(path: PathBuf) -> Vec { let extension = path.extension().map(OsStr::to_ascii_lowercase); if extension .as_deref() .is_some_and(|extension| extension == "com" || extension == "exe") { return vec![path]; } let mut paths = Vec::new(); for suffix in [".com", ".exe"] { let mut name = path.clone().into_os_string(); name.push(suffix); paths.push(name.into()); } if extension.is_some() { paths.insert(0, path); } paths } /// CreateProcess runs `.bat` and `.cmd` files through cmd.exe, with its own /// quoting rules; there is no implicit shell, so they need an explicit one. /// Path normalization drops trailing dots and spaces, so CreateProcess does /// too (the BatBadBut lesson in Rust's standard library). fn is_batch(path: &Path) -> bool { path.file_name().is_some_and(|name| { let name = name.to_string_lossy().to_ascii_lowercase(); let name = name.trim_end_matches(['.', ' ']); name.ends_with(".bat") || name.ends_with(".cmd") }) } /// The authorized executable, opened without delete or write sharing so /// it cannot be replaced or renamed until CreateProcess has mapped it. pub struct Executable { _image: std::fs::File, } impl Executable { pub fn open(target: &Path) -> io::Result<(Self, std::fs::Metadata)> { if is_batch(target) { return Err(io::Error::new( io::ErrorKind::Unsupported, "Batch files need an explicit cmd.exe invocation", )); } let file = std::fs::OpenOptions::new() .read(true) .share_mode(FILE_SHARE_READ) .open(target)?; let metadata = file.metadata()?; Ok((Self { _image: file }, metadata)) } } pub fn controlling_foreground() -> bool { false } /// Canonical paths are verbatim (`\\?\C:\…`), which CreateProcess and many /// programs mishandle as a working directory or image path; use the plain /// spelling, as Deno's `strip_unc_prefix` does, but only where Win32 /// normalization leaves it alone. Otherwise it would name another file: /// trailing dots and spaces are dropped, device names are remapped, and /// long paths need the prefix. fn simplify(path: &Path) -> PathBuf { let mut components = path.components(); let Some(Component::Prefix(prefix)) = components.next() else { return path.to_owned(); }; let mut plain = match prefix.kind() { Prefix::VerbatimDisk(letter) => PathBuf::from(format!("{}:\\", letter as char)), Prefix::VerbatimUNC(server, share) => { let mut root = OsString::from(r"\\"); root.push(server); root.push(r"\"); root.push(share); root.push(r"\"); PathBuf::from(root) } _ => return path.to_owned(), }; plain.extend(components.filter(|component| !matches!(component, Component::RootDir))); if plain.as_os_str().encode_wide().count() < MAX_PATH as usize && std::path::absolute(&plain).is_ok_and(|absolute| absolute == plain) { plain } else { path.to_owned() } } /// MSVC command-line quoting, as in Rust's standard library: arguments with /// whitespace (or none at all) are quoted, and backslashes are doubled only /// where they precede a quote. fn append_argument(line: &mut Vec, argument: &OsStr) { let quote = argument.is_empty() || argument .encode_wide() .any(|unit| unit == b' ' as u16 || unit == b'\t' as u16); if quote { line.push(b'"' as u16); } let mut backslashes = 0; for unit in argument.encode_wide() { if unit == b'\\' as u16 { backslashes += 1; } else { if unit == b'"' as u16 { line.extend(std::iter::repeat_n(b'\\' as u16, backslashes + 1)); } backslashes = 0; } line.push(unit); } if quote { line.extend(std::iter::repeat_n(b'\\' as u16, backslashes)); line.push(b'"' as u16); } } pub struct Child { pid: u32, process: OwnedHandle, /// Kill-on-close: if this process dies, so does the job's tree. job: std::sync::Arc, } impl Child { pub fn id(&self) -> u32 { self.pid } fn status(&self) -> io::Result { let mut code = 0; if unsafe { GetExitCodeProcess(self.process.as_raw_handle(), &mut code) } == 0 { return Err(last_error()); } Ok(ExitStatus::from_raw(code)) } pub fn wait(&mut self) -> io::Result { if unsafe { WaitForSingleObject(self.process.as_raw_handle(), INFINITE) } != WAIT_OBJECT_0 { return Err(last_error()); } self.status() } pub fn observe_exit(&self) -> io::Result> { match unsafe { WaitForSingleObject(self.process.as_raw_handle(), 0) } { WAIT_OBJECT_0 => self.status().map(Some), WAIT_TIMEOUT => Ok(None), _ => Err(last_error()), } } /// Group scope terminates the child's job, which holds its descendants /// unless they explicitly broke away (as setsid would on Unix). pub fn signal(&self, group: bool, name: &str) -> io::Result { validate_signal(name)?; if group { return self.job.terminate(); } if self.observe_exit()?.is_some() { return Ok(false); } if unsafe { TerminateProcess(self.process.as_raw_handle(), 1) } == 0 { let error = last_error(); // Termination races with a natural exit. return if self.observe_exit()?.is_some() { Ok(false) } else { Err(error) }; } Ok(true) } } /// Owns an initialized attribute list. struct Attributes(Vec); impl Attributes { fn new(count: u32) -> io::Result { let mut size = 0; unsafe { InitializeProcThreadAttributeList(std::ptr::null_mut(), count, 0, &mut size); } let mut list = Self(vec![0; size.div_ceil(std::mem::size_of::())]); if unsafe { InitializeProcThreadAttributeList(list.as_ptr(), count, 0, &mut size) } == 0 { list.0.clear(); return Err(last_error()); } Ok(list) } fn as_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST { self.0.as_mut_ptr().cast() } /// The value is borrowed, not copied, until CreateProcess returns. fn set(&mut self, attribute: u32, value: &[T]) -> io::Result<()> { if unsafe { UpdateProcThreadAttribute( self.as_ptr(), 0, attribute as usize, value.as_ptr().cast(), std::mem::size_of_val(value), std::ptr::null_mut(), std::ptr::null(), ) } == 0 { return Err(last_error()); } Ok(()) } } impl Drop for Attributes { fn drop(&mut self) { if !self.0.is_empty() { unsafe { DeleteProcThreadAttributeList(self.as_ptr()) } } } } pub(crate) fn spawn(launch: Launch) -> io::Result { let Launch { executable, program, args, env, cwd, group, join, terminal, endpoints, } = launch; if terminal { return Err(io::Error::new( io::ErrorKind::Unsupported, "Subprocess terminals are not supported on Windows", )); } let program = simplify(program); let application = wide(program.as_os_str()); // MSVC's parser takes argv[0] verbatim up to the closing quote, and // Windows paths cannot contain quotes. let mut line = vec![b'"' as u16]; line.extend(program.as_os_str().encode_wide()); line.push(b'"' as u16); for argument in args { line.push(b' ' as u16); append_argument(&mut line, OsStr::new(argument)); } line.push(0); // Names arrive sorted by their case-insensitive key, as CreateProcess // expects. let mut environment = Vec::new(); for (name, value) in &env { environment.extend(OsStr::new(name).encode_wide()); environment.push(b'=' as u16); environment.extend(OsStr::new(value).encode_wide()); environment.push(0); } if environment.is_empty() { environment.push(0); } environment.push(0); let mut directory = simplify(cwd); // libuv: a working directory of MAX_PATH or more needs its short form. if directory.as_os_str().encode_wide().count() >= MAX_PATH as usize { let long = wide(cwd.as_os_str()); let mut short = vec![0u16; long.len()]; let length = unsafe { GetShortPathNameW(long.as_ptr(), short.as_mut_ptr(), short.len() as u32) }; if length == 0 || length as usize >= short.len() { return Err(last_error()); } short.truncate(length as usize); directory = simplify(Path::new(&OsString::from_wide(&short))); } let directory = wide(directory.as_os_str()); // The CRT reads inherited descriptors from lpReserved2: a count, one // flag byte per descriptor, then one unaligned handle per descriptor. const FOPEN: u8 = 0x01; const FPIPE: u8 = 0x08; const FDEV: u8 = 0x40; let count = endpoints .iter() .map(|(fd, _)| *fd as usize + 1) .max() .unwrap_or(0) .max(3); let handle_size = std::mem::size_of::(); let mut descriptors = vec![0u8; 4 + count + count * handle_size]; descriptors[..4].copy_from_slice(&(count as u32).to_ne_bytes()); for fd in 0..count { let at = 4 + count + fd * handle_size; descriptors[at..at + handle_size] .copy_from_slice(&(INVALID_HANDLE_VALUE as usize).to_ne_bytes()); } let mut standard = [INVALID_HANDLE_VALUE; 3]; let mut inherited = Vec::with_capacity(endpoints.len()); for (fd, endpoint) in &endpoints { let handle = endpoint.as_raw_handle(); if unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) } == 0 { return Err(last_error()); } let flags = match unsafe { GetFileType(handle) } { FILE_TYPE_DISK => FOPEN, FILE_TYPE_PIPE => FOPEN | FPIPE, FILE_TYPE_CHAR => FOPEN | FDEV, _ => FOPEN | FDEV, }; let fd = *fd as usize; descriptors[4 + fd] = flags; let at = 4 + count + fd * handle_size; descriptors[at..at + handle_size].copy_from_slice(&(handle as usize).to_ne_bytes()); if fd < 3 { standard[fd] = handle; } inherited.push(handle); } // Child scope keeps only the child in its job; descendants silently // stay out, as they stay out of termination on Unix. let flags = if group { JOB_OBJECT_LIMIT_BREAKAWAY_OK } else { JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK }; let job = match join { Some(group) => group.job()?, None => std::sync::Arc::new(crate::job::Job::new(flags)?), }; let jobs = [job.as_raw_handle()]; // Only the listed handles reach the child, even when other threads // spawn with inheritable handles of their own; the job list places the // child in its job before it runs. let mut attributes = Attributes::new(2)?; attributes.set(PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &inherited)?; attributes.set(PROC_THREAD_ATTRIBUTE_JOB_LIST, &jobs)?; let mut startup: STARTUPINFOEXW = unsafe { std::mem::zeroed() }; startup.StartupInfo.cb = std::mem::size_of::() as u32; startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; startup.StartupInfo.hStdInput = standard[0]; startup.StartupInfo.hStdOutput = standard[1]; startup.StartupInfo.hStdError = standard[2]; startup.StartupInfo.cbReserved2 = descriptors.len() as u16; startup.StartupInfo.lpReserved2 = descriptors.as_mut_ptr(); startup.lpAttributeList = attributes.as_ptr(); let mut information: PROCESS_INFORMATION = unsafe { std::mem::zeroed() }; let created = unsafe { CreateProcessW( application.as_ptr(), line.as_mut_ptr(), std::ptr::null(), std::ptr::null(), 1, CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT, environment.as_ptr().cast::(), directory.as_ptr(), &raw const startup.StartupInfo, &mut information, ) }; let error = last_error(); drop(attributes); drop(executable); if created == 0 { return Err(error); } unsafe { drop(OwnedHandle::from_raw_handle(information.hThread)); } Ok(Child { pid: information.dwProcessId, process: unsafe { OwnedHandle::from_raw_handle(information.hProcess) }, job, }) }