/* 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, Type }, sym::{ SymTable, ValueType, Symbol, Args, }, eval::eval, run, }, libc::{ sigaddset, sigemptyset, sigprocmask, SIGINT, SIGCHLD, SIGTTOU, SIGTTIN, SIGQUIT, SIGTSTP, SIG_BLOCK, SIG_UNBLOCK }, std::{ collections::VecDeque, cell::{ RefCell, RefMut, BorrowMutError, }, io::Result as IOResult, rc::Rc, fs::{ File, OpenOptions }, path::Path, process::{ Command, Stdio, Child }, env::set_current_dir, os::unix::process::CommandExt, mem, }, nix::{ unistd, unistd::Pid, sys::termios::{ Termios, tcgetattr, tcsetattr, SetArg, }, }, }; pub struct ShellState { pub parent_pid: Pid, pub parent_sid: Pid, pub children: Vec, pub last_exit_code: i32, pub attr: Option, } pub fn ign_sigs() { unsafe { let mut mask = mem::zeroed(); sigemptyset(&mut mask); sigaddset(&mut mask, SIGINT); sigaddset(&mut mask, SIGQUIT); sigaddset(&mut mask, SIGCHLD); sigaddset(&mut mask, SIGTSTP); sigaddset(&mut mask, SIGTTOU); sigaddset(&mut mask, SIGTTIN); sigprocmask( SIG_BLOCK, &mask as *const libc::sigset_t, std::ptr::null_mut() ); } } pub fn unign_sigs() { unsafe { let mut mask = mem::zeroed(); sigemptyset(&mut mask); sigaddset(&mut mask, SIGINT); sigaddset(&mut mask, SIGQUIT); sigaddset(&mut mask, SIGCHLD); sigaddset(&mut mask, SIGTSTP); sigaddset(&mut mask, SIGTTOU); sigaddset(&mut mask, SIGTTIN); sigprocmask( SIG_UNBLOCK, &mask as *const libc::sigset_t, std::ptr::null_mut() ); } } // TODO: trigger this on SIGCHLD instead of traditional shell once per input pub fn check_jobs(state: &mut ShellState) { let mut idx = 0 as usize; while idx < state.children.len() { if let Ok(_) = state.children[idx].try_wait() { state.children.remove(idx); continue; } idx += 1; } } 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: {}", eval_res.err().unwrap()) != () } }, 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: {}", eval_res.err().unwrap()) != () } }, 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: Result; unsafe { newchld = Command::new(path) .pre_exec(move || -> IOResult<()> { let pid = unistd::getpid(); if let Err(_) = unistd::setpgid(pid, pid) { // crying would be nice... if you had eyes } if !background { if let Err(_) = unistd::tcsetpgrp(0, pid) { // you wish to scream, but fork() has taken from you your throat } } unign_sigs(); Ok(()) }) .args(args) .stdin(infd) .stdout(outfd) .stderr(errfd) .process_group(0) .spawn(); } // glibc says to do this in both parent and child if let Ok(child) = newchld { let pid = child.id(); if let Err(e) = unistd::setpgid( Pid::from_raw(pid as i32), Pid::from_raw(pid as i32), ) { eprintln!("parent couldnt setpgid on chld: {}", e); } 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 pid_fancy = Pid::from_raw(pid as i32); if let Err(e) = unistd::tcsetpgrp(0, pid_fancy) { eprintln!("couldn't give child the terminal: {}!", e); } nix::sys::signal::kill( pid_fancy, nix::sys::signal::Signal::SIGCONT, ).expect( "couldnt sigcont... better get another tty." ); loop { match i.try_wait() { Ok(maybe) => { if let Some(exit) = maybe { if let Err(e) = unistd::tcsetpgrp(0, state.parent_pid) { return Err(format!( "error re-acquiring terminal: {}!", e )); } // TODO: this could be more elegant if let Some(ref attr) = state.attr { tcsetattr(0, SetArg::TCSADRAIN, attr).ok(); } else { panic!("somehow no attrs") } state.last_exit_code = exit .code() .or(Some(-1)) .unwrap(); break; } }, Err(e) => { return Err(format!("error waiting on child: {}", e)) } } } } } if let Some(pos) = state.children.iter().position(|x| x.id() == pid) { state.children.remove(pos); Ok(()) } else { Err(format!("pid not found in active children")) } } 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()) } } } } 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())) .stdout(Stdio::piped()) .process_group(0); if let Some(pos) = state.children .iter() .position(|x| x.id() == lastpid) { newchld.stdin(state.children.remove(pos).stdout.take().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.trim_end().to_string())) } else { Err(format!("couldn't 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 = "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). The command executed in the background while your shell remains active. Output from your command may appear in the terminal, but it will not be able to read from the terminal. examples: (bg ping -c4 google.com) (bg vim) ;; vim waits patiently for you to foreground the process with 'fg' "; fn bg_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(), true, state, )?; Ok(Ctr::Integer(state.last_exit_code.into())) } else { Err("file not found".to_string()) } } } } pub const FG_DOCSTRING: &str = "takes one argument (an integer). Integer is presumed to be a PID of a running background job. If PID corresponds with a background job, fg will try to foreground it. If PID is not a current running background job, fg will return an error. Examples: (bg vim) (fg 13871) "; pub fn fg_callback( ast: &Seg, _syms: &mut SymTable, shell_state: &mut ShellState ) -> Result { if let Ctr::Integer(i) = *ast.car { make_foreground(i as u32, shell_state)?; Ok(Ctr::None) } else { Err(format!("illegal args to fg")) } } pub const CD_DOCSTRING: &str = "Expects 1 argument (a string). Changes to a new directory"; pub fn cd_callback(ast: &Seg, _syms: &mut SymTable) -> Result { if let Ctr::String(ref dir) = *ast.car { let dirp = Path::new(dir); if let Err(s) = set_current_dir(&dirp) { Err(format!("{}", s)) } else { Ok(Ctr::None) } } else { Err(format!("impossible err: arg not a string")) } } pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc>) { let pid = unistd::getpid(); let pgid_res = unistd::getpgid(Some(pid)); let sid_res = unistd::getsid(Some(pid)); if !pgid_res.is_ok() || !sid_res.is_ok() { panic!("couldn't get pgid") } let pgid = pgid_res.ok().unwrap(); let sid = sid_res.ok().unwrap(); let shattr: Termios; // one mut borrow { let mut state = shell_state.borrow_mut(); state.parent_pid = pid; state.parent_sid = sid; if let Ok(attr) = tcgetattr(0) { state.attr = Some(attr.clone()); shattr = attr.clone(); } else { panic!("couldn't get term attrs"); } } let term_pgrp_res = unistd::tcgetpgrp(0); if !term_pgrp_res.is_ok() { panic!("couldn't get terminal's pgrp: {:?}", term_pgrp_res) } 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 pid != pgid { if let Err(e) = unistd::setpgid(pid, pid) { 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(); let fg_ss = shell_state.clone(); ign_sigs(); 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.borrow_mut()) })), ..Default::default() }, ); syms.insert( String::from("fg"), Symbol { name: String::from("foreground"), args: Args::Strict(vec![Type::Integer]), conditional_branches: true, docs: String::from(FG_DOCSTRING), value: ValueType::Internal(Rc::new(move |ast: &Seg, symtable: &mut SymTable| -> Result { fg_callback(ast, symtable, &mut fg_ss.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.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.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 { // extra nonsense needed to allow nested calls load_to_string_callback( ast, symtable, &mut lts_ss .try_borrow_mut() .or(Ok::, BorrowMutError>( RefCell::from(ShellState{ parent_pid: pid, parent_sid: pgid, children: vec![], last_exit_code: 0, attr: Some(shattr.clone()), }).borrow_mut())) .unwrap() ) })), ..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.borrow_mut()) })), ..Default::default() }, ); }