Files
fish-shell/src/builtins/ulimit.rs
2025-11-04 14:13:22 +01:00

551 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::cmp::Ordering;
use libc::{RLIM_INFINITY, c_uint, rlim_t};
use nix::errno::Errno;
use nix::sys::resource::Resource as ResourceEnum;
use once_cell::sync::Lazy;
use crate::fallback::{fish_wcswidth, wcscasecmp};
use crate::wutil::perror;
use super::prelude::*;
pub mod limits {
/// Constants that exist everywhere (except perhaps Cygwin).
/// Note these are uints on Linux but ints everywhere else - we use -1 as a sentinel
/// so cast to int.
pub mod common {
use cfg_if::cfg_if;
use libc;
pub const CORE: libc::c_int = libc::RLIMIT_CORE as _;
pub const DATA: libc::c_int = libc::RLIMIT_DATA as _;
pub const FSIZE: libc::c_int = libc::RLIMIT_FSIZE as _;
cfg_if!(
if #[cfg(cygwin)] {
pub const MEMLOCK: libc::c_int = -1;
} else {
pub const MEMLOCK: libc::c_int = libc::RLIMIT_MEMLOCK as _;
}
);
pub const NOFILE: libc::c_int = libc::RLIMIT_NOFILE as _;
pub const STACK: libc::c_int = libc::RLIMIT_STACK as _;
pub const CPU: libc::c_int = libc::RLIMIT_CPU as _;
cfg_if!(
if #[cfg(cygwin)] {
pub const NPROC: libc::c_int = -1;
} else {
pub const NPROC: libc::c_int = libc::RLIMIT_NPROC as _;
}
);
}
pub use self::common::*;
// Define NAME as libc::LIBC_NAME on the listed OSes; -1 elsewhere.
macro_rules! define_on {
($name:ident, $libc_name:ident; $($os:literal),+ $(,)?) => {
#[cfg(any($(target_os = $os),+))]
pub const $name: libc::c_int = libc::$libc_name as _;
#[cfg(not(any($(target_os = $os),+)))]
pub const $name: libc::c_int = -1;
};
}
define_on!(SIGPENDING, RLIMIT_SIGPENDING; "linux");
define_on!(MSGQUEUE, RLIMIT_MSGQUEUE; "linux");
define_on!(RTPRIO, RLIMIT_RTPRIO; "linux");
define_on!(RTTIME, RLIMIT_RTTIME; "linux");
define_on!(RSS, RLIMIT_RSS; "linux", "freebsd", "netbsd", "openbsd", "dragonfly");
// TODO(MSRV >= 1.86): target_os = "cygwin" triggers a warning on Rust 1.85.
#[cfg(any(
target_os = "linux",
target_os = "ios",
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "dragonfly",
cygwin
))]
pub const AS: libc::c_int = libc::RLIMIT_AS as _;
// TODO(MSRV >= 1.86): target_os = "cygwin" triggers a warning on Rust 1.85.
#[cfg(not(any(
target_os = "linux",
target_os = "ios",
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "dragonfly",
cygwin
)))]
pub const AS: libc::c_int = -1;
define_on!(SBSIZE, RLIMIT_SBSIZE; "freebsd", "netbsd", "dragonfly");
define_on!(NICE, RLIMIT_NICE; "linux");
define_on!(KQUEUES, RLIMIT_KQUEUES; "freebsd");
define_on!(SWAP, RLIMIT_SWAP; "freebsd");
define_on!(NPTS, RLIMIT_NPTS; "freebsd");
define_on!(NTHR, RLIMIT_NTHR; "netbsd");
}
fn convert_resource(resource: c_uint) -> ResourceEnum {
let resource: i32 = resource.try_into().unwrap();
use std::mem::{size_of, transmute};
// Resource is #[repr(i32)] so this is ok
const _: () = assert!(size_of::<c_uint>() == size_of::<ResourceEnum>());
unsafe { transmute(resource) }
}
/// Calls getrlimit.
fn getrlimit(resource: c_uint) -> Option<(rlim_t, rlim_t)> {
nix::sys::resource::getrlimit(convert_resource(resource))
.map_err(|_| perror("getrlimit"))
.ok()
}
fn setrlimit(resource: c_uint, rlim_cur: rlim_t, rlim_max: rlim_t) -> Result<(), Errno> {
nix::sys::resource::setrlimit(convert_resource(resource), rlim_cur, rlim_max)
}
/// Print the value of the specified resource limit.
fn print(resource: c_uint, hard: bool, streams: &mut IoStreams) {
let Some(l) = get(resource, hard) else {
streams.out.append(wgettext!("error\n"));
return;
};
if l == RLIM_INFINITY {
streams.out.append(wgettext!("unlimited\n"));
} else {
streams
.out
.append(wgettext_fmt!("%u\n", l / get_multiplier(resource)));
}
}
/// Print values of all resource limits.
fn print_all(hard: bool, streams: &mut IoStreams) {
let mut w = 0;
for resource in RESOURCE_ARR.iter() {
w = w.max(fish_wcswidth(resource.desc));
}
for resource in RESOURCE_ARR.iter() {
let Some((rlim_cur, rlim_max)) = getrlimit(resource.resource) else {
continue;
};
let l = if hard { rlim_max } else { rlim_cur };
let unit = if resource.resource == limits::CPU as c_uint {
"(seconds, "
} else if get_multiplier(resource.resource) == 1 {
"("
} else {
"(kB, "
};
streams.out.append(sprintf!(
"%-*s %10s-%c) ",
w,
resource.desc,
unit,
resource.switch_char,
));
if l == RLIM_INFINITY {
streams.out.append(wgettext!("unlimited\n"));
} else {
streams
.out
.append(wgettext_fmt!("%u\n", l / get_multiplier(resource.resource)));
}
}
}
/// Returns the description for the specified resource limit.
fn get_desc(what: c_uint) -> &'static wstr {
for resource in RESOURCE_ARR.iter() {
if resource.resource == what {
return resource.desc;
}
}
unreachable!()
}
/// Set the new value of the specified resource limit. This function does _not_ multiply the limit
/// value by the multiplier constant used by the commandline ulimit.
fn set_limit(
resource: c_uint,
hard: bool,
soft: bool,
value: rlim_t,
streams: &mut IoStreams,
) -> BuiltinResult {
let Some((mut rlim_cur, mut rlim_max)) = getrlimit(resource) else {
return Err(STATUS_CMD_ERROR);
};
if hard {
rlim_max = value;
}
if soft {
rlim_cur = value;
// Do not attempt to set the soft limit higher than the hard limit.
if (value > rlim_max || value == RLIM_INFINITY) && rlim_max != RLIM_INFINITY {
rlim_cur = rlim_max;
}
}
if let Err(errno) = setrlimit(resource, rlim_cur, rlim_max) {
if errno == Errno::EPERM {
streams.err.append(wgettext_fmt!(
"ulimit: Permission denied when changing resource of type '%s'\n",
get_desc(resource)
));
} else {
builtin_wperror(L!("ulimit"), streams);
}
Err(STATUS_CMD_ERROR)
} else {
Ok(SUCCESS)
}
}
/// Get the implicit multiplication factor for the specified resource limit.
fn get_multiplier(what: c_uint) -> rlim_t {
for resource in RESOURCE_ARR.iter() {
if resource.resource == what {
return resource.multiplier as rlim_t;
}
}
unreachable!()
}
fn get(resource: c_uint, hard: bool) -> Option<rlim_t> {
let (rlim_cur, rlim_max) = getrlimit(resource)?;
Some(if hard { rlim_max } else { rlim_cur })
}
#[derive(Debug, Clone, Copy)]
struct Options {
what: c_int,
report_all: bool,
hard: bool,
soft: bool,
}
impl Default for Options {
fn default() -> Self {
Options {
what: limits::FSIZE,
report_all: false,
hard: false,
soft: false,
}
}
}
pub fn ulimit(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> BuiltinResult {
let cmd = args[0];
const SHORT_OPTS: &wstr = L!("HSabcdefilmnqrstuvwyKPTh");
const LONG_OPTS: &[WOption] = &[
wopt(L!("all"), ArgType::NoArgument, 'a'),
wopt(L!("hard"), ArgType::NoArgument, 'H'),
wopt(L!("soft"), ArgType::NoArgument, 'S'),
wopt(L!("socket-buffers"), ArgType::NoArgument, 'b'),
wopt(L!("core-size"), ArgType::NoArgument, 'c'),
wopt(L!("data-size"), ArgType::NoArgument, 'd'),
wopt(L!("nice"), ArgType::NoArgument, 'e'),
wopt(L!("file-size"), ArgType::NoArgument, 'f'),
wopt(L!("pending-signals"), ArgType::NoArgument, 'i'),
wopt(L!("lock-size"), ArgType::NoArgument, 'l'),
wopt(L!("resident-set-size"), ArgType::NoArgument, 'm'),
wopt(L!("file-descriptor-count"), ArgType::NoArgument, 'n'),
wopt(L!("queue-size"), ArgType::NoArgument, 'q'),
wopt(L!("realtime-priority"), ArgType::NoArgument, 'r'),
wopt(L!("stack-size"), ArgType::NoArgument, 's'),
wopt(L!("cpu-time"), ArgType::NoArgument, 't'),
wopt(L!("process-count"), ArgType::NoArgument, 'u'),
wopt(L!("virtual-memory-size"), ArgType::NoArgument, 'v'),
wopt(L!("swap-size"), ArgType::NoArgument, 'w'),
wopt(L!("realtime-maxtime"), ArgType::NoArgument, 'y'),
wopt(L!("kernel-queues"), ArgType::NoArgument, 'K'),
wopt(L!("ptys"), ArgType::NoArgument, 'P'),
wopt(L!("threads"), ArgType::NoArgument, 'T'),
wopt(L!("help"), ArgType::NoArgument, 'h'),
];
let mut opts = Options::default();
let mut w = WGetopter::new(SHORT_OPTS, LONG_OPTS, args);
while let Some(c) = w.next_opt() {
match c {
'a' => opts.report_all = true,
'H' => opts.hard = true,
'S' => opts.soft = true,
'b' => opts.what = limits::SBSIZE,
'c' => opts.what = limits::CORE,
'd' => opts.what = limits::DATA,
'e' => opts.what = limits::NICE,
'f' => opts.what = limits::FSIZE,
'i' => opts.what = limits::SIGPENDING,
'l' => opts.what = limits::MEMLOCK,
'm' => opts.what = limits::RSS,
'n' => opts.what = limits::NOFILE,
'q' => opts.what = limits::MSGQUEUE,
'r' => opts.what = limits::RTPRIO,
's' => opts.what = limits::STACK,
't' => opts.what = limits::CPU,
'u' => opts.what = limits::NPROC,
'v' => opts.what = limits::AS,
'w' => opts.what = limits::SWAP,
'y' => opts.what = limits::RTTIME,
'K' => opts.what = limits::KQUEUES,
'P' => opts.what = limits::NPTS,
'T' => opts.what = limits::NTHR,
'h' => {
builtin_print_help(parser, streams, cmd);
return Ok(SUCCESS);
}
':' => {
builtin_missing_argument(parser, streams, cmd, w.argv[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
';' => {
builtin_unexpected_argument(parser, streams, cmd, w.argv[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
builtin_unknown_option(parser, streams, cmd, w.argv[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
_ => {
panic!("unexpected retval from WGetopter");
}
}
}
if opts.report_all {
print_all(opts.hard, streams);
}
if opts.what == -1 {
streams.err.append(wgettext_fmt!(
"%s: Resource limit not available on this operating system\n",
cmd
));
builtin_print_error_trailer(parser, streams.err, cmd);
return Err(STATUS_INVALID_ARGS);
}
let what: c_uint = opts.what.try_into().unwrap();
let argc = w.argv.len();
let arg_count = argc - w.wopt_index;
if arg_count == 0 {
print(what, opts.hard, streams);
return Ok(SUCCESS);
} else if arg_count != 1 {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
return Err(STATUS_INVALID_ARGS);
}
let mut hard = opts.hard;
let mut soft = opts.soft;
if !hard && !soft {
// Set both hard and soft limits if neither was specified.
hard = true;
soft = true;
}
let new_limit: rlim_t = if w.wopt_index == argc {
streams.err.append(wgettext_fmt!(
"%s: New limit cannot be an empty string\n",
cmd
));
builtin_print_error_trailer(parser, streams.err, cmd);
return Err(STATUS_INVALID_ARGS);
} else if wcscasecmp(w.argv[w.wopt_index], L!("unlimited")) == Ordering::Equal {
RLIM_INFINITY
} else if wcscasecmp(w.argv[w.wopt_index], L!("hard")) == Ordering::Equal {
match get(what, true) {
Some(limit) => limit,
None => return Err(STATUS_CMD_ERROR),
}
} else if wcscasecmp(w.argv[w.wopt_index], L!("soft")) == Ordering::Equal {
match get(what, soft) {
Some(limit) => limit,
None => return Err(STATUS_CMD_ERROR),
}
} else if let Ok(limit) = fish_wcstol(w.argv[w.wopt_index]) {
let Some(x) = get_multiplier(what).checked_mul(limit as rlim_t) else {
streams.err.append(wgettext_fmt!(
"%s: Invalid limit '%s'\n",
cmd,
w.argv[w.wopt_index]
));
builtin_print_error_trailer(parser, streams.err, cmd);
return Err(STATUS_INVALID_ARGS);
};
x
} else {
streams.err.append(wgettext_fmt!(
"%s: Invalid limit '%s'\n",
cmd,
w.argv[w.wopt_index]
));
builtin_print_error_trailer(parser, streams.err, cmd);
return Err(STATUS_INVALID_ARGS);
};
set_limit(what, hard, soft, new_limit, streams)
}
/// Struct describing a resource limit.
struct Resource {
resource: c_uint, // resource ID
desc: &'static wstr, // description of resource
switch_char: char, // switch used on commandline to specify resource
multiplier: c_uint, // the implicit multiplier used when setting getting values
}
impl Resource {
fn new(
resource: c_uint,
desc: &'static wstr,
switch_char: char,
multiplier: c_uint,
) -> Resource {
Resource {
resource,
desc,
switch_char,
multiplier,
}
}
}
/// Array of resource_t structs, describing all known resource types.
static RESOURCE_ARR: Lazy<Box<[Resource]>> = Lazy::new(|| {
let resources_info = [
(
limits::SBSIZE,
L!("Maximum size of socket buffers"),
'b',
1024,
),
(
limits::CORE,
L!("Maximum size of core files created"),
'c',
1024,
),
(
limits::DATA,
L!("Maximum size of a processs data segment"),
'd',
1024,
),
(limits::NICE, L!("Control of maximum nice priority"), 'e', 1),
(
limits::FSIZE,
L!("Maximum size of files created by the shell"),
'f',
1024,
),
(
limits::SIGPENDING,
L!("Maximum number of pending signals"),
'i',
1,
),
(
limits::MEMLOCK,
L!("Maximum size that may be locked into memory"),
'l',
1024,
),
(limits::RSS, L!("Maximum resident set size"), 'm', 1024),
(
limits::NOFILE,
L!("Maximum number of open file descriptors"),
'n',
1,
),
(
limits::MSGQUEUE,
L!("Maximum bytes in POSIX message queues"),
'q',
1024,
),
(
limits::RTPRIO,
L!("Maximum realtime scheduling priority"),
'r',
1,
),
(limits::STACK, L!("Maximum stack size"), 's', 1024),
(
limits::CPU,
L!("Maximum amount of CPU time in seconds"),
't',
1,
),
(
limits::NPROC,
L!("Maximum number of processes available to current user"),
'u',
1,
),
(
limits::AS,
L!("Maximum amount of virtual memory available to each process"),
'v',
1024,
),
(limits::SWAP, L!("Maximum swap space"), 'w', 1024),
(
limits::RTTIME,
L!("Maximum contiguous realtime CPU time"),
'y',
1,
),
(limits::KQUEUES, L!("Maximum number of kqueues"), 'K', 1),
(
limits::NPTS,
L!("Maximum number of pseudo-terminals"),
'P',
1,
),
(
limits::NTHR,
L!("Maximum number of simultaneous threads"),
'T',
1,
),
];
let unknown = -1;
let mut resources = Vec::new();
for resource in resources_info {
let (resource, desc, switch_char, multiplier) = resource;
if resource != unknown {
resources.push(Resource::new(
resource as c_uint,
desc,
switch_char,
multiplier,
));
}
}
resources.into_boxed_slice()
});