Add sys subcommands (#12747)

# Description
Adds subcommands to `sys` corresponding to each column of the record
returned by `sys`. This is to alleviate the fact that `sys` now returns
a regular record, meaning that it must compute every column which might
take a noticeable amount of time. The subcommands, on the other hand,
only need to compute and return a subset of the data which should be
much faster. In fact, it should be as fast as before, since this is how
the lazy record worked (it would compute only each column as necessary).

I choose to add subcommands instead of having an optional cell-path
parameter on `sys`, since the cell-path parameter would:
- increase the code complexity (can access any value at any row or
nested column)
- prevents discovery with tab-completion
- hinders type checking and allows users to pass potentially invalid
columns

# User-Facing Changes
Deprecates `sys` in favor of the new `sys` subcommands.
This commit is contained in:
Ian Manske 2024-05-06 23:20:27 +00:00 committed by GitHub
parent 68adc4657f
commit 1038c64f80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 522 additions and 283 deletions

View file

@ -119,6 +119,12 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
Exec, Exec,
NuCheck, NuCheck,
Sys, Sys,
SysCpu,
SysDisks,
SysHost,
SysMem,
SysNet,
SysTemp,
UName, UName,
}; };

View file

@ -124,11 +124,6 @@ If multiple cell paths are given, this will produce a list of values."#
example: "ls | get 2.name", example: "ls | get 2.name",
result: None, result: None,
}, },
Example {
description: "Extract the cpu list from the sys information record",
example: "sys | get cpu",
result: None,
},
Example { Example {
description: "Getting Path/PATH in a case insensitive way", description: "Getting Path/PATH in a case insensitive way",
example: "$env | get paTH", example: "$env | get paTH",

View file

@ -66,8 +66,8 @@ Each stage in the pipeline works together to load, parse, and display informatio
List the files in the current directory, sorted by size: List the files in the current directory, sorted by size:
ls | sort-by size ls | sort-by size
Get information about the current system: Get the current system host name:
sys | get host sys host | get hostname
Get the processes on your system actively using CPU: Get the processes on your system actively using CPU:
ps | where cpu > 0 ps | where cpu > 0

View file

@ -30,6 +30,6 @@ pub use ps::Ps;
#[cfg(windows)] #[cfg(windows)]
pub use registry_query::RegistryQuery; pub use registry_query::RegistryQuery;
pub use run_external::{External, ExternalCommand}; pub use run_external::{External, ExternalCommand};
pub use sys::Sys; pub use sys::*;
pub use uname::UName; pub use uname::UName;
pub use which_::Which; pub use which_::Which;

View file

@ -1,269 +0,0 @@
use chrono::{DateTime, Local};
use nu_engine::command_prelude::*;
use std::time::{Duration, UNIX_EPOCH};
use sysinfo::{
Components, CpuRefreshKind, Disks, Networks, System, Users, MINIMUM_CPU_UPDATE_INTERVAL,
};
#[derive(Clone)]
pub struct Sys;
impl Command for Sys {
fn name(&self) -> &str {
"sys"
}
fn signature(&self) -> Signature {
Signature::build("sys")
.filter()
.category(Category::System)
.input_output_types(vec![(Type::Nothing, Type::record())])
}
fn usage(&self) -> &str {
"View information about the system."
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(all_columns(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Show info about the system",
example: "sys",
result: None,
},
Example {
description: "Show the os system name with get",
example: "(sys).host | get name",
result: None,
},
Example {
description: "Show the os system name",
example: "(sys).host.name",
result: None,
},
]
}
}
#[derive(Debug, Clone)]
pub struct SysResult {
pub span: Span,
}
fn all_columns(span: Span) -> Value {
Value::record(
record! {
"host" => host(span),
"cpu" => cpu(span),
"disks" => disks(span),
"mem" => mem(span),
"temp" => temp(span),
"net" => net(span),
},
span,
)
}
pub fn trim_cstyle_null(s: String) -> String {
s.trim_matches(char::from(0)).to_string()
}
pub fn disks(span: Span) -> Value {
let disks = Disks::new_with_refreshed_list();
let mut output = vec![];
for disk in disks.list() {
let device = trim_cstyle_null(disk.name().to_string_lossy().to_string());
let typ = trim_cstyle_null(disk.file_system().to_string_lossy().to_string());
let record = record! {
"device" => Value::string(device, span),
"type" => Value::string(typ, span),
"mount" => Value::string(disk.mount_point().to_string_lossy(), span),
"total" => Value::filesize(disk.total_space() as i64, span),
"free" => Value::filesize(disk.available_space() as i64, span),
"removable" => Value::bool(disk.is_removable(), span),
"kind" => Value::string(format!("{:?}", disk.kind()), span),
};
output.push(Value::record(record, span));
}
Value::list(output, span)
}
pub fn net(span: Span) -> Value {
let networks = Networks::new_with_refreshed_list();
let mut output = vec![];
for (iface, data) in networks.list() {
let record = record! {
"name" => Value::string(trim_cstyle_null(iface.to_string()), span),
"sent" => Value::filesize(data.total_transmitted() as i64, span),
"recv" => Value::filesize(data.total_received() as i64, span),
};
output.push(Value::record(record, span));
}
Value::list(output, span)
}
pub fn cpu(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_cpu_specifics(CpuRefreshKind::everything());
// We must refresh the CPU twice a while apart to get valid usage data.
// In theory we could just sleep MINIMUM_CPU_UPDATE_INTERVAL, but I've noticed that
// that gives poor results (error of ~5%). Decided to wait 2x that long, somewhat arbitrarily
std::thread::sleep(MINIMUM_CPU_UPDATE_INTERVAL * 2);
sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage());
let mut output = vec![];
for cpu in sys.cpus() {
// sysinfo CPU usage numbers are not very precise unless you wait a long time between refreshes.
// Round to 1DP (chosen somewhat arbitrarily) so people aren't misled by high-precision floats.
let rounded_usage = (cpu.cpu_usage() * 10.0).round() / 10.0;
let load_avg = System::load_average();
let load_avg = trim_cstyle_null(format!(
"{:.2}, {:.2}, {:.2}",
load_avg.one, load_avg.five, load_avg.fifteen
));
let record = record! {
"name" => Value::string(trim_cstyle_null(cpu.name().to_string()), span),
"brand" => Value::string(trim_cstyle_null(cpu.brand().to_string()), span),
"freq" => Value::int(cpu.frequency() as i64, span),
"cpu_usage" => Value::float(rounded_usage as f64, span),
"load_average" => Value::string(load_avg, span),
"vendor_id" => Value::string(trim_cstyle_null(cpu.vendor_id().to_string()), span),
};
output.push(Value::record(record, span));
}
Value::list(output, span)
}
pub fn mem(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_memory();
let total_mem = sys.total_memory();
let free_mem = sys.free_memory();
let used_mem = sys.used_memory();
let avail_mem = sys.available_memory();
let total_swap = sys.total_swap();
let free_swap = sys.free_swap();
let used_swap = sys.used_swap();
let record = record! {
"total" => Value::filesize(total_mem as i64, span),
"free" => Value::filesize(free_mem as i64, span),
"used" => Value::filesize(used_mem as i64, span),
"available" => Value::filesize(avail_mem as i64, span),
"swap total" => Value::filesize(total_swap as i64, span),
"swap free" => Value::filesize(free_swap as i64, span),
"swap used" => Value::filesize(used_swap as i64, span),
};
Value::record(record, span)
}
pub fn host(span: Span) -> Value {
let mut record = Record::new();
if let Some(name) = System::name() {
record.push("name", Value::string(trim_cstyle_null(name), span));
}
if let Some(version) = System::os_version() {
record.push("os_version", Value::string(trim_cstyle_null(version), span));
}
if let Some(long_version) = System::long_os_version() {
record.push(
"long_os_version",
Value::string(trim_cstyle_null(long_version), span),
);
}
if let Some(version) = System::kernel_version() {
record.push(
"kernel_version",
Value::string(trim_cstyle_null(version), span),
);
}
if let Some(hostname) = System::host_name() {
record.push("hostname", Value::string(trim_cstyle_null(hostname), span));
}
record.push(
"uptime",
Value::duration(1000000000 * System::uptime() as i64, span),
);
// Creates a new SystemTime from the specified number of whole seconds
let d = UNIX_EPOCH + Duration::from_secs(System::boot_time());
// Create DateTime from SystemTime
let datetime = DateTime::<Local>::from(d);
// Convert to local time and then rfc3339
let timestamp_str = datetime.with_timezone(datetime.offset()).to_rfc3339();
record.push("boot_time", Value::string(timestamp_str, span));
let users = Users::new_with_refreshed_list();
let mut users_list = vec![];
for user in users.list() {
let mut groups = vec![];
for group in user.groups() {
groups.push(Value::string(
trim_cstyle_null(group.name().to_string()),
span,
));
}
let record = record! {
"name" => Value::string(trim_cstyle_null(user.name().to_string()), span),
"groups" => Value::list(groups, span),
};
users_list.push(Value::record(record, span));
}
if !users.is_empty() {
record.push("sessions", Value::list(users_list, span));
}
Value::record(record, span)
}
pub fn temp(span: Span) -> Value {
let components = Components::new_with_refreshed_list();
let mut output = vec![];
for component in components.list() {
let mut record = record! {
"unit" => Value::string(component.label(), span),
"temp" => Value::float(component.temperature() as f64, span),
"high" => Value::float(component.max() as f64, span),
};
if let Some(critical) = component.critical() {
record.push("critical", Value::float(critical as f64, span));
}
output.push(Value::record(record, span));
}
Value::list(output, span)
}

View file

@ -0,0 +1,39 @@
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct SysCpu;
impl Command for SysCpu {
fn name(&self) -> &str {
"sys cpu"
}
fn signature(&self) -> Signature {
Signature::build("sys cpu")
.filter()
.category(Category::System)
.input_output_types(vec![(Type::Nothing, Type::table())])
}
fn usage(&self) -> &str {
"View information about the system CPUs."
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::cpu(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Show info about the system CPUs",
example: "sys cpu",
result: None,
}]
}
}

