94 lines
3.1 KiB
Rust
94 lines
3.1 KiB
Rust
//! The third tool, `shell`: run one command under `/bin/sh -c` in a directory and report its
|
|
//! output (stdout and stderr together, in order) followed by how the command ended. The command's
|
|
//! own exit status is part of the result, not `toolkit`'s.
|
|
|
|
use std::io::{self, Read};
|
|
use std::os::unix::process::ExitStatusExt;
|
|
use std::path::Path;
|
|
use std::process::{Command, Stdio};
|
|
|
|
use proto::tools::ShellArgs;
|
|
|
|
use crate::Outcome;
|
|
|
|
pub const SHELL: &str = "/bin/sh";
|
|
pub const DEFAULT_CWD: &str = "/tmp";
|
|
pub const MAX_OUTPUT: usize = 1024 * 1024;
|
|
|
|
pub fn shell(args: &ShellArgs) -> Outcome {
|
|
let cwd = match args.cwd.as_deref() {
|
|
Some(c) => c,
|
|
None => DEFAULT_CWD,
|
|
};
|
|
if !Path::new(cwd).is_dir() {
|
|
return Outcome::tool_error(format!("shell: {cwd}: no such directory"));
|
|
}
|
|
|
|
let (mut reader, writer) = match io::pipe() {
|
|
Ok(p) => p,
|
|
Err(e) => return Outcome::tool_error(format!("shell: cannot make a pipe: {e}")),
|
|
};
|
|
let writer2 = match writer.try_clone() {
|
|
Ok(w) => w,
|
|
Err(e) => return Outcome::tool_error(format!("shell: cannot make a pipe: {e}")),
|
|
};
|
|
|
|
let mut command = Command::new(SHELL);
|
|
command.arg("-c").arg(&args.command).current_dir(cwd);
|
|
command.stdin(Stdio::null());
|
|
command.stdout(writer);
|
|
command.stderr(writer2);
|
|
let mut child = match command.spawn() {
|
|
Ok(c) => c,
|
|
Err(e) => return Outcome::tool_error(format!("shell: cannot start {SHELL}: {e}")),
|
|
};
|
|
// The Command still owns the write ends; while they are open reading never reaches EOF.
|
|
drop(command);
|
|
|
|
let mut buf = [0u8; 8 * 1024];
|
|
let mut out: Vec<u8> = Vec::new();
|
|
let mut dropped = false;
|
|
loop {
|
|
let n = match reader.read(&mut buf) {
|
|
Ok(0) => break,
|
|
Ok(n) => n,
|
|
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
|
Err(e) => return Outcome::tool_error(format!("shell: cannot read the command: {e}")),
|
|
};
|
|
if out.len() >= MAX_OUTPUT {
|
|
// Past the limit: keep draining so the command never blocks on a full pipe.
|
|
dropped = true;
|
|
continue;
|
|
}
|
|
let chunk = match buf.get(..n) {
|
|
Some(c) => c,
|
|
None => continue,
|
|
};
|
|
let room = MAX_OUTPUT - out.len();
|
|
if chunk.len() <= room {
|
|
out.extend_from_slice(chunk);
|
|
} else {
|
|
out.extend_from_slice(chunk.get(..room).unwrap_or_default());
|
|
dropped = true;
|
|
}
|
|
}
|
|
|
|
let status = match child.wait() {
|
|
Ok(s) => s,
|
|
Err(e) => return Outcome::tool_error(format!("shell: cannot wait for the command: {e}")),
|
|
};
|
|
|
|
let mut text = String::from_utf8_lossy(&out).into_owned();
|
|
if dropped {
|
|
text.push_str(&format!("\n[output after {MAX_OUTPUT} bytes dropped]"));
|
|
}
|
|
match status.code() {
|
|
Some(code) => text.push_str(&format!("\n[exit {code}]")),
|
|
None => match status.signal() {
|
|
Some(sig) => text.push_str(&format!("\n[killed by signal {sig}]")),
|
|
None => text.push_str("\n[ended without a status]"),
|
|
},
|
|
}
|
|
Outcome::done(text)
|
|
}
|