fully interactive shell
* Background task implementation * Foreground an existing task implementation * Documentation for both * Refactors to signal handling, process pre-exec
This commit is contained in:
parent
69216db72a
commit
c127118465
8 changed files with 535 additions and 107 deletions
|
|
@ -25,7 +25,7 @@ use {
|
|||
stdlib::{
|
||||
static_stdlib, dynamic_stdlib
|
||||
},
|
||||
aux::ShellState,
|
||||
aux::{ShellState, check_jobs},
|
||||
},
|
||||
std::{
|
||||
cell::RefCell,
|
||||
|
|
@ -169,7 +169,6 @@ impl Completer for CustomCompleter {
|
|||
if e.file_name()
|
||||
.to_string_lossy()
|
||||
.starts_with(path.file_name()
|
||||
// TODO: dont
|
||||
.expect("bad file somehow?")
|
||||
.to_string_lossy()
|
||||
.to_mut()
|
||||
|
|
@ -220,12 +219,16 @@ fn main() {
|
|||
let hist_file_name = home_dir.clone() + HIST_FILE;
|
||||
let cfg_file_name = home_dir + CONFIG_FILE_DEFAULT;
|
||||
|
||||
// TODO: the next two decls should probably be conditional on CFG_RELISH_POSIX
|
||||
// but in both cases the data must live for the whole program
|
||||
let shell_state_bindings = Rc::new(RefCell::from(ShellState {
|
||||
parent_pid: unistd::Pid::from_raw(0),
|
||||
parent_pgid: unistd::Pid::from_raw(0),
|
||||
parent_sid: unistd::Pid::from_raw(0),
|
||||
children: vec![],
|
||||
last_exit_code: 0,
|
||||
attr: None,
|
||||
}));
|
||||
let prompt_ss = shell_state_bindings.clone();
|
||||
|
||||
// setup symtable
|
||||
let mut syms = SymTable::new();
|
||||
|
|
@ -281,8 +284,10 @@ fn main() {
|
|||
.with_style(Style::new().italic().fg(Color::LightGray)),
|
||||
)).with_validator(Box::new(DefaultValidator));
|
||||
|
||||
// repl :)
|
||||
loop {
|
||||
{
|
||||
check_jobs(&mut prompt_ss.borrow_mut());
|
||||
}
|
||||
let readline_prompt = make_prompt(&mut syms);
|
||||
let completer = make_completer(&mut syms);
|
||||
// realloc with each loop because syms can change
|
||||
|
|
|
|||
|
|
@ -37,4 +37,5 @@ pub mod stdlib {
|
|||
pub mod aux {
|
||||
pub use crate::stl::posix::args_from_ast;
|
||||
pub use crate::stl::posix::ShellState;
|
||||
pub use crate::stl::posix::check_jobs;
|
||||
}
|
||||
|
|
|
|||
285
src/stl/posix.rs
285
src/stl/posix.rs
|
|
@ -17,7 +17,9 @@
|
|||
|
||||
use {
|
||||
crate::{
|
||||
segment::{Ctr, Seg},
|
||||
segment::{
|
||||
Ctr, Seg, Type
|
||||
},
|
||||
sym::{
|
||||
SymTable, ValueType,
|
||||
Symbol, Args,
|
||||
|
|
@ -25,32 +27,93 @@ use {
|
|||
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},
|
||||
fs::{
|
||||
File, OpenOptions
|
||||
},
|
||||
path::Path,
|
||||
process::{
|
||||
Command, Child,
|
||||
Stdio,
|
||||
Command, Stdio, Child
|
||||
},
|
||||
env::set_current_dir,
|
||||
os::unix::process::CommandExt,
|
||||
mem,
|
||||
},
|
||||
nix::{
|
||||
unistd, unistd::Pid,
|
||||
sys::termios::{
|
||||
Termios, tcgetattr,
|
||||
tcsetattr, SetArg,
|
||||
},
|
||||
},
|
||||
|
||||
nix::unistd,
|
||||
ctrlc,
|
||||
};
|
||||
|
||||
pub struct ShellState {
|
||||
pub parent_pid: unistd::Pid,
|
||||
pub parent_pgid: unistd::Pid,
|
||||
pub parent_pid: Pid,
|
||||
pub parent_sid: Pid,
|
||||
pub children: Vec<Child>,
|
||||
pub last_exit_code: i32,
|
||||
pub attr: Option<Termios>,
|
||||
}
|
||||
|
||||
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<String> {
|
||||
|
|
@ -107,14 +170,42 @@ fn launch_command(
|
|||
background: bool,
|
||||
state: &mut ShellState
|
||||
) -> Result<(), String> {
|
||||
let newchld = Command::new(path)
|
||||
.args(args)
|
||||
.stdin(infd)
|
||||
.stdout(outfd)
|
||||
.stderr(errfd).spawn();
|
||||
let newchld: Result<std::process::Child, std::io::Error>;
|
||||
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)
|
||||
|
|
@ -130,23 +221,57 @@ fn launch_command(
|
|||
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;
|
||||
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);
|
||||
}
|
||||
if let Err(e) = unistd::tcsetpgrp(0, state.parent_pgid) {
|
||||
return Err(format!("error setting 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"))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const LOAD_DOCSTRING: &str = "Calls a binary off disk with given arguments.
|
||||
|
|
@ -185,7 +310,6 @@ fn load_callback(ast: &Seg, syms: &mut SymTable, state: &mut ShellState) -> Resu
|
|||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
|
|
@ -212,14 +336,14 @@ fn pipe_callback(ast: &Seg, syms: &mut SymTable, state: &mut ShellState) -> Resu
|
|||
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());
|
||||
.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.unwrap());
|
||||
newchld.stdin(state.children.remove(pos).stdout.take().unwrap());
|
||||
}
|
||||
|
||||
let chld_spwn = newchld.spawn();
|
||||
|
|
@ -458,9 +582,62 @@ fn q_callback(_ast: &Seg, _syms: &SymTable, state: &mut ShellState) -> Result<Ct
|
|||
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!()
|
||||
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<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 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 <Ctr, String> {
|
||||
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 =
|
||||
|
|
@ -481,21 +658,31 @@ pub fn cd_callback(ast: &Seg, _syms: &mut SymTable) -> Result<Ctr, String> {
|
|||
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() {
|
||||
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_pgid = pgid;
|
||||
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")
|
||||
panic!("couldn't get terminal's pgrp: {:?}", term_pgrp_res)
|
||||
}
|
||||
|
||||
let term_owner = term_pgrp_res.ok().unwrap();
|
||||
|
|
@ -509,14 +696,13 @@ pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc<RefCell<ShellState>
|
|||
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();
|
||||
|
|
@ -524,6 +710,9 @@ pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc<RefCell<ShellState>
|
|||
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"),
|
||||
|
|
@ -567,6 +756,20 @@ pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc<RefCell<ShellState>
|
|||
},
|
||||
);
|
||||
|
||||
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<Ctr, String> {
|
||||
fg_callback(ast, symtable, &mut fg_ss.borrow_mut())
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
syms.insert(
|
||||
String::from("?"),
|
||||
Symbol {
|
||||
|
|
@ -612,11 +815,11 @@ pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc<RefCell<ShellState>
|
|||
.or(Ok::<RefMut<'_, ShellState>, BorrowMutError>(
|
||||
RefCell::from(ShellState{
|
||||
parent_pid: pid,
|
||||
parent_pgid: pgid,
|
||||
parent_sid: pgid,
|
||||
children: vec![],
|
||||
last_exit_code: 0,
|
||||
}).borrow_mut()
|
||||
))
|
||||
attr: Some(shattr.clone()),
|
||||
}).borrow_mut()))
|
||||
.unwrap()
|
||||
)
|
||||
})),
|
||||
|
|
@ -637,8 +840,4 @@ pub fn load_posix_shell(syms: &mut SymTable, shell_state: Rc<RefCell<ShellState>
|
|||
..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