View file

@ -0,0 +1,39 @@
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct SysDisks;
impl Command for SysDisks {
fn name(&self) -> &str {
"sys disks"
}
fn signature(&self) -> Signature {
Signature::build("sys disks")
.filter()
.category(Category::System)
.input_output_types(vec![(Type::Nothing, Type::table())])
}
fn usage(&self) -> &str {
"View information about the system disks."
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::disks(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Show info about the system disks",
example: "sys disks",
result: None,
}]
}
}

View file

@ -0,0 +1,39 @@
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct SysHost;
impl Command for SysHost {
fn name(&self) -> &str {
"sys host"
}
fn signature(&self) -> Signature {
Signature::build("sys host")
.filter()
.category(Category::System)
.input_output_types(vec![(Type::Nothing, Type::record())])
}
fn usage(&self) -> &str {
"View information about the system host."
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::host(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Show info about the system host",
example: "sys host",
result: None,
}]
}
}

View file

@ -0,0 +1,39 @@
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct SysMem;
impl Command for SysMem {
fn name(&self) -> &str {
"sys mem"
}
fn signature(&self) -> Signature {
Signature::build("sys mem")
.filter()
.category(Category::System)
.input_output_types(vec![(Type::Nothing, Type::record())])
}
fn usage(&self) -> &str {
"View information about the system memory."
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::mem(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Show info about the system memory",
example: "sys mem",
result: None,
}]
}
}

