1use nix::sys::resource::{RLIM_INFINITY, Resource, rlim_t};
2use nu_engine::command_prelude::*;
3use nu_protocol::shell_error::generic::GenericError;
4
5use std::sync::LazyLock;
6
7struct ResourceInfo<'a> {
9 name: &'a str,
10 desc: &'a str,
11 flag: char,
12 multiplier: rlim_t,
13 resource: Resource,
14}
15
16impl<'a> ResourceInfo<'a> {
17 fn new(
19 name: &'a str,
20 desc: &'a str,
21 flag: char,
22 multiplier: rlim_t,
23 resource: Resource,
24 ) -> Self {
25 Self {
26 name,
27 desc,
28 flag,
29 multiplier,
30 resource,
31 }
32 }
33
34 fn get_unit(&self) -> &str {
36 if self.resource == Resource::RLIMIT_CPU {
37 "(seconds, "
38 } else if self.multiplier == 1 {
39 "("
40 } else {
41 "(kB, "
42 }
43 }
44}
45
46impl Default for ResourceInfo<'_> {
47 fn default() -> Self {
48 Self {
49 name: "file-size",
50 desc: "Maximum size of files created by the shell",
51 flag: 'f',
52 multiplier: 1024,
53 resource: Resource::RLIMIT_FSIZE,
54 }
55 }
56}
57
58static RESOURCE_ARRAY: LazyLock<Vec<ResourceInfo>> = LazyLock::new(|| {
59 let resources = [
60 #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
61 (
62 "socket-buffers",
63 "Maximum size of socket buffers",
64 'b',
65 1024,
66 Resource::RLIMIT_SBSIZE,
67 ),
68 (
69 "core-size",
70 "Maximum size of core files created",
71 'c',
72 1024,
73 Resource::RLIMIT_CORE,
74 ),
75 (
76 "data-size",
77 "Maximum size of a process's data segment",
78 'd',
79 1024,
80 Resource::RLIMIT_DATA,
81 ),
82 #[cfg(any(target_os = "android", target_os = "linux"))]
83 (
84 "nice",
85 "Controls of maximum nice priority",
86 'e',
87 1,
88 Resource::RLIMIT_NICE,
89 ),
90 (
91 "file-size",
92 "Maximum size of files created by the shell",
93 'f',
94 1024,
95 Resource::RLIMIT_FSIZE,
96 ),
97 #[cfg(any(target_os = "android", target_os = "linux"))]
98 (
99 "pending-signals",
100 "Maximum number of pending signals",
101 'i',
102 1,
103 Resource::RLIMIT_SIGPENDING,
104 ),
105 #[cfg(any(
106 target_os = "android",
107 target_os = "freebsd",
108 target_os = "openbsd",
109 target_os = "linux",
110 target_os = "freebsd",
111 target_os = "netbsd"
112 ))]
113 (
114 "lock-size",
115 "Maximum size that may be locked into memory",
116 'l',
117 1024,
118 Resource::RLIMIT_MEMLOCK,
119 ),
120 #[cfg(any(
121 target_os = "android",
122 target_os = "freebsd",
123 target_os = "netbsd",
124 target_os = "openbsd",
125 target_os = "linux",
126 target_os = "freebsd",
127 target_os = "aix",
128 ))]
129 (
130 "resident-set-size",
131 "Maximum resident set size",
132 'm',
133 1024,
134 Resource::RLIMIT_RSS,
135 ),
136 (
137 "file-descriptor-count",
138 "Maximum number of open file descriptors",
139 'n',
140 1,
141 Resource::RLIMIT_NOFILE,
142 ),
143 #[cfg(any(target_os = "android", target_os = "linux"))]
144 (
145 "queue-size",
146 "Maximum bytes in POSIX message queues",
147 'q',
148 1024,
149 Resource::RLIMIT_MSGQUEUE,
150 ),
151 #[cfg(any(target_os = "android", target_os = "linux"))]
152 (
153 "realtime-priority",
154 "Maximum realtime scheduling priority",
155 'r',
156 1,
157 Resource::RLIMIT_RTPRIO,
158 ),
159 (
160 "stack-size",
161 "Maximum stack size",
162 's',
163 1024,
164 Resource::RLIMIT_STACK,
165 ),
166 (
167 "cpu-time",
168 "Maximum amount of CPU time in seconds",
169 't',
170 1,
171 Resource::RLIMIT_CPU,
172 ),
173 #[cfg(any(
174 target_os = "android",
175 target_os = "freebsd",
176 target_os = "netbsd",
177 target_os = "openbsd",
178 target_os = "linux",
179 target_os = "freebsd",
180 target_os = "aix",
181 ))]
182 (
183 "process-count",
184 "Maximum number of processes available to the current user",
185 'u',
186 1,
187 Resource::RLIMIT_NPROC,
188 ),
189 #[cfg(not(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd")))]
190 (
191 "virtual-memory-size",
192 "Maximum amount of virtual memory available to each process",
193 'v',
194 1024,
195 Resource::RLIMIT_AS,
196 ),
197 #[cfg(target_os = "freebsd")]
198 (
199 "swap-size",
200 "Maximum swap space",
201 'w',
202 1024,
203 Resource::RLIMIT_SWAP,
204 ),
205 #[cfg(any(target_os = "android", target_os = "linux"))]
206 (
207 "file-locks",
208 "Maximum number of file locks",
209 'x',
210 1,
211 Resource::RLIMIT_LOCKS,
212 ),
213 #[cfg(target_os = "linux")]
214 (
215 "realtime-maxtime",
216 "Maximum contiguous realtime CPU time",
217 'y',
218 1,
219 Resource::RLIMIT_RTTIME,
220 ),
221 #[cfg(target_os = "freebsd")]
222 (
223 "kernel-queues",
224 "Maximum number of kqueues",
225 'K',
226 1,
227 Resource::RLIMIT_KQUEUES,
228 ),
229 #[cfg(target_os = "freebsd")]
230 (
231 "ptys",
232 "Maximum number of pseudo-terminals",
233 'P',
234 1,
235 Resource::RLIMIT_NPTS,
236 ),
237 ];
238
239 let mut resource_array = Vec::new();
240 for (name, desc, flag, multiplier, res) in resources {
241 resource_array.push(ResourceInfo::new(name, desc, flag, multiplier, res));
242 }
243
244 resource_array
245});
246
247fn limit_to_value(limit: rlim_t, multiplier: rlim_t, span: Span) -> Result<Value, ShellError> {
249 if limit == RLIM_INFINITY {
250 return Ok(Value::string("unlimited", span));
251 }
252
253 let val = match i64::try_from(limit / multiplier) {
254 Ok(v) => v,
255 Err(e) => {
256 return Err(ShellError::CantConvert {
257 to_type: "i64".into(),
258 from_type: "rlim_t".into(),
259 span,
260 help: Some(e.to_string()),
261 });
262 }
263 };
264
265 Ok(Value::int(val, span))
266}
267
268fn max_desc_len(
270 call: &Call,
271 engine_state: &EngineState,
272 stack: &mut Stack,
273 print_all: bool,
274) -> Result<usize, ShellError> {
275 let mut desc_len = 0;
276 let mut unit_len = 0;
277
278 for res in RESOURCE_ARRAY.iter() {
279 if !print_all && !call.has_flag(engine_state, stack, res.name)? {
280 continue;
281 }
282
283 desc_len = res.desc.len().max(desc_len);
284 unit_len = res.get_unit().len().max(unit_len);
285 }
286
287 if desc_len == 0 {
289 let res = ResourceInfo::default();
290 desc_len = res.desc.len().max(desc_len);
291 unit_len = res.get_unit().len().max(unit_len);
292 }
293
294 Ok(desc_len + unit_len + 3)
296}
297
298fn fill_record(
300 res: &ResourceInfo,
301 max_len: usize,
302 soft: bool,
303 hard: bool,
304 span: Span,
305) -> Result<Record, ShellError> {
306 let mut record = Record::new();
307 let mut desc = String::new();
308
309 desc.push_str(res.desc);
310
311 debug_assert!(res.desc.len() + res.get_unit().len() + 3 <= max_len);
312 let width = max_len - res.desc.len() - res.get_unit().len() - 3;
313 if width == 0 {
314 desc.push_str(format!(" {}-{})", res.get_unit(), res.flag).as_str());
315 } else {
316 desc.push_str(format!("{:>width$} {}-{})", ' ', res.get_unit(), res.flag).as_str());
317 }
318
319 record.push("description", Value::string(desc, span));
320
321 let (soft_limit, hard_limit) = getrlimit(res.resource)?;
322
323 if soft {
324 let soft_limit = limit_to_value(soft_limit, res.multiplier, span)?;
325 record.push("soft", soft_limit);
326 }
327
328 if hard {
329 let hard_limit = limit_to_value(hard_limit, res.multiplier, span)?;
330 record.push("hard", hard_limit);
331 }
332
333 Ok(record)
334}
335
336fn set_limits(
338 limit_value: &Value,
339 res: &ResourceInfo,
340 soft: bool,
341 hard: bool,
342 call_span: Span,
343) -> Result<(), ShellError> {
344 let (mut soft_limit, mut hard_limit) = getrlimit(res.resource)?;
345 let new_limit = parse_limit(limit_value, res, soft, soft_limit, hard_limit, call_span)?;
346
347 if hard {
348 hard_limit = new_limit;
349 }
350
351 if soft {
352 soft_limit = new_limit;
353
354 if (new_limit > hard_limit || new_limit == RLIM_INFINITY) && hard_limit != RLIM_INFINITY {
356 soft_limit = hard_limit;
357 }
358 }
359
360 setrlimit(res.resource, soft_limit, hard_limit)
361}
362
363fn print_limits(
365 call: &Call,
366 engine_state: &EngineState,
367 stack: &mut Stack,
368 print_all: bool,
369 soft: bool,
370 hard: bool,
371) -> Result<PipelineData, ShellError> {
372 let mut output = Vec::new();
373 let mut print_default_limit = true;
374 let max_len = max_desc_len(call, engine_state, stack, print_all)?;
375
376 for res in RESOURCE_ARRAY.iter() {
377 if !print_all {
378 if !call.has_flag(engine_state, stack, res.name)? {
380 continue;
381 }
382 }
383
384 let record = fill_record(res, max_len, soft, hard, call.head)?;
385 output.push(Value::record(record, call.head));
386
387 if print_default_limit {
388 print_default_limit = false;
389 }
390 }
391
392 if print_default_limit {
394 let res = ResourceInfo::default();
395 let record = fill_record(&res, max_len, soft, hard, call.head)?;
396 output.push(Value::record(record, call.head));
397 }
398
399 Ok(Value::list(output, call.head).into_pipeline_data())
400}
401
402fn setrlimit(res: Resource, soft_limit: rlim_t, hard_limit: rlim_t) -> Result<(), ShellError> {
404 nix::sys::resource::setrlimit(res, soft_limit, hard_limit)
405 .map_err(|e| ShellError::Generic(GenericError::new_internal(e.to_string(), "")))
406}
407
408fn getrlimit(res: Resource) -> Result<(rlim_t, rlim_t), ShellError> {
410 nix::sys::resource::getrlimit(res)
411 .map_err(|e| ShellError::Generic(GenericError::new_internal(e.to_string(), "")))
412}
413
414fn parse_limit(
416 limit_value: &Value,
417 res: &ResourceInfo,
418 soft: bool,
419 soft_limit: rlim_t,
420 hard_limit: rlim_t,
421 call_span: Span,
422) -> Result<rlim_t, ShellError> {
423 let val_span = limit_value.span();
424 match limit_value {
425 Value::Int { val, .. } => {
426 let value = rlim_t::try_from(*val).map_err(|e| ShellError::CantConvert {
427 to_type: "rlim_t".into(),
428 from_type: "i64".into(),
429 span: val_span,
430 help: Some(e.to_string()),
431 })?;
432
433 let (limit, overflow) = value.overflowing_mul(res.multiplier);
434 if overflow {
435 Ok(RLIM_INFINITY)
436 } else {
437 Ok(limit)
438 }
439 }
440 Value::Filesize { val, .. } => {
441 if res.multiplier != 1024 {
442 return Err(ShellError::TypeMismatch {
443 err_message: format!(
444 "filesize is not compatible with resource {:?}",
445 res.resource
446 ),
447 span: val_span,
448 });
449 }
450
451 rlim_t::try_from(*val).map_err(|e| ShellError::CantConvert {
452 to_type: "rlim_t".into(),
453 from_type: "i64".into(),
454 span: val_span,
455 help: Some(e.to_string()),
456 })
457 }
458 Value::String { val, .. } => {
459 if val == "unlimited" {
460 Ok(RLIM_INFINITY)
461 } else if val == "soft" {
462 if soft { Ok(hard_limit) } else { Ok(soft_limit) }
463 } else if val == "hard" {
464 Ok(hard_limit)
465 } else {
466 Err(ShellError::IncorrectValue {
467 msg: "Only unlimited, soft and hard are supported for strings".into(),
468 val_span,
469 call_span,
470 })
471 }
472 }
473 _ => Err(ShellError::TypeMismatch {
474 err_message: format!(
475 "string, int or filesize required, you provide {}",
476 limit_value.get_type()
477 ),
478 span: limit_value.span(),
479 }),
480 }
481}
482
483#[derive(Clone)]
484pub struct ULimit;
485
486impl Command for ULimit {
487 fn name(&self) -> &str {
488 "ulimit"
489 }
490
491 fn description(&self) -> &str {
492 "Set or get resource usage limits."
493 }
494
495 fn signature(&self) -> Signature {
496 let mut sig = Signature::build("ulimit")
497 .input_output_types(vec![(Type::Nothing, Type::Any)])
498 .switch("soft", "Sets soft resource limit.", Some('S'))
499 .switch("hard", "Sets hard resource limit.", Some('H'))
500 .switch("all", "Prints all current limits.", Some('a'))
501 .optional("limit", SyntaxShape::Any, "The limit value to set.")
502 .category(Category::Platform);
503
504 for res in RESOURCE_ARRAY.iter() {
505 sig = sig.switch(res.name, res.desc, Some(res.flag));
506 }
507
508 sig
509 }
510
511 fn run(
512 &self,
513 engine_state: &EngineState,
514 stack: &mut Stack,
515 call: &Call,
516 _input: PipelineData,
517 ) -> Result<PipelineData, ShellError> {
518 let mut soft = call.has_flag(engine_state, stack, "soft")?;
519 let mut hard = call.has_flag(engine_state, stack, "hard")?;
520 let all = call.has_flag(engine_state, stack, "all")?;
521
522 if !hard && !soft {
523 hard = true;
525 soft = true;
526 }
527
528 if let Some(limit_value) = call.opt::<Value>(engine_state, stack, 0)? {
529 let mut set_default_limit = true;
530
531 for res in RESOURCE_ARRAY.iter() {
532 if call.has_flag(engine_state, stack, res.name)? {
533 set_limits(&limit_value, res, soft, hard, call.head)?;
534
535 if set_default_limit {
536 set_default_limit = false;
537 }
538 }
539 }
540
541 if set_default_limit {
543 let res = ResourceInfo::default();
544 set_limits(&limit_value, &res, hard, soft, call.head)?;
545 }
546
547 Ok(PipelineData::empty())
548 } else {
549 print_limits(call, engine_state, stack, all, soft, hard)
550 }
551 }
552
553 fn examples(&self) -> Vec<Example<'_>> {
554 vec![
555 Example {
556 description: "Print all current limits.",
557 example: "ulimit -a",
558 result: None,
559 },
560 Example {
561 description: "Print specified limits.",
562 example: "ulimit --core-size --data-size --file-size",
563 result: None,
564 },
565 Example {
566 description: "Set limit.",
567 example: "ulimit --core-size 102400",
568 result: None,
569 },
570 Example {
571 description: "Set stack size soft limit.",
572 example: "ulimit -s -S 10240",
573 result: None,
574 },
575 Example {
576 description: "Set virtual memory size hard limit.",
577 example: "ulimit -v -H 10240",
578 result: None,
579 },
580 Example {
581 description: "Set core size limit to unlimited.",
582 example: "ulimit -c unlimited",
583 result: None,
584 },
585 ]
586 }
587
588 fn search_terms(&self) -> Vec<&str> {
589 vec!["resource", "limits"]
590 }
591}