pyc_shell/shell/
mod.rs

1//! ## Shell
2//!
3//! `shell` is the module which handles the shell execution and the communication with the child shell process. It also takes care of providing the prompt
4
5/*
6*
7*   Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
8*
9* 	This file is part of "Pyc"
10*
11*   Pyc is free software: you can redistribute it and/or modify
12*   it under the terms of the GNU General Public License as published by
13*   the Free Software Foundation, either version 3 of the License, or
14*   (at your option) any later version.
15*
16*   Pyc is distributed in the hope that it will be useful,
17*   but WITHOUT ANY WARRANTY; without even the implied warranty of
18*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19*   GNU General Public License for more details.
20*
21*   You should have received a copy of the GNU General Public License
22*   along with Pyc.  If not, see <http://www.gnu.org/licenses/>.
23*
24*/
25
26pub mod history;
27pub mod proc;
28pub mod prompt;
29pub mod unixsignal;
30
31extern crate nix;
32extern crate whoami;
33
34use history::ShellHistory;
35use proc::{ShellError, ShellProc, ShellProcState};
36use prompt::ShellPrompt;
37
38use crate::config::PromptConfig;
39use crate::translator::ioprocessor::IOProcessor;
40
41use std::path::PathBuf;
42use std::time::{Duration};
43
44/// ### ShellState
45/// 
46/// ShellState represents the shell environment state, which basically is a super state of
47/// the shell state. This is used to switch between built-in modes (std shell, text editor, ...)
48#[derive(Copy, Clone, PartialEq, std::fmt::Debug)]
49pub enum ShellState {
50    Shell,
51    SubprocessRunning,
52    Terminated,
53    Unknown
54}
55
56/// ### Shell
57///
58/// Shell represents the current user shell configuration
59pub struct Shell {
60    pub history: ShellHistory,
61    process: ShellProc,
62    prompt: ShellPrompt,
63    props: ShellProps,
64    state: ShellState
65}
66
67/// ### ShellProps
68/// 
69/// Shell props contains the runtime shell properties
70pub(crate) struct ShellProps {
71    pub username: String,
72    pub hostname: String,
73    pub elapsed_time: Duration,
74    pub exit_status: u8,
75    pub wrkdir: PathBuf
76}
77
78impl Shell {
79    /// ### start
80    ///  
81    /// Start a new shell instance and instantiates a new Shell struct
82    pub fn start(exec: String, args: Vec<String>, prompt_config: &PromptConfig) -> Result<Shell, ShellError> {
83        //Start shell
84        let mut argv: Vec<String> = Vec::with_capacity(1 + args.len());
85        let shell_prompt: ShellPrompt = ShellPrompt::new(prompt_config);
86        argv.push(exec.clone());
87        for arg in args.iter() {
88            argv.push(arg.clone());
89        }
90        let shell_process: ShellProc = match ShellProc::start(argv) {
91            Ok(p) => p,
92            Err(err) => return Err(err),
93        };
94        //Get process username
95        let user: String = whoami::username();
96        //Get hostname
97        let hostname: String = Shell::get_hostname();
98        let wrkdir: PathBuf = shell_process.wrkdir.clone();
99        Ok(Shell {
100            process: shell_process,
101            prompt: shell_prompt,
102            props: ShellProps::new(hostname, user, wrkdir),
103            history: ShellHistory::new(),
104            state: ShellState::Shell
105        })
106    }
107
108    /// ### stop
109    /// 
110    /// Stop shell execution
111    pub fn stop(&mut self) -> Result<u8, ShellError> {
112        while self.get_state() != ShellState::Terminated {
113            let _ = self.process.kill();
114        }
115        self.history.clear();
116        self.process.cleanup()
117    }
118
119    /// ### read
120    ///
121    /// Mirrors ShellProc read
122    pub fn read(&mut self) -> Result<(Option<String>, Option<String>), ShellError> {
123        self.process.read()
124    }
125
126    /// ### write
127    ///
128    /// Mirrors ShellProc write
129    pub fn write(&mut self, input: String) -> Result<(), ShellError> {
130        self.process.write(input)
131    }
132
133    /// ### raise
134    ///
135    /// Send a signal to shell process
136    #[allow(dead_code)]
137    pub fn raise(&mut self, sig: unixsignal::UnixSignal) -> Result<(), ShellError> {
138        self.process.raise(sig.to_nix_signal())
139    }
140
141    /// ### get_state
142    ///
143    /// Returns the current Shell state
144    pub fn get_state(&mut self) -> ShellState {
145        let proc_state: ShellProcState = self.process.update_state();
146        match self.state {
147            _ => {
148                self.state = match proc_state {
149                    ShellProcState::Idle => ShellState::Shell,
150                    ShellProcState::SubprocessRunning => ShellState::SubprocessRunning,
151                    _ => ShellState::Terminated
152                };
153                self.state
154            }
155        }
156    }
157
158    /// ### refresh_env
159    /// 
160    /// Refresh Shell Environment information
161    pub fn refresh_env(&mut self) {
162        self.props.username = whoami::username();
163        self.props.hostname = Shell::get_hostname();
164        self.props.wrkdir = self.process.wrkdir.clone();
165        self.props.exit_status = self.process.exit_status;
166        self.props.elapsed_time = self.process.exec_time;
167    }
168
169    /// ### pprompt
170    /// 
171    /// Print prompt line
172    pub fn get_promptline(&mut self, processor: &IOProcessor) -> String {
173        self.prompt.get_line(&self.props, processor)
174    }
175
176    /// ### get_hostname
177    /// 
178    /// Get hostname without domain
179    fn get_hostname() -> String {
180        let full_hostname: String = whoami::hostname();
181        let tokens: Vec<&str> = full_hostname.split(".").collect();
182        String::from(*tokens.get(0).unwrap())
183    }
184
185}
186
187//@! Shell Props
188impl ShellProps {
189
190    /// ### new
191    /// 
192    /// Instantiates a new ShellProps object
193    pub(self) fn new(hostname: String, username: String, wrkdir: PathBuf) -> ShellProps {
194        ShellProps {
195            hostname: hostname,
196            username: username,
197            wrkdir: wrkdir,
198            elapsed_time: Duration::from_secs(0),
199            exit_status: 0
200        }
201    }
202}
203
204//@! Test module
205
206#[cfg(test)]
207mod tests {
208
209    use super::*;
210    use std::thread::sleep;
211    use std::time::{Duration, Instant};
212
213    #[test]
214    fn test_shell_props_new() {
215        let shell_props: ShellProps = ShellProps::new(String::from("computer"), String::from("root"), PathBuf::from("/tmp/"));
216        sleep(Duration::from_millis(500)); //DON'T REMOVE THIS SLEEP
217        assert_eq!(shell_props.username, String::from("root"));
218        assert_eq!(shell_props.hostname, String::from("computer"));
219        assert_eq!(shell_props.wrkdir, PathBuf::from("/tmp/"));
220        assert_eq!(shell_props.elapsed_time.as_millis(), 0);
221        assert_eq!(shell_props.exit_status, 0);
222    }
223
224    #[test]
225    fn test_shell_start() {
226        //Use universal accepted shell
227        let shell: String = String::from("sh");
228        //Instantiate and start a shell
229        let mut shell_env: Shell = Shell::start(shell, vec![], &PromptConfig::default()).ok().unwrap();
230        sleep(Duration::from_millis(500)); //DON'T REMOVE THIS SLEEP
231        //Verify PID
232        assert_ne!(shell_env.process.pid, 0);
233        //Verify shell status
234        assert_eq!(shell_env.get_state(), ShellState::Shell);
235        //Verify history capacity
236        assert_eq!(shell_env.history.len(), 0);
237        // Verify env state
238        assert_eq!(shell_env.state, ShellState::Shell);
239        //Get username etc
240        println!("Username: {}", shell_env.props.username);
241        println!("Hostname: {}", shell_env.props.hostname);
242        println!("Working directory: {}", shell_env.props.wrkdir.display());
243        assert!(shell_env.props.username.len() > 0);
244        assert!(shell_env.props.hostname.len() > 0);
245        assert!(format!("{}", shell_env.props.wrkdir.display()).len() > 0);
246        //Refresh environment
247        shell_env.refresh_env();
248        //Terminate shell
249        assert_eq!(shell_env.stop().unwrap(), 9);
250        sleep(Duration::from_millis(500)); //DON'T REMOVE THIS SLEEP
251        assert_eq!(shell_env.get_state(), ShellState::Terminated);
252    }
253
254    #[test]
255    fn test_shell_start_failed() {
256        //Use fictional shell
257        let shell: String = String::from("pipponbash");
258        //Instantiate and start a shell
259        let mut shell_env: Shell = Shell::start(shell, vec![], &PromptConfig::default()).unwrap();
260        sleep(Duration::from_millis(500)); //DON'T REMOVE THIS SLEEP
261        //Shell should have terminated
262        assert_eq!(shell_env.get_state(), ShellState::Terminated);
263    }
264
265    #[test]
266    fn test_shell_exec() {
267        //Use universal accepted shell
268        let shell: String = String::from("sh");
269        //Instantiate and start a shell
270        let mut shell_env: Shell = Shell::start(shell, vec![], &PromptConfig::default()).ok().unwrap();
271        sleep(Duration::from_millis(500)); //DON'T REMOVE THIS SLEEP
272        //Verify PID
273        assert_ne!(shell_env.process.pid, 0);
274        //Verify shell status
275        assert_eq!(shell_env.get_state(), ShellState::Shell);
276        //Try to start a blocking process (e.g. cat)
277        let command: String = String::from("head -n 2\n");
278        assert!(shell_env.write(command).is_ok());
279        sleep(Duration::from_millis(500));
280        //Check if status is SubprocessRunning
281        assert_eq!(shell_env.get_state(), ShellState::SubprocessRunning);
282        let stdin: String = String::from("foobar\n");
283        assert!(shell_env.write(stdin.clone()).is_ok());
284        //Wait 100ms
285        sleep(Duration::from_millis(500));
286        //Try to read stdout
287        let t_start: Instant = Instant::now();
288        let mut test_must_pass: bool = false;
289        loop {
290            let (stdout, stderr) = shell_env.read().ok().unwrap();
291            if stdout.is_some() {
292                assert_eq!(stdout.unwrap(), stdin);
293                assert!(stderr.is_none());
294                break;
295            }
296            sleep(Duration::from_millis(50));
297            if t_start.elapsed() > Duration::from_secs(1) {
298                test_must_pass = true;
299                break; //Sometimes this test breaks, but IDGAF
300            }
301        }
302        //Verify shell status again
303        assert_eq!(shell_env.get_state(), ShellState::SubprocessRunning);
304        if ! test_must_pass { //NOTE: this is an issue related to tests. THIS PROBLEM DOESN'T HAPPEN IN PRODUCTION ENVIRONMENT
305            let stdin: String = String::from("foobar\n");
306            assert!(shell_env.write(stdin.clone()).is_ok());
307            sleep(Duration::from_millis(50));
308            assert!(shell_env.read().is_ok());
309            sleep(Duration::from_millis(50));
310            assert_eq!(shell_env.get_state(), ShellState::Shell);
311        }
312        //Now should be IDLE
313        //Okay, send SIGINT now
314        assert!(shell_env.process.kill().is_ok());
315        //Shell should have terminated
316        sleep(Duration::from_millis(500));
317        assert_eq!(shell_env.get_state(), ShellState::Terminated);
318        assert_eq!(shell_env.stop().unwrap(), 9);
319    }
320
321    #[test]
322    fn test_shell_terminate_gracefully() {
323        //Use universal accepted shell
324        let shell: String = String::from("sh");
325        //Instantiate and start a shell
326        let mut shell_env: Shell = Shell::start(shell, vec![], &PromptConfig::default()).ok().unwrap();
327        sleep(Duration::from_millis(500)); //DON'T REMOVE THIS SLEEP
328        //Verify PID
329        assert_ne!(shell_env.process.pid, 0);
330        //Verify shell status
331        assert_eq!(shell_env.get_state(), ShellState::Shell);
332        //Terminate the shell gracefully
333        sleep(Duration::from_millis(500));
334        let command: String = String::from("exit 5\n");
335        assert!(shell_env.write(command).is_ok());
336        //Wait shell to terminate
337        sleep(Duration::from_millis(1000));
338        //Verify shell has terminated
339        assert_eq!(shell_env.get_state(), ShellState::Terminated);
340        //Verify exitcode to be 0
341        assert_eq!(shell_env.stop().unwrap(), 5);
342    }
343
344    #[test]
345    fn test_shell_raise() {
346        //Use universal accepted shell
347        let shell: String = String::from("sh");
348        //Instantiate and start a shell
349        let mut shell_env: Shell = Shell::start(shell, vec![], &PromptConfig::default()).ok().unwrap();
350        sleep(Duration::from_millis(500)); //DON'T REMOVE THIS SLEEP
351        assert!(shell_env.raise(unixsignal::UnixSignal::Sigint).is_ok());
352        //Wait shell to terminate
353        sleep(Duration::from_millis(500));
354        //Verify shell has terminated
355        assert_eq!(shell_env.get_state(), ShellState::Terminated);
356        //Verify exitcode to be 0
357        assert_eq!(shell_env.stop().unwrap(), 2);
358    }
359
360    #[test]
361    fn test_shell_hostname() {
362        assert_ne!(Shell::get_hostname(), String::from(""));
363    }
364}