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
10.8 KiB339 linesraw
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)]
17    unsafe {
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.
26        use std::io::Read as _;
27        let mut stdin = std::io::stdin();
28        let mut buffer = [0; 1024];
29        loop {
30            match stdin.read(&mut buffer) {
31                Ok(0) | Err(_) => {
32                    unsafe {
33                        libc::kill(-libc::getpid(), libc::SIGKILL);
34                    }
35                    std::process::exit(1);
36                }
37                Ok(_) => {}
38            }
39        }
40    }
41    #[cfg(not(unix))]
42    loop {
43        std::thread::park();
44    }
45}
46
47#[cfg(unix)]
48struct AnchorProcess {
49    child: 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)]
59    pid: u32,
60    #[cfg(unix)]
61    anchor: Mutex<Option<AnchorProcess>>,
62    #[cfg(windows)]
63    job: Mutex<Option<std::sync::Arc<crate::job::Job>>>,
64}
65
66impl Group {
67    pub fn new(host: &Path) -> io::Result<Self> {
68        #[cfg(unix)]
69        {
70            use std::os::unix::process::CommandExt as _;
71            let (reader, writer) = std::io::pipe()?;
72            let 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()?;
79            Ok(Self {
80                pid: child.id(),
81                anchor: Mutex::new(Some(AnchorProcess { child, _owner: writer })),
82            })
83        }
84        #[cfg(windows)]
85        {
86            let _ = host;
87            Ok(Self {
88                job: Mutex::new(Some(std::sync::Arc::new(crate::job::Job::new(0)?))),
89            })
90        }
91        #[cfg(not(any(unix, windows)))]
92        {
93            let _ = host;
94            Err(io::Error::new(
95                io::ErrorKind::Unsupported,
96                "Process groups are not supported",
97            ))
98        }
99    }
100
101    #[cfg(unix)]
102    pub(crate) fn pgid(&self) -> io::Result<u32> {
103        if self.anchor.lock().unwrap().is_none() {
104            return Err(io::Error::new(
105                io::ErrorKind::NotConnected,
106                "Process group is closed",
107            ));
108        }
109        Ok(self.pid)
110    }
111    #[cfg(windows)]
112    pub(crate) fn job(&self) -> io::Result<std::sync::Arc<crate::job::Job>> {
113        self.job
114            .lock()
115            .unwrap()
116            .clone()
117            .ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, "Process group is closed"))
118    }
119
120    pub fn signal(&self, name: &str) -> io::Result<bool> {
121        #[cfg(unix)]
122        {
123            let anchor = self.anchor.lock().unwrap();
124            if anchor.is_none() {
125                return Ok(false);
126            }
127            crate::signals::send(self.pid, true, crate::signals::signal_number(name)?)
128        }
129        #[cfg(windows)]
130        {
131            crate::validate_signal(name)?;
132            self.job
133                .lock()
134                .unwrap()
135                .as_ref()
136                .map_or(Ok(false), |job| job.terminate())
137        }
138        #[cfg(not(any(unix, windows)))]
139        {
140            let _ = name;
141            Ok(false)
142        }
143    }
144
145    /// Uncertainty counts as alive; the Unix anchor and zombies do not.
146    pub fn alive(&self) -> bool {
147        #[cfg(unix)]
148        {
149            let anchor = self.anchor.lock().unwrap();
150            if anchor.is_none() {
151                return 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"))]
156            if let Some(alive) = linux_group_alive(self.pid) {
157                return alive;
158            }
159            #[cfg(target_os = "macos")]
160            if let Some(alive) = macos_group_alive(self.pid) {
161                return 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.
166            true
167        }
168        #[cfg(windows)]
169        {
170            self.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)))]
177        false
178    }
179
180    /// Kills the scope and prevents further launches. Does not wait for members.
181    pub fn close(&self) {
182        #[cfg(unix)]
183        if let Some(mut anchor) = self.anchor.lock().unwrap().take() {
184            let _ = crate::signals::send(self.pid, true, libc::SIGKILL);
185            let _ = anchor.child.kill();
186            let _ = anchor.child.wait();
187        }
188        #[cfg(windows)]
189        if let Some(job) = self.job.lock().unwrap().take() {
190            let _ = job.terminate();
191        }
192    }
193}
194
195impl Drop for Group {
196    fn drop(&mut self) {
197        self.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 {
210    let hidepid = std::fs::read_to_string("/proc/mounts").is_ok_and(|mounts| {
211        mounts.lines().any(|line| {
212            let mut fields = line.split(' ');
213            let _device = fields.next();
214            let mountpoint = fields.next();
215            let _kind = fields.next();
216            let options = fields.next().unwrap_or("");
217            mountpoint == Some("/proc")
218                && options.split(',').any(|option| {
219                    matches!(
220                        option,
221                        "hidepid=1" | "hidepid=2" | "hidepid=4" | "hidepid=invisible"
222                    )
223                })
224        })
225    });
226    if !hidepid {
227        return true;
228    }
229    let 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> {
241    if !enumeration_complete() {
242        return 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.
247    let mut found = false;
248    for entry in std::fs::read_dir("/proc").ok()? {
249        let Ok(entry) = entry else { continue };
250        let Ok(pid) = entry.file_name().to_string_lossy().parse::<u32>() else {
251            continue;
252        };
253        if pid == pgid {
254            continue;
255        }
256        // Membership first, via syscall: an unrelated process that
257        // cannot be read must not pin this group.
258        let member = unsafe { libc::getpgid(pid as i32) };
259        if member < 0 {
260            if std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH) {
261                return Some(true);
262            }
263            continue;
264        }
265        if member as u32 != pgid {
266            continue;
267        }
268        let Ok(stat) = std::fs::read(entry.path().join("stat")) else {
269            return Some(true);
270        };
271        let Some(close) = stat.iter().rposition(|byte| *byte == b')') else {
272            return Some(true);
273        };
274        let mut fields = stat[close + 1..]
275            .split(|byte| byte.is_ascii_whitespace())
276            .filter(|field| !field.is_empty());
277        let state = fields.next();
278        let _ppid = fields.next();
279        let group = fields
280            .next()
281            .and_then(|value| std::str::from_utf8(value).ok())
282            .and_then(|value| value.parse::<u32>().ok());
283        let Some(group) = group else {
284            return Some(true);
285        };
286        if group == pgid && state != Some(b"Z".as_slice()) && state != Some(b"X".as_slice()) {
287            found = true;
288        }
289    }
290    Some(found)
291}
292
293#[cfg(target_os = "macos")]
294fn macos_group_alive(pgid: u32) -> Option<bool> {
295    let mut capacity = 1024usize;
296    let listed = loop {
297        let mut pids_buffer = vec![0i32; capacity];
298        let 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        };
304        if listed <= 0 {
305            return None;
306        }
307        if (listed as usize) < capacity {
308            pids_buffer.truncate(listed as usize);
309            break pids_buffer;
310        }
311        capacity *= 4;
312        if capacity > 1 << 20 {
313            return None;
314        }
315        pids_buffer.clear();
316    };
317    let mut found = false;
318    for pid in listed {
319        if pid <= 0 || pid as u32 == pgid {
320            continue;
321        }
322        let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
323        let size = std::mem::size_of::<libc::proc_bsdinfo>() as i32;
324        let read = unsafe {
325            libc::proc_pidinfo(pid, libc::PROC_PIDTBSDINFO, 0, (&raw mut info).cast(), size)
326        };
327        if read != size {
328            if std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH) {
329                return Some(true);
330            }
331            continue;
332        }
333        // Status 5 is SZOMB: a zombie is already dead.
334        if info.pbi_pgid == pgid && info.pbi_status != 5 {
335            found = true;
336        }
337    }
338    Some(found)
339}