extern crate nix;
extern crate tempfile;
extern crate uuid;
use super::{ShellError, ShellProc, ShellState};
use super::pipe::Pipe;
use std::ffi::{CStr, CString};
use std::os::unix::io::RawFd;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use uuid::Uuid;
impl ShellProc {
pub fn start(argv: Vec<String>) -> Result<ShellProc, ShellError> {
if argv.len() == 0 {
return Err(ShellError::CouldNotStartProcess)
}
let uuid: String = Uuid::new_v4().to_hyphenated().to_string();
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let stdin_pipe: Pipe = match Pipe::open(&tmpdir.path().join("stdin.fifo")) {
Ok(p) => p,
Err(err) => return Err(err)
};
let stderr_pipe: Pipe = match Pipe::open(&tmpdir.path().join("stderr.fifo")) {
Ok(p) => p,
Err(err) => return Err(err)
};
let stdout_pipe: Pipe = match Pipe::open(&tmpdir.path().join("stdout.fifo")) {
Ok(p) => p,
Err(err) => return Err(err)
};
match nix::unistd::fork() {
Ok(nix::unistd::ForkResult::Parent { child, .. }) => {
let echo_command: String = format!("echo \"\x02$?;`pwd`;{}\x03\"\n", uuid);
let wrkdir: PathBuf = match std::env::current_dir() {
Err(_) => PathBuf::from("/"),
Ok(path) => PathBuf::from(path.as_path())
};
Ok(ShellProc {
state: ShellState::Idle,
uuid: uuid,
exit_status: 0,
exec_time: Duration::from_millis(0),
wrkdir: wrkdir,
pid: child.as_raw(),
rc: 255,
stdout_cache: None,
start_time: Instant::now(),
echo_command: echo_command,
stdin_pipe: stdin_pipe,
stderr_pipe: stderr_pipe,
stdout_pipe: stdout_pipe
})
},
Ok(nix::unistd::ForkResult::Child) => {
std::process::exit(ShellProc::run(argv, stdin_pipe.fd, stderr_pipe.fd, stdout_pipe.fd));
},
Err(_) => {
return Err(ShellError::CouldNotStartProcess)
}
}
}
pub fn cleanup(&mut self) -> Result<u8, ShellError> {
if self.update_state() != ShellState::Terminated {
return Err(ShellError::ShellRunning)
}
let _ = self.stdin_pipe.close();
let _ = self.stdout_pipe.close();
let _ = self.stderr_pipe.close();
Ok(self.rc)
}
pub fn raise(&self, signal: nix::sys::signal::Signal) -> Result<(), ShellError> {
match nix::sys::signal::kill(nix::unistd::Pid::from_raw(self.pid), signal) {
Ok(_) => Ok(()),
Err(_) => Err(ShellError::CouldNotKill)
}
}
pub fn kill(&self) -> Result<(), ShellError> {
self.raise(nix::sys::signal::Signal::SIGKILL)
}
pub fn read(&mut self) -> Result<(Option<String>, Option<String>), ShellError> {
let stdout: Option<String> = match self.stdout_pipe.read(50, false) {
Ok(stdout) => self.parse_stdout(stdout),
Err(err) => return Err(err)
};
let stderr: Option<String> = match self.stderr_pipe.read(50, false) {
Ok(stderr) => match stderr {
None => None,
Some(stderr) => Some(stderr)
},
Err(err) => return Err(err)
};
Ok((stdout, stderr))
}
pub fn write(&mut self, mut data: String) -> Result<(), ShellError> {
if self.update_state() == ShellState::Terminated {
return Err(ShellError::ShellTerminated)
}
if self.state == ShellState::Idle {
while data.ends_with('\n') {
data.pop();
}
if ! data.ends_with(';') {
data.push(';');
}
data.push_str(self.echo_command.as_str());
self.set_state_running();
}
self.stdin_pipe.write(data, 5000)
}
fn run(argv: Vec<String>, stdin: RawFd, stderr: RawFd, stdout: RawFd) -> i32 {
if let Err(_) = nix::unistd::dup2(stdin, 0) {
return 255
}
if let Err(_) = nix::unistd::dup2(stdout, 1) {
return 255
}
if let Err(_) = nix::unistd::dup2(stderr, 2) {
return 255
}
let mut c_argv: Vec<CString> = Vec::with_capacity(argv.len());
for arg in argv.iter() {
c_argv.push(CString::new(arg.as_str()).unwrap());
}
let mut c_argv_refs: Vec<&CStr> = Vec::with_capacity(c_argv.len());
for arg in c_argv.iter() {
c_argv_refs.push(arg);
}
if let Err(_) = nix::unistd::execvp(c_argv_refs.get(0).unwrap(), c_argv_refs.as_slice()) {
return 255
}
return 0
}
pub fn update_state(&mut self) -> ShellState {
match nix::sys::wait::waitpid(nix::unistd::Pid::from_raw(self.pid), Some(nix::sys::wait::WaitPidFlag::WNOHANG)) {
Err(_) => {},
Ok(status) => match status {
nix::sys::wait::WaitStatus::Exited(_, rc) => {
self.state = ShellState::Terminated;
self.rc = rc as u8;
},
nix::sys::wait::WaitStatus::Signaled(_, signal, _) => {
self.state = ShellState::Terminated;
self.rc = signal as u8;
},
_ => {},
}
};
self.state
}
fn parse_stdout(&mut self, stdout: Option<String>) -> Option<String> {
match stdout {
None => None,
Some(stdout) => {
let termination_string: String = format!("{}\x03\n", self.uuid);
let check_string: String = match &self.stdout_cache {
None => stdout.clone(),
Some(cache) => {
let mut s: String = String::with_capacity(stdout.len() + cache.len());
s.push_str(cache.as_str());
s.push_str(stdout.as_str());
s
}
};
if check_string.ends_with(termination_string.as_str()) {
let mut stx_index: usize = check_string.len();
for c in check_string.chars().rev() {
if c == '\x02' {
break;
}
stx_index -= 1;
}
let metadata: String = String::from(&check_string[stx_index..check_string.len() - 2]);
let stx_index_stdout: usize = stx_index - match &self.stdout_cache {
Some(s) => s.len(),
None => 0
};
let stdout: String = String::from(&stdout[..stx_index_stdout - 1]);
self.set_state_idle(metadata);
self.stdout_cache = None;
match stdout.len() {
0 => None,
_ => Some(stdout)
}
} else {
self.stdout_cache = Some(stdout.clone());
Some(stdout)
}
}
}
}
fn set_state_idle(&mut self, metadata: String) {
for (index, token) in metadata.split(";").enumerate() {
match index {
0 => self.exit_status = token.parse::<u8>().unwrap_or(255),
1 => self.wrkdir = PathBuf::from(token),
_ => continue
}
}
self.exec_time = self.start_time.elapsed();
self.state = ShellState::Idle;
}
fn set_state_running(&mut self) {
self.start_time = Instant::now();
self.state = ShellState::SubprocessRunning;
}
}
impl Drop for ShellProc {
fn drop(&mut self) {
if let Err(_) = self.cleanup() {
let _ = self.kill();
let _ = self.cleanup();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use nix::NixPath;
use std::time::Duration;
use std::thread::sleep;
#[test]
fn test_process_start_stop() {
let mut shell_proc: ShellProc = ShellProc::start(vec![String::from("sh")]).unwrap();
println!("A new shell started with PID {}", shell_proc.pid);
assert_eq!(shell_proc.state, ShellState::Idle);
assert_eq!(shell_proc.exit_status, 0);
assert_ne!(shell_proc.pid, 0);
assert_ne!(shell_proc.wrkdir.len(), 0);
assert_eq!(shell_proc.exec_time, Duration::from_millis(0));
assert_eq!(shell_proc.rc, 255);
assert_ne!(shell_proc.uuid.len(), 0);
assert!(shell_proc.stdout_cache.is_none());
assert_eq!(shell_proc.echo_command, format!("echo \"\x02$?;`pwd`;{}\x03\"\n", shell_proc.uuid));
sleep(Duration::from_millis(500));
assert_eq!(shell_proc.update_state(), ShellState::Idle);
assert!(shell_proc.kill().is_ok());
sleep(Duration::from_millis(500));
assert_eq!(shell_proc.update_state(), ShellState::Terminated);
assert_eq!(shell_proc.state, ShellState::Terminated);
assert_eq!(shell_proc.rc, 9);
assert!(shell_proc.cleanup().is_ok());
}
#[test]
fn test_process_start_error() {
let mut shell_proc: ShellProc = ShellProc::start(vec![String::from("piroporopero")]).unwrap();
println!("A new shell started with PID {}", shell_proc.pid);
sleep(Duration::from_millis(1000));
assert_eq!(shell_proc.update_state(), ShellState::Terminated);
assert_eq!(shell_proc.rc, 255);
}
#[test]
fn test_process_raise() {
let mut shell_proc: ShellProc = ShellProc::start(vec![String::from("sh")]).unwrap();
println!("A new shell started with PID {}", shell_proc.pid);
sleep(Duration::from_millis(500));
assert_eq!(shell_proc.update_state(), ShellState::Idle);
assert!(shell_proc.raise(nix::sys::signal::Signal::SIGINT).is_ok());
sleep(Duration::from_millis(500));
assert_eq!(shell_proc.update_state(), ShellState::Terminated);
assert_eq!(shell_proc.rc, 2);
}
#[test]
fn test_process_parse_metadata() {
let mut shell_proc: ShellProc = ShellProc::start(vec![String::from("sh")]).unwrap();
println!("A new shell started with PID {}", shell_proc.pid);
sleep(Duration::from_millis(500));
let metadata: String = String::from("128;/home;ee9ec814-a751-4329-850f-6d54d12c8a5c");
shell_proc.state = ShellState::SubprocessRunning;
shell_proc.set_state_idle(metadata);
assert_eq!(shell_proc.exit_status, 128);
assert_eq!(shell_proc.wrkdir, PathBuf::from("/home"));
assert_eq!(shell_proc.state, ShellState::Idle);
assert!(shell_proc.kill().is_ok());
}
#[test]
fn test_process_parse_stdout() {
let mut shell_proc: ShellProc = ShellProc::start(vec![String::from("sh")]).unwrap();
println!("A new shell started with PID {}", shell_proc.pid);
sleep(Duration::from_millis(500));
assert!(shell_proc.parse_stdout(None).is_none());
shell_proc.state = ShellState::SubprocessRunning;
assert!(shell_proc.parse_stdout(Some(format!("\x02128;/home;{}\x03\n", shell_proc.uuid))).is_none());
assert_eq!(shell_proc.exit_status, 128);
assert_eq!(shell_proc.wrkdir, PathBuf::from("/home"));
assert_eq!(shell_proc.state, ShellState::Idle);
shell_proc.state = ShellState::SubprocessRunning;
assert_eq!(shell_proc.parse_stdout(Some(String::from("HELLO\n"))).unwrap(), String::from("HELLO\n"));
assert_eq!(shell_proc.state, ShellState::SubprocessRunning);
assert_eq!(*shell_proc.stdout_cache.as_ref().unwrap(), String::from("HELLO\n"));
shell_proc.state = ShellState::SubprocessRunning;
assert_eq!(shell_proc.parse_stdout(Some(format!("HELLO\n\x022;/tmp;{}\x03\n", shell_proc.uuid))).unwrap(), String::from("HELLO\n"));
assert_eq!(shell_proc.exit_status, 2);
assert_eq!(shell_proc.wrkdir, PathBuf::from("/tmp"));
assert_eq!(shell_proc.state, ShellState::Idle);
assert!(shell_proc.stdout_cache.is_none());
assert!(shell_proc.kill().is_ok());
}
#[test]
fn test_process_command() {
let mut shell_proc: ShellProc = ShellProc::start(vec![String::from("sh")]).unwrap();
println!("A new shell started with PID {}", shell_proc.pid);
assert!(shell_proc.write(String::from("cd /tmp\n")).is_ok());
assert_eq!(shell_proc.state, ShellState::SubprocessRunning);
sleep(Duration::from_millis(50));
let (stdout, stderr) = shell_proc.read().unwrap();
assert!(stdout.is_none());
assert!(stderr.is_none());
sleep(Duration::from_millis(100));
assert_eq!(shell_proc.update_state(), ShellState::Idle);
assert_eq!(shell_proc.wrkdir, PathBuf::from("/tmp"));
assert_eq!(shell_proc.exit_status, 0);
assert_ne!(shell_proc.exec_time.as_nanos(), 0);
assert!(shell_proc.kill().is_ok());
sleep(Duration::from_millis(500));
assert_eq!(shell_proc.update_state(), ShellState::Terminated);
assert_eq!(shell_proc.state, ShellState::Terminated);
assert_eq!(shell_proc.rc, 9);
assert!(shell_proc.cleanup().is_ok());
}
}