Skip to content

Commit

Permalink
Merge pull request #22 from tinythings/isbm-sys_run-module
Browse files Browse the repository at this point in the history
Add sys.run module
  • Loading branch information
isbm authored Oct 23, 2024
2 parents faf8872 + 4c5449b commit b4d5442
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 17 deletions.
41 changes: 26 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@ check:

devel:
cargo build -v
cargo build -v -p proc -p net
cargo build -v -p proc -p net -p run

# Move plugin binaries
rm -rf target/debug/sys
mkdir -p target/debug/sys
mv target/debug/proc target/debug/sys/
mv target/debug/net target/debug/sys/
mv target/debug/run target/debug/sys/

build:
cargo build --release
cargo build -p proc --release
cargo build -p proc -p net -p run --release

# Move plugin binaries
rm -rf target/release/sys
mkdir -p target/release/sys
mv target/release/proc target/release/sys/
mv target/release/net target/release/sys/
mv target/release/run target/release/sys/

man:
pandoc --standalone --to man docs/manpages/sysinspect.8.md -o docs/manpages/sysinspect.8
Expand Down
11 changes: 11 additions & 0 deletions modules/sys/run/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "run"
version = "0.1.0"
edition = "2021"

[dependencies]
libsysinspect = { path = "../../../libsysinspect" }
serde = "1.0.213"
serde_json = "1.0.132"
serde_yaml = "0.9.34"
shlex = "1.3.0"
158 changes: 158 additions & 0 deletions modules/sys/run/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use core::str;
use libsysinspect::{
init_mod_doc,
modlib::{
modinit::ModInterface,
response::ModResponse,
runtime::{self, get_arg, get_call_args, get_opt, send_call_response, ModRequest},
},
};
use serde_json::json;
use shlex::Shlex;
use std::{
collections::HashMap,
io::Write,
process::{Command, Stdio},
};

/// Parse passed environment.
/// Env is passed in the form of key=value. The following form is supported:
///
/// `VAR_ONE="value" VAR_TWO=value VAR_THREE="spaces are supported"`
fn getenv(env: &str) -> HashMap<String, String> {
let mut out = HashMap::new();
for elm in Shlex::new(env) {
if let Some(pos) = elm.find('=') {
out.insert(elm[..pos].to_string(), elm[pos + 1..].to_string().trim_matches('"').to_string());
}
}

out
}

/// Call an external command.
/// In a pretty ugly way...
fn call(cmd: &str, send: &str, locale: &str, env: &str, disown: bool) -> ModResponse {
let mut resp = runtime::new_call_response();
resp.set_retcode(1);

let args = cmd.split_whitespace().collect::<Vec<&str>>();
let mut l_loc = locale;
if locale.is_empty() {
l_loc = "C";
}

let mut process = Command::new(args[0]);
process.env_clear();
process.args(&args[1..]);

// Set locale
[("LC_ALL", l_loc), ("LANG", l_loc)].iter().for_each(|(n, v)| {
process.env(n, v);
});

// Set env
getenv(env).into_iter().for_each(|(vr, vl)| {
process.env(vr, vl);
});

if disown {
match process.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()).spawn() {
Ok(_) => {
resp.set_retcode(0);
resp.set_message(&format!("'{}' is running in background", cmd));
}
Err(err) => resp.set_message(&err.to_string()),
}
return resp;
}

match process.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn() {
Ok(mut p) => {
if !send.is_empty() {
if let Some(mut stdin) = p.stdin.take() {
if let Err(err) = stdin.write_all(send.as_bytes()) {
resp.set_message(&err.to_string());
return resp;
}
}
}

// XXX: In the moment this is blocking. If a command blocks,
// then the whole thing will wait until forever. A better approach
// would be to take stdout and then read it in a reader, while maintaining
// a timeout and then kill the child.
match p.wait_with_output() {
Ok(out) => {
if out.status.success() {
match str::from_utf8(&out.stdout) {
Ok(stdout) => {
if let Err(err) = resp.set_data(json!({"stdout": stdout})) {
resp.set_message(&err.to_string());
} else {
resp.set_retcode(0);
resp.set_message(&format!("\"{}\" finished", cmd));
}
resp
}
Err(err) => {
resp.set_message(&format!("Error getting output: {:?}", err));
resp
}
}
} else {
match str::from_utf8(&out.stderr) {
Ok(stderr) => {
let mut r = runtime::new_call_response();
r.set_retcode(out.status.code().unwrap_or(1));
r.set_message(stderr);
}
Err(err) => {
resp.set_message(&err.to_string());
}
}
resp
}
}
Err(err) => {
resp.set_message(&err.to_string());
resp
}
}
}
Err(err) => {
resp.set_message(&format!("Error running '{}': {}", cmd, err));
resp
}
}
}

fn run_mod(rt: &ModRequest) -> ModResponse {
let mut res = ModResponse::new();

let cmd = get_arg(rt, "cmd");
if cmd.is_empty() {
res.set_retcode(1);
res.set_message("Missing command");
return res;
}

call(&cmd, &get_arg(rt, "send"), &get_arg(rt, "locale"), &get_arg(rt, "env"), get_opt(rt, "disown"))
}

fn main() {
let mod_doc = init_mod_doc!(ModInterface);
if mod_doc.print_help() {
return;
}

match get_call_args() {
Ok(rt) => match send_call_response(&run_mod(&rt)) {
Ok(_) => {}
Err(err) => println!("Runtime error: {}", err),
},
Err(err) => {
println!("Arguments error: {}", err)
}
}
}
52 changes: 52 additions & 0 deletions modules/sys/run/src/mod_doc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: "sys.run"
version: "0.0.1"
author: "Bo Maryniuk"
description: |
Plugin to run just a raw commands and return generically
structured output from them.
# Options, flags, switches
options:
- name: disown
description: "Leave command running in the background"

# Keyword arguments
arguments:
- name: cmd
type: string
required: true
description: "Full command to run"

- name: send
type: string
required: false
description: "Send uninterpolated data to the program input (STDIN)"

- name: env
type: string
required: false
description: "Modify the environment for the target running command"

- name: locale
type: string
required: false
description: "Set the locale for this command (default: LC_CTYPE=C)"

examples:
- description: "Module usage example"
code: |
"arguments": {
"cmd": "spotify --headless",
"env": "PATH=$PATH:/opt/spotify/bin"
}
# Description of additional data format
returns:
description: |
Returns just a regular text of the command STDOUT.
NOTE: if "disown" flag is specified, no data is returned.
example: |
{
"stdout": "..."
},

0 comments on commit b4d5442

Please sign in to comment.