1use crate::Pid;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum Action {
10 Kill {
11 pid: Pid,
12 signal: i32,
13 },
14 Renice {
15 pid: Pid,
16 nice: i8,
17 },
18 Priority {
20 pid: Pid,
21 delta: i8,
22 },
23}
24
25pub const SIGNALS: &[(i32, &str)] = &[
27 (1, "SIGHUP"),
28 (2, "SIGINT"),
29 (3, "SIGQUIT"),
30 (4, "SIGILL"),
31 (6, "SIGABRT"),
32 (9, "SIGKILL"),
33 (11, "SIGSEGV"),
34 (15, "SIGTERM"),
35 (17, "SIGCHLD"),
36 (18, "SIGCONT"),
37 (19, "SIGSTOP"),
38 (20, "SIGTSTP"),
39];
40
41#[cfg(unix)]
47pub fn execute(action: &Action) -> Result<(), std::io::Error> {
48 use std::io;
49 match *action {
50 Action::Kill { pid, signal } => {
51 let rc = unsafe { libc::kill(pid as libc::pid_t, signal as libc::c_int) };
53 if rc == 0 {
54 Ok(())
55 } else {
56 Err(io::Error::last_os_error())
57 }
58 }
59 Action::Renice { pid, nice } => set_nice(pid, nice as i32),
60 Action::Priority { pid, delta } => {
61 unsafe { set_errno(0) };
66 let cur = unsafe { libc::getpriority(libc::PRIO_PROCESS, who(pid)) };
68 if cur == -1 && unsafe { errno() } != 0 {
70 return Err(io::Error::last_os_error());
71 }
72 let target = (cur + delta as i32).clamp(-20, 19);
73 set_nice(pid, target)
74 }
75 }
76}
77
78#[cfg(unix)]
82unsafe fn errno() -> i32 {
83 #[cfg(any(target_os = "linux", target_os = "android"))]
84 {
85 *libc::__errno_location()
86 }
87 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
88 {
89 *libc::__error()
90 }
91 #[cfg(not(any(
92 target_os = "linux",
93 target_os = "android",
94 target_os = "macos",
95 target_os = "ios",
96 target_os = "freebsd"
97 )))]
98 {
99 0
100 }
101}
102
103#[cfg(unix)]
107unsafe fn set_errno(value: i32) {
108 #[cfg(any(target_os = "linux", target_os = "android"))]
109 {
110 *libc::__errno_location() = value;
111 }
112 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
113 {
114 *libc::__error() = value;
115 }
116 #[cfg(not(any(
117 target_os = "linux",
118 target_os = "android",
119 target_os = "macos",
120 target_os = "ios",
121 target_os = "freebsd"
122 )))]
123 {
124 let _ = value;
125 }
126}
127
128#[cfg(any(target_os = "linux", target_os = "macos"))]
135fn who(pid: Pid) -> libc::id_t {
136 pid as libc::id_t
137}
138
139#[cfg(target_os = "freebsd")]
140fn who(pid: Pid) -> libc::c_int {
141 pid as libc::c_int
142}
143
144#[cfg(unix)]
145fn set_nice(pid: Pid, nice: i32) -> Result<(), std::io::Error> {
146 use std::io;
147 let rc = unsafe { libc::setpriority(libc::PRIO_PROCESS, who(pid), nice) };
149 if rc == 0 {
150 Ok(())
151 } else {
152 Err(io::Error::last_os_error())
153 }
154}
155
156#[cfg(not(unix))]
159pub fn execute(_action: &Action) -> Result<(), std::io::Error> {
160 Err(std::io::Error::new(
161 std::io::ErrorKind::Unsupported,
162 "process actions require a unix target",
163 ))
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn actions_are_comparable() {
172 assert_eq!(
173 Action::Kill { pid: 1, signal: 9 },
174 Action::Kill { pid: 1, signal: 9 }
175 );
176 assert_ne!(
177 Action::Kill { pid: 1, signal: 9 },
178 Action::Kill { pid: 1, signal: 15 }
179 );
180 }
181
182 #[test]
183 fn signal_table_is_sorted_and_unique() {
184 let mut signals: Vec<i32> = SIGNALS.iter().map(|(n, _)| *n).collect();
185 let len = signals.len();
186 signals.dedup();
187 assert_eq!(signals.len(), len, "duplicate signal numbers");
188 signals.sort_unstable();
189 assert_eq!(signals.first(), Some(&1));
190 assert!(signals.contains(&9) && signals.contains(&15));
191 }
192}
193
194#[derive(Copy, Clone, PartialEq, Eq, Debug)]
196pub enum IoClass {
197 None,
198 Realtime,
199 BestEffort,
200 Idle,
201}
202
203impl IoClass {
204 pub fn bits(self) -> u32 {
206 match self {
207 IoClass::None => 0,
208 IoClass::Realtime => 1,
209 IoClass::BestEffort => 2,
210 IoClass::Idle => 3,
211 }
212 }
213
214 pub fn from_bits(bits: u32) -> Option<Self> {
215 Some(match bits {
216 0 => IoClass::None,
217 1 => IoClass::Realtime,
218 2 => IoClass::BestEffort,
219 3 => IoClass::Idle,
220 _ => return None,
221 })
222 }
223
224 pub fn name(self) -> &'static str {
225 match self {
226 IoClass::None => "none",
227 IoClass::Realtime => "realtime",
228 IoClass::BestEffort => "best-effort",
229 IoClass::Idle => "idle",
230 }
231 }
232
233 pub fn next(self) -> Self {
235 match self {
236 IoClass::None => IoClass::BestEffort,
237 IoClass::BestEffort => IoClass::Idle,
238 IoClass::Idle => IoClass::Realtime,
239 IoClass::Realtime => IoClass::None,
240 }
241 }
242}
243
244#[derive(Copy, Clone, PartialEq, Eq, Debug)]
246pub struct IoPriority {
247 pub class: IoClass,
248 pub level: u8,
249}
250
251impl Default for IoPriority {
252 fn default() -> Self {
253 Self {
254 class: IoClass::None,
255 level: 0,
256 }
257 }
258}
259
260impl IoPriority {
261 pub fn new(class: IoClass, level: u8) -> Self {
262 Self {
263 class,
264 level: level.min(7),
265 }
266 }
267
268 pub fn to_bits(self) -> i32 {
270 (((self.class.bits() as i32) << 13) | self.level as i32) & 0xFFFF
271 }
272
273 pub fn from_bits(bits: i32) -> Self {
274 let class = IoClass::from_bits(((bits >> 13) & 0x7) as u32).unwrap_or(IoClass::None);
275 Self {
276 class,
277 level: (bits & 0x7) as u8,
278 }
279 }
280
281 pub fn label(self) -> String {
283 format!("{}/{}", self.class.name(), self.level)
284 }
285}
286
287pub fn ioprio_supported() -> bool {
289 cfg!(target_os = "linux")
290}
291
292#[cfg(target_os = "linux")]
297pub fn ioprio_get(pid: Pid) -> Result<IoPriority, std::io::Error> {
298 use std::io;
299 let rc = unsafe { libc::syscall(libc::SYS_ioprio_get, 1 , pid) };
301 if rc < 0 {
302 return Err(io::Error::last_os_error());
303 }
304 Ok(IoPriority::from_bits(rc as i32))
305}
306
307#[cfg(not(target_os = "linux"))]
308pub fn ioprio_get(_pid: Pid) -> Result<IoPriority, std::io::Error> {
309 Err(std::io::Error::new(
310 std::io::ErrorKind::Unsupported,
311 "I/O priority is a Linux feature (ioprio_get)",
312 ))
313}
314
315#[cfg(target_os = "linux")]
316pub fn ioprio_set(pid: Pid, priority: IoPriority) -> Result<(), std::io::Error> {
317 use std::io;
318 let rc = unsafe {
320 libc::syscall(
321 libc::SYS_ioprio_set,
322 1, pid,
324 priority.to_bits(),
325 )
326 };
327 if rc < 0 {
328 Err(io::Error::last_os_error())
329 } else {
330 Ok(())
331 }
332}
333
334#[cfg(not(target_os = "linux"))]
335pub fn ioprio_set(_pid: Pid, _priority: IoPriority) -> Result<(), std::io::Error> {
336 Err(std::io::Error::new(
337 std::io::ErrorKind::Unsupported,
338 "I/O priority is a Linux feature (ioprio_set)",
339 ))
340}
341
342#[cfg(test)]
343mod ioprio_tests {
344 use super::*;
345
346 #[test]
347 fn bits_pack_and_unpack() {
348 for class in [
349 IoClass::None,
350 IoClass::Realtime,
351 IoClass::BestEffort,
352 IoClass::Idle,
353 ] {
354 for level in 0..=7u8 {
355 let p = IoPriority::new(class, level);
356 let back = IoPriority::from_bits(p.to_bits());
357 assert_eq!(back, p, "{:?}/{}", class, level);
358 }
359 }
360 }
361
362 #[test]
363 fn the_default_is_class_none() {
364 let p = IoPriority::default();
365 assert_eq!(p.class, IoClass::None);
366 assert_eq!(p.level, 0);
367 assert_eq!(p.to_bits(), 0);
368 }
369
370 #[test]
371 fn levels_are_clamped_to_seven() {
372 let p = IoPriority::new(IoClass::BestEffort, 200);
373 assert_eq!(p.level, 7);
374 }
375
376 #[test]
377 fn classes_cycle_through_every_variant() {
378 let mut c = IoClass::None;
379 let mut seen = vec![c];
380 for _ in 0..3 {
381 c = c.next();
382 seen.push(c);
383 }
384 assert_eq!(seen.len(), 4);
385 assert_eq!(c.next(), IoClass::None, "the cycle closes");
386 }
387
388 #[test]
389 fn labels_are_readable() {
390 let p = IoPriority::new(IoClass::BestEffort, 4);
391 assert_eq!(p.label(), "best-effort/4");
392 }
393
394 #[test]
395 fn an_out_of_range_class_degrades_to_none() {
396 let p = IoPriority::from_bits((7 << 13) | 3);
397 assert_eq!(p.class, IoClass::None);
398 assert_eq!(p.level, 3);
399 }
400
401 #[cfg(target_os = "linux")]
402 #[test]
403 fn reading_our_own_ioprio_works() {
404 let p = ioprio_get(std::process::id()).expect("ioprio_get on self");
405 assert!(matches!(
407 p.class,
408 IoClass::None | IoClass::Realtime | IoClass::BestEffort | IoClass::Idle
409 ));
410 }
411
412 #[cfg(target_os = "linux")]
413 #[test]
414 fn setting_our_own_ioprio_round_trips() {
415 let original = ioprio_get(std::process::id()).unwrap();
416 let target = IoPriority::new(IoClass::BestEffort, 6);
417 if ioprio_set(std::process::id(), target).is_ok() {
418 assert_eq!(ioprio_get(std::process::id()).unwrap(), target);
419 }
420 let _ = ioprio_set(std::process::id(), original);
421 }
422
423 #[cfg(not(target_os = "linux"))]
424 #[test]
425 fn unsupported_platforms_explain_themselves() {
426 assert!(!ioprio_supported());
427 let err = ioprio_get(std::process::id()).unwrap_err();
428 assert_eq!(err.kind(), std::io::ErrorKind::Unsupported);
429 assert!(err.to_string().contains("Linux"));
430 }
431}