char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somrearrange libs into lib and extensions into extsf4a0f5b

main
18.7 KiB544 linesraw
1//! Windows subprocess backend. Launch details follow libuv and Rust's
2//! standard library as adapted by Deno: CRT descriptor inheritance through
3//! `lpReserved2`, MSVC argument quoting, and verbatim path simplification.
4
5use std::ffi::{OsStr, OsString, c_void};
6use std::io;
7use std::os::windows::ffi::{OsStrExt, OsStringExt};
8use std::os::windows::fs::OpenOptionsExt;
9use std::os::windows::io::{AsRawHandle, BorrowedHandle, FromRawHandle, OwnedHandle};
10use std::os::windows::process::ExitStatusExt;
11use std::path::{Component, Path, PathBuf, Prefix};
12use std::process::ExitStatus;
13use std::sync::atomic::{AtomicU64, Ordering};
14
15use crate::Launch;
16use futures::FutureExt;
17use tokio::net::windows::named_pipe::{NamedPipeServer, PipeMode, ServerOptions};
18use windows_sys::Win32::Foundation::{
19    HANDLE, HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE, MAX_PATH, SetHandleInformation,
20    WAIT_OBJECT_0, WAIT_TIMEOUT,
21};
22use windows_sys::Win32::Storage::FileSystem::{
23    FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, FILE_TYPE_CHAR,
24    FILE_TYPE_DISK, FILE_TYPE_PIPE, FILE_WRITE_ATTRIBUTES, GetFileType, GetShortPathNameW,
25};
26use windows_sys::Win32::System::JobObjects::{
27    JOB_OBJECT_LIMIT_BREAKAWAY_OK, JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK,
28};
29use windows_sys::Win32::System::Threading::{
30    CREATE_UNICODE_ENVIRONMENT, CreateProcessW, DeleteProcThreadAttributeList,
31    EXTENDED_STARTUPINFO_PRESENT, GetExitCodeProcess, INFINITE, InitializeProcThreadAttributeList,
32    LPPROC_THREAD_ATTRIBUTE_LIST, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
33    PROC_THREAD_ATTRIBUTE_JOB_LIST, PROCESS_INFORMATION, STARTF_USESTDHANDLES, STARTUPINFOEXW,
34    TerminateProcess, UpdateProcThreadAttribute, WaitForSingleObject,
35};
36
37/// A handle handed to the child at its declared CRT descriptor number.
38pub type Endpoint = OwnedHandle;
39/// The parent's end of a stdio pipe.
40pub type Pipe = NamedPipeServer;
41/// Terminals are not supported on Windows yet.
42pub enum Window {}
43/// The CRT's inherited descriptor table holds at most 255 entries.
44pub const MAX_DESCRIPTOR: u32 = 254;
45
46const SIGNALS: [&str; 16] = [
47    "SIGHUP", "SIGINT", "SIGQUIT", "SIGABRT", "SIGKILL", "SIGUSR1", "SIGUSR2", "SIGPIPE",
48    "SIGALRM", "SIGTERM", "SIGCONT", "SIGSTOP", "SIGTSTP", "SIGTTIN", "SIGTTOU", "SIGWINCH",
49];
50
51/// Like libuv, the signals that end a process all terminate it immediately:
52/// Windows has no general way to ask a process to stop.
53pub fn validate_signal(name: &str) -> io::Result<()> {
54    if matches!(name, "SIGKILL" | "SIGTERM" | "SIGINT" | "SIGQUIT") {
55        Ok(())
56    } else if SIGNALS.contains(&name) {
57        Err(io::Error::new(
58            io::ErrorKind::Unsupported,
59            "Windows subprocesses accept only SIGTERM, SIGKILL, SIGINT, and SIGQUIT",
60        ))
61    } else {
62        Err(io::Error::new(
63            io::ErrorKind::InvalidInput,
64            "Unsupported subprocess signal",
65        ))
66    }
67}
68
69pub fn exit_signal(_: &ExitStatus) -> Option<String> {
70    None
71}
72
73fn last_error() -> io::Error {
74    io::Error::last_os_error()
75}
76
77fn wide(value: &OsStr) -> Vec<u16> {
78    value.encode_wide().chain([0]).collect()
79}
80
81/// Returns the parent's end and the child's end of a new pipe. The parent's
82/// end is overlapped for async IO; the child's end is synchronous, which is
83/// what most programs expect of their standard streams.
84pub fn pipe(parent_writes: bool) -> io::Result<(Pipe, Endpoint)> {
85    static NEXT: AtomicU64 = AtomicU64::new(0);
86    let name = format!(
87        r"\\.\pipe\avril-{}-{}-{:016x}",
88        std::process::id(),
89        NEXT.fetch_add(1, Ordering::Relaxed),
90        getrandom::u64().map_err(io::Error::other)?,
91    );
92    // first_pipe_instance refuses a squatted name; max_instances keeps out
93    // a second client.
94    let server = ServerOptions::new()
95        .first_pipe_instance(true)
96        .max_instances(1)
97        .reject_remote_clients(true)
98        .pipe_mode(PipeMode::Byte)
99        .access_inbound(!parent_writes)
100        .access_outbound(parent_writes)
101        .create(&name)?;
102    // libuv's access masks: the child may adjust its end's pipe state.
103    let access = if parent_writes {
104        FILE_GENERIC_READ | FILE_WRITE_ATTRIBUTES
105    } else {
106        FILE_GENERIC_WRITE | FILE_READ_ATTRIBUTES
107    };
108    let client = std::fs::OpenOptions::new()
109        .access_mode(access)
110        .share_mode(0)
111        .open(&name)?;
112    match server.connect().now_or_never() {
113        Some(result) => result?,
114        None => return Err(io::Error::other("Subprocess pipe did not connect")),
115    }
116    Ok((server, client.into()))
117}
118
119pub fn null(read: bool) -> io::Result<Endpoint> {
120    Ok(std::fs::OpenOptions::new()
121        .read(read)
122        .write(!read)
123        .open("NUL")?
124        .into())
125}
126
127pub fn inherit(fd: u32) -> io::Result<Endpoint> {
128    let handle = match fd {
129        0 => std::io::stdin().as_raw_handle(),
130        1 => std::io::stdout().as_raw_handle(),
131        _ => std::io::stderr().as_raw_handle(),
132    };
133    // Without a console or redirection there is nothing to inherit.
134    if handle.is_null() || handle == INVALID_HANDLE_VALUE {
135        return null(fd == 0);
136    }
137    unsafe { BorrowedHandle::borrow_raw(handle) }.try_clone_to_owned()
138}
139
140pub fn duplicate(endpoint: &Endpoint) -> io::Result<Endpoint> {
141    endpoint.try_clone()
142}
143
144pub struct Pty {
145    pub reader: Pipe,
146    pub writer: Pipe,
147    pub endpoints: [Endpoint; 3],
148    pub window: Window,
149}
150
151pub fn pty(_columns: u16, _rows: u16) -> io::Result<Pty> {
152    Err(io::Error::new(
153        io::ErrorKind::Unsupported,
154        "Subprocess terminals are not supported on Windows",
155    ))
156}
157
158pub fn resize(window: &Window, _columns: u16, _rows: u16) -> io::Result<()> {
159    match *window {}
160}
161
162pub fn is_executable(metadata: &std::fs::Metadata) -> bool {
163    metadata.is_file()
164}
165
166/// Names without an executable extension also try `.com` and `.exe`, like
167/// libuv's search. Batch extensions are never added: they need a shell.
168pub fn expand(path: PathBuf) -> Vec<PathBuf> {
169    let extension = path.extension().map(OsStr::to_ascii_lowercase);
170    if extension
171        .as_deref()
172        .is_some_and(|extension| extension == "com" || extension == "exe")
173    {
174        return vec![path];
175    }
176    let mut paths = Vec::new();
177    for suffix in [".com", ".exe"] {
178        let mut name = path.clone().into_os_string();
179        name.push(suffix);
180        paths.push(name.into());
181    }
182    if extension.is_some() {
183        paths.insert(0, path);
184    }
185    paths
186}
187
188/// CreateProcess runs `.bat` and `.cmd` files through cmd.exe, with its own
189/// quoting rules; there is no implicit shell, so they need an explicit one.
190/// Path normalization drops trailing dots and spaces, so CreateProcess does
191/// too (the BatBadBut lesson in Rust's standard library).
192fn is_batch(path: &Path) -> bool {
193    path.file_name().is_some_and(|name| {
194        let name = name.to_string_lossy().to_ascii_lowercase();
195        let name = name.trim_end_matches(['.', ' ']);
196        name.ends_with(".bat") || name.ends_with(".cmd")
197    })
198}
199
200/// The authorized executable, opened without delete or write sharing so
201/// it cannot be replaced or renamed until CreateProcess has mapped it.
202pub struct Executable {
203    _image: std::fs::File,
204}
205
206impl Executable {
207    pub fn open(target: &Path) -> io::Result<(Self, std::fs::Metadata)> {
208        if is_batch(target) {
209            return Err(io::Error::new(
210                io::ErrorKind::Unsupported,
211                "Batch files need an explicit cmd.exe invocation",
212            ));
213        }
214        let file = std::fs::OpenOptions::new()
215            .read(true)
216            .share_mode(FILE_SHARE_READ)
217            .open(target)?;
218        let metadata = file.metadata()?;
219        Ok((Self { _image: file }, metadata))
220    }
221}
222
223pub fn controlling_foreground() -> bool {
224    false
225}
226
227/// Canonical paths are verbatim (`\\?\C:\…`), which CreateProcess and many
228/// programs mishandle as a working directory or image path; use the plain
229/// spelling, as Deno's `strip_unc_prefix` does, but only where Win32
230/// normalization leaves it alone. Otherwise it would name another file:
231/// trailing dots and spaces are dropped, device names are remapped, and
232/// long paths need the prefix.
233fn simplify(path: &Path) -> PathBuf {
234    let mut components = path.components();
235    let Some(Component::Prefix(prefix)) = components.next() else {
236        return path.to_owned();
237    };
238    let mut plain = match prefix.kind() {
239        Prefix::VerbatimDisk(letter) => PathBuf::from(format!("{}:\\", letter as char)),
240        Prefix::VerbatimUNC(server, share) => {
241            let mut root = OsString::from(r"\\");
242            root.push(server);
243            root.push(r"\");
244            root.push(share);
245            root.push(r"\");
246            PathBuf::from(root)
247        }
248        _ => return path.to_owned(),
249    };
250    plain.extend(components.filter(|component| !matches!(component, Component::RootDir)));
251    if plain.as_os_str().encode_wide().count() < MAX_PATH as usize
252        && std::path::absolute(&plain).is_ok_and(|absolute| absolute == plain)
253    {
254        plain
255    } else {
256        path.to_owned()
257    }
258}
259
260/// MSVC command-line quoting, as in Rust's standard library: arguments with
261/// whitespace (or none at all) are quoted, and backslashes are doubled only
262/// where they precede a quote.
263fn append_argument(line: &mut Vec<u16>, argument: &OsStr) {
264    let quote = argument.is_empty()
265        || argument
266            .encode_wide()
267            .any(|unit| unit == b' ' as u16 || unit == b'\t' as u16);
268    if quote {
269        line.push(b'"' as u16);
270    }
271    let mut backslashes = 0;
272    for unit in argument.encode_wide() {
273        if unit == b'\\' as u16 {
274            backslashes += 1;
275        } else {
276            if unit == b'"' as u16 {
277                line.extend(std::iter::repeat_n(b'\\' as u16, backslashes + 1));
278            }
279            backslashes = 0;
280        }
281        line.push(unit);
282    }
283    if quote {
284        line.extend(std::iter::repeat_n(b'\\' as u16, backslashes));
285        line.push(b'"' as u16);
286    }
287}
288
289pub struct Child {
290    pid: u32,
291    process: OwnedHandle,
292    /// Kill-on-close: if this process dies, so does the job's tree.
293    job: std::sync::Arc<crate::job::Job>,
294}
295impl Child {
296    pub fn id(&self) -> u32 {
297        self.pid
298    }
299    fn status(&self) -> io::Result<ExitStatus> {
300        let mut code = 0;
301        if unsafe { GetExitCodeProcess(self.process.as_raw_handle(), &mut code) } == 0 {
302            return Err(last_error());
303        }
304        Ok(ExitStatus::from_raw(code))
305    }
306    pub fn wait(&mut self) -> io::Result<ExitStatus> {
307        if unsafe { WaitForSingleObject(self.process.as_raw_handle(), INFINITE) } != WAIT_OBJECT_0 {
308            return Err(last_error());
309        }
310        self.status()
311    }
312    pub fn observe_exit(&self) -> io::Result<Option<ExitStatus>> {
313        match unsafe { WaitForSingleObject(self.process.as_raw_handle(), 0) } {
314            WAIT_OBJECT_0 => self.status().map(Some),
315            WAIT_TIMEOUT => Ok(None),
316            _ => Err(last_error()),
317        }
318    }
319    /// Group scope terminates the child's job, which holds its descendants
320    /// unless they explicitly broke away (as setsid would on Unix).
321    pub fn signal(&self, group: bool, name: &str) -> io::Result<bool> {
322        validate_signal(name)?;
323        if group {
324            return self.job.terminate();
325        }
326        if self.observe_exit()?.is_some() {
327            return Ok(false);
328        }
329        if unsafe { TerminateProcess(self.process.as_raw_handle(), 1) } == 0 {
330            let error = last_error();
331            // Termination races with a natural exit.
332            return if self.observe_exit()?.is_some() {
333                Ok(false)
334            } else {
335                Err(error)
336            };
337        }
338        Ok(true)
339    }
340}
341
342/// Owns an initialized attribute list.
343struct Attributes(Vec<usize>);
344impl Attributes {
345    fn new(count: u32) -> io::Result<Self> {
346        let mut size = 0;
347        unsafe {
348            InitializeProcThreadAttributeList(std::ptr::null_mut(), count, 0, &mut size);
349        }
350        let mut list = Self(vec![0; size.div_ceil(std::mem::size_of::<usize>())]);
351        if unsafe { InitializeProcThreadAttributeList(list.as_ptr(), count, 0, &mut size) } == 0 {
352            list.0.clear();
353            return Err(last_error());
354        }
355        Ok(list)
356    }
357    fn as_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST {
358        self.0.as_mut_ptr().cast()
359    }
360    /// The value is borrowed, not copied, until CreateProcess returns.
361    fn set<T>(&mut self, attribute: u32, value: &[T]) -> io::Result<()> {
362        if unsafe {
363            UpdateProcThreadAttribute(
364                self.as_ptr(),
365                0,
366                attribute as usize,
367                value.as_ptr().cast(),
368                std::mem::size_of_val(value),
369                std::ptr::null_mut(),
370                std::ptr::null(),
371            )
372        } == 0
373        {
374            return Err(last_error());
375        }
376        Ok(())
377    }
378}
379impl Drop for Attributes {
380    fn drop(&mut self) {
381        if !self.0.is_empty() {
382            unsafe { DeleteProcThreadAttributeList(self.as_ptr()) }
383        }
384    }
385}
386
387pub(crate) fn spawn(launch: Launch) -> io::Result<Child> {
388    let Launch {
389        executable,
390        program,
391        args,
392        env,
393        cwd,
394        group,
395        join,
396        terminal,
397        endpoints,
398    } = launch;
399    if terminal {
400        return Err(io::Error::new(
401            io::ErrorKind::Unsupported,
402            "Subprocess terminals are not supported on Windows",
403        ));
404    }
405    let program = simplify(program);
406    let application = wide(program.as_os_str());
407    // MSVC's parser takes argv[0] verbatim up to the closing quote, and
408    // Windows paths cannot contain quotes.
409    let mut line = vec![b'"' as u16];
410    line.extend(program.as_os_str().encode_wide());
411    line.push(b'"' as u16);
412    for argument in args {
413        line.push(b' ' as u16);
414        append_argument(&mut line, OsStr::new(argument));
415    }
416    line.push(0);
417    // Names arrive sorted by their case-insensitive key, as CreateProcess
418    // expects.
419    let mut environment = Vec::new();
420    for (name, value) in &env {
421        environment.extend(OsStr::new(name).encode_wide());
422        environment.push(b'=' as u16);
423        environment.extend(OsStr::new(value).encode_wide());
424        environment.push(0);
425    }
426    if environment.is_empty() {
427        environment.push(0);
428    }
429    environment.push(0);
430    let mut directory = simplify(cwd);
431    // libuv: a working directory of MAX_PATH or more needs its short form.
432    if directory.as_os_str().encode_wide().count() >= MAX_PATH as usize {
433        let long = wide(cwd.as_os_str());
434        let mut short = vec![0u16; long.len()];
435        let length =
436            unsafe { GetShortPathNameW(long.as_ptr(), short.as_mut_ptr(), short.len() as u32) };
437        if length == 0 || length as usize >= short.len() {
438            return Err(last_error());
439        }
440        short.truncate(length as usize);
441        directory = simplify(Path::new(&OsString::from_wide(&short)));
442    }
443    let directory = wide(directory.as_os_str());
444
445    // The CRT reads inherited descriptors from lpReserved2: a count, one
446    // flag byte per descriptor, then one unaligned handle per descriptor.
447    const FOPEN: u8 = 0x01;
448    const FPIPE: u8 = 0x08;
449    const FDEV: u8 = 0x40;
450    let count = endpoints
451        .iter()
452        .map(|(fd, _)| *fd as usize + 1)
453        .max()
454        .unwrap_or(0)
455        .max(3);
456    let handle_size = std::mem::size_of::<HANDLE>();
457    let mut descriptors = vec![0u8; 4 + count + count * handle_size];
458    descriptors[..4].copy_from_slice(&(count as u32).to_ne_bytes());
459    for fd in 0..count {
460        let at = 4 + count + fd * handle_size;
461        descriptors[at..at + handle_size]
462            .copy_from_slice(&(INVALID_HANDLE_VALUE as usize).to_ne_bytes());
463    }
464    let mut standard = [INVALID_HANDLE_VALUE; 3];
465    let mut inherited = Vec::with_capacity(endpoints.len());
466    for (fd, endpoint) in &endpoints {
467        let handle = endpoint.as_raw_handle();
468        if unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) } == 0 {
469            return Err(last_error());
470        }
471        let flags = match unsafe { GetFileType(handle) } {
472            FILE_TYPE_DISK => FOPEN,
473            FILE_TYPE_PIPE => FOPEN | FPIPE,
474            FILE_TYPE_CHAR => FOPEN | FDEV,
475            _ => FOPEN | FDEV,
476        };
477        let fd = *fd as usize;
478        descriptors[4 + fd] = flags;
479        let at = 4 + count + fd * handle_size;
480        descriptors[at..at + handle_size].copy_from_slice(&(handle as usize).to_ne_bytes());
481        if fd < 3 {
482            standard[fd] = handle;
483        }
484        inherited.push(handle);
485    }
486
487    // Child scope keeps only the child in its job; descendants silently
488    // stay out, as they stay out of termination on Unix.
489    let flags = if group {
490        JOB_OBJECT_LIMIT_BREAKAWAY_OK
491    } else {
492        JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK
493    };
494    let job = match join {
495        Some(group) => group.job()?,
496        None => std::sync::Arc::new(crate::job::Job::new(flags)?),
497    };
498    let jobs = [job.as_raw_handle()];
499
500    // Only the listed handles reach the child, even when other threads
501    // spawn with inheritable handles of their own; the job list places the
502    // child in its job before it runs.
503    let mut attributes = Attributes::new(2)?;
504    attributes.set(PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &inherited)?;
505    attributes.set(PROC_THREAD_ATTRIBUTE_JOB_LIST, &jobs)?;
506    let mut startup: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
507    startup.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
508    startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
509    startup.StartupInfo.hStdInput = standard[0];
510    startup.StartupInfo.hStdOutput = standard[1];
511    startup.StartupInfo.hStdError = standard[2];
512    startup.StartupInfo.cbReserved2 = descriptors.len() as u16;
513    startup.StartupInfo.lpReserved2 = descriptors.as_mut_ptr();
514    startup.lpAttributeList = attributes.as_ptr();
515    let mut information: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
516    let created = unsafe {
517        CreateProcessW(
518            application.as_ptr(),
519            line.as_mut_ptr(),
520            std::ptr::null(),
521            std::ptr::null(),
522            1,
523            CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT,
524            environment.as_ptr().cast::<c_void>(),
525            directory.as_ptr(),
526            &raw const startup.StartupInfo,
527            &mut information,
528        )
529    };
530    let error = last_error();
531    drop(attributes);
532    drop(executable);
533    if created == 0 {
534        return Err(error);
535    }
536    unsafe {
537        drop(OwnedHandle::from_raw_handle(information.hThread));
538    }
539    Ok(Child {
540        pid: information.dwProcessId,
541        process: unsafe { OwnedHandle::from_raw_handle(information.hProcess) },
542        job,
543    })
544}