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:
Ava Apples Affine 2023-05-18 06:53:23 +00:00
parent 69216db72a
commit c127118465
8 changed files with 535 additions and 107 deletions

View file

@ -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

View file

@ -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;
}

View file

@ -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);
}
}