//! 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 = 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) }