View file

@ -0,0 +1,205 @@
mod cpu;
mod disks;
mod host;
mod mem;
mod net;
mod sys_;
mod temp;
pub use cpu::SysCpu;
pub use disks::SysDisks;
pub use host::SysHost;
pub use mem::SysMem;
pub use net::SysNet;
pub use sys_::Sys;
pub use temp::SysTemp;
use chrono::{DateTime, Local};
use nu_protocol::{record, Record, Span, Value};
use std::time::{Duration, UNIX_EPOCH};
use sysinfo::{
Components, CpuRefreshKind, Disks, Networks, System, Users, MINIMUM_CPU_UPDATE_INTERVAL,
};
pub fn trim_cstyle_null(s: impl AsRef<str>) -> String {
s.as_ref().trim_matches('\0').into()
}
pub fn disks(span: Span) -> Value {
let disks = Disks::new_with_refreshed_list()
.iter()
.map(|disk| {
let device = trim_cstyle_null(disk.name().to_string_lossy());
let typ = trim_cstyle_null(disk.file_system().to_string_lossy());
let record = record! {
"device" => Value::string(device, span),
"type" => Value::string(typ, span),
"mount" => Value::string(disk.mount_point().to_string_lossy(), span),
"total" => Value::filesize(disk.total_space() as i64, span),
"free" => Value::filesize(disk.available_space() as i64, span),
"removable" => Value::bool(disk.is_removable(), span),
"kind" => Value::string(disk.kind().to_string(), span),
};
Value::record(record, span)
})
.collect();
Value::list(disks, span)
}
pub fn net(span: Span) -> Value {
let networks = Networks::new_with_refreshed_list()
.iter()
.map(|(iface, data)| {
let record = record! {
"name" => Value::string(trim_cstyle_null(iface), span),
"sent" => Value::filesize(data.total_transmitted() as i64, span),
"recv" => Value::filesize(data.total_received() as i64, span),
};
Value::record(record, span)
})
.collect();
Value::list(networks, span)
}
pub fn cpu(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_cpu_specifics(CpuRefreshKind::everything());
// We must refresh the CPU twice a while apart to get valid usage data.
// In theory we could just sleep MINIMUM_CPU_UPDATE_INTERVAL, but I've noticed that
// that gives poor results (error of ~5%). Decided to wait 2x that long, somewhat arbitrarily
std::thread::sleep(MINIMUM_CPU_UPDATE_INTERVAL * 2);
sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage());
let cpus = sys
.cpus()
.iter()
.map(|cpu| {
// sysinfo CPU usage numbers are not very precise unless you wait a long time between refreshes.
// Round to 1DP (chosen somewhat arbitrarily) so people aren't misled by high-precision floats.
let rounded_usage = (cpu.cpu_usage() * 10.0).round() / 10.0;
let load_avg = System::load_average();
let load_avg = format!(
"{:.2}, {:.2}, {:.2}",
load_avg.one, load_avg.five, load_avg.fifteen
);
let record = record! {
"name" => Value::string(trim_cstyle_null(cpu.name()), span),
"brand" => Value::string(trim_cstyle_null(cpu.brand()), span),
"freq" => Value::int(cpu.frequency() as i64, span),
"cpu_usage" => Value::float(rounded_usage.into(), span),
"load_average" => Value::string(load_avg, span),
"vendor_id" => Value::string(trim_cstyle_null(cpu.vendor_id()), span),
};
Value::record(record, span)
})
.collect();
Value::list(cpus, span)
}
pub fn mem(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_memory();
let record = record! {
"total" => Value::filesize(sys.total_memory() as i64, span),
"free" => Value::filesize(sys.free_memory() as i64, span),
"used" => Value::filesize(sys.used_memory() as i64, span),
"available" => Value::filesize(sys.available_memory() as i64, span),
"swap total" => Value::filesize(sys.total_swap() as i64, span),
"swap free" => Value::filesize(sys.free_swap() as i64, span),
"swap used" => Value::filesize(sys.used_swap() as i64, span),
};
Value::record(record, span)
}
pub fn host(span: Span) -> Value {
let mut record = Record::new();
if let Some(name) = System::name() {
record.push("name", Value::string(trim_cstyle_null(name), span));
}
if let Some(version) = System::os_version() {
record.push("os_version", Value::string(trim_cstyle_null(version), span));
}
if let Some(long_version) = System::long_os_version() {
record.push(
"long_os_version",
Value::string(trim_cstyle_null(long_version), span),
);
}
if let Some(version) = System::kernel_version() {
record.push(
"kernel_version",
Value::string(trim_cstyle_null(version), span),
);
}
if let Some(hostname) = System::host_name() {
record.push("hostname", Value::string(trim_cstyle_null(hostname), span));
}
record.push(
"uptime",
Value::duration(1000000000 * System::uptime() as i64, span),
);
// Creates a new SystemTime from the specified number of whole seconds
let d = UNIX_EPOCH + Duration::from_secs(System::boot_time());
// Create DateTime from SystemTime
let datetime = DateTime::<Local>::from(d);
// Convert to local time and then rfc3339
let timestamp_str = datetime.with_timezone(datetime.offset()).to_rfc3339();
record.push("boot_time", Value::string(timestamp_str, span));
let users = Users::new_with_refreshed_list()
.iter()
.map(|user| {
let groups = user
.groups()
.iter()
.map(|group| Value::string(trim_cstyle_null(group.name()), span))
.collect();
let record = record! {
"name" => Value::string(trim_cstyle_null(user.name()), span),
"groups" => Value::list(groups, span),
};
Value::record(record, span)
})
.collect();
record.push("sessions", Value::list(users, span));
Value::record(record, span)
}
pub fn temp(span: Span) -> Value {
let components = Components::new_with_refreshed_list()
.iter()
.map(|component| {
let mut record = record! {
"unit" => Value::string(component.label(), span),
"temp" => Value::float(component.temperature().into(), span),
"high" => Value::float(component.max().into(), span),
};
if let Some(critical) = component.critical() {
record.push("critical", Value::float(critical.into(), span));
}
Value::record(record, span)
})
.collect();
Value::list(components, span)
}

