1use crate::Pid;
7
8#[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 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
97pub 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 let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
115 if fd >= 0 {
116 let fd = unsafe { OwnedFd::from_raw_fd(fd as i32) };
118 target.verify()?;
119 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 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 Priority {
160 pid: Pid,
161 delta: i8,
162 },
163}
164
165pub 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#[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 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 unsafe { set_errno(0) };
210 let cur = unsafe { libc::getpriority(libc::PRIO_PROCESS, who(pid)) };
212 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#[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#[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#[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 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#[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#[derive(Copy, Clone, PartialEq, Eq, Debug)]
340pub enum IoClass {
341 None,
342 Realtime,
343 BestEffort,
344 Idle,
345}
346
347impl IoClass {
348 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 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#[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 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 pub fn label(self) -> String {
427 format!("{}/{}", self.class.name(), self.level)
428 }
429}
430
431pub fn ioprio_supported() -> bool {
433 cfg!(target_os = "linux")
434}
435
436#[cfg(target_os = "linux")]
441pub fn ioprio_get(pid: Pid) -> Result<IoPriority, std::io::Error> {
442 use std::io;
443 let rc = unsafe {
445 libc::syscall(libc::SYS_ioprio_get, 1 , 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 let rc = unsafe {
466 libc::syscall(
467 libc::SYS_ioprio_set,
468 1, 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 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}