pyc_shell/runtime/
mod.rs

1//! ## Runtime
2//!
3//! `runtime` contains the runtime functions for pyc core
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
26//Deps
27extern crate ansi_term;
28extern crate nix;
29
30// Runtime modules
31mod props;
32mod imiop;
33
34use ansi_term::Colour;
35use std::path::{Path, PathBuf};
36use std::thread::sleep;
37use std::time::{Duration};
38
39//Config
40use crate::config;
41//Props
42use props::RuntimeProps;
43//Shell
44use crate::shell::{Shell, ShellState};
45use crate::shell::unixsignal::UnixSignal;
46// Translator
47use crate::translator::ioprocessor::IOProcessor;
48use crate::translator::lang::Language;
49use crate::translator::new_translator;
50//Utils
51use crate::utils::console;
52use crate::utils::file;
53
54//@! Runners
55
56/// ### run_interactive
57///
58/// Run pyc in interactive mode
59
60pub fn run_interactive(language: Language, config: config::Config, shell: Option<String>, history_file: Option<PathBuf>) -> u8 {
61    //Instantiate Runtime Props
62    let mut props: RuntimeProps = RuntimeProps::new(true, config, language);
63    let processor: IOProcessor = IOProcessor::new(language, new_translator(language));
64    //Determine the shell to use
65    let (shell, args): (String, Vec<String>) = resolve_shell(&props.config, shell);
66    //Intantiate and start a new shell
67    let mut shell: Shell = match Shell::start(shell, args, &props.config.prompt_config) {
68        Ok(sh) => sh,
69        Err(err) => {
70            print_err(
71                String::from(format!("Could not start shell: {}", err)),
72                props.config.output_config.translate_output,
73                &processor,
74            );
75            return 255;
76        }
77    };
78    //If history file is set, load history
79    if let Some(history_file) = history_file.clone() {
80        match file::read_lines(history_file.clone()) {
81            Ok(lines) => shell.history.load(lines),
82            Err(err) => print_err(
83                String::from(format!("Could not load history from '{}': {}", history_file.display(), err)),
84                props.config.output_config.translate_output,
85                &processor,
86            )
87        }
88    };
89    //@! Main loop
90    while props.get_last_state() != ShellState::Terminated {
91        //@! Print prompt if state is Idle and state has changed
92        let current_state: ShellState = shell.get_state();
93        if current_state != props.get_last_state() {
94            props.update_state(current_state);
95        }
96        if props.get_state_changed() && current_state == ShellState::Shell {
97            //Force shellenv to refresh info
98            shell.refresh_env();
99            //Print prompt
100            console::print(format!("{} ", shell.get_promptline(&processor)));
101            props.report_state_changed_notified(); //Force state changed to false
102        } else if props.get_state_changed() {
103            props.report_state_changed_notified(); //Check has been done, nothing to do
104        }
105        //@! Read user input
106        if let Some(ev) = console::read() {
107            props.handle_input_event(ev, &mut shell);
108        };
109        //Update state after write
110        let new_state = shell.get_state(); //Force last state to be changed
111        if new_state != props.get_last_state() {
112            props.update_state(new_state);
113        }
114        //@! Read Shell stdout
115        read_from_shell(&mut shell, &props.config, &processor);
116        //Check if shell has terminated
117        sleep(Duration::from_nanos(100)); //Sleep for 100ns
118    } //@! End of loop
119    //Write history back to file
120    if let Some(history_file) = history_file {
121        let lines: Vec<String> = shell.history.dump();
122        if let Err(err) = file::write_lines(history_file.clone(), lines) {
123            print_err(
124                String::from(format!("Could not write history to '{}': {}", history_file.display(), err)),
125                props.config.output_config.translate_output,
126                &processor,
127            );
128        }
129    };
130    //Return shell exitcode
131    match shell.stop() {
132        Ok(rc) => rc,
133        Err(err) => {
134            print_err(format!("Could not stop shell: {}", err), props.config.output_config.translate_output, &processor);
135            255
136        }
137    }
138}
139
140/// ### run_command
141/// 
142/// Run command in shell and return
143pub fn run_command(mut command: String, language: Language, config: config::Config, shell: Option<String>) -> u8 {
144    //Instantiate Runtime Props
145    let mut props: RuntimeProps = RuntimeProps::new(false, config, language);
146    let processor: IOProcessor = IOProcessor::new(language, new_translator(language));
147    //Determine the shell to use
148    let (shell, args): (String, Vec<String>) = resolve_shell(&props.config, shell);
149    //Intantiate and start a new shell
150    let mut shell: Shell = match Shell::start(shell, args, &props.config.prompt_config) {
151        Ok(sh) => sh,
152        Err(err) => {
153            print_err(
154                String::from(format!("Could not start shell: {}", err)),
155                props.config.output_config.translate_output,
156                &processor,
157            );
158            return 255;
159        }
160    };
161    //Prepare command
162    while command.ends_with('\n') {
163        command.pop();
164    }
165    while command.ends_with(';') {
166        command.pop();
167    }
168    //FIXME: handle fish $status
169    command.push_str("; exit $?\n");
170    //Write command
171    if let Err(err) = shell.write(command) {
172        print_err(
173            String::from(format!("Could not start shell: {}", err)),
174            props.config.output_config.translate_output,
175            &processor,
176        );
177        return 255;
178    }
179    let _ = shell.write(String::from("\n"));
180    //@! Main loop
181    loop { //Check state after reading/writing, since program could have already terminate
182        //@! Read user input
183        if let Some(ev) = console::read() {
184            props.handle_input_event(ev, &mut shell);
185        };
186        //@! Read Shell stdout
187        read_from_shell(&mut shell, &props.config, &processor);
188        //Check if shell has terminated
189        if shell.get_state() == ShellState::Terminated {
190            break;
191        }
192        sleep(Duration::from_nanos(100)); //Sleep for 100ns
193    } //@! End of main loop
194    //Return shell exitcode
195    match shell.stop() {
196        Ok(rc) => rc,
197        Err(err) => {
198            print_err(format!("Could not stop shell: {}", err), props.config.output_config.translate_output, &processor);
199            255
200        }
201    }
202}
203
204/// ### run_file
205/// 
206/// Run shell reading commands from file
207pub fn run_file(file: String, language: Language, config: config::Config, shell: Option<String>) -> u8 {
208    let file_path: &Path = Path::new(file.as_str());
209    let processor: IOProcessor = IOProcessor::new(language, new_translator(language));
210    let lines: Vec<String> = match file::read_lines(file_path) {
211        Ok(lines) => lines,
212        Err(_) => {
213            print_err(format!("{}: No such file or directory", file), config.output_config.translate_output, &processor);
214            return 255
215        }
216    };
217    //Join lines in a single command
218    let command: String = script_lines_to_string(&lines);
219    //Execute command
220    run_command(command, language, config, shell)
221}
222
223//@! Shell functions
224
225/// ### read_from_shell
226/// 
227/// Read from shell stderr and stdout
228fn read_from_shell(shell: &mut Shell, config: &config::Config, processor: &IOProcessor) {
229    if let Ok((out, err)) = shell.read() {
230        if out.is_some() {
231            //Convert out to cyrillic
232            print_out(out.unwrap(), config.output_config.translate_output, &processor);
233        }
234        if err.is_some() {
235            //Convert err to cyrillic
236            print_err(err.unwrap().to_string(), config.output_config.translate_output, &processor);
237        }
238    }
239}
240
241/// ### resolve_shell
242/// 
243/// Resolve shell to use from configuration and arguments
244fn resolve_shell(config: &config::Config, shellopt: Option<String>) -> (String, Vec<String>) {
245    match shellopt {
246        Some(sh) => (sh, vec![]),
247        None => (config.shell_config.exec.clone(), config.shell_config.args.clone()) //Get shell from config
248    }
249}
250
251/// ### script_lines_to_string
252/// 
253/// Converts script lines to a single command as string
254fn script_lines_to_string(lines: &Vec<String>) -> String {
255    let mut command: String = String::new();
256    for line in lines.iter() {
257        if line.starts_with("#") {
258            continue;
259        }
260        if line.len() == 0 {
261            continue;
262        }
263        command.push_str(line);
264        //Don't add multiple semicolons
265        if ! line.ends_with(";") {
266            command.push(';');
267        }
268    }
269    command
270}
271
272/// ### resolve_command
273///
274/// resolve command according to configured alias
275
276fn resolve_command(argv: &mut Vec<String>, config: &config::Config) {
277    //Process arg 0
278    match config.get_alias(&argv[0]) {
279        Some(resolved) => argv[0] = resolved,
280        None => {}
281    };
282}
283
284/*
285/// ### get_shell_from_env
286///
287/// Try to get the shell path from SHELL environment variable
288fn get_shell_from_env() -> Result<String, ()> {
289    if let Ok(val) = env::var("SHELL") {
290        Ok(val)
291    } else {
292        Err(())
293    }
294}
295*/
296
297//@! Prompt functions
298
299/// ### print_err
300/// 
301/// print error message; the message is may converted to cyrillic if translate config is true
302
303fn print_err(err: String, to_cyrillic: bool, processor: &IOProcessor) {
304    match to_cyrillic {
305        true => eprintln!("{}", Colour::Red.paint(processor.text_to_cyrillic(&err))),
306        false => eprintln!("{}", Colour::Red.paint(err)),
307    };
308}
309
310/// ### print_out
311///
312/// print normal message; the message is may converted to cyrillic if translate config is true
313
314fn print_out(out: String, to_cyrillic: bool, processor: &IOProcessor) {
315    match to_cyrillic {
316        true => console::println(format!("{}", processor.text_to_cyrillic(&out))),
317        false => console::println(format!("{}", out)),
318    };
319}
320
321/// ### console_fmt
322/// 
323/// Format console message
324
325fn console_fmt(out: String, to_cyrillic: bool, processor: &IOProcessor) -> String {
326    match to_cyrillic {
327        true => format!("{}", processor.text_to_cyrillic(&out)),
328        false => format!("{}", out)
329    }
330}
331
332/// ### shellsignal_to_signal
333/// 
334/// Converts a signal received on prompt to a UnixSignal
335#[allow(dead_code)]
336fn shellsignal_to_signal(sig: u8) -> Option<UnixSignal> {
337    match sig {
338        3 => Some(UnixSignal::Sigint),
339        26 => Some(UnixSignal::Sigstop),
340        _ => None
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    use crate::config::Config;
349
350    use crate::translator::ioprocessor::IOProcessor;
351    use crate::translator::new_translator;
352    use crate::translator::lang::Language;
353
354    use std::collections::HashMap;
355    use std::time::Duration;
356    use std::thread::sleep;
357
358    #[test]
359    fn test_runtime_read_from_shell() {
360        let mut cfg: Config = Config::default();
361        cfg.output_config.translate_output = true;
362        let iop: IOProcessor = IOProcessor::new(Language::Russian, new_translator(Language::Russian));
363        let mut shell: Shell = Shell::start(String::from("sh"), vec![], &cfg.prompt_config).unwrap();
364        sleep(Duration::from_millis(500)); //DON'T REMOVE THIS SLEEP
365        //Write
366        let _ = shell.write(String::from("echo 4\n"));
367        sleep(Duration::from_millis(100));
368        //Read
369        read_from_shell(&mut shell, &cfg, &iop);
370        //Don't translate
371        cfg.output_config.translate_output = false;
372        let _ = shell.write(String::from("echo 5\n"));
373        sleep(Duration::from_millis(100));
374        read_from_shell(&mut shell, &cfg, &iop);
375        //Try stderr
376        cfg.output_config.translate_output = true;
377        let _ = shell.write(String::from("poropero\n"));
378        sleep(Duration::from_millis(100));
379        read_from_shell(&mut shell, &cfg, &iop);
380        //Try stderr not translated
381        cfg.output_config.translate_output = false;
382        let _ = shell.write(String::from("poropero\n"));
383        sleep(Duration::from_millis(100));
384        read_from_shell(&mut shell, &cfg, &iop);
385        //Terminate shell
386        sleep(Duration::from_millis(500)); //DON'T REMOVE THIS SLEEP
387        assert!(shell.stop().is_ok());
388        sleep(Duration::from_millis(500)); //DON'T REMOVE THIS SLEEP
389    }
390
391    #[test]
392    fn test_runtime_resolve_shell() {
393        let mut cfg: Config = Config::default();
394        cfg.shell_config.args = vec![String::from("-i")];
395        //Resolve shell without cli option
396        assert_eq!(resolve_shell(&cfg, None), (String::from("bash"), vec![String::from("-i")]));
397        //Resolve shell with cli option
398        assert_eq!(resolve_shell(&cfg, Some(String::from("fish"))), (String::from("fish"), vec![]));
399    }
400
401    #[test]
402    fn test_runtime_script_lines_to_command() {
403        let lines: Vec<String> = vec![String::from("#!/bin/bash"), String::from(""), String::from("echo 4"), String::from("#this is a comment"), String::from("cat /tmp/output;")];
404        assert_eq!(script_lines_to_string(&lines), String::from("echo 4;cat /tmp/output;"));
405    }
406
407    #[test]
408    fn test_runtime_resolve_command() {
409        let mut alias_cfg: HashMap<String, String> = HashMap::new();
410        alias_cfg.insert(String::from("ll"), String::from("ls -l"));
411        let cfg: Config = Config {
412            language: String::from(""),
413            shell_config: config::ShellConfig::default(),
414            alias: alias_cfg,
415            output_config: config::OutputConfig::default(),
416            prompt_config: config::PromptConfig::default()
417        };
418        //Resolve command
419        let mut argv: Vec<String> = vec![String::from("ll"), String::from("/tmp/")];
420        resolve_command(&mut argv, &cfg);
421        assert_eq!(*argv.get(0).unwrap(), String::from("ls -l"));
422
423        //Unresolved command
424        let mut argv: Vec<String> = vec![String::from("du"), String::from("-hs")];
425        resolve_command(&mut argv, &cfg);
426        assert_eq!(*argv.get(0).unwrap(), String::from("du"));
427    }
428
429    #[test]
430    fn test_runtime_print() {
431        let iop: IOProcessor = IOProcessor::new(Language::Russian, new_translator(Language::Russian));
432        //Out
433        print_out(String::from("Hello"), true, &iop);
434        print_out(String::from("Hello"), false, &iop);
435        //Err
436        print_err(String::from("Hello"), true, &iop);
437        print_err(String::from("Hello"), false, &iop);
438    }
439
440    #[test]
441    fn test_runtime_console_fmt() {
442        let iop: IOProcessor = IOProcessor::new(Language::Russian, new_translator(Language::Russian));
443        //Out
444        assert_eq!(console_fmt(String::from("Hello"), true, &iop), String::from("Хелло"));
445        assert_eq!(console_fmt(String::from("Hello"), false, &iop), String::from("Hello"));
446    }
447
448    #[test]
449    fn test_runtime_shellsignal() {
450        assert_eq!(shellsignal_to_signal(3).unwrap(), UnixSignal::Sigint);
451        assert_eq!(shellsignal_to_signal(26).unwrap(), UnixSignal::Sigstop);
452        assert!(shellsignal_to_signal(255).is_none());
453    }
454
455}