1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use cols::{OutputMode, Table, WidthHint, print_table};
7use rustix::process::{Pid, Resource, Rlimit, getrlimit, prlimit, setrlimit};
8use std::{fs, os::unix::process::CommandExt, process, process::ExitCode};
9
10#[derive(Parser)]
11#[command(
12 name = "prlimit",
13 about = "Get and set process resource limits",
14 override_usage = "prlimit [options] [--<resource>=<limit>] [-p PID]\n \
15 prlimit [options] [--<resource>=<limit>] COMMAND [args...]"
16)]
17pub struct Args {
18 #[arg(short, long, value_name = "pid")]
20 pid: Option<u32>,
21
22 #[arg(short = 'o', long, value_delimiter = ',')]
24 output: Option<Vec<String>>,
25
26 #[arg(long)]
28 noheadings: bool,
29
30 #[arg(long)]
32 raw: bool,
33
34 #[arg(short = 'c', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
38 core: Option<String>,
39
40 #[arg(short = 'd', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
42 data: Option<String>,
43
44 #[arg(short = 'e', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
46 nice: Option<String>,
47
48 #[arg(short = 'f', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
50 fsize: Option<String>,
51
52 #[arg(short = 'i', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
54 sigpending: Option<String>,
55
56 #[arg(short = 'l', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
58 memlock: Option<String>,
59
60 #[arg(short = 'm', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
62 rss: Option<String>,
63
64 #[arg(short = 'n', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
66 nofile: Option<String>,
67
68 #[arg(short = 'q', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
70 msgqueue: Option<String>,
71
72 #[arg(short = 'r', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
74 rtprio: Option<String>,
75
76 #[arg(short = 's', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
78 stack: Option<String>,
79
80 #[arg(short = 't', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
82 cpu: Option<String>,
83
84 #[arg(short = 'u', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
86 nproc: Option<String>,
87
88 #[arg(short = 'v', long = "as", value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
90 addr_space: Option<String>,
91
92 #[arg(short = 'x', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
94 locks: Option<String>,
95
96 #[arg(short = 'y', long, value_name = "limit", require_equals = true, num_args = 0..=1, default_missing_value = "")]
98 rttime: Option<String>,
99
100 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
102 command: Vec<String>,
103}
104
105#[derive(Debug, Clone, Copy)]
106struct ResourceInfo {
107 resource: Resource,
108 name: &'static str,
109 description: &'static str,
110 units: &'static str,
111}
112
113const ALL_RESOURCES: &[ResourceInfo] = &[
114 ResourceInfo {
115 resource: Resource::As,
116 name: "AS",
117 description: "address space limit",
118 units: "bytes",
119 },
120 ResourceInfo {
121 resource: Resource::Core,
122 name: "CORE",
123 description: "max core file size",
124 units: "bytes",
125 },
126 ResourceInfo {
127 resource: Resource::Cpu,
128 name: "CPU",
129 description: "CPU time",
130 units: "seconds",
131 },
132 ResourceInfo {
133 resource: Resource::Data,
134 name: "DATA",
135 description: "max data size",
136 units: "bytes",
137 },
138 ResourceInfo {
139 resource: Resource::Fsize,
140 name: "FSIZE",
141 description: "max file size",
142 units: "bytes",
143 },
144 ResourceInfo {
145 resource: Resource::Locks,
146 name: "LOCKS",
147 description: "max number of file locks held",
148 units: "locks",
149 },
150 ResourceInfo {
151 resource: Resource::Memlock,
152 name: "MEMLOCK",
153 description: "max locked-in-memory address space",
154 units: "bytes",
155 },
156 ResourceInfo {
157 resource: Resource::Msgqueue,
158 name: "MSGQUEUE",
159 description: "max bytes in POSIX mqueues",
160 units: "bytes",
161 },
162 ResourceInfo {
163 resource: Resource::Nice,
164 name: "NICE",
165 description: "max nice prio allowed to raise",
166 units: "",
167 },
168 ResourceInfo {
169 resource: Resource::Nofile,
170 name: "NOFILE",
171 description: "max number of open files",
172 units: "files",
173 },
174 ResourceInfo {
175 resource: Resource::Nproc,
176 name: "NPROC",
177 description: "max number of processes",
178 units: "processes",
179 },
180 ResourceInfo {
181 resource: Resource::Rss,
182 name: "RSS",
183 description: "max resident set size",
184 units: "bytes",
185 },
186 ResourceInfo {
187 resource: Resource::Rtprio,
188 name: "RTPRIO",
189 description: "max real-time priority",
190 units: "",
191 },
192 ResourceInfo {
193 resource: Resource::Rttime,
194 name: "RTTIME",
195 description: "timeout for real-time tasks",
196 units: "microsecs",
197 },
198 ResourceInfo {
199 resource: Resource::Sigpending,
200 name: "SIGPENDING",
201 description: "max number of pending signals",
202 units: "signals",
203 },
204 ResourceInfo {
205 resource: Resource::Stack,
206 name: "STACK",
207 description: "max stack size",
208 units: "bytes",
209 },
210];
211
212fn resource_spec(args: &Args) -> Vec<(ResourceInfo, Option<&str>)> {
213 macro_rules! entry {
216 ($field:expr, $name:expr) => {
217 if let Some(ref val) = $field {
218 let ri = ALL_RESOURCES
219 .iter()
220 .find(|r| r.name == $name)
221 .copied()
222 .unwrap();
223 let limit = if val.is_empty() {
224 None
225 } else {
226 Some(val.as_str())
227 };
228 Some((ri, limit))
229 } else {
230 None
231 }
232 };
233 }
234
235 let selected: Vec<Option<(ResourceInfo, Option<&str>)>> = vec![
236 entry!(args.addr_space, "AS"),
237 entry!(args.core, "CORE"),
238 entry!(args.cpu, "CPU"),
239 entry!(args.data, "DATA"),
240 entry!(args.fsize, "FSIZE"),
241 entry!(args.locks, "LOCKS"),
242 entry!(args.memlock, "MEMLOCK"),
243 entry!(args.msgqueue, "MSGQUEUE"),
244 entry!(args.nice, "NICE"),
245 entry!(args.nofile, "NOFILE"),
246 entry!(args.nproc, "NPROC"),
247 entry!(args.rss, "RSS"),
248 entry!(args.rtprio, "RTPRIO"),
249 entry!(args.rttime, "RTTIME"),
250 entry!(args.sigpending, "SIGPENDING"),
251 entry!(args.stack, "STACK"),
252 ];
253
254 selected.into_iter().flatten().collect()
255}
256
257fn parse_limit(s: &str) -> Result<(Option<u64>, Option<u64>), String> {
258 if let Some(colon) = s.find(':') {
260 let soft_str = &s[..colon];
261 let hard_str = &s[colon + 1..];
262 let soft = parse_one_limit(soft_str)?;
263 let hard = parse_one_limit(hard_str)?;
264 Ok((soft, hard))
265 } else {
266 let v = parse_single_limit(s)?;
267 Ok((Some(v), Some(v)))
268 }
269}
270
271fn parse_one_limit(s: &str) -> Result<Option<u64>, String> {
273 if s.is_empty() {
274 Ok(None)
275 } else {
276 parse_single_limit(s).map(Some)
277 }
278}
279
280fn parse_single_limit(s: &str) -> Result<u64, String> {
281 match s {
282 "unlimited" | "infinity" => Ok(u64::MAX),
283 _ => s.parse::<u64>().map_err(|_| format!("invalid limit: {s}")),
284 }
285}
286
287fn limit_str(val: Option<u64>) -> String {
288 match val {
289 None | Some(u64::MAX) => "unlimited".to_string(),
290 Some(v) => v.to_string(),
291 }
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295enum Col {
296 Resource,
297 Description,
298 Soft,
299 Hard,
300 Units,
301}
302
303impl Col {
304 fn name(self) -> &'static str {
305 match self {
306 Col::Resource => "RESOURCE",
307 Col::Description => "DESCRIPTION",
308 Col::Soft => "SOFT",
309 Col::Hard => "HARD",
310 Col::Units => "UNITS",
311 }
312 }
313
314 fn whint(self) -> WidthHint {
315 match self {
316 Col::Resource => WidthHint::Fixed(10),
317 Col::Description => WidthHint::Fixed(36),
318 Col::Soft => WidthHint::Fixed(9),
319 Col::Hard => WidthHint::Fixed(9),
320 Col::Units => WidthHint::Fixed(9),
321 }
322 }
323
324 fn is_right(self) -> bool {
325 matches!(self, Col::Soft | Col::Hard)
326 }
327
328 fn from_name(s: &str) -> Option<Self> {
329 match s.to_uppercase().as_str() {
330 "RESOURCE" => Some(Col::Resource),
331 "DESCRIPTION" => Some(Col::Description),
332 "SOFT" => Some(Col::Soft),
333 "HARD" => Some(Col::Hard),
334 "UNITS" => Some(Col::Units),
335 _ => None,
336 }
337 }
338}
339
340const DEFAULT_COLUMNS: &[Col] = &[
341 Col::Resource,
342 Col::Description,
343 Col::Soft,
344 Col::Hard,
345 Col::Units,
346];
347
348fn read_limit(pid: Option<u32>, resource: Resource) -> Option<Rlimit> {
351 if let Some(pid) = pid {
352 read_limit_from_proc(pid, resource)
353 } else {
354 Some(getrlimit(resource))
355 }
356}
357
358fn read_limit_from_proc(pid: u32, resource: Resource) -> Option<Rlimit> {
360 let content = fs::read_to_string(format!("/proc/{pid}/limits")).ok()?;
361 let proc_name = match resource {
363 Resource::Cpu => "Max cpu time",
364 Resource::Fsize => "Max file size",
365 Resource::Data => "Max data size",
366 Resource::Stack => "Max stack size",
367 Resource::Core => "Max core file size",
368 Resource::Rss => "Max resident set",
369 Resource::Nproc => "Max processes",
370 Resource::Nofile => "Max open files",
371 Resource::Memlock => "Max locked memory",
372 Resource::As => "Max address space",
373 Resource::Locks => "Max file locks",
374 Resource::Sigpending => "Max pending signals",
375 Resource::Msgqueue => "Max msgqueue size",
376 Resource::Nice => "Max nice priority",
377 Resource::Rtprio => "Max realtime priority",
378 Resource::Rttime => "Max realtime timeout",
379 _ => return None,
380 };
381 for line in content.lines().skip(1) {
382 if line.starts_with(proc_name) {
383 let rest = line.strip_prefix(proc_name).unwrap().trim();
384 let parts: Vec<&str> = rest.split_whitespace().collect();
385 if parts.len() >= 2 {
386 let current = parse_proc_limit(parts[0]);
387 let maximum = parse_proc_limit(parts[1]);
388 return Some(Rlimit { current, maximum });
389 }
390 }
391 }
392 None
393}
394
395fn parse_proc_limit(s: &str) -> Option<u64> {
396 if s == "unlimited" {
397 None
398 } else {
399 s.parse().ok()
400 }
401}
402
403fn u64_to_rlimit_half(v: u64) -> Option<u64> {
404 if v == u64::MAX { None } else { Some(v) }
405}
406
407pub fn run(args: Args) -> ExitCode {
408 let selected = resource_spec(&args);
409
410 let display: Vec<ResourceInfo> = if selected.is_empty() {
412 ALL_RESOURCES.to_vec()
413 } else {
414 selected.iter().map(|(ri, _)| *ri).collect()
415 };
416
417 let target_pid = args.pid;
418
419 for (ri, limit_str_opt) in &selected {
421 let Some(limit_str_val) = limit_str_opt else {
422 continue;
423 };
424 let (new_soft, new_hard) = match parse_limit(limit_str_val) {
425 Ok(v) => v,
426 Err(e) => {
427 eprintln!("prlimit: {e}");
428 return ExitCode::FAILURE;
429 }
430 };
431
432 let current = match read_limit(target_pid, ri.resource) {
434 Some(r) => r,
435 None => {
436 eprintln!("prlimit: {}: failed to read current limit", ri.name);
437 return ExitCode::FAILURE;
438 }
439 };
440
441 let merged = Rlimit {
442 current: new_soft
443 .map(u64_to_rlimit_half)
444 .unwrap_or(current.current),
445 maximum: new_hard
446 .map(u64_to_rlimit_half)
447 .unwrap_or(current.maximum),
448 };
449
450 let result = if let Some(pid) = target_pid {
451 let raw_pid = unsafe { Pid::from_raw_unchecked(pid as i32) };
452 prlimit(Some(raw_pid), ri.resource, merged).map(|_| ())
453 } else {
454 setrlimit(ri.resource, merged)
455 };
456
457 if let Err(e) = result {
458 eprintln!("prlimit: failed to set {}: {e}", ri.name);
459 return ExitCode::FAILURE;
460 }
461 }
462
463 if !args.command.is_empty() {
465 let (prog, prog_args) = args.command.split_first().unwrap();
466 let err = process::Command::new(prog).args(prog_args).exec();
467 eprintln!("prlimit: {prog}: {err}");
468 return ExitCode::FAILURE;
469 }
470
471 let columns: Vec<Col> = if let Some(ref names) = args.output {
473 let mut cols = Vec::new();
474 for name in names {
475 match Col::from_name(name.trim()) {
476 Some(c) => cols.push(c),
477 None => {
478 eprintln!("prlimit: unknown column: {name}");
479 return ExitCode::FAILURE;
480 }
481 }
482 }
483 cols
484 } else {
485 DEFAULT_COLUMNS.to_vec()
486 };
487
488 let mut table = Table::new();
489 if args.raw {
490 table.output_mode_set(OutputMode::Raw);
491 }
492 if args.noheadings {
493 table.headings_set(false);
494 }
495 for col in &columns {
496 let idx = table.new_column(col.name());
497 table.column_mut(idx).unwrap().width_hint_set(col.whint());
498 if col.is_right() {
499 table.column_mut(idx).unwrap().right_set(true);
500 }
501 }
502
503 for ri in &display {
504 let lim = match read_limit(target_pid, ri.resource) {
505 Some(r) => r,
506 None => {
507 eprintln!("prlimit: {}: failed to read limit", ri.name);
508 return ExitCode::FAILURE;
509 }
510 };
511
512 let line_id = table.new_line(None);
513 let line = table.line_mut(line_id);
514
515 for (ci, col) in columns.iter().enumerate() {
516 let val = match col {
517 Col::Resource => ri.name.to_string(),
518 Col::Description => ri.description.to_string(),
519 Col::Soft => limit_str(lim.current),
520 Col::Hard => limit_str(lim.maximum),
521 Col::Units => ri.units.to_string(),
522 };
523 line.data_set(ci, &val);
524 }
525 }
526
527 let stdout = std::io::stdout();
528 let mut out = stdout.lock();
529 if let Err(e) = print_table(&table, &mut out) {
530 eprintln!("prlimit: {e}");
531 return ExitCode::FAILURE;
532 }
533
534 ExitCode::SUCCESS
535}