elementary shell behavior: can kick off a foreground process and
handle signals
This commit is contained in:
parent
3b1ae0efd5
commit
99cb9e5a2e
17 changed files with 619 additions and 167 deletions
285
src/stl/posix.rs
Normal file
285
src/stl/posix.rs
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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::process::{Command, Child, Stdio};
|
||||
use nix::unistd;
|
||||
use ctrlc;
|
||||
|
||||
/* WONTDO: flag, switch
|
||||
* Step 6: ? function
|
||||
* Step 7: pipe control flow that circuits across a list of calls hooking stdin to stdout
|
||||
* Step 8.1: load=binary script=call
|
||||
* Step 8.2: call-to-string function
|
||||
* Step 9: ignore job control signals
|
||||
* Step 10: background processes
|
||||
* Step 11: Be able to list all background processes with j function
|
||||
* Step 12: Be able to fg a bg process
|
||||
* Step 13: Be able to bg an fg process (Ctrl Z)
|
||||
* Step 14: changedir
|
||||
* Step 15: pwd
|
||||
* Step 16: circuit casts int to bool automatically
|
||||
* Step 17: Documentation
|
||||
* - list functions and their docstrings
|
||||
* - examples of pipe and circuit
|
||||
* - job control
|
||||
* Step 18: call-with
|
||||
* - let style setting of files as stream targets
|
||||
* - and of course documentation
|
||||
*/
|
||||
|
||||
pub struct ShellState {
|
||||
pub parent_pid: unistd::Pid,
|
||||
pub parent_pgid: unistd::Pid,
|
||||
pub children: Vec<Child>,
|
||||
pub last_exit_code: i32,
|
||||
}
|
||||
|
||||
pub fn args_from_ast(ast: &Seg, syms: &mut SymTable) -> Vec<String> {
|
||||
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<Ctr, String> {
|
||||
if ast.is_empty() {
|
||||
Err("need at least one argument".to_string())
|
||||
} else {
|
||||
let mut args = VecDeque::from(args_from_ast(ast, syms));
|
||||
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 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<Ctr, String> {
|
||||
Ok(Ctr::Integer(state.last_exit_code.into()))
|
||||
}
|
||||
|
||||
const BG_DOCSTRING: &str = "";
|
||||
fn bg_callback(_ast: &Seg, _syms: &mut SymTable, _state: &mut ShellState) -> Result<Ctr, String> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc<RefCell<ShellState>>) {
|
||||
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 consume references
|
||||
let bg_ss = shell_state.clone();
|
||||
let q_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<Ctr, String> {
|
||||
load_callback(ast, symtable, &mut shell_state.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<Ctr, String> {
|
||||
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<Ctr, String> {
|
||||
q_callback(ast, symtable, &mut q_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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue