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:: path:: Path ; 3# [ cfg ( unix )] 4use std:: process:: Stdio ; 5# [ cfg ( unix )] 6use std:: process::{ Child , Command }; 7# [ cfg ( any ( unix , windows ))] 8use std:: sync:: Mutex ; 9 10pub const ANCHOR_ARG : & str ="__avril_task_anchor" ; 11 12/// Hidden re-exec target: a single process that leads the process group 13/// a watched run's commands join. It ignores INT/TERM so the group keeps 14/// a live, run-owned identity until the watcher escalates to KILL. 15pub fn anchor () -> !{ 16# [ cfg ( unix )] 17unsafe { 18 libc:: signal ( libc:: SIGINT , libc:: SIG_IGN ); 19 libc:: signal ( libc:: SIGTERM , libc:: SIG_IGN ); 20} 21# [ cfg ( unix )] 22{ 23// Standard input is a pipe owned by the watcher. Its EOF means 24// the watcher died without cleaning up (SIGKILL, SIGHUP, ...): 25// take the whole group down rather than park forever, orphaned. 26use std:: io:: Read as _; 27let mut stdin = std:: io:: stdin (); 28let mut buffer =[ 0 ; 1024 ]; 29loop { 30match stdin. read ( & mut buffer) { 31Ok ( 0 ) |Err ( _) =>{ 32unsafe { 33 libc:: kill ( -libc:: getpid (), libc:: SIGKILL ); 34} 35 std:: process:: exit ( 1 ); 36} 37Ok ( _) =>{} 38} 39} 40} 41# [ cfg ( not ( unix ))] 42loop { 43 std:: thread:: park (); 44} 45} 46 47# [ cfg ( unix )] 48struct AnchorProcess { 49child : Child , 50/// Held so its closing (watcher death or cleanup) tells the anchor to 51/// take the group down. 52_owner : std:: io:: PipeWriter , 53} 54 55/// A process group whose lifetime is independent of any one command. 56/// On Unix the host executable must dispatch ANCHOR_ARG to anchor(). 57pub struct Group { 58# [ cfg ( unix )] 59pid : u32 , 60# [ cfg ( unix )] 61anchor : Mutex < Option < AnchorProcess >>, 62# [ cfg ( windows )] 63job : Mutex < Option < std:: sync:: Arc < crate :: job:: Job >>>, 64} 65 66impl Group { 67pub fn new ( host : & Path ) -> io:: Result < Self > { 68# [ cfg ( unix )] 69{ 70use std:: os:: unix:: process:: CommandExt as _; 71let ( reader, writer) = std:: io:: pipe () ?; 72let child =Command :: new ( host) 73. arg ( ANCHOR_ARG ) 74. stdin ( Stdio :: from ( reader)) 75. stdout ( Stdio :: null ()) 76. stderr ( Stdio :: null ()) 77. process_group ( 0 ) 78. spawn () ?; 79Ok ( Self { 80pid : child. id (), 81anchor : Mutex :: new ( Some ( AnchorProcess { child, _owner : writer})), 82}) 83} 84# [ cfg ( windows )] 85{ 86let _ = host; 87Ok ( Self { 88job : Mutex :: new ( Some ( std:: sync:: Arc :: new ( crate :: job:: Job :: new ( 0 ) ?))), 89}) 90} 91# [ cfg ( not ( any ( unix , windows )))] 92{ 93let _ = host; 94Err ( io:: Error :: new ( 95 io:: ErrorKind :: Unsupported , 96"Process groups are not supported" , 97)) 98} 99} 100 101# [ cfg ( unix )] 102pub ( crate ) fn pgid ( & self ) -> io:: Result < u32 > { 103if self . anchor . lock (). unwrap (). is_none () { 104return Err ( io:: Error :: new ( 105 io:: ErrorKind :: NotConnected , 106"Process group is closed" , 107)); 108} 109Ok ( self . pid ) 110} 111# [ cfg ( windows )] 112pub ( crate ) fn job ( & self ) -> io:: Result < std:: sync:: Arc < crate :: job:: Job >> { 113self . job 114. lock () 115. unwrap () 116. clone () 117. ok_or_else ( || io:: Error :: new ( io:: ErrorKind :: NotConnected , "Process group is closed" )) 118} 119 120pub fn signal ( & self , name : & str ) -> io:: Result < bool > { 121# [ cfg ( unix )] 122{ 123let anchor =self . anchor . lock (). unwrap (); 124if anchor. is_none () { 125return Ok ( false ); 126} 127crate :: signals:: send ( self . pid , true , crate :: signals:: signal_number ( name) ?) 128} 129# [ cfg ( windows )] 130{ 131crate :: validate_signal ( name) ?; 132self . job 133. lock () 134. unwrap () 135. as_ref () 136. map_or ( Ok ( false ), |job| job. terminate ()) 137} 138# [ cfg ( not ( any ( unix , windows )))] 139{ 140let _ = name; 141Ok ( false ) 142} 143} 144 145/// Uncertainty counts as alive; the Unix anchor and zombies do not. 146pub fn alive ( & self ) ->bool { 147# [ cfg ( unix )] 148{ 149let anchor =self . anchor . lock (). unwrap (); 150if anchor. is_none () { 151return false ; 152} 153// The anchor itself is not a task process; only other members 154// count, and zombies do not: they are already dead. 155# [ cfg ( any ( target_os = "linux" , target_os = "android" ))] 156if let Some ( alive) =linux_group_alive ( self . pid ) { 157return alive; 158} 159# [ cfg ( target_os = "macos" )] 160if let Some ( alive) =macos_group_alive ( self . pid ) { 161return alive; 162} 163// No anchor-excluding membership check on this platform: report 164// uncertainty rather than reap the anchor (which would release 165// the group identity) or count the anchor itself. 166true 167} 168# [ cfg ( windows )] 169{ 170self . job 171. lock () 172. unwrap () 173. as_ref () 174. is_some_and ( |job| job. alive (). unwrap_or ( true )) 175} 176# [ cfg ( not ( any ( unix , windows )))] 177false 178} 179 180/// Kills the scope and prevents further launches. Does not wait for members. 181pub fn close ( & self ) { 182# [ cfg ( unix )] 183if let Some ( mut anchor) =self . anchor . lock (). unwrap (). take () { 184let _ =crate :: signals:: send ( self . pid , true , libc:: SIGKILL ); 185let _ = anchor. child . kill (); 186let _ = anchor. child . wait (); 187} 188# [ cfg ( windows )] 189if let Some ( job) =self . job . lock (). unwrap (). take () { 190let _ = job. terminate (); 191} 192} 193} 194 195impl Drop for Group { 196fn drop ( & mut self ) { 197self . close (); 198} 199} 200 201/// Whether any non-zombie process other than the anchor leads a life in 202/// the group. Zombies are dead; waiting for someone else to reap them 203/// would hang termination. 204/// Whether process enumeration can be trusted to see this group: a 205/// hidepid mount without CAP_SYS_PTRACE can hide even same-uid members 206/// (e.g. a task that cleared its dumpable flag), and an empty listing 207/// from such a mount is not proof that the group is gone. 208# [ cfg ( any ( target_os = "linux" , target_os = "android" ))] 209fn enumeration_complete () ->bool { 210let hidepid = std:: fs:: read_to_string ( "/proc/mounts" ). is_ok_and ( |mounts|{ 211 mounts. lines (). any ( |line|{ 212let mut fields = line. split ( ' ' ); 213let _device = fields. next (); 214let mountpoint = fields. next (); 215let _kind = fields. next (); 216let options = fields. next (). unwrap_or ( "" ); 217 mountpoint ==Some ( "/proc" ) 218 && options. split ( ',' ). any ( |option|{ 219matches! ( 220 option, 221"hidepid=1" |"hidepid=2" |"hidepid=4" |"hidepid=invisible" 222) 223}) 224}) 225}); 226if !hidepid{ 227return true ; 228} 229let cap_sys_ptrace =1u64 <<19 ; 230 std:: fs:: read_to_string ( "/proc/self/status" ). is_ok_and ( |status|{ 231 status. lines (). any ( |line|{ 232 line. strip_prefix ( "CapEff:" ) 233. and_then ( |value| u64:: from_str_radix ( value. trim (), 16 ). ok ()) 234. is_some_and ( |caps| caps& cap_sys_ptrace !=0 ) 235}) 236}) 237} 238 239# [ cfg ( any ( target_os = "linux" , target_os = "android" ))] 240fn linux_group_alive ( pgid : u32 ) ->Option < bool > { 241if !enumeration_complete () { 242return None ; 243} 244// Parsed as bytes: a process name may hold arbitrary bytes, and an 245// entry that cannot be understood counts as uncertainty (alive), 246// never as proof that the group is gone. 247let mut found =false ; 248for entryin std:: fs:: read_dir ( "/proc" ). ok () ?{ 249let Ok ( entry) = entryelse { continue }; 250let Ok ( pid) = entry. file_name (). to_string_lossy (). parse ::< u32 >() else { 251continue ; 252}; 253if pid == pgid{ 254continue ; 255} 256// Membership first, via syscall: an unrelated process that 257// cannot be read must not pin this group. 258let member =unsafe { libc:: getpgid ( pidas i32 ) }; 259if member <0 { 260if std:: io:: Error :: last_os_error (). raw_os_error () !=Some ( libc:: ESRCH ) { 261return Some ( true ); 262} 263continue ; 264} 265if memberas u32 != pgid{ 266continue ; 267} 268let Ok ( stat) = std:: fs:: read ( entry. path (). join ( "stat" )) else { 269return Some ( true ); 270}; 271let Some ( close) = stat. iter (). rposition ( |byte|* byte ==b')' ) else { 272return Some ( true ); 273}; 274let mut fields = stat[ close +1 ..] 275. split ( |byte| byte. is_ascii_whitespace ()) 276. filter ( |field| !field. is_empty ()); 277let state = fields. next (); 278let _ppid = fields. next (); 279let group = fields 280. next () 281. and_then ( |value| std:: str:: from_utf8 ( value). ok ()) 282. and_then ( |value| value. parse ::< u32 >(). ok ()); 283let Some ( group) = groupelse { 284return Some ( true ); 285}; 286if group == pgid && state !=Some ( b"Z" . as_slice ()) && state !=Some ( b"X" . as_slice ()) { 287 found =true ; 288} 289} 290Some ( found) 291} 292 293# [ cfg ( target_os = "macos" )] 294fn macos_group_alive ( pgid : u32 ) ->Option < bool > { 295let mut capacity =1024usize ; 296let listed =loop { 297let mut pids_buffer =vec! [ 0i32 ; capacity]; 298let listed =unsafe { 299 libc:: proc_listallpids ( 300 pids_buffer. as_mut_ptr (). cast (), 301( pids_buffer. len () * std:: mem:: size_of ::< i32 >()) as i32 , 302) 303}; 304if listed <=0 { 305return None ; 306} 307if ( listedas usize ) < capacity{ 308 pids_buffer. truncate ( listedas usize ); 309break pids_buffer; 310} 311 capacity *=4 ; 312if capacity >1 <<20 { 313return None ; 314} 315 pids_buffer. clear (); 316}; 317let mut found =false ; 318for pidin listed{ 319if pid <=0 || pidas u32 == pgid{ 320continue ; 321} 322let mut info: libc:: proc_bsdinfo =unsafe { std:: mem:: zeroed () }; 323let size = std:: mem:: size_of ::< libc:: proc_bsdinfo >() as i32 ; 324let read =unsafe { 325 libc:: proc_pidinfo ( pid, libc:: PROC_PIDTBSDINFO , 0 , ( & raw mut info). cast (), size) 326}; 327if read != size{ 328if std:: io:: Error :: last_os_error (). raw_os_error () !=Some ( libc:: ESRCH ) { 329return Some ( true ); 330} 331continue ; 332} 333// Status 5 is SZOMB: a zombie is already dead. 334if info. pbi_pgid == pgid && info. pbi_status !=5 { 335 found =true ; 336} 337} 338Some ( found) 339}