diff --git a/Cargo.lock b/Cargo.lock index e03ba19..70ed98d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -621,6 +621,7 @@ dependencies = [ "uu_pwdx", "uu_slabtop", "uu_snice", + "uu_sysctl", "uu_top", "uu_w", "uu_watch", @@ -1121,6 +1122,16 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_sysctl" +version = "0.0.1" +dependencies = [ + "clap", + "sysinfo", + "uucore", + "walkdir", +] + [[package]] name = "uu_top" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index e4d9217..b370523 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ feat_common_core = [ "slabtop", "snice", "pkill", + "sysctl", "top", "w", "watch", @@ -84,6 +85,7 @@ pwdx = { optional = true, version = "0.0.1", package = "uu_pwdx", path = "src/uu slabtop = { optional = true, version = "0.0.1", package = "uu_slabtop", path = "src/uu/slabtop" } snice = { optional = true, version = "0.0.1", package = "uu_snice", path = "src/uu/snice" } pkill = { optional = true, version = "0.0.1", package = "uu_pkill", path = "src/uu/pkill" } +sysctl = { optional = true, version = "0.0.1", package = "uu_sysctl", path = "src/uu/sysctl" } top = { optional = true, version = "0.0.1", package = "uu_top", path = "src/uu/top" } w = { optional = true, version = "0.0.1", package = "uu_w", path = "src/uu/w" } watch = { optional = true, version = "0.0.1", package = "uu_watch", path = "src/uu/watch" } diff --git a/src/uu/sysctl/Cargo.toml b/src/uu/sysctl/Cargo.toml new file mode 100644 index 0000000..f426085 --- /dev/null +++ b/src/uu/sysctl/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "uu_sysctl" +version = "0.0.1" +edition = "2021" +authors = ["uutils developers"] +license = "MIT" +description = "sysctl ~ (uutils) Show or modify kernel parameters at runtime" + +homepage = "https://github.com/uutils/procps" +repository = "https://github.com/uutils/procps/tree/main/src/uu/sysctl" +keywords = ["acl", "uutils", "cross-platform", "cli", "utility"] +categories = ["command-line-utilities"] + +[dependencies] +uucore = { workspace = true } +clap = { workspace = true } +sysinfo = { workspace = true } +walkdir = { workspace = true } + +[lib] +path = "src/sysctl.rs" + +[[bin]] +name = "sysctl" +path = "src/main.rs" diff --git a/src/uu/sysctl/src/main.rs b/src/uu/sysctl/src/main.rs new file mode 100644 index 0000000..7b14f30 --- /dev/null +++ b/src/uu/sysctl/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_sysctl); diff --git a/src/uu/sysctl/src/sysctl.rs b/src/uu/sysctl/src/sysctl.rs new file mode 100644 index 0000000..78aceb6 --- /dev/null +++ b/src/uu/sysctl/src/sysctl.rs @@ -0,0 +1,199 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use clap::{crate_version, Arg, ArgAction, Command}; +use std::env; +use uucore::error::UResult; +use uucore::{format_usage, help_about, help_usage}; + +const ABOUT: &str = help_about!("sysctl.md"); +const USAGE: &str = help_usage!("sysctl.md"); + +#[cfg(target_os = "linux")] +mod linux { + use std::path::{Path, PathBuf}; + use uucore::error::{FromIo, UIoError}; + use walkdir::WalkDir; + + const PROC_SYS_ROOT: &str = "/proc/sys"; + + pub fn get_all_sysctl_variables() -> Vec { + let mut ret = vec![]; + for entry in WalkDir::new(PROC_SYS_ROOT) { + match entry { + Ok(e) => { + if e.file_type().is_file() { + let var = e + .path() + .strip_prefix(PROC_SYS_ROOT) + .expect("Always should be ancestor of of sysctl root"); + if let Some(s) = var.as_os_str().to_str() { + ret.push(s.to_owned()); + } + } + } + Err(e) => { + uucore::show_error!("{}", e); + } + } + } + ret + } + + pub fn normalize_var(var: &str) -> String { + var.replace('/', ".") + } + + pub fn variable_path(var: &str) -> PathBuf { + Path::new(PROC_SYS_ROOT).join(var.replace('.', "/")) + } + + pub fn get_sysctl(var: &str) -> std::io::Result { + Ok(std::fs::read_to_string(variable_path(var))? + .trim_end() + .to_string()) + } + + pub fn set_sysctl(var: &str, value: &str) -> std::io::Result<()> { + std::fs::write(variable_path(var), value) + } + + pub fn handle_one_arg( + var_or_assignment: &str, + quiet: bool, + ) -> Result, Box> { + let mut splitted = var_or_assignment.splitn(2, '='); + let var = normalize_var( + splitted + .next() + .expect("Split always returns at least 1 value"), + ); + + if let Some(value_to_set) = splitted.next() { + set_sysctl(&var, value_to_set) + .map_err(|e| e.map_err_context(|| format!("error writing key '{}'", var)))?; + if quiet { + Ok(None) + } else { + Ok(Some((var, value_to_set.to_string()))) + } + } else { + let value = get_sysctl(&var) + .map_err(|e| e.map_err_context(|| format!("error reading key '{}'", var)))?; + Ok(Some((var, value))) + } + } +} +#[cfg(target_os = "linux")] +use linux::*; + +#[cfg(target_os = "linux")] +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().try_get_matches_from(args)?; + + let vars = if matches.get_flag("all") { + get_all_sysctl_variables() + } else if let Some(vars) = matches.get_many::("variables") { + vars.cloned().collect() + } else { + uu_app().print_help()?; + return Ok(()); + }; + + for var_or_assignment in vars { + match handle_one_arg(&var_or_assignment, matches.get_flag("quiet")) { + Ok(None) => (), + Ok(Some((var, value_to_print))) => { + for line in value_to_print.split('\n') { + if matches.get_flag("names") { + println!("{}", var); + } else if matches.get_flag("values") { + println!("{}", line); + } else { + println!("{} = {}", var, line); + } + } + } + Err(e) => { + if !matches.get_flag("ignore") { + uucore::show!(e); + } + } + } + } + + Ok(()) +} + +#[cfg(not(target_os = "linux"))] +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let _matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; + + Err(uucore::error::USimpleError::new( + 1, + "`sysctl` currently only supports Linux.", + )) +} + +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .arg( + Arg::new("variables") + .value_name("VARIABLE[=VALUE]") + .action(ArgAction::Append), + ) + .arg( + Arg::new("all") + .short('a') + .visible_short_aliases(['A', 'X']) + .long("all") + .action(ArgAction::SetTrue) + .help("Display all variables"), + ) + .arg( + Arg::new("names") + .short('N') + .long("names") + .action(ArgAction::SetTrue) + .help("Only print names"), + ) + .arg( + Arg::new("values") + .short('n') + .long("values") + .action(ArgAction::SetTrue) + .help("Only print values"), + ) + .arg( + Arg::new("ignore") + .short('e') + .long("ignore") + .action(ArgAction::SetTrue) + .help("Ignore errors"), + ) + .arg( + Arg::new("quiet") + .short('q') + .long("quiet") + .action(ArgAction::SetTrue) + .help("Do not print when setting variables"), + ) + .arg( + Arg::new("noop_o") + .short('o') + .help("Does nothing, for BSD compatibility"), + ) + .arg( + Arg::new("noop_x") + .short('x') + .help("Does nothing, for BSD compatibility"), + ) +} diff --git a/src/uu/sysctl/sysctl.md b/src/uu/sysctl/sysctl.md new file mode 100644 index 0000000..94e4b12 --- /dev/null +++ b/src/uu/sysctl/sysctl.md @@ -0,0 +1,7 @@ +# sysctl + +``` +sysctl [options] [variable[=value]]... +``` + +Show or modify kernel parameters at runtime. diff --git a/tests/by-util/test_sysctl.rs b/tests/by-util/test_sysctl.rs new file mode 100644 index 0000000..c3931bb --- /dev/null +++ b/tests/by-util/test_sysctl.rs @@ -0,0 +1,81 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::common::util::TestScenario; + +#[test] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); +} + +#[cfg(target_os = "linux")] +mod linux { + use crate::common::util::TestScenario; + + #[test] + fn test_get_simple() { + new_ucmd!() + .arg("kernel.ostype") + .arg("fs.overflowuid") + .succeeds() + .stdout_is("kernel.ostype = Linux\nfs.overflowuid = 65534\n"); + } + + #[test] + fn test_get_value_only() { + new_ucmd!() + .arg("-n") + .arg("kernel.ostype") + .arg("fs.overflowuid") + .succeeds() + .stdout_is("Linux\n65534\n"); + } + + #[test] + fn test_get_key_only() { + new_ucmd!() + .arg("-N") + .arg("kernel.ostype") + .arg("fs.overflowuid") + .succeeds() + .stdout_is("kernel.ostype\nfs.overflowuid\n"); + } + + #[test] + fn test_continues_on_error() { + new_ucmd!() + .arg("nonexisting") + .arg("kernel.ostype") + .fails() + .stdout_is("kernel.ostype = Linux\n") + .stderr_is("sysctl: error reading key 'nonexisting': No such file or directory\n"); + } + + #[test] + fn test_ignoring_errors() { + new_ucmd!() + .arg("-e") + .arg("nonexisting") + .arg("nonexisting2=foo") + .arg("kernel.ostype") + .succeeds() + .stdout_is("kernel.ostype = Linux\n") + .stderr_is(""); + } +} + +#[cfg(not(target_os = "linux"))] +mod non_linux { + use crate::common::util::TestScenario; + + #[test] + fn test_fails_on_unsupported_platforms() { + new_ucmd!() + .arg("-a") + .fails() + .code_is(1) + .stderr_is("sysctl: `sysctl` currently only supports Linux.\n"); + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 77ce68f..2749cba 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -56,3 +56,7 @@ mod test_snice; #[cfg(feature = "pkill")] #[path = "by-util/test_pkill.rs"] mod test_pkill; + +#[cfg(feature = "sysctl")] +#[path = "by-util/test_sysctl.rs"] +mod test_sysctl;