use std::env;
use std::io::BufRead;
use std::io::BufReader;
use std::path::Path;
use std::process::ExitStatus;
use std::process::{Command, Output, Stdio};
use std::sync::{Arc, Mutex};
use std::thread;
#[cfg(target_os = "linux")]
use std::os::unix::process::ExitStatusExt;
#[cfg(target_os = "windows")]
use std::os::windows::process::ExitStatusExt;
#[cfg(feature = "logging")]
use log::{error, info, warn};
macro_rules! leech_output {
($out:ident, $out_buf:ident, $log_method:ident) => {
thread::spawn({
let output_buffer_clone = Arc::clone($out_buf);
move || {
if let Some(output) = $out {
let reader = BufReader::new(output);
for line in reader.lines() {
if let Ok(line) = line {
#[cfg(feature = "logging")]
$log_method!("{}", line);
match output_buffer_clone.lock() {
Err(_err) => {
#[cfg(feature = "logging")]
error!("Failed to lock {} buffer! {}", stringify!($out), _err);
return;
}
Ok(mut vec) => {
vec.push(line);
}
}
}
}
}
}
})
};
}
pub struct IShell {
initial_dir: String,
current_dir: Arc<Mutex<String>>,
}
impl IShell {
pub fn new(initial_dir: Option<&str>) -> Self {
let current_dir = env::current_dir().expect(
"Failed to get current directory; it may not exist or you may not have permissions.",
);
let current_dir = current_dir
.to_str()
.expect("Current directory contains invalid UTF-8.")
.to_string();
let initial_dir = initial_dir.map_or_else(|| current_dir.clone(), |dir| dir.to_string());
IShell {
initial_dir: initial_dir.clone(),
current_dir: Arc::new(Mutex::new(initial_dir)),
}
}
pub fn run_command(&self, command: &str) -> Output {
#[cfg(feature = "logging")]
info!("Running: `{}`", command);
if command.starts_with("cd") {
let new_dir = command[2..].trim();
let mut current_dir = self.current_dir.lock().unwrap();
let wanted_dir = Path::new(current_dir.as_str()).join(new_dir);
let new_dir = if wanted_dir.exists() && wanted_dir.is_dir() {
Path::new(&wanted_dir)
} else {
Path::new(new_dir)
};
if let Err(e) = env::set_current_dir(new_dir) {
#[cfg(feature = "logging")]
{
error!(
"Failed to change directory to either of {:?} or \"{}/{}\": {}",
wanted_dir,
current_dir,
new_dir.to_str().unwrap_or(""),
e
);
error!("Current directory: '{}'", current_dir);
}
return self.create_output(
ExitStatus::from_raw(1),
Vec::new(),
Vec::from(format!("Error: {}", e)),
);
} else {
let new_current_dir = env::current_dir()
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_else(|_err| {
#[cfg(feature = "logging")]
error!("Failed to get current directory: {}", _err);
current_dir.clone() });
*current_dir = new_current_dir;
}
return self.create_output(ExitStatus::from_raw(0), Vec::new(), Vec::new());
}
let child_process = self.spawn_process(command);
match child_process {
Ok(mut process) => {
let (stdout_buffer, stderr_buffer) = (
Arc::new(Mutex::new(Vec::new())),
Arc::new(Mutex::new(Vec::new())),
);
let (stdout_handle, stderr_handle) = self.spawn_output_threads(
process.stdout.take(),
process.stderr.take(),
&stdout_buffer,
&stderr_buffer,
);
let status = match process.wait() {
Ok(status) => status,
Err(_err) => {
#[cfg(feature = "logging")]
error!("Failed to wait for process: {}", _err);
ExitStatus::default()
}
};
if let Err(_err) = stdout_handle.join() {
#[cfg(feature = "logging")]
error!("Failed to join stdout thread: {:?}", _err);
}
if let Err(_err) = stderr_handle.join() {
#[cfg(feature = "logging")]
error!("Failed to join stderr thread: {:?}", _err);
}
let stdout = self.collect_output(&stdout_buffer);
let stderr = self.collect_output(&stderr_buffer);
Output {
status,
stdout,
stderr,
}
}
Err(e) => {
#[cfg(feature = "logging")]
error!("Couldn't spawn child process! {}", e);
self.create_output(
ExitStatus::from_raw(-1),
Vec::new(),
Vec::from(format!("Error: {}", e)),
)
}
}
}
pub fn forget_current_directory(&self) {
let mut current_dir = self.current_dir.lock().unwrap();
*current_dir = self.initial_dir.clone();
}
fn create_output(&self, status: ExitStatus, stdout: Vec<u8>, stderr: Vec<u8>) -> Output {
Output {
status,
stdout,
stderr,
}
}
fn spawn_process(&self, command: &str) -> std::io::Result<std::process::Child> {
let current_dir = self.current_dir.lock().unwrap().clone();
if cfg!(target_os = "windows") {
Command::new("cmd")
.args(["/C", command])
.current_dir(current_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
} else {
Command::new("sh")
.arg("-c")
.arg(command)
.current_dir(current_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
}
}
fn spawn_output_threads(
&self,
stdout: Option<std::process::ChildStdout>,
stderr: Option<std::process::ChildStderr>,
stdout_buffer: &Arc<Mutex<Vec<String>>>,
stderr_buffer: &Arc<Mutex<Vec<String>>>,
) -> (thread::JoinHandle<()>, thread::JoinHandle<()>) {
let stdout_handle = leech_output!(stdout, stdout_buffer, info);
let stderr_handle = leech_output!(stderr, stderr_buffer, warn);
(stdout_handle, stderr_handle)
}
fn collect_output(&self, buffer: &Arc<Mutex<Vec<String>>>) -> Vec<u8> {
match buffer.lock() {
Ok(buffer) => buffer.join("\n").into_bytes(),
Err(_err) => {
#[cfg(feature = "logging")]
error!("Couldn't lock buffer! {}", _err);
Vec::new()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn true_command() {
let shell = IShell::new(None);
let result = shell.run_command("true");
assert_eq!(result.status.code().unwrap_or(1), 0);
}
#[test]
fn false_command() {
let shell = IShell::new(None);
let result = shell.run_command("false");
assert_eq!(result.status.code().unwrap_or(0), 1);
}
#[test]
fn echo_command() {
let shell = IShell::new(None);
let result = shell.run_command("echo \"Hello, World!\"");
let stdout_res = unsafe { String::from_utf8_unchecked(result.stdout) };
assert_eq!(stdout_res, "Hello, World!");
}
#[test]
fn dir_memory() {
let shell = IShell::new(None);
let unique_dir_1 = format!("test_{}", rand::random::<u32>());
let unique_dir_2 = format!("test2_{}", rand::random::<u32>());
shell.run_command(&format!("mkdir {}", unique_dir_1));
shell.run_command(&format!("cd {}", unique_dir_1));
shell.run_command(&format!("mkdir {}", unique_dir_2));
let result = shell.run_command("ls");
let stdout_res = unsafe { String::from_utf8_unchecked(result.stdout) };
assert_eq!(stdout_res.trim(), unique_dir_2);
shell.run_command("cd ..");
shell.run_command(&format!("rm -r {}", unique_dir_1));
}
#[test]
fn forget_current_dir() {
let shell = IShell::new(None);
let result = shell.run_command("echo $PWD");
let pwd = unsafe { String::from_utf8_unchecked(result.stdout) };
let unique_dir = format!("test_{}", rand::random::<u32>());
shell.run_command(&format!("mkdir {}", unique_dir));
shell.run_command(&format!("cd {}", unique_dir));
shell.forget_current_directory();
let result = shell.run_command("echo $PWD");
let forgotten_pwd = unsafe { String::from_utf8_unchecked(result.stdout) };
assert_eq!(pwd, forgotten_pwd);
shell.run_command(&format!("rm -r {}", unique_dir));
}
#[test]
fn dir_doesnt_exist() {
let shell = IShell::new(None);
let current_dir = shell.current_dir.lock().unwrap().clone();
shell.run_command("cd directory_that_doesnt_exist");
let next_dir = shell.current_dir.lock().unwrap().clone();
assert_eq!(current_dir, next_dir);
}
}