diff --git a/src/builtin/jobctl.rs b/src/builtin/jobctl.rs index 9b6165c..c7a647e 100644 --- a/src/builtin/jobctl.rs +++ b/src/builtin/jobctl.rs @@ -128,7 +128,7 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult { } else { Err(ShErr::full( ShErrKind::SyntaxErr, - format!("Invalid fd arg: {}", arg), + format!("Invalid arg: {}", arg), blame, )) } diff --git a/src/jobs.rs b/src/jobs.rs index 88f2df4..ee7a75a 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -164,10 +164,26 @@ impl JobTab { &mut self.jobs } pub fn curr_job(&self) -> Option { - self.order.last().copied() + // Find the most recent valid job (order can have stale entries) + for &id in self.order.iter().rev() { + if self.jobs.get(id).is_some_and(|slot| slot.is_some()) { + return Some(id); + } + } + None } pub fn prev_job(&self) -> Option { - self.order.last().copied() + // Find the second most recent valid job + let mut found_curr = false; + for &id in self.order.iter().rev() { + if self.jobs.get(id).is_some_and(|slot| slot.is_some()) { + if found_curr { + return Some(id); + } + found_curr = true; + } + } + None } pub fn close_job_fds(&mut self, pid: Pid) { self.fd_registry.retain(|fd| fd.owner_pid != pid) @@ -507,21 +523,25 @@ impl Job { flog!(TRACE, "waiting on children"); flog!(TRACE, self.children); for child in self.children.iter_mut() { - flog!(TRACE, "shell pid {}", Pid::this()); - flog!(TRACE, "child pid {}", child.pid); + flog!(TRACE, "shell pid {}", Pid::this()); + flog!(TRACE, "child pid {}", child.pid); if child.pid == Pid::this() { // TODO: figure out some way to get the exit code of builtins let code = state::get_status(); stats.push(WtStat::Exited(child.pid, code)); continue; } - let result = child.wait(Some(WtFlag::WSTOPPED)); - match result { - Ok(stat) => { - stats.push(stat); + loop { + let result = child.wait(Some(WtFlag::WSTOPPED)); + match result { + Ok(stat) => { + stats.push(stat); + break; + } + Err(Errno::ECHILD) => break, + Err(Errno::EINTR) => continue, // Retry on signal interruption + Err(e) => return Err(e.into()), } - Err(Errno::ECHILD) => break, - Err(e) => return Err(e.into()), } } Ok(stats) @@ -649,6 +669,7 @@ pub fn wait_fg(job: Job) -> ShResult<()> { } flog!(TRACE, "Waiting on foreground job"); let mut code = 0; + let mut was_stopped = false; attach_tty(job.pgid())?; disable_reaping(); let statuses = write_jobs(|j| j.new_fg(job))?; @@ -658,11 +679,13 @@ pub fn wait_fg(job: Job) -> ShResult<()> { code = exit_code; } WtStat::Stopped(_, sig) => { + was_stopped = true; write_jobs(|j| j.fg_to_bg(status))?; code = SIG_EXIT_OFFSET + sig as i32; } WtStat::Signaled(_, sig, _) => { if sig == Signal::SIGTSTP { + was_stopped = true; write_jobs(|j| j.fg_to_bg(status))?; } code = SIG_EXIT_OFFSET + sig as i32; @@ -670,6 +693,10 @@ pub fn wait_fg(job: Job) -> ShResult<()> { _ => { /* Do nothing */ } } } + // If job wasn't stopped (moved to bg), clear the fg slot + if !was_stopped { + write_jobs(|j| { j.take_fg(); }); + } take_term()?; set_status(code); flog!(TRACE, "exit code: {}", code); diff --git a/src/libsh/error.rs b/src/libsh/error.rs index dfff5b3..bc4026e 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -374,9 +374,9 @@ impl Display for ShErr { } impl From for ShErr { - fn from(_: std::io::Error) -> Self { + fn from(e: std::io::Error) -> Self { let msg = std::io::Error::last_os_error(); - ShErr::simple(ShErrKind::IoErr, msg.to_string()) + ShErr::simple(ShErrKind::IoErr(e.kind()), msg.to_string()) } } @@ -394,7 +394,7 @@ impl From for ShErr { #[derive(Debug, Clone)] pub enum ShErrKind { - IoErr, + IoErr(io::ErrorKind), SyntaxErr, ParseErr, InternalErr, @@ -420,7 +420,7 @@ pub enum ShErrKind { impl Display for ShErrKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let output = match self { - Self::IoErr => "I/O Error", + Self::IoErr(e) => &format!("I/O Error: {e}"), Self::SyntaxErr => "Syntax Error", Self::ParseErr => "Parse Error", Self::InternalErr => "Internal Error", diff --git a/src/main.rs b/src/main.rs index 900440f..1d859a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,6 +106,17 @@ fn fern_interactive() { let mut partial_input = String::new(); 'outer: loop { + while signals_pending() { + if let Err(e) = check_signals() { + if let ShErrKind::ClearReadline = e.kind() { + partial_input.clear(); + if !signals_pending() { + continue 'outer; + } + }; + eprintln!("{e}"); + } + } // Main loop let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode")) .unwrap() diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index da6f30b..3affc12 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -48,11 +48,22 @@ impl Readline for FernVi { loop { raw_mode_guard.disable_for(|| self.print_line())?; - let Some(key) = self.reader.read_key()? else { - raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; - std::mem::drop(raw_mode_guard); - return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF")); - }; + let key = match self.reader.read_key() { + Ok(Some(key)) => key, + Err(e) if matches!(e.kind(), ShErrKind::IoErr(std::io::ErrorKind::Interrupted)) => { + flog!(DEBUG, "readline interrupted"); + let partial: String = self.editor.as_str().to_string(); + return Err(ShErr::simple(ShErrKind::ReadlineIntr(partial), "")); + } + Err(_) | Ok(None) => { + flog!(DEBUG, "EOF detected"); + raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; + std::mem::drop(raw_mode_guard); + return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF")); + } + + }; + flog!(DEBUG, key); if self.should_accept_hint(&key) { diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index b1d5909..eaddb8d 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -28,6 +28,8 @@ pub fn raw_mode() -> RawModeGuard { .expect("Failed to get terminal attributes"); let mut raw = orig.clone(); termios::cfmakeraw(&mut raw); + // Keep ISIG enabled so Ctrl+C/Ctrl+Z still generate signals + raw.local_flags |= termios::LocalFlags::ISIG; termios::tcsetattr( unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }, termios::SetArg::TCSANOW, @@ -230,11 +232,17 @@ impl TermBuffer { impl Read for TermBuffer { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { assert!(isatty(self.tty).is_ok_and(|r| r)); - match nix::unistd::read(self.tty, buf) { - Ok(n) => Ok(n), - Err(Errno::EINTR) => Err(Errno::EINTR.into()), - Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)), - } + flog!(DEBUG, "TermBuffer::read() ENTERING read syscall"); + let result = nix::unistd::read(self.tty, buf); + flog!(DEBUG, "TermBuffer::read() EXITED read syscall: {:?}", result); + match result { + Ok(n) => Ok(n), + Err(Errno::EINTR) => { + flog!(DEBUG, "TermBuffer::read() returning EINTR"); + Err(Errno::EINTR.into()) + } + Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)), + } } } @@ -258,6 +266,8 @@ impl RawModeGuard { // Re-enable raw mode let mut raw = self.orig.clone(); termios::cfmakeraw(&mut raw); + // Keep ISIG enabled so Ctrl+C/Ctrl+Z still generate signals + raw.local_flags |= termios::LocalFlags::ISIG; termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw).expect("Failed to re-enable raw mode"); result @@ -315,7 +325,7 @@ impl TermReader { pub fn next_byte(&mut self) -> std::io::Result { let mut buf = [0u8]; - self.buffer.read_exact(&mut buf)?; + let _n = self.buffer.read(&mut buf)?; Ok(buf[0]) } diff --git a/src/signal.rs b/src/signal.rs index 31e9ef6..4e8a614 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -29,29 +29,38 @@ pub fn signals_pending() -> bool { pub fn check_signals() -> ShResult<()> { if GOT_SIGINT.swap(false, Ordering::SeqCst) { + flog!(DEBUG, "check_signals: processing SIGINT"); interrupt()?; return Err(ShErr::simple(ShErrKind::ClearReadline, "")); } if GOT_SIGHUP.swap(false, Ordering::SeqCst) { + flog!(DEBUG, "check_signals: processing SIGHUP"); hang_up(0); } if GOT_SIGTSTP.swap(false, Ordering::SeqCst) { + flog!(DEBUG, "check_signals: processing SIGTSTP"); terminal_stop()?; } if REAPING_ENABLED.load(Ordering::SeqCst) && GOT_SIGCHLD.swap(false, Ordering::SeqCst) { + flog!(DEBUG, "check_signals: processing SIGCHLD (reaping enabled)"); wait_child()?; + } else if GOT_SIGCHLD.load(Ordering::SeqCst) { + flog!(DEBUG, "check_signals: SIGCHLD pending but reaping disabled"); } if SHOULD_QUIT.load(Ordering::SeqCst) { let code = QUIT_CODE.load(Ordering::SeqCst); + flog!(DEBUG, "check_signals: SHOULD_QUIT set, exiting with code {}", code); return Err(ShErr::simple(ShErrKind::CleanExit(code), "exit")); } Ok(()) } pub fn disable_reaping() { + flog!(DEBUG, "disable_reaping: turning off SIGCHLD processing"); REAPING_ENABLED.store(false, Ordering::SeqCst); } pub fn enable_reaping() { + flog!(DEBUG, "enable_reaping: turning on SIGCHLD processing"); REAPING_ENABLED.store(true, Ordering::SeqCst); } @@ -142,12 +151,15 @@ extern "C" fn handle_sigint(_: libc::c_int) { } pub fn interrupt() -> ShResult<()> { + flog!(DEBUG, "interrupt: checking for fg job to send SIGINT"); write_jobs(|j| { if let Some(job) = j.get_fg_mut() { + flog!(DEBUG, "interrupt: sending SIGINT to fg job pgid {}", job.pgid()); job.killpg(Signal::SIGINT) } else { - Ok(()) - } + flog!(DEBUG, "interrupt: no fg job, clearing readline"); + Ok(()) + } }) } @@ -161,18 +173,34 @@ extern "C" fn handle_sigchld(_: libc::c_int) { } pub fn wait_child() -> ShResult<()> { + flog!(DEBUG, "wait_child: starting reap loop"); let flags = WtFlag::WNOHANG | WtFlag::WSTOPPED; while let Ok(status) = waitpid(None, Some(flags)) { match status { - WtStat::Exited(pid, _) => child_exited(pid, status)?, - WtStat::Signaled(pid, signal, _) => child_signaled(pid, signal)?, - WtStat::Stopped(pid, signal) => child_stopped(pid, signal)?, - WtStat::Continued(pid) => child_continued(pid)?, - WtStat::StillAlive => break, + WtStat::Exited(pid, code) => { + flog!(DEBUG, "wait_child: pid {} exited with code {}", pid, code); + child_exited(pid, status)?; + } + WtStat::Signaled(pid, signal, _) => { + flog!(DEBUG, "wait_child: pid {} signaled with {:?}", pid, signal); + child_signaled(pid, signal)?; + } + WtStat::Stopped(pid, signal) => { + flog!(DEBUG, "wait_child: pid {} stopped with {:?}", pid, signal); + child_stopped(pid, signal)?; + } + WtStat::Continued(pid) => { + flog!(DEBUG, "wait_child: pid {} continued", pid); + child_continued(pid)?; + } + WtStat::StillAlive => { + flog!(DEBUG, "wait_child: no more children to reap"); + break; + } _ => unimplemented!(), } } - Ok(()) + Ok(()) } pub fn child_signaled(pid: Pid, sig: Signal) -> ShResult<()> {