View file

@ -0,0 +1,39 @@
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct SysNet;
impl Command for SysNet {
fn name(&self) -> &str {
"sys net"
}
fn signature(&self) -> Signature {
Signature::build("sys net")
.filter()
.category(Category::System)
.input_output_types(vec![(Type::Nothing, Type::table())])
}
fn usage(&self) -> &str {
"View information about the system network interfaces."
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::net(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Show info about the system network",
example: "sys net",
result: None,
}]
}
}

View file

@ -0,0 +1,64 @@
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct Sys;
impl Command for Sys {
fn name(&self) -> &str {
"sys"
}
fn signature(&self) -> Signature {
Signature::build("sys")
.filter()
.category(Category::System)
.input_output_types(vec![(Type::Nothing, Type::record())])
}
fn usage(&self) -> &str {
"View information about the system."
}
fn extra_usage(&self) -> &str {
"Note that this command may take a noticeable amount of time to run. To reduce the time taken, you can use the various `sys` sub commands to get the subset of information you are interested in."
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
nu_protocol::report_error_new(
engine_state,
&ShellError::GenericError {
error: "Deprecated command".into(),
msg: "the `sys` command is deprecated, please use the new subcommands (`sys host`, `sys mem`, etc.)."
.into(),
span: Some(call.head),
help: None,
inner: vec![],
},
);
let head = call.head;
let record = record! {
"host" => super::host(head),
"cpu" => super::cpu(head),
"disks" => super::disks(head),
"mem" => super::mem(head),
"temp" => super::temp(head),
"net" => super::net(head),
};
Ok(Value::record(record, head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Show info about the system",
example: "sys",
result: None,
}]
}
}

View file

@ -0,0 +1,43 @@
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct SysTemp;
impl Command for SysTemp {
fn name(&self) -> &str {
"sys temp"
}
fn signature(&self) -> Signature {
Signature::build("sys temp")
.filter()
.category(Category::System)
.input_output_types(vec![(Type::Nothing, Type::table())])
}
fn usage(&self) -> &str {
"View the temperatures of system components."
}
fn extra_usage(&self) -> &str {
"Some system components do not support temperature readings, so this command may return an empty list if no components support temperature."
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::temp(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Show the system temperatures",
example: "sys temp",
result: None,
}]
}
}

View file

@ -172,7 +172,7 @@ fn select_ignores_errors_successfully2() {
#[test] #[test]
fn select_ignores_errors_successfully3() { fn select_ignores_errors_successfully3() {
let actual = nu!("sys | select invalid_key? | to nuon"); let actual = nu!("{foo: bar} | select invalid_key? | to nuon");
assert_eq!(actual.out, "{invalid_key: null}".to_string()); assert_eq!(actual.out, "{invalid_key: null}".to_string());
assert!(actual.err.is_empty()); assert!(actual.err.is_empty());

View file

@ -110,8 +110,8 @@ impl Command for Explore {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example { Example {
description: "Explore the system information record", description: "Explore the system host information record",
example: r#"sys | explore"#, example: r#"sys host | explore"#,
result: None, result: None,
}, },
Example { Example {

View file

@ -739,8 +739,8 @@ Each stage in the pipeline works together to load, parse, and display informatio
List the files in the current directory, sorted by size List the files in the current directory, sorted by size
> ('ls | sort-by size' | nu-highlight) > ('ls | sort-by size' | nu-highlight)
Get information about the current system Get the current system host name
> ('sys | get host' | nu-highlight) > ('sys host | get hostname' | nu-highlight)
Get the processes on your system actively using CPU Get the processes on your system actively using CPU
> ('ps | where cpu > 0' | nu-highlight) > ('ps | where cpu > 0' | nu-highlight)

View file

@ -1,7 +1,7 @@
use std log use std log
def "nu-complete threads" [] { def "nu-complete threads" [] {
seq 1 (sys|get cpu|length) seq 1 (sys cpu | length)
} }
# Here we store the map of annotations internal names and the annotation actually used during test creation # Here we store the map of annotations internal names and the annotation actually used during test creation