char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
b2fc926
main
1# [ path = "supervision.rs" ] 2pub ( super ) mod supervision; 3 4use super :: sys; 5use avril_subprocess::{ Options , Status , StdioSpec }; 6use std:: cell:: RefCell ; 7use std:: collections::{ BTreeMap , HashMap }; 8use std:: convert:: Infallible ; 9use std:: io; 10use std:: path:: Path ; 11use std:: rc:: Rc ; 12use std:: sync:: atomic::{ AtomicBool , AtomicU64 , Ordering }; 13use std:: sync::{ Arc , Mutex }; 14use std:: time::{ Duration , Instant }; 15 16use avril_caps:: Capabilities ; 17use avril_core:: serde_v8::{ JsBuffer , ToJsBuffer }; 18use avril_core::{ Extension , Op }; 19use avril_host:: environment:: Environment ; 20use serde:: Serialize ; 21use tokio:: runtime:: Handle ; 22use tokio_util:: sync:: CancellationToken ; 23 24use avril_ext_file::{ Files , process_file}; 25use avril_host:: io:: response; 26 27const MAX_ID : u64 =9_007_199_254_740_991 ; 28 29fn invalid ( message : & str ) -> io:: Error { 30 io:: Error :: new ( io:: ErrorKind :: InvalidInput , message) 31} 32fn missing () -> io:: Error { 33 io:: Error :: new ( io:: ErrorKind :: NotConnected , "Subprocess handle is closed" ) 34} 35 36# [ derive ( Serialize )] 37# [ serde ( rename_all = "camelCase" )] 38struct SpawnInfo { 39id : u64 , 40pid : u32 , 41readable : Vec < u32 >, 42writable : Vec < u32 >, 43terminal : bool , 44} 45# [ derive ( Serialize )] 46struct SyncOutput { 47status : Status , 48stdout : Option < ToJsBuffer >, 49stderr : Option < ToJsBuffer >, 50} 51 52struct Process { 53native : Arc < sys:: Process >, 54/// Set while a thread is blocked inside run_sync on this process, so the 55/// REPL can interrupt exactly the subprocess its evaluation is waiting on. 56sync_wait : AtomicBool , 57worker : bool , 58} 59impl std:: ops:: Deref for Process { 60type Target = sys:: Process ; 61fn deref ( & self ) ->& Self :: Target { 62& self . native 63} 64} 65 66struct Registry { 67runtime : Handle , 68caps : Arc < Capabilities >, 69files : Arc < Files >, 70processes : Mutex < HashMap < u64 , Arc < Process >>>, 71next : AtomicU64 , 72owner : CancellationToken , 73tree : Arc < supervision:: Tree >, 74worker : bool , 75signal : super :: SignalMode , 76signals : Arc < super :: signals:: State >, 77} 78impl Registry { 79fn get ( & self , id : u64 ) -> io:: Result < Arc < Process >> { 80self . processes 81. lock () 82. unwrap () 83. get ( & id) 84. cloned () 85. ok_or_else ( missing) 86} 87fn release ( & self , id : u64 ) { 88if let Some ( process) =self . processes . lock (). unwrap (). remove ( & id) { 89 process. request ( "dispose" , None , false ); 90} 91} 92fn spawn ( & self , options : Options , environment : & Environment ) -> io:: Result < SpawnInfo > { 93if self . owner . is_cancelled () ||self . tree . stopped . is_cancelled () ||self . tree . is_closed () { 94return Err ( missing ()); 95} 96if self . worker && options. stdin . mode =="inherit" { 97return Err ( io:: Error :: new ( 98 io:: ErrorKind :: Unsupported , 99"Worker subprocesses cannot inherit host stdin" , 100)); 101} 102// Only run mode owns SIGINT; serve and the REPL leave it to their 103// own handlers. SIGTERM fate belongs to whoever claims it, if anyone. 104self . tree 105. install ( & self . runtime , matches! ( self . signal , super :: SignalMode :: Run )) ?; 106let id =self 107. next 108. fetch_update ( Ordering :: Relaxed , Ordering :: Relaxed , |id|{ 109( id <=MAX_ID ). then_some ( id +1 ) 110}) 111. map_err ( |_| io:: Error :: other ( "Subprocess handle space exhausted" )) ?; 112let terminal = options. terminal . is_some (); 113let native = sys:: Launcher { 114runtime : self . runtime . clone (), 115caps : self . caps . clone (), 116owners : vec! [ self . owner . clone (), self . tree . stopped . clone ()], 117} 118. spawn ( 119 options, 120 environment 121. to_object ( & self . caps ) 122. into_iter () 123. map ( |( k, v) |( k. into (), v. into ())), 124None , 125 |spec, read, cwd|self . file ( spec, read, cwd), 126) ?; 127let process =Arc :: new ( Process { 128 native, 129sync_wait : AtomicBool :: new ( false ), 130worker : self . worker , 131}); 132let info =SpawnInfo { 133 id, 134pid : process. pid , 135 terminal, 136readable : process. outputs . keys (). copied (). collect (), 137writable : process. inputs . keys (). copied (). collect (), 138}; 139self . processes . lock (). unwrap (). insert ( id, process. clone ()); 140self . tree . register ( & process); 141Ok ( info) 142} 143fn file ( & self , spec : & StdioSpec , read : bool , cwd : & Path ) -> io:: Result < std:: fs:: File > { 144if let Some ( id) = spec. handle { 145return self . files . duplicate_for_process ( id, read, !read); 146} 147let path = cwd. join ( 148 spec. file 149. as_ref () 150. ok_or_else ( ||invalid ( "Missing stdio file" )) ?, 151); 152let flags =if read{ 153( true , false , false , false , false , false , None ) 154} else { 155match spec. file_mode . as_deref () { 156Some ( "append" ) =>( false , true , true , false , true , false , None ), 157Some ( "truncate" ) =>( false , true , false , true , true , false , None ), 158Some ( "create-new" ) =>( false , true , false , false , false , true , None ), 159 _ =>return Err ( invalid ( "Invalid stdio file mode" )), 160} 161}; 162process_file ( 163self . caps . clone (), 164 path. to_str () 165. ok_or_else ( ||invalid ( "Stdio paths must be UTF-8" )) ? 166. into (), 167 flags, 168) 169} 170} 171impl Drop for Registry { 172fn drop ( & mut self ) { 173// Release signal bindings first, so a dying isolate stops pinning 174// listened signals process-wide before processes drain. 175self . signals . shutdown (); 176let processes: Vec < _ > =self 177. processes 178. get_mut () 179. unwrap () 180. drain () 181. map ( |( _, process) | process) 182. collect (); 183for processin & processes{ 184 process. request ( "owner" , None , true ); 185} 186let deadline =Instant :: now () +Duration :: from_secs ( 2 ); 187for processin processes{ 188 process. wait_blocking ( deadline. saturating_duration_since ( Instant :: now ())); 189} 190} 191} 192 193pub fn init ( 194runtime : Handle , 195caps : Arc < Capabilities >, 196environment : Rc < RefCell < Environment >>, 197files : Arc < Files >, 198owner : CancellationToken , 199worker : bool , 200signal : super :: SignalMode , 201) ->Extension { 202let tree = supervision:: Tree :: global (); 203let signals =super :: signals:: State :: create ( runtime. clone (), worker); 204let registry =Arc :: new ( Registry { 205 runtime, 206 caps, 207 files, 208processes : Mutex :: new ( HashMap :: new ()), 209next : AtomicU64 :: new ( 1 ), 210 owner, 211tree : tree. clone (), 212 worker, 213 signal, 214signals : signals. clone (), 215}); 216let mut ops =Vec :: new (); 217 ops. extend ( super :: signals:: ops ( & signals)); 218 ops. push ( Op :: new ( "op_process_spawn" , { 219let registry = registry. clone (); 220let environment = environment. clone (); 221move |options : Options |{ 222Ok ::< _ , Infallible >( response ( registry. spawn ( options, & environment. borrow ()))) 223} 224})); 225 ops. push ( Op :: new ( "op_process_run_sync" , { 226let registry = registry. clone (); 227let environment = environment. clone (); 228move |( mut options, input): ( Options , Option < JsBuffer >) |{ 229let generation = registry. tree . sync_generation (); 230// The blocking reader below drains both streams; backpressure keeps 231// a burst from outrunning it into the unread limit. 232 options. backpressure =[( 1 , & options. stdout ), ( 2 , & options. stderr )] 233. into_iter () 234. filter ( |( _, spec) | spec. mode =="piped" ) 235. map ( |( fd, _) | fd) 236. collect (); 237let input = input. map ( |bytes| bytes. to_vec ()); 238let result =( ||{ 239let info = registry. spawn ( options, & environment. borrow ()) ?; 240let process = registry. get ( info. id ) ?; 241 process. sync_wait . store ( true , Ordering :: SeqCst ); 242if !registry. worker && registry. tree . sync_generation () != generation{ 243 process. request ( "terminated" , None , false ); 244} 245let output =async { 246let write =async { 247if let Some ( pipe) = process. inputs . get ( & 0 ) { 248if let Some ( bytes) = input 249 && !bytes. is_empty () 250 &&let Err ( error) = pipe. write ( bytes). await 251 && error. kind () != io:: ErrorKind :: BrokenPipe 252{ 253 process. request ( "io-error" , Some ( error. to_string ()), false ); 254} 255 pipe. close (). await ; 256} 257}; 258let reads = futures:: future:: join_all ( process. outputs . iter (). map ( 259 |( fd, pipe) |async move { 260let mut bytes =Vec :: new (); 261while let Some ( chunk) = pipe. read (). await { 262 bytes. extend_from_slice ( & chunk); 263} 264( * fd, bytes) 265}, 266)); 267let ( _, bytes) = futures:: future:: join ( write, reads). await ; 268let status = process. wait ( true , None ). await ?. ok_or_else ( missing) ?; 269let mut bytes: BTreeMap < _ , _ > = bytes. into_iter (). collect (); 270Ok ::< _ , io:: Error >( SyncOutput { 271 status, 272stdout : bytes. remove ( & 1 ). map ( ToJsBuffer :: from), 273stderr : bytes. remove ( & 2 ). map ( ToJsBuffer :: from), 274}) 275}; 276// Sync ops may run inside a Tokio-driven JS event loop. 277let _enter = registry. runtime . enter (); 278let output = avril_host:: wait ( output, ||false ). unwrap (); 279 process. sync_wait . store ( false , Ordering :: SeqCst ); 280 registry. release ( info. id ); 281 output 282})(); 283Ok ::< _ , Infallible >( response ( result)) 284} 285})); 286 ops. push ( Op :: asynchronous_unreferenced ( "op_process_read" , { 287let registry = registry. clone (); 288move |( id, fd): ( u64 , u32 ) |{ 289let output = registry 290. get ( id) 291. and_then ( |process| process. outputs . get ( & fd). cloned (). ok_or_else ( missing)); 292async move { 293Ok ::< _ , Infallible >( response ( match output{ 294Ok ( output) =>Ok ( output 295. read () 296. await 297. map ( |bytes|ToJsBuffer :: from ( bytes. to_vec ()))), 298Err ( error) =>Err ( error), 299})) 300} 301} 302})); 303 ops. push ( Op :: asynchronous_unreferenced ( "op_process_write" , { 304let registry = registry. clone (); 305move |( id, fd, bytes): ( u64 , u32 , JsBuffer ) |{ 306let bytes = bytes. to_vec (); 307let input = registry 308. get ( id) 309. and_then ( |process| process. inputs . get ( & fd). cloned (). ok_or_else ( missing)); 310async move { 311Ok ::< _ , Infallible >( response ( match input{ 312Ok ( input) =>{ 313let result = input. write ( bytes). await ; 314if result 315. as_ref () 316. is_err_and ( |error| error. kind () == io:: ErrorKind :: BrokenPipe ) 317{ 318 input. close (). await ; 319} 320 result 321} 322Err ( error) =>Err ( error), 323})) 324} 325} 326})); 327 ops. push ( Op :: asynchronous_unreferenced ( "op_process_end" , { 328let registry = registry. clone (); 329move |( id, fd): ( u64 , u32 ) |{ 330let input = registry 331. get ( id) 332. and_then ( |process| process. inputs . get ( & fd). cloned (). ok_or_else ( missing)); 333async move { 334Ok ::< _ , Infallible >( response ( match input{ 335Ok ( input) =>{ 336 input. close (). await ; 337Ok (()) 338} 339Err ( error) =>Err ( error), 340})) 341} 342} 343})); 344 ops. push ( Op :: new ( "op_process_cancel_read" , { 345let registry = registry. clone (); 346move |( id, fd): ( u64 , u32 ) |{ 347let result = registry. get ( id). and_then ( |process|{ 348 process. outputs . get ( & fd). ok_or_else ( missing) ?. cancel (); 349Ok (()) 350}); 351Ok ::< _ , Infallible >( response ( result)) 352} 353})); 354 ops. push ( Op :: asynchronous_unreferenced ( "op_process_wait" , { 355let registry = registry. clone (); 356move |( id, completed): ( u64 , bool ) |{ 357let process = registry. get ( id); 358async move { 359Ok ::< _ , Infallible >( response ( match process{ 360Ok ( process) => process 361. wait ( completed, None ) 362. await 363. and_then ( |status| status. ok_or_else ( missing)), 364Err ( error) =>Err ( error), 365})) 366} 367} 368})); 369 ops. push ( Op :: asynchronous ( "op_process_keepalive" , { 370let registry = registry. clone (); 371move |( id, epoch): ( u64 , u32 ) |{ 372let process = registry. get ( id); 373async move { 374Ok ::< _ , Infallible >( response ( match process{ 375Ok ( process) => process. wait ( true , Some ( epoch)). await . map ( |_|()), 376Err ( error) =>Err ( error), 377})) 378} 379} 380})); 381 ops. push ( Op :: new ( "op_process_ref" , { 382let registry = registry. clone (); 383move |( id, _referenced): ( u64 , bool ) |{ 384let result = registry. get ( id). map ( |process| process. invalidate_waiters ()); 385Ok ::< _ , Infallible >( response ( result)) 386} 387})); 388 ops. push ( Op :: new ( "op_process_kill" , { 389let registry = registry. clone (); 390move |( id, signal): ( u64 , String ) |{ 391Ok ::< _ , Infallible >( response ( 392 registry. get ( id). and_then ( |process| process. kill ( & signal)), 393)) 394} 395})); 396 ops. push ( Op :: new ( "op_process_stop" , { 397let registry = registry. clone (); 398move |( id, reason): ( u64 , String ) |{ 399let result = registry 400. get ( id) 401. map ( |process| process. request ( & reason, None , false )); 402Ok ::< _ , Infallible >( response ( result)) 403} 404})); 405 ops. push ( Op :: new ( "op_process_release" , { 406let registry = registry. clone (); 407move |id : u64 |{ 408 registry. release ( id); 409Ok ::< _ , Infallible >(()) 410} 411})); 412 ops. push ( Op :: new ( "op_process_resize" , { 413let registry = registry. clone (); 414move |( id, columns, rows): ( u64 , u16 , u16 ) |{ 415let result = registry 416. get ( id) 417. and_then ( |process| process. resize ( columns, rows)); 418Ok ::< _ , Infallible >( response ( result)) 419} 420})); 421Extension { 422name : "ext:process" , 423dependencies : & [ "ext:encoding" , "ext:file" , "ext:web" , "ext:webstreams" ], 424 ops, 425modules : & [ 426( 427"ext:process/main.js" , 428include_str! ( concat! ( env! ( "OUT_DIR" ), "/process.js" )), 429), 430( 431"ext:process/signals.js" , 432include_str! ( concat! ( env! ( "OUT_DIR" ), "/signals.js" )), 433), 434], 435entry_point : None , 436} 437}