char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
f4a0f5b
main
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 ::{ 19HANDLE , HANDLE_FLAG_INHERIT , INVALID_HANDLE_VALUE , MAX_PATH , SetHandleInformation , 20WAIT_OBJECT_0 , WAIT_TIMEOUT , 21}; 22use windows_sys:: Win32 :: Storage :: FileSystem ::{ 23FILE_GENERIC_READ , FILE_GENERIC_WRITE , FILE_READ_ATTRIBUTES , FILE_SHARE_READ , FILE_TYPE_CHAR , 24FILE_TYPE_DISK , FILE_TYPE_PIPE , FILE_WRITE_ATTRIBUTES , GetFileType , GetShortPathNameW , 25}; 26use windows_sys:: Win32 :: System :: JobObjects ::{ 27JOB_OBJECT_LIMIT_BREAKAWAY_OK , JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK , 28}; 29use windows_sys:: Win32 :: System :: Threading ::{ 30CREATE_UNICODE_ENVIRONMENT , CreateProcessW , DeleteProcThreadAttributeList , 31EXTENDED_STARTUPINFO_PRESENT , GetExitCodeProcess , INFINITE , InitializeProcThreadAttributeList , 32LPPROC_THREAD_ATTRIBUTE_LIST , PROC_THREAD_ATTRIBUTE_HANDLE_LIST , 33PROC_THREAD_ATTRIBUTE_JOB_LIST , PROCESS_INFORMATION , STARTF_USESTDHANDLES , STARTUPINFOEXW , 34TerminateProcess , 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 <()> { 54if matches! ( name, "SIGKILL" |"SIGTERM" |"SIGINT" |"SIGQUIT" ) { 55Ok (()) 56} else if SIGNALS . contains ( & name) { 57Err ( io:: Error :: new ( 58 io:: ErrorKind :: Unsupported , 59"Windows subprocesses accept only SIGTERM, SIGKILL, SIGINT, and SIGQUIT" , 60)) 61} else { 62Err ( io:: Error :: new ( 63 io:: ErrorKind :: InvalidInput , 64"Unsupported subprocess signal" , 65)) 66} 67} 68 69pub fn exit_signal ( _: & ExitStatus ) ->Option < String > { 70None 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 )> { 85static NEXT : AtomicU64 =AtomicU64 :: new ( 0 ); 86let name =format! ( 87r"\\.\pipe\avril-{}-{}-{:016x}" , 88 std:: process:: id (), 89NEXT . 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. 94let 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. 103let access =if parent_writes{ 104FILE_GENERIC_READ |FILE_WRITE_ATTRIBUTES 105} else { 106FILE_GENERIC_WRITE |FILE_READ_ATTRIBUTES 107}; 108let client = std:: fs:: OpenOptions :: new () 109. access_mode ( access) 110. share_mode ( 0 ) 111. open ( & name) ?; 112match server. connect (). now_or_never () { 113Some ( result) => result?, 114None =>return Err ( io:: Error :: other ( "Subprocess pipe did not connect" )), 115} 116Ok (( server, client. into ())) 117} 118 119pub fn null ( read : bool ) -> io:: Result < Endpoint > { 120Ok ( 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 > { 128let handle =match fd{ 1290 => std:: io:: stdin (). as_raw_handle (), 1301 => 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. 134if handle. is_null () || handle ==INVALID_HANDLE_VALUE { 135return null ( fd ==0 ); 136} 137unsafe { 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 { 145pub reader : Pipe , 146pub writer : Pipe , 147pub endpoints : [ Endpoint ; 3 ], 148pub window : Window , 149} 150 151pub fn pty ( _columns : u16 , _rows : u16 ) -> io:: Result < Pty > { 152Err ( 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 <()> { 159match * 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 > { 169let extension = path. extension (). map ( OsStr :: to_ascii_lowercase); 170if extension 171. as_deref () 172. is_some_and ( |extension| extension =="com" || extension =="exe" ) 173{ 174return vec! [ path]; 175} 176let mut paths =Vec :: new (); 177for suffixin [ ".com" , ".exe" ] { 178let mut name = path. clone (). into_os_string (); 179 name. push ( suffix); 180 paths. push ( name. into ()); 181} 182if 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|{ 194let name = name. to_string_lossy (). to_ascii_lowercase (); 195let 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 { 207pub fn open ( target : & Path ) -> io:: Result <( Self , std:: fs:: Metadata )> { 208if is_batch ( target) { 209return Err ( io:: Error :: new ( 210 io:: ErrorKind :: Unsupported , 211"Batch files need an explicit cmd.exe invocation" , 212)); 213} 214let file = std:: fs:: OpenOptions :: new () 215. read ( true ) 216. share_mode ( FILE_SHARE_READ ) 217. open ( target) ?; 218let metadata = file. metadata () ?; 219Ok (( Self { _image : file}, metadata)) 220} 221} 222 223pub fn controlling_foreground () ->bool { 224false 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 { 234let mut components = path. components (); 235let Some ( Component :: Prefix ( prefix)) = components. next () else { 236return path. to_owned (); 237}; 238let mut plain =match prefix. kind () { 239Prefix :: VerbatimDisk ( letter) =>PathBuf :: from ( format! ( "{}:\\" , letteras char )), 240Prefix :: VerbatimUNC ( server, share) =>{ 241let mut root =OsString :: from ( r"\\" ); 242 root. push ( server); 243 root. push ( r"\" ); 244 root. push ( share); 245 root. push ( r"\" ); 246PathBuf :: from ( root) 247} 248 _ =>return path. to_owned (), 249}; 250 plain. extend ( components. filter ( |component| !matches! ( component, Component :: RootDir ))); 251if 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 ) { 264let quote = argument. is_empty () 265 || argument 266. encode_wide () 267. any ( |unit| unit ==b' ' as u16 || unit ==b'\t' as u16 ); 268if quote{ 269 line. push ( b'"' as u16 ); 270} 271let mut backslashes =0 ; 272for unitin argument. encode_wide () { 273if unit ==b'\\' as u16 { 274 backslashes +=1 ; 275} else { 276if 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} 283if quote{ 284 line. extend ( std:: iter:: repeat_n ( b'\\' as u16 , backslashes)); 285 line. push ( b'"' as u16 ); 286} 287} 288 289pub struct Child { 290pid : u32 , 291process : OwnedHandle , 292/// Kill-on-close: if this process dies, so does the job's tree. 293job : std:: sync:: Arc < crate :: job:: Job >, 294} 295impl Child { 296pub fn id ( & self ) ->u32 { 297self . pid 298} 299fn status ( & self ) -> io:: Result < ExitStatus > { 300let mut code =0 ; 301if unsafe { GetExitCodeProcess ( self . process . as_raw_handle (), & mut code) } ==0 { 302return Err ( last_error ()); 303} 304Ok ( ExitStatus :: from_raw ( code)) 305} 306pub fn wait ( & mut self ) -> io:: Result < ExitStatus > { 307if unsafe { WaitForSingleObject ( self . process . as_raw_handle (), INFINITE ) } !=WAIT_OBJECT_0 { 308return Err ( last_error ()); 309} 310self . status () 311} 312pub fn observe_exit ( & self ) -> io:: Result < Option < ExitStatus >> { 313match unsafe { WaitForSingleObject ( self . process . as_raw_handle (), 0 ) } { 314WAIT_OBJECT_0 =>self . status (). map ( Some ), 315WAIT_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). 321pub fn signal ( & self , group : bool , name : & str ) -> io:: Result < bool > { 322validate_signal ( name) ?; 323if group{ 324return self . job . terminate (); 325} 326if self . observe_exit () ?. is_some () { 327return Ok ( false ); 328} 329if unsafe { TerminateProcess ( self . process . as_raw_handle (), 1 ) } ==0 { 330let error =last_error (); 331// Termination races with a natural exit. 332return if self . observe_exit () ?. is_some () { 333Ok ( false ) 334} else { 335Err ( error) 336}; 337} 338Ok ( true ) 339} 340} 341 342/// Owns an initialized attribute list. 343struct Attributes ( Vec < usize >); 344impl Attributes { 345fn new ( count : u32 ) -> io:: Result < Self > { 346let mut size =0 ; 347unsafe { 348InitializeProcThreadAttributeList ( std:: ptr:: null_mut (), count, 0 , & mut size); 349} 350let mut list =Self ( vec! [ 0 ; size. div_ceil ( std:: mem:: size_of ::< usize >())]); 351if unsafe { InitializeProcThreadAttributeList ( list. as_ptr (), count, 0 , & mut size) } ==0 { 352 list. 0 . clear (); 353return Err ( last_error ()); 354} 355Ok ( list) 356} 357fn as_ptr ( & mut self ) ->LPPROC_THREAD_ATTRIBUTE_LIST { 358self . 0 . as_mut_ptr (). cast () 359} 360/// The value is borrowed, not copied, until CreateProcess returns. 361fn set < T >( & mut self , attribute : u32 , value : & [ T ]) -> io:: Result <()> { 362if unsafe { 363UpdateProcThreadAttribute ( 364self . as_ptr (), 3650 , 366 attributeas 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{ 374return Err ( last_error ()); 375} 376Ok (()) 377} 378} 379impl Drop for Attributes { 380fn drop ( & mut self ) { 381if !self . 0 . is_empty () { 382unsafe { DeleteProcThreadAttributeList ( self . as_ptr ()) } 383} 384} 385} 386 387pub ( crate ) fn spawn ( launch : Launch ) -> io:: Result < Child > { 388let Launch { 389 executable, 390 program, 391 args, 392 env, 393 cwd, 394 group, 395 join, 396 terminal, 397 endpoints, 398} = launch; 399if terminal{ 400return Err ( io:: Error :: new ( 401 io:: ErrorKind :: Unsupported , 402"Subprocess terminals are not supported on Windows" , 403)); 404} 405let program =simplify ( program); 406let 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. 409let mut line =vec! [ b'"' as u16 ]; 410 line. extend ( program. as_os_str (). encode_wide ()); 411 line. push ( b'"' as u16 ); 412for argumentin args{ 413 line. push ( b' ' as u16 ); 414append_argument ( & mut line, OsStr :: new ( argument)); 415} 416 line. push ( 0 ); 417// Names arrive sorted by their case-insensitive key, as CreateProcess 418// expects. 419let mut environment =Vec :: new (); 420for ( 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} 426if environment. is_empty () { 427 environment. push ( 0 ); 428} 429 environment. push ( 0 ); 430let mut directory =simplify ( cwd); 431// libuv: a working directory of MAX_PATH or more needs its short form. 432if directory. as_os_str (). encode_wide (). count () >=MAX_PATH as usize { 433let long =wide ( cwd. as_os_str ()); 434let mut short =vec! [ 0u16 ; long. len ()]; 435let length = 436unsafe { GetShortPathNameW ( long. as_ptr (), short. as_mut_ptr (), short. len () as u32 ) }; 437if length ==0 || lengthas usize >= short. len () { 438return Err ( last_error ()); 439} 440 short. truncate ( lengthas usize ); 441 directory =simplify ( Path :: new ( & OsString :: from_wide ( & short))); 442} 443let 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. 447const FOPEN : u8 =0x01 ; 448const FPIPE : u8 =0x08 ; 449const FDEV : u8 =0x40 ; 450let count = endpoints 451. iter () 452. map ( |( fd, _) |* fdas usize +1 ) 453. max () 454. unwrap_or ( 0 ) 455. max ( 3 ); 456let handle_size = std:: mem:: size_of ::< HANDLE >(); 457let mut descriptors =vec! [ 0u8 ; 4 + count + count* handle_size]; 458 descriptors[ ..4 ]. copy_from_slice ( & ( countas u32 ). to_ne_bytes ()); 459for fdin 0 ..count{ 460let 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} 464let mut standard =[ INVALID_HANDLE_VALUE ; 3 ]; 465let mut inherited =Vec :: with_capacity ( endpoints. len ()); 466for ( fd, endpoint) in & endpoints{ 467let handle = endpoint. as_raw_handle (); 468if unsafe { SetHandleInformation ( handle, HANDLE_FLAG_INHERIT , HANDLE_FLAG_INHERIT ) } ==0 { 469return Err ( last_error ()); 470} 471let flags =match unsafe { GetFileType ( handle) } { 472FILE_TYPE_DISK =>FOPEN , 473FILE_TYPE_PIPE =>FOPEN |FPIPE , 474FILE_TYPE_CHAR =>FOPEN |FDEV , 475 _ =>FOPEN |FDEV , 476}; 477let fd =* fdas usize ; 478 descriptors[ 4 + fd] = flags; 479let at =4 + count + fd* handle_size; 480 descriptors[ at..at + handle_size]. copy_from_slice ( & ( handleas usize ). to_ne_bytes ()); 481if 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. 489let flags =if group{ 490JOB_OBJECT_LIMIT_BREAKAWAY_OK 491} else { 492JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK 493}; 494let job =match join{ 495Some ( group) => group. job () ?, 496None => std:: sync:: Arc :: new ( crate :: job:: Job :: new ( flags) ?), 497}; 498let 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. 503let mut attributes =Attributes :: new ( 2 ) ?; 504 attributes. set ( PROC_THREAD_ATTRIBUTE_HANDLE_LIST , & inherited) ?; 505 attributes. set ( PROC_THREAD_ATTRIBUTE_JOB_LIST , & jobs) ?; 506let 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 (); 515let mut information: PROCESS_INFORMATION =unsafe { std:: mem:: zeroed () }; 516let created =unsafe { 517CreateProcessW ( 518 application. as_ptr (), 519 line. as_mut_ptr (), 520 std:: ptr:: null (), 521 std:: ptr:: null (), 5221 , 523CREATE_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}; 530let error =last_error (); 531drop ( attributes); 532drop ( executable); 533if created ==0 { 534return Err ( error); 535} 536unsafe { 537drop ( OwnedHandle :: from_raw_handle ( information. hThread )); 538} 539Ok ( Child { 540pid : information. dwProcessId , 541process : unsafe { OwnedHandle :: from_raw_handle ( information. hProcess ) }, 542 job, 543}) 544}