char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
f4a0f5b
main
1use std:: io; 2use std:: sync:: atomic::{ AtomicBool , AtomicU64 , Ordering }; 3use std:: sync::{ Arc , Mutex , OnceLock , Weak }; 4use std:: time:: Duration ; 5 6use tokio:: runtime:: Handle ; 7use tokio_util:: sync:: CancellationToken ; 8 9pub struct Tree { 10processes : Mutex < Vec < Weak < super :: Process >>>, 11sync_interrupt : AtomicU64 , 12installed : AtomicBool , 13/// Whether the install owns SIGINT (run mode). Serve and the REPL 14/// claim SIGINT through the tree instead; listener feeds consult 15/// holds() to tell a supervisor-owned death from one they must 16/// emulate themselves. 17interrupt : AtomicBool , 18/// Claimed by the subsystem that owns SIGINT shutdown or interrupt 19/// handling (serve's CLI listener, the REPL). Read by the supervisor 20/// loop when it holds SIGINT; otherwise served by claim_interrupt's 21/// dedicated feed. Either way the owner shares one chaining registry 22/// with listener feeds, so install order never matters. 23# [ cfg_attr ( windows , allow ( dead_code ))] 24interrupt_owner : Mutex < Option < Arc < dyn Fn () +Send +Sync >>>, 25/// Guards claim_interrupt's dedicated feed against double installs. 26# [ cfg ( unix )] 27interrupt_claimed : AtomicBool , 28/// Blocks new spawns once the process is dying, so respawn loops cannot 29/// relaunch children during the shutdown grace period. 30closed : AtomicBool , 31/// Claimed by the subsystem that owns SIGTERM shutdown (serve's CLI 32/// listener). The tree is the sole TERM listener, so claiming and 33/// dispatch serialize on this slot: a delivery either runs the owner or 34/// predates it, never both and never neither. 35# [ cfg_attr ( windows , allow ( dead_code ))] 36term_owner : Mutex < Option < Arc < dyn Fn () +Send +Sync >>>, 37pub ( super ) stopped : CancellationToken , 38} 39impl Tree { 40pub fn global () ->Arc < Self > { 41static TREE : OnceLock < Arc < Tree >> =OnceLock :: new (); 42TREE . get_or_init ( ||{ 43Arc :: new ( Self { 44processes : Mutex :: new ( Vec :: new ()), 45sync_interrupt : AtomicU64 :: new ( 0 ), 46installed : AtomicBool :: new ( false ), 47interrupt : AtomicBool :: new ( false ), 48interrupt_owner : Mutex :: new ( None ), 49# [ cfg ( unix )] 50interrupt_claimed : AtomicBool :: new ( false ), 51closed : AtomicBool :: new ( false ), 52term_owner : Mutex :: new ( None ), 53stopped : CancellationToken :: new (), 54}) 55}) 56. clone () 57} 58pub ( super ) fn register ( & self , process : & Arc < super :: Process >) { 59let mut processes =self . processes . lock (). unwrap (); 60 processes. retain ( |process| process. strong_count () !=0 ); 61 processes. push ( Arc :: downgrade ( process)); 62} 63pub fn force_live_processes ( & self ) { 64self . closed . store ( true , Ordering :: Release ); 65for processin self . live () { 66 process. request ( "terminated" , None , true ); 67} 68} 69pub ( super ) fn sync_generation ( & self ) ->u64 { 70self . sync_interrupt . load ( Ordering :: SeqCst ) 71} 72# [ cfg ( unix )] 73pub fn claim_term ( 74self : & Arc < Self >, 75runtime : & Handle , 76owner : Arc < dyn Fn () +Send +Sync >, 77) -> io:: Result <()> { 78* self . term_owner . lock (). unwrap () =Some ( owner); 79self . install ( runtime, false ) 80} 81/// Hands SIGINT deliveries to a subsystem owner through the shared 82/// chaining registry, alongside any listener feeds. Modes whose 83/// supervisor loop does not hold SIGINT get a dedicated feed; run 84/// mode's loop routes to the owner itself. 85# [ cfg ( unix )] 86pub fn claim_interrupt ( 87self : & Arc < Self >, 88runtime : & Handle , 89owner : Arc < dyn Fn () +Send +Sync >, 90) -> io:: Result <()> { 91* self . interrupt_owner . lock (). unwrap () =Some ( owner. clone ()); 92if self . interrupt . load ( Ordering :: Acquire ) 93 ||self . interrupt_claimed . swap ( true , Ordering :: AcqRel ) 94{ 95return Ok (()); 96} 97let _enter = runtime. enter (); 98let mut stream = 99match tokio:: signal:: unix:: signal ( tokio:: signal:: unix:: SignalKind :: interrupt ()) { 100Ok ( stream) => stream, 101Err ( error) =>{ 102* self . interrupt_owner . lock (). unwrap () =None ; 103self . interrupt_claimed . store ( false , Ordering :: Release ); 104return Err ( error); 105} 106}; 107 runtime. spawn ( async move { 108while stream. recv (). await . is_some () { 109owner (); 110} 111}); 112Ok (()) 113} 114pub ( super ) fn is_closed ( & self ) ->bool { 115self . closed . load ( Ordering :: Acquire ) 116} 117/// Whether the supervisor itself holds this signal's handler, and so 118/// owns its default fate. Listener feeds emulate the prior disposition 119/// only for signals nobody holds. 120# [ cfg ( unix )] 121pub ( crate ) fn holds ( & self , signal : libc:: c_int ) ->bool { 122if !self . installed . load ( Ordering :: Acquire ) { 123return false ; 124} 125 signal == libc:: SIGTERM 126 ||( signal == libc:: SIGINT 127 &&( self . interrupt . load ( Ordering :: Acquire ) 128 ||self . interrupt_owner . lock (). unwrap (). is_some ())) 129} 130/// Asks every live process to shut down with its configured signal and 131/// grace period. One shot: already-finished processes ignore the request, 132/// and later spawns start fresh. 133pub ( crate ) fn stop_live_processes ( & self ) { 134for processin self . live () { 135 process. request ( "terminated" , None , false ); 136} 137} 138/// Stops only processes blocked in a synchronous wait, leaving other 139/// session work alone. The REPL uses this so Ctrl-C interrupts runSync 140/// without cancelling background operations. 141pub fn stop_sync_processes ( & self ) { 142// Pair the generation with runSync's waiter publication. SeqCst 143// ensures an interrupt racing spawn sees the waiter, or the waiter 144// sees the interrupt, rather than both missing each other. 145self . sync_interrupt . fetch_add ( 1 , Ordering :: SeqCst ); 146for processin self . live () { 147if !process. worker && process. sync_wait . load ( Ordering :: SeqCst ) { 148 process. request ( "terminated" , None , false ); 149} 150} 151} 152fn live ( & self ) ->Vec < Arc < super :: Process >> { 153self . processes 154. lock () 155. unwrap () 156. iter () 157. filter_map ( Weak :: upgrade) 158. collect () 159} 160pub ( super ) fn install ( 161self : & Arc < Self >, 162runtime : & Handle , 163handle_interrupt : bool , 164) -> io:: Result <()> { 165if self . installed . swap ( true , Ordering :: AcqRel ) { 166return Ok (()); 167} 168self . interrupt . store ( handle_interrupt, Ordering :: Release ); 169let _enter = runtime. enter (); 170let mut signals =match Signals :: listen ( handle_interrupt) { 171Ok ( Some ( signals)) => signals, 172Ok ( None ) =>return Ok (()), 173Err ( error) =>{ 174self . installed . store ( false , Ordering :: Release ); 175return Err ( error); 176} 177}; 178let tree =self . clone (); 179 runtime. spawn ( async move { 180loop { 181let delivery = signals. recv (). await ; 182// A claimed interrupt belongs to its owner (serve 183// shutdown, REPL eval interrupt); listeners still fire 184// through their own feeds. 185# [ cfg ( unix )] 186if matches! ( delivery, Delivery :: Interrupt ) { 187let owner = tree. interrupt_owner . lock (). unwrap (). clone (); 188if let Some ( owner) = owner{ 189owner (); 190continue ; 191} 192} 193// Claimed TERM belongs to its owner, including repeats. 194// In-flight children keep running during serve's drain. 195# [ cfg ( unix )] 196if matches! ( delivery, Delivery :: Terminate ) { 197let owner = tree. term_owner . lock (). unwrap (). clone (); 198if let Some ( owner) = owner{ 199owner (); 200continue ; 201} 202} 203// Listened signals belong to their JS listeners: skip the 204// shutdown and death below, so delivery does not kill the 205// process while listeners exist. Serve's claimed TERM still 206// runs its owner above. 207if super :: super :: signals:: listener_count ( delivery. name ()) >0 { 208continue ; 209} 210 tree. closed . store ( true , Ordering :: Release ); 211 tree. stop_live_processes (); 212// Bounded shutdown, abortable by an impatient repeat. 213let live = tree. live (); 214let pending = 215 futures:: future:: join_all ( live. iter (). map ( |process| process. wait ( true , None ))); 216 tokio:: select!{ 217 _ = tokio:: time:: timeout ( Duration :: from_secs ( 2 ), pending) =>{}, 218 _ = signals. recv () =>{}, 219} 220// Grace-resisting descendants cannot outlive the owner. 221for processin tree. live () { 222 process. kill_scope (); 223} 224 tree. stopped . cancel (); 225 delivery. die (); 226return ; 227} 228}); 229Ok (()) 230} 231} 232 233# [ derive ( Clone , Copy )] 234enum Delivery { 235Interrupt , 236# [ cfg_attr ( windows , allow ( dead_code ))] 237Terminate , 238} 239 240impl Delivery { 241fn name ( self ) ->&' static str { 242match self { 243Self :: Interrupt =>"SIGINT" , 244Self :: Terminate =>"SIGTERM" , 245} 246} 247} 248 249struct Signals { 250interrupt : Option < Stream >, 251terminate : Option < Stream >, 252} 253impl Signals { 254async fn recv ( & mut self ) ->Delivery { 255async fn next ( stream : & mut Option < Stream >) { 256if let Some ( stream) = stream 257 && stream. recv (). await . is_some () 258{ 259return ; 260} 261 std:: future:: pending (). await 262} 263 tokio:: select!{ 264 _ =next ( & mut self . interrupt ) =>Delivery :: Interrupt , 265 _ =next ( & mut self . terminate ) =>Delivery :: Terminate , 266} 267} 268} 269 270# [ cfg ( unix )] 271type Stream = tokio:: signal:: unix:: Signal ; 272# [ cfg ( unix )] 273impl Signals { 274fn listen ( handle_interrupt : bool ) -> io:: Result < Option < Self >> { 275use tokio:: signal:: unix::{ SignalKind , signal}; 276// An inherited SIG_IGN stays ignored: taking over the signal would 277// change the fate of a process that was launched to survive it. 278// Consult the startup snapshot, never the live disposition, which 279// earlier listener feeds may already have taken over. 280let interrupt = 281if handle_interrupt && !avril_subprocess:: signals:: originally_ignored ( libc:: SIGINT ) { 282Some ( signal ( SignalKind :: interrupt ()) ?) 283} else { 284None 285}; 286let terminate =if avril_subprocess:: signals:: originally_ignored ( libc:: SIGTERM ) { 287None 288} else { 289Some ( signal ( SignalKind :: terminate ()) ?) 290}; 291Ok (( interrupt. is_some () || terminate. is_some ()). then_some ( Self { interrupt, terminate})) 292} 293} 294# [ cfg ( unix )] 295impl Delivery { 296fn die ( self ) { 297let number =match self { 298Self :: Interrupt => libc:: SIGINT , 299Self :: Terminate => libc:: SIGTERM , 300}; 301unsafe { 302 libc:: signal ( number, libc:: SIG_DFL ); 303 libc:: raise ( number); 304} 305} 306} 307 308// Windows has no termination signal to supervise: console close, logoff and 309// shutdown end the process, and kill-on-close jobs take the children along. 310# [ cfg ( windows )] 311type Stream = tokio:: signal:: windows:: CtrlC ; 312# [ cfg ( windows )] 313impl Signals { 314fn listen ( handle_interrupt : bool ) -> io:: Result < Option < Self >> { 315if !handle_interrupt{ 316return Ok ( None ); 317} 318Ok ( Some ( Self { 319interrupt : Some ( tokio:: signal:: windows:: ctrl_c () ?), 320terminate : None , 321})) 322} 323} 324# [ cfg ( windows )] 325impl Delivery { 326fn die ( self ) { 327// The default console control handler exits with STATUS_CONTROL_C_EXIT. 328 std:: process:: exit ( 0xC000013Au32 as i32 ); 329} 330}