Skip to main content

nu_command/platform/
ulimit.rs

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
7/// An object contains resource related parameters
8struct 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    /// Create a `ResourceInfo` object
18    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    /// Get unit
35    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
247/// Convert `rlim_t` to `Value` representation
248fn 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
268/// Get maximum length of all flag descriptions
269fn 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    // Use `RLIMIT_FSIZE` limit if no resource flag provided.
288    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    // desc.len() + unit.len() + '-X)'.len()
295    Ok(desc_len + unit_len + 3)
296}
297
298/// Fill `ResourceInfo` to the record entry
299fn 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
336/// Set limits
337fn 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        // Do not attempt to set the soft limit higher than the hard limit.
355        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
363/// Print limits
364fn 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            // Print specified limit.
379            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    // Print `RLIMIT_FSIZE` limit if no resource flag provided.
393    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
402/// Wrap `nix::sys::resource::getrlimit`
403fn 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
408/// Wrap `nix::sys::resource::setrlimit`
409fn 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
414/// Parse user input
415fn 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            // Set both hard and soft limits if neither was specified.
524            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            // Set `RLIMIT_FSIZE` limit if no resource flag provided.
542            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}