pub mod history;
pub mod proc;
pub mod prompt;
pub mod unixsignal;
extern crate nix;
extern crate whoami;
use history::ShellHistory;
use proc::{ShellError, ShellProc, ShellProcState};
use prompt::ShellPrompt;
use crate::config::PromptConfig;
use crate::translator::ioprocessor::IOProcessor;
use std::path::PathBuf;
use std::time::{Duration};
#[derive(Copy, Clone, PartialEq, std::fmt::Debug)]
pub enum ShellState {
Shell,
SubprocessRunning,
Terminated,
Unknown
}
pub struct Shell {
pub history: ShellHistory,
process: ShellProc,
prompt: ShellPrompt,
props: ShellProps,
state: ShellState
}
pub(crate) struct ShellProps {
pub username: String,
pub hostname: String,
pub elapsed_time: Duration,
pub exit_status: u8,
pub wrkdir: PathBuf
}
impl Shell {
pub fn start(exec: String, args: Vec<String>, prompt_config: &PromptConfig) -> Result<Shell, ShellError> {
let mut argv: Vec<String> = Vec::with_capacity(1 + args.len());
let shell_prompt: ShellPrompt = ShellPrompt::new(prompt_config);
argv.push(exec.clone());
for arg in args.iter() {
argv.push(arg.clone());
}
let shell_process: ShellProc = match ShellProc::start(argv) {
Ok(p) => p,
Err(err) => return Err(err),
};
let user: String = whoami::username();
let hostname: String = Shell::get_hostname();
let wrkdir: PathBuf = shell_process.wrkdir.clone();
Ok(Shell {
process: shell_process,
prompt: shell_prompt,
props: ShellProps::new(hostname, user, wrkdir),
history: ShellHistory::new(),
state: ShellState::Shell
})
}
pub fn stop(&mut self) -> Result<u8, ShellError> {
while self.get_state() != ShellState::Terminated {
let _ = self.process.kill();
}
self.history.clear();
self.process.cleanup()
}
pub fn read(&mut self) -> Result<(Option<String>, Option<String>), ShellError> {
self.process.read()
}
pub fn write(&mut self, input: String) -> Result<(), ShellError> {
self.process.write(input)
}
#[allow(dead_code)]
pub fn raise(&mut self, sig: unixsignal::UnixSignal) -> Result<(), ShellError> {
self.process.raise(sig.to_nix_signal())
}
pub fn get_state(&mut self) -> ShellState {
let proc_state: ShellProcState = self.process.update_state();
match self.state {
_ => {
self.state = match proc_state {
ShellProcState::Idle => ShellState::Shell,
ShellProcState::SubprocessRunning => ShellState::SubprocessRunning,
_ => ShellState::Terminated
};
self.state
}
}
}
pub fn refresh_env(&mut self) {
self.props.username = whoami::username();
self.props.hostname = Shell::get_hostname();
self.props.wrkdir = self.process.wrkdir.clone();
self.props.exit_status = self.process.exit_status;
self.props.elapsed_time = self.process.exec_time;
}
pub fn get_promptline(&mut self, processor: &IOProcessor) -> String {
self.prompt.get_line(&self.props, processor)
}
fn get_hostname() -> String {
let full_hostname: String = whoami::hostname();
let tokens: Vec<&str> = full_hostname.split(".").collect();
String::from(*tokens.get(0).unwrap())
}
}
impl ShellProps {
pub(self) fn new(hostname: String, username: String, wrkdir: PathBuf) -> ShellProps {
ShellProps {
hostname: hostname,
username: username,
wrkdir: wrkdir,
elapsed_time: Duration::from_secs(0),
exit_status: 0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
use std::time::{Duration, Instant};
#[test]
fn test_shell_props_new() {
let shell_props: ShellProps = ShellProps::new(String::from("computer"), String::from("root"), PathBuf::from("/tmp/"));
sleep(Duration::from_millis(500)); assert_eq!(shell_props.username, String::from("root"));
assert_eq!(shell_props.hostname, String::from("computer"));
assert_eq!(shell_props.wrkdir, PathBuf::from("/tmp/"));
assert_eq!(shell_props.elapsed_time.as_millis(), 0);
assert_eq!(shell_props.exit_status, 0);
}
#[test]
fn test_shell_start() {
let shell: String = String::from("sh");
let mut shell_env: Shell = Shell::start(shell, vec![], &PromptConfig::default()).ok().unwrap();
sleep(Duration::from_millis(500)); assert_ne!(shell_env.process.pid, 0);
assert_eq!(shell_env.get_state(), ShellState::Shell);
assert_eq!(shell_env.history.len(), 0);
assert_eq!(shell_env.state, ShellState::Shell);
println!("Username: {}", shell_env.props.username);
println!("Hostname: {}", shell_env.props.hostname);
println!("Working directory: {}", shell_env.props.wrkdir.display());
assert!(shell_env.props.username.len() > 0);
assert!(shell_env.props.hostname.len() > 0);
assert!(format!("{}", shell_env.props.wrkdir.display()).len() > 0);
shell_env.refresh_env();
assert_eq!(shell_env.stop().unwrap(), 9);
sleep(Duration::from_millis(500)); assert_eq!(shell_env.get_state(), ShellState::Terminated);
}
#[test]
fn test_shell_start_failed() {
let shell: String = String::from("pipponbash");
let mut shell_env: Shell = Shell::start(shell, vec![], &PromptConfig::default()).unwrap();
sleep(Duration::from_millis(500)); assert_eq!(shell_env.get_state(), ShellState::Terminated);
}
#[test]
fn test_shell_exec() {
let shell: String = String::from("sh");
let mut shell_env: Shell = Shell::start(shell, vec![], &PromptConfig::default()).ok().unwrap();
sleep(Duration::from_millis(500)); assert_ne!(shell_env.process.pid, 0);
assert_eq!(shell_env.get_state(), ShellState::Shell);
let command: String = String::from("head -n 2\n");
assert!(shell_env.write(command).is_ok());
sleep(Duration::from_millis(500));
assert_eq!(shell_env.get_state(), ShellState::SubprocessRunning);
let stdin: String = String::from("foobar\n");
assert!(shell_env.write(stdin.clone()).is_ok());
sleep(Duration::from_millis(500));
let t_start: Instant = Instant::now();
let mut test_must_pass: bool = false;
loop {
let (stdout, stderr) = shell_env.read().ok().unwrap();
if stdout.is_some() {
assert_eq!(stdout.unwrap(), stdin);
assert!(stderr.is_none());
break;
}
sleep(Duration::from_millis(50));
if t_start.elapsed() > Duration::from_secs(1) {
test_must_pass = true;
break; }
}
assert_eq!(shell_env.get_state(), ShellState::SubprocessRunning);
if ! test_must_pass { let stdin: String = String::from("foobar\n");
assert!(shell_env.write(stdin.clone()).is_ok());
sleep(Duration::from_millis(50));
assert!(shell_env.read().is_ok());
sleep(Duration::from_millis(50));
assert_eq!(shell_env.get_state(), ShellState::Shell);
}
assert!(shell_env.process.kill().is_ok());
sleep(Duration::from_millis(500));
assert_eq!(shell_env.get_state(), ShellState::Terminated);
assert_eq!(shell_env.stop().unwrap(), 9);
}
#[test]
fn test_shell_terminate_gracefully() {
let shell: String = String::from("sh");
let mut shell_env: Shell = Shell::start(shell, vec![], &PromptConfig::default()).ok().unwrap();
sleep(Duration::from_millis(500)); assert_ne!(shell_env.process.pid, 0);
assert_eq!(shell_env.get_state(), ShellState::Shell);
sleep(Duration::from_millis(500));
let command: String = String::from("exit 5\n");
assert!(shell_env.write(command).is_ok());
sleep(Duration::from_millis(1000));
assert_eq!(shell_env.get_state(), ShellState::Terminated);
assert_eq!(shell_env.stop().unwrap(), 5);
}
#[test]
fn test_shell_raise() {
let shell: String = String::from("sh");
let mut shell_env: Shell = Shell::start(shell, vec![], &PromptConfig::default()).ok().unwrap();
sleep(Duration::from_millis(500)); assert!(shell_env.raise(unixsignal::UnixSignal::Sigint).is_ok());
sleep(Duration::from_millis(500));
assert_eq!(shell_env.get_state(), ShellState::Terminated);
assert_eq!(shell_env.stop().unwrap(), 2);
}
#[test]
fn test_shell_hostname() {
assert_ne!(Shell::get_hostname(), String::from(""));
}
}