/* relish: versatile lisp shell * Copyright (C) 2021 Aidan Hahn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ use crate::segment::{Ctr, Seg}; use crate::sym::{SymTable, ValueType, Symbol, Args}; use crate::eval::eval; use crate::run; use std::collections::VecDeque; use std::cell::RefCell; use std::rc::Rc; use std::fs::File; use std::process::{Command, Child, Stdio}; use nix::unistd; use ctrlc; use std::fs::OpenOptions; pub struct ShellState { pub parent_pid: unistd::Pid, pub parent_pgid: unistd::Pid, pub children: Vec, pub last_exit_code: i32, } pub fn args_from_ast(ast: &Seg, syms: &mut SymTable) -> Vec { let mut args = vec![]; ast.circuit(&mut |arg_ctr: &Ctr| -> bool { match arg_ctr { Ctr::String(s) => args.push(s.clone()) == (), Ctr::Symbol(ref s) => if !syms.contains_key(s) { args.push(s.clone()) == () } else { let eval_args = Seg::new(); let eval_res = syms.call_symbol(s, &eval_args, false); if let Ok(res_ctr) = eval_res { match *res_ctr { Ctr::Lambda(_) => false, Ctr::Seg(_) => false, Ctr::String(s) => args.push(s.clone()) == (), _ => args.push(res_ctr.to_string()) == (), } } else { eprintln!("couldnt eval args!") != () } }, Ctr::Seg(ref form) => { let eval_res = eval(form, syms); if let Ok(res_ctr) = eval_res { match *res_ctr { Ctr::Lambda(_) => false, Ctr::Seg(_) => false, Ctr::String(s) => args.push(s) == (), _ => args.push(res_ctr.to_string()) == (), } } else { eprintln!("couldnt eval args!") != () } }, Ctr::Lambda(_) => eprintln!("lambda passed as shell parameter") != (), _ => args.push(arg_ctr.to_string()) == (), } }); args } fn launch_command( path: String, args: &[String], infd: Stdio, outfd: Stdio, errfd: Stdio, background: bool, state: &mut ShellState ) -> Result<(), String> { let newchld = Command::new(path) .args(args) .stdin(infd) .stdout(outfd) .stderr(errfd).spawn(); if let Ok(child) = newchld { let pid = child.id(); state.children.push(child); if !background { make_foreground(pid, state) } else { Ok(()) } } else { Err(format!("couldnt spawn command: {}", newchld.err().unwrap())) } } // the analogous make_background happens in the app when the user sends it the corresponding signal fn make_foreground(pid: u32, state: &mut ShellState) -> Result<(), String>{ for i in &mut state.children { if i.id() == pid { let exit = i.wait().unwrap(); if let Some(code) = exit.code() { state.last_exit_code = code; } else { state.last_exit_code = -1; } if let Err(e) = unistd::tcsetpgrp(0, state.parent_pgid) { return Err(format!("error setting terminal: {}!", e)); } } } if let Some(pos) = state.children.iter().position(|x| x.id() == pid) { state.children.remove(pos); } Ok(()) } const LOAD_DOCSTRING: &str = "Calls a binary off disk with given arguments. Arguments may be of any type except lambda. If a symbol is not defined it will be passed as a string. first argument (command name) will be found on path (or an error returned). examples: (l ping google.com) (let ((ping-count 4)) (l ping -c ping-count google.com)) (l emacs -nw (concat HOME '/.relishrc')) "; fn load_callback(ast: &Seg, syms: &mut SymTable, state: &mut ShellState) -> Result { if ast.is_empty() { Err("need at least one argument".to_string()) } else { let mut args = VecDeque::from(args_from_ast(ast, syms)); if args.is_empty() { Err("empty command".to_string()) } else { if let Some(filepath) = run::find_on_path(args.pop_front().unwrap()) { launch_command( filepath, &Vec::from(args.make_contiguous()), Stdio::inherit(), Stdio::inherit(), Stdio::inherit(), false, state, )?; Ok(Ctr::Integer(state.last_exit_code.into())) } else { Err("file not found".to_string()) } } } } // TODO: maybe flesh out this docstring a bit const PIPE_DOCSTRING: &str = "Calls a sequence of shell commands. Each one has their output redirected to the input of the next one. Example: (pipe (ls -la) (grep '.rs') (tr -d '.rs'))"; fn pipe_callback(ast: &Seg, syms: &mut SymTable, state: &mut ShellState) -> Result { if ast.is_empty() { return Err("need at least one argument".to_string()) } let mut err: String = String::new(); // posix layer will never control pid 0 let mut lastpid = 0; if !ast.circuit(&mut |arg: &Ctr| -> bool { if let Ctr::Seg(command_form) = arg { let mut args = VecDeque::from(args_from_ast(command_form, syms)); if args.is_empty() { return false } if let Some(filepath) = run::find_on_path(args.pop_front().unwrap()) { let mut newchld = Command::new(filepath); newchld.args(Vec::from(args.make_contiguous())) .stderr(Stdio::inherit()) .stdout(Stdio::piped()); if let Some(pos) = state.children .iter() .position(|x| x.id() == lastpid) { newchld.stdin(state.children.remove(pos).stdout.unwrap()); } let chld_spwn = newchld.spawn(); if let Ok(child) = chld_spwn { let pid = child.id(); lastpid = pid; state.children.push(child); true } else { err = format!("failed to spawn: {}", chld_spwn.err().unwrap()); false } } else { err = "file not found".to_string(); false } } else { err = format!("{} not a shell command", arg); false } }) { Err(err) } else { if lastpid > 0 { if let Some(pos) = state.children .iter() .position(|x| x.id() == lastpid) { let chld = state.children.remove(pos); let exit = chld.wait_with_output() .expect("failed to wait on last child"); state.last_exit_code = exit.status .code() .or_else(|| {Some(-1)}) .unwrap(); println!("{}", String::from_utf8_lossy(&exit.stdout)); Ok(Ctr::Integer(state.last_exit_code.into())) } else { Err(format!("lost last child (pid {})", lastpid)) } } else { Err("impossible error state".to_string()) } } } const LOAD_TO_STRING_DOCSTRING: &str = "Calls a binary off disk with given arguments. Arguments may be of any type except lambda. If a symbol is not defined it will be passed as a string. first argument (command name) will be found on path (or an error returned). examples: (l ping google.com) (let ((ping-count 4)) (l ping -c ping-count google.com)) (l emacs -nw (concat HOME '/.relishrc')) Unlike with the normal load function, the load to string function collects stdout output and returns it as a string. "; fn load_to_string_callback(ast: &Seg, syms: &mut SymTable, state: &mut ShellState) -> Result { if ast.is_empty() { Err("need at least one argument".to_string()) } else { let mut args = VecDeque::from(args_from_ast(ast, syms)); if args.is_empty() { Err("empty command".to_string()) } else { if let Some(filepath) = run::find_on_path(args.pop_front().unwrap()) { let res = Command::new(filepath).args(args).output(); if let Ok(output) = res { if let Some(code) = output.status.code() { state.last_exit_code = code; } if let Ok(string) = String::from_utf8(output.stdout) { Ok(Ctr::String(string)) } else { Err(format!("could'nt marshall utf-8 command output into a string")) } } else { Err(format!("{}", res.err().unwrap())) } } else { Err("file not found".to_string()) } } } } // TODO: rework this callback to reduce spaghett const LOAD_WITH_DOCSTRING: &str = "Takes two arguments. 1. a list of up to three redirects - A redirect looks like a variable declaration in a let form Ex: ('stdout' '/dev/null') - The first value is one of 'stdout', 'stdin', or 'stderr' - The second value is a filename. The file must exist for stdin redirects. For other redirects the file will be created if not existing. Example: (('stdin' '/path/to/myinput.txt') ('stdout' '/dev/null')) 2. a shell command - exactly as would be passed to load Example: (ping -c 4 google.com) Example invocation: (load-with (('stdout' '/dev/null')) (ping -c 4 google.com))"; fn load_with_callback(ast: &Seg, syms: &mut SymTable, state: &mut ShellState) -> Result { if ast.len() != 2 { Err("exactly two arguments needed".to_string()) } else { if let Ctr::Seg(ref fd_redirect_forms) = *ast.car { let mut stdout: Option = None; let mut stdin: Option = None; let mut stderr: Option = None; let mut e: Option = None; if !fd_redirect_forms.circuit(&mut |arg: &Ctr| -> bool { // of the form Seg(Str(fd), Seg(Str(filepath))) if let Ctr::Seg(ref io_redir) = arg { if let Ctr::String(ref fd) = *io_redir.car { if let Ctr::Seg(ref io_file) = *io_redir.cdr { if let Ctr::String(ref filename) = *io_file.car { match fd.as_str() { "stdin" => { if !stdin.is_none() { e = Some(format!("stdin already defined")); return false } let input = File::open(filename); if !input.is_ok() { e = Some(format!("input file does not exist")); return false } stdin = Some(Stdio::from(input.ok().unwrap())); return true }, "stdout" => { if !stdout.is_none() { e = Some(format!("stdout already defined")); return false } let out = OpenOptions::new() .write(true) .create(true) .open(filename); if !out.is_ok() { e = Some(out.unwrap_err().to_string()); return false } stdout = Some(Stdio::from(out.ok().unwrap())); return true }, "stderr" => { if !stderr.is_none() { e = Some(format!("stderr already defined")); return false } let err = OpenOptions::new() .write(true) .create(true) .open(filename); if !err.is_ok() { e = Some(err.unwrap_err().to_string()); return false } stderr = Some(Stdio::from(err.ok().unwrap())); return true }, _ => { e = Some(format!("can only override stdin, stdout, or stderr")); false } } } else { e = Some(format!("{} is not a string", *io_file.car)); false } } else { e = Some(format!("fd override must have both an fd and a filename")); false } } else { e = Some(format!("{} is not a string", *io_redir.car)); false } } else { e = Some(format!("not a list: {}", arg)); false } }) { return Err(e.unwrap()) } // now deal with the actual command if let Ctr::Seg(ref command_forms) = *ast.cdr { let mut args: VecDeque; if let Ctr::Seg(ref command) = *command_forms.car { args = VecDeque::from(args_from_ast(command, syms)); } else { return Err("command not list".to_string()) } if args.is_empty() { Err("empty command".to_string()) } else { if let Some(filepath) = run::find_on_path(args.pop_front().unwrap()) { launch_command( filepath, &Vec::from(args.make_contiguous()), stdin.or(Some(Stdio::inherit())).unwrap(), stdout.or(Some(Stdio::inherit())).unwrap(), stderr.or(Some(Stdio::inherit())).unwrap(), false, state, )?; Ok(Ctr::Integer(state.last_exit_code.into())) } else { Err("file not found".to_string()) } } } else { Err("second argument expected to be a list of command elements".to_string()) } } else { Err("first argument expected to be a list of lists".to_string()) } } } const Q_DOCSTRING: &str = "returns exit code of last process to be run in posix layer"; fn q_callback(_ast: &Seg, _syms: &SymTable, state: &mut ShellState) -> Result { Ok(Ctr::Integer(state.last_exit_code.into())) } const BG_DOCSTRING: &str = ""; fn bg_callback(_ast: &Seg, _syms: &mut SymTable, _state: &mut ShellState) -> Result { unimplemented!() } pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc>) { let pid = unistd::getpid(); let pgid_res = unistd::getpgid(Some(pid)); if !pgid_res.is_ok() { panic!("couldn't get pgid") } let pgid = pgid_res.ok().unwrap(); // one mut borrow { let mut state = shell_state.borrow_mut(); state.parent_pid = pid; state.parent_pgid = pgid; } let term_pgrp_res = unistd::tcgetpgrp(0); if !term_pgrp_res.is_ok() { panic!("couldn't get terminal's pgrp") } let term_owner = term_pgrp_res.ok().unwrap(); if pgid != term_owner { nix::sys::signal::kill( term_owner, nix::sys::signal::Signal::SIGTTIN, ).expect("couldn't take terminal from owning process") } if let Err(e) = unistd::setpgid( unistd::Pid::from_raw(0), unistd::Pid::from_raw(0) ) { panic!("couldn't set PGID: {}", e) }; if let Err(e) = unistd::tcsetpgrp(0, pid) { panic!("couldn't grab terminal: {}", e) } // these clones are needed for callbacks which move references let load_ss = shell_state.clone(); let bg_ss = shell_state.clone(); let q_ss = shell_state.clone(); let lw_ss = shell_state.clone(); let lts_ss = shell_state.clone(); let p_ss = shell_state.clone(); syms.insert( String::from("l"), Symbol { name: String::from("load"), args: Args::Infinite, conditional_branches: true, docs: String::from(LOAD_DOCSTRING), value: ValueType::Internal(Rc::new(move |ast: &Seg, symtable: &mut SymTable| -> Result { load_callback(ast, symtable, &mut shell_state.borrow_mut()) })), ..Default::default() }, ); syms.insert( String::from("load"), Symbol { name: String::from("load"), args: Args::Infinite, conditional_branches: true, docs: String::from(LOAD_DOCSTRING), value: ValueType::Internal(Rc::new(move |ast: &Seg, symtable: &mut SymTable| -> Result { load_callback(ast, symtable, &mut load_ss.borrow_mut()) })), ..Default::default() }, ); syms.insert( String::from("bg"), Symbol { name: String::from("background"), args: Args::Infinite, conditional_branches: true, docs: String::from(BG_DOCSTRING), value: ValueType::Internal(Rc::new(move |ast: &Seg, symtable: &mut SymTable| -> Result { bg_callback(ast, symtable, &mut bg_ss.clone().borrow_mut()) })), ..Default::default() }, ); syms.insert( String::from("?"), Symbol { name: String::from("?"), args: Args::None, conditional_branches: false, docs: String::from(Q_DOCSTRING), value: ValueType::Internal(Rc::new(move |ast: &Seg, symtable: &mut SymTable| -> Result { q_callback(ast, symtable, &mut q_ss.clone().borrow_mut()) })), ..Default::default() }, ); syms.insert( String::from("load-with"), Symbol { name: String::from("load-with"), args: Args::Lazy(2), conditional_branches: true, docs: String::from(LOAD_WITH_DOCSTRING), value: ValueType::Internal(Rc::new(move |ast: &Seg, symtable: &mut SymTable| -> Result { load_with_callback(ast, symtable, &mut lw_ss.clone().borrow_mut()) })), ..Default::default() }, ); syms.insert( String::from("load-to-string"), Symbol { name: String::from("load-to-string"), args: Args::Infinite, conditional_branches: true, docs: String::from(LOAD_TO_STRING_DOCSTRING), value: ValueType::Internal(Rc::new(move |ast: &Seg, symtable: &mut SymTable| -> Result { load_to_string_callback(ast, symtable, &mut lts_ss.clone().borrow_mut()) })), ..Default::default() }, ); syms.insert( String::from("pipe"), Symbol { name: String::from("pipe"), args: Args::Infinite, conditional_branches: true, docs: String::from(PIPE_DOCSTRING), value: ValueType::Internal(Rc::new(move |ast: &Seg, symtable: &mut SymTable| -> Result { pipe_callback(ast, symtable, &mut p_ss.clone().borrow_mut()) })), ..Default::default() }, ); if let Err(e) = ctrlc::set_handler(move || println!("POSIX layer caught SIG-something")) { eprintln!("WARNING: couldn't set sig handler: {}", e); } }