Skip to main content

gander_core/
action.rs

1//! Process actions.
2//!
3//! `core` never performs a side effect directly: it produces an `Action` that
4//! the front end executes, so the same logic can be unit-tested and dry-run.
5
6use crate::Pid;
7
8/// A process incarnation, not just a recyclable PID.
9#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
10pub struct ProcessTarget {
11    pub pid: Pid,
12    pub starttime: u64,
13}
14
15impl From<&crate::row::Row> for ProcessTarget {
16    fn from(row: &crate::row::Row) -> Self {
17        Self {
18            pid: row.pid,
19            starttime: row.starttime,
20        }
21    }
22}
23
24impl ProcessTarget {
25    pub fn capture(pid: Pid) -> std::io::Result<Self> {
26        valid_pid(pid)?;
27        Ok(Self {
28            pid,
29            starttime: current_starttime(pid)?,
30        })
31    }
32
33    /// Fail closed if the process exited, was replaced, or cannot be inspected.
34    /// On platforms without process handles, a small check/use race remains.
35    pub fn verify(self) -> std::io::Result<()> {
36        let current = Self::capture(self.pid)?;
37        if current == self {
38            Ok(())
39        } else {
40            Err(std::io::Error::other(
41                "process identity changed; refusing operation",
42            ))
43        }
44    }
45}
46
47fn valid_pid(pid: Pid) -> std::io::Result<()> {
48    if pid == 0 || pid > i32::MAX as u32 {
49        Err(std::io::Error::new(
50            std::io::ErrorKind::InvalidInput,
51            "invalid process PID",
52        ))
53    } else {
54        Ok(())
55    }
56}
57
58#[cfg(target_os = "linux")]
59fn current_starttime(pid: Pid) -> std::io::Result<u64> {
60    crate::linux::scan::read_stat(std::path::Path::new("/proc"), pid)
61        .map(|(_, stat)| stat.starttime)
62        .ok_or_else(|| std::io::Error::other("cannot verify process start time"))
63}
64
65#[cfg(target_os = "macos")]
66fn current_starttime(pid: Pid) -> std::io::Result<u64> {
67    crate::darwin::kinfo::list_processes()?
68        .into_iter()
69        .find(|p| p.pid == pid)
70        .map(|p| {
71            p.start_sec
72                .saturating_mul(1_000_000)
73                .saturating_add(p.start_usec)
74        })
75        .ok_or_else(|| std::io::Error::other("cannot verify process start time"))
76}
77
78#[cfg(target_os = "freebsd")]
79fn current_starttime(pid: Pid) -> std::io::Result<u64> {
80    crate::freebsd::scan::process(pid)
81        .map(|p| {
82            (p.start_sec.max(0) as u64)
83                .saturating_mul(1_000_000)
84                .saturating_add(p.start_usec.max(0) as u64)
85        })
86        .ok_or_else(|| std::io::Error::other("cannot verify process start time"))
87}
88
89#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
90fn current_starttime(_pid: Pid) -> std::io::Result<u64> {
91    Err(std::io::Error::new(
92        std::io::ErrorKind::Unsupported,
93        "cannot verify process identity",
94    ))
95}
96
97/// Execute against the sampled incarnation. Linux signals use a pidfd when
98/// available; priority changes and other platforms verify immediately before use.
99pub fn execute_checked(target: ProcessTarget, action: &Action) -> std::io::Result<()> {
100    let pid = match *action {
101        Action::Kill { pid, .. } | Action::Renice { pid, .. } | Action::Priority { pid, .. } => pid,
102    };
103    valid_pid(pid)?;
104    if pid != target.pid {
105        return Err(std::io::Error::new(
106            std::io::ErrorKind::InvalidInput,
107            "action target mismatch",
108        ));
109    }
110    #[cfg(target_os = "linux")]
111    if let Action::Kill { signal, .. } = *action {
112        use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
113        // SAFETY: pidfd_open takes a positive PID and zero flags.
114        let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
115        if fd >= 0 {
116            // SAFETY: the syscall returned a new, owned descriptor.
117            let fd = unsafe { OwnedFd::from_raw_fd(fd as i32) };
118            target.verify()?;
119            // SAFETY: the descriptor is live, siginfo is null and flags are zero.
120            let rc = unsafe {
121                libc::syscall(
122                    libc::SYS_pidfd_send_signal,
123                    fd.as_raw_fd(),
124                    signal,
125                    std::ptr::null::<libc::siginfo_t>(),
126                    0,
127                )
128            };
129            return if rc == 0 {
130                Ok(())
131            } else {
132                Err(std::io::Error::last_os_error())
133            };
134        }
135        let error = std::io::Error::last_os_error();
136        // Older kernels and non-leader thread IDs do not support pidfd_open.
137        if !matches!(
138            error.raw_os_error(),
139            Some(libc::ENOSYS) | Some(libc::EINVAL) | Some(libc::ENOENT)
140        ) {
141            return Err(error);
142        }
143    }
144    target.verify()?;
145    execute(action)
146}
147
148#[derive(Clone, Debug, PartialEq, Eq)]
149pub enum Action {
150    Kill {
151        pid: Pid,
152        signal: i32,
153    },
154    Renice {
155        pid: Pid,
156        nice: i8,
157    },
158    /// Raw priority adjustment; `Delta` is applied to the current nice value.
159    Priority {
160        pid: Pid,
161        delta: i8,
162    },
163}
164
165/// Signals offered by the signal picker, in the order htop shows them.
166pub const SIGNALS: &[(i32, &str)] = &[
167    (libc::SIGHUP, "SIGHUP"),
168    (libc::SIGINT, "SIGINT"),
169    (libc::SIGQUIT, "SIGQUIT"),
170    (libc::SIGILL, "SIGILL"),
171    (libc::SIGABRT, "SIGABRT"),
172    (libc::SIGKILL, "SIGKILL"),
173    (libc::SIGSEGV, "SIGSEGV"),
174    (libc::SIGTERM, "SIGTERM"),
175    (libc::SIGCHLD, "SIGCHLD"),
176    (libc::SIGCONT, "SIGCONT"),
177    (libc::SIGSTOP, "SIGSTOP"),
178    (libc::SIGTSTP, "SIGTSTP"),
179];
180
181/// Executes actions against the real system.
182///
183/// `kill(2)`, `getpriority(2)` and `setpriority(2)` are POSIX, so this works on
184/// every unix. Gating it on Linux meant `F9` silently failed on macOS with
185/// "process actions are only implemented for Linux".
186#[cfg(unix)]
187pub fn execute(action: &Action) -> Result<(), std::io::Error> {
188    use std::io;
189    let pid = match *action {
190        Action::Kill { pid, .. } | Action::Renice { pid, .. } | Action::Priority { pid, .. } => pid,
191    };
192    valid_pid(pid)?;
193    match *action {
194        Action::Kill { pid, signal } => {
195            // SAFETY: `kill` is async-signal-safe and takes only integers.
196            let rc = unsafe { libc::kill(pid as libc::pid_t, signal as libc::c_int) };
197            if rc == 0 {
198                Ok(())
199            } else {
200                Err(io::Error::last_os_error())
201            }
202        }
203        Action::Renice { pid, nice } => set_nice(pid, nice as i32),
204        Action::Priority { pid, delta } => {
205            // Read the current nice value. `-1` is both a valid priority and the
206            // documented error return, so errno is cleared first to tell them
207            // apart (the classic getpriority trap).
208            // SAFETY: both calls take only integers.
209            unsafe { set_errno(0) };
210            // SAFETY: `getpriority` has no preconditions.
211            let cur = unsafe { libc::getpriority(libc::PRIO_PROCESS, who(pid)) };
212            // SAFETY: reading errno has no preconditions.
213            if cur == -1 && unsafe { errno() } != 0 {
214                return Err(io::Error::last_os_error());
215            }
216            let target = (cur + delta as i32).clamp(-20, 19);
217            set_nice(pid, target)
218        }
219    }
220}
221
222/// Read the C `errno`. `libc` exposes it differently per platform.
223///
224/// SAFETY: no preconditions; the pointer is thread-local and always valid.
225#[cfg(unix)]
226unsafe fn errno() -> i32 {
227    #[cfg(any(target_os = "linux", target_os = "android"))]
228    {
229        *libc::__errno_location()
230    }
231    #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
232    {
233        *libc::__error()
234    }
235    #[cfg(not(any(
236        target_os = "linux",
237        target_os = "android",
238        target_os = "macos",
239        target_os = "ios",
240        target_os = "freebsd"
241    )))]
242    {
243        0
244    }
245}
246
247/// Set the C `errno`.
248///
249/// SAFETY: as above.
250#[cfg(unix)]
251unsafe fn set_errno(value: i32) {
252    #[cfg(any(target_os = "linux", target_os = "android"))]
253    {
254        *libc::__errno_location() = value;
255    }
256    #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
257    {
258        *libc::__error() = value;
259    }
260    #[cfg(not(any(
261        target_os = "linux",
262        target_os = "android",
263        target_os = "macos",
264        target_os = "ios",
265        target_os = "freebsd"
266    )))]
267    {
268        let _ = value;
269    }
270}
271
272/// The `who` argument for `getpriority`/`setpriority`.
273///
274/// POSIX types it `id_t`, and FreeBSD's `id_t` is genuinely 64-bit while its
275/// `getpriority` still takes an `int`; `libc` reproduces each platform's actual
276/// prototype, so the two spellings differ. Naming the type once keeps the call
277/// sites identical.
278#[cfg(any(target_os = "linux", target_os = "macos"))]
279fn who(pid: Pid) -> libc::id_t {
280    pid as libc::id_t
281}
282
283#[cfg(target_os = "freebsd")]
284fn who(pid: Pid) -> libc::c_int {
285    pid as libc::c_int
286}
287
288#[cfg(unix)]
289fn set_nice(pid: Pid, nice: i32) -> Result<(), std::io::Error> {
290    use std::io;
291    // SAFETY: integer arguments only.
292    let rc = unsafe { libc::setpriority(libc::PRIO_PROCESS, who(pid), nice) };
293    if rc == 0 {
294        Ok(())
295    } else {
296        Err(io::Error::last_os_error())
297    }
298}
299
300/// Non-unix fallback. There is no portable `kill(2)` here, and no supported
301/// target needs one yet.
302#[cfg(not(unix))]
303pub fn execute(_action: &Action) -> Result<(), std::io::Error> {
304    Err(std::io::Error::new(
305        std::io::ErrorKind::Unsupported,
306        "process actions require a unix target",
307    ))
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn actions_are_comparable() {
316        assert_eq!(
317            Action::Kill { pid: 1, signal: 9 },
318            Action::Kill { pid: 1, signal: 9 }
319        );
320        assert_ne!(
321            Action::Kill { pid: 1, signal: 9 },
322            Action::Kill { pid: 1, signal: 15 }
323        );
324    }
325
326    #[test]
327    fn signal_table_is_unique_and_contains_common_signals() {
328        let mut signals: Vec<i32> = SIGNALS.iter().map(|(n, _)| *n).collect();
329        let len = signals.len();
330        signals.sort_unstable();
331        signals.dedup();
332        assert_eq!(signals.len(), len, "duplicate signal numbers");
333        assert_eq!(signals.first(), Some(&1));
334        assert!(signals.contains(&9) && signals.contains(&15));
335    }
336}
337
338/// I/O scheduling class, as Linux's `ioprio_set` understands it.
339#[derive(Copy, Clone, PartialEq, Eq, Debug)]
340pub enum IoClass {
341    None,
342    Realtime,
343    BestEffort,
344    Idle,
345}
346
347impl IoClass {
348    /// The `IOPRIO_CLASS_*` value.
349    pub fn bits(self) -> u32 {
350        match self {
351            IoClass::None => 0,
352            IoClass::Realtime => 1,
353            IoClass::BestEffort => 2,
354            IoClass::Idle => 3,
355        }
356    }
357
358    pub fn from_bits(bits: u32) -> Option<Self> {
359        Some(match bits {
360            0 => IoClass::None,
361            1 => IoClass::Realtime,
362            2 => IoClass::BestEffort,
363            3 => IoClass::Idle,
364            _ => return None,
365        })
366    }
367
368    pub fn name(self) -> &'static str {
369        match self {
370            IoClass::None => "none",
371            IoClass::Realtime => "realtime",
372            IoClass::BestEffort => "best-effort",
373            IoClass::Idle => "idle",
374        }
375    }
376
377    /// The next class in the cycle the panel walks.
378    pub fn next(self) -> Self {
379        match self {
380            IoClass::None => IoClass::BestEffort,
381            IoClass::BestEffort => IoClass::Idle,
382            IoClass::Idle => IoClass::Realtime,
383            IoClass::Realtime => IoClass::None,
384        }
385    }
386}
387
388/// An I/O priority: a class plus a 0..=7 level within that class.
389#[derive(Copy, Clone, PartialEq, Eq, Debug)]
390pub struct IoPriority {
391    pub class: IoClass,
392    pub level: u8,
393}
394
395impl Default for IoPriority {
396    fn default() -> Self {
397        Self {
398            class: IoClass::None,
399            level: 0,
400        }
401    }
402}
403
404impl IoPriority {
405    pub fn new(class: IoClass, level: u8) -> Self {
406        Self {
407            class,
408            level: level.min(7),
409        }
410    }
411
412    /// The packed `ioprio` value the syscall takes.
413    pub fn to_bits(self) -> i32 {
414        (((self.class.bits() as i32) << 13) | self.level as i32) & 0xFFFF
415    }
416
417    pub fn from_bits(bits: i32) -> Self {
418        let class = IoClass::from_bits(((bits >> 13) & 0x7) as u32).unwrap_or(IoClass::None);
419        Self {
420            class,
421            level: (bits & 0x7) as u8,
422        }
423    }
424
425    /// `class/level`, e.g. `best-effort/4`.
426    pub fn label(self) -> String {
427        format!("{}/{}", self.class.name(), self.level)
428    }
429}
430
431/// Whether this build can read and set I/O priority.
432pub fn ioprio_supported() -> bool {
433    cfg!(target_os = "linux")
434}
435
436/// Read a process's I/O priority.
437///
438/// Linux only; elsewhere this returns `Err` with an explanation, because unlike
439/// CPU affinity there is no partial story to tell.
440#[cfg(target_os = "linux")]
441pub fn ioprio_get(pid: Pid) -> Result<IoPriority, std::io::Error> {
442    use std::io;
443    // SAFETY: `ioprio_get` is a plain syscall taking integers.
444    let rc = unsafe {
445        libc::syscall(libc::SYS_ioprio_get, 1 /* IOPRIO_WHO_PROCESS */, pid)
446    };
447    if rc < 0 {
448        return Err(io::Error::last_os_error());
449    }
450    Ok(IoPriority::from_bits(rc as i32))
451}
452
453#[cfg(not(target_os = "linux"))]
454pub fn ioprio_get(_pid: Pid) -> Result<IoPriority, std::io::Error> {
455    Err(std::io::Error::new(
456        std::io::ErrorKind::Unsupported,
457        "I/O priority is a Linux feature (ioprio_get)",
458    ))
459}
460
461#[cfg(target_os = "linux")]
462pub fn ioprio_set(pid: Pid, priority: IoPriority) -> Result<(), std::io::Error> {
463    use std::io;
464    // SAFETY: `ioprio_set` is a plain syscall taking integers.
465    let rc = unsafe {
466        libc::syscall(
467            libc::SYS_ioprio_set,
468            1, /* IOPRIO_WHO_PROCESS */
469            pid,
470            priority.to_bits(),
471        )
472    };
473    if rc < 0 {
474        Err(io::Error::last_os_error())
475    } else {
476        Ok(())
477    }
478}
479
480#[cfg(not(target_os = "linux"))]
481pub fn ioprio_set(_pid: Pid, _priority: IoPriority) -> Result<(), std::io::Error> {
482    Err(std::io::Error::new(
483        std::io::ErrorKind::Unsupported,
484        "I/O priority is a Linux feature (ioprio_set)",
485    ))
486}
487
488#[cfg(test)]
489mod ioprio_tests {
490    use super::*;
491
492    #[test]
493    fn bits_pack_and_unpack() {
494        for class in [
495            IoClass::None,
496            IoClass::Realtime,
497            IoClass::BestEffort,
498            IoClass::Idle,
499        ] {
500            for level in 0..=7u8 {
501                let p = IoPriority::new(class, level);
502                let back = IoPriority::from_bits(p.to_bits());
503                assert_eq!(back, p, "{:?}/{}", class, level);
504            }
505        }
506    }
507
508    #[test]
509    fn the_default_is_class_none() {
510        let p = IoPriority::default();
511        assert_eq!(p.class, IoClass::None);
512        assert_eq!(p.level, 0);
513        assert_eq!(p.to_bits(), 0);
514    }
515
516    #[test]
517    fn levels_are_clamped_to_seven() {
518        let p = IoPriority::new(IoClass::BestEffort, 200);
519        assert_eq!(p.level, 7);
520    }
521
522    #[test]
523    fn classes_cycle_through_every_variant() {
524        let mut c = IoClass::None;
525        let mut seen = vec![c];
526        for _ in 0..3 {
527            c = c.next();
528            seen.push(c);
529        }
530        assert_eq!(seen.len(), 4);
531        assert_eq!(c.next(), IoClass::None, "the cycle closes");
532    }
533
534    #[test]
535    fn labels_are_readable() {
536        let p = IoPriority::new(IoClass::BestEffort, 4);
537        assert_eq!(p.label(), "best-effort/4");
538    }
539
540    #[test]
541    fn an_out_of_range_class_degrades_to_none() {
542        let p = IoPriority::from_bits((7 << 13) | 3);
543        assert_eq!(p.class, IoClass::None);
544        assert_eq!(p.level, 3);
545    }
546
547    #[cfg(target_os = "linux")]
548    #[test]
549    fn reading_our_own_ioprio_works() {
550        let p = ioprio_get(std::process::id()).expect("ioprio_get on self");
551        // Any class is fine; the call must simply succeed.
552        assert!(matches!(
553            p.class,
554            IoClass::None | IoClass::Realtime | IoClass::BestEffort | IoClass::Idle
555        ));
556    }
557
558    #[cfg(target_os = "linux")]
559    #[test]
560    fn setting_our_own_ioprio_round_trips() {
561        let original = ioprio_get(std::process::id()).unwrap();
562        let target = IoPriority::new(IoClass::BestEffort, 6);
563        if ioprio_set(std::process::id(), target).is_ok() {
564            assert_eq!(ioprio_get(std::process::id()).unwrap(), target);
565        }
566        let _ = ioprio_set(std::process::id(), original);
567    }
568
569    #[cfg(not(target_os = "linux"))]
570    #[test]
571    fn unsupported_platforms_explain_themselves() {
572        assert!(!ioprio_supported());
573        let err = ioprio_get(std::process::id()).unwrap_err();
574        assert_eq!(err.kind(), std::io::ErrorKind::Unsupported);
575        assert!(err.to_string().contains("Linux"));
576    }
577}