use std::cell::RefCell;
use std::ffi::OsStr;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use alacritty_terminal::event::{Event as TermEvent, EventListener};
use alacritty_terminal::grid::Dimensions;
use alacritty_terminal::index::{Column, Line, Point};
use alacritty_terminal::term::{Config as TermConfig, Term};
use alacritty_terminal::vte::ansi::Processor;
use anyhow::{Context, Result};
use crossbeam_channel::Sender;
use portable_pty::{Child, CommandBuilder, MasterPty, PtySize};
use super::commands::Commands;
use super::guard::KeyboardStackGuard;
use super::*;
use crate::layout::PaneId;
use crate::server::Event;
#[derive(Clone, Default)]
pub struct Listener(pub(super) Rc<RefCell<Vec<TermEvent>>>);
impl EventListener for Listener {
fn send_event(&self, event: TermEvent) {
self.0.borrow_mut().push(event);
}
}
#[derive(Clone, Copy)]
pub(super) struct Size {
pub(super) cols: u16,
pub(super) rows: u16,
}
impl Dimensions for Size {
fn total_lines(&self) -> usize {
self.rows as usize
}
fn screen_lines(&self) -> usize {
self.rows as usize
}
fn columns(&self) -> usize {
self.cols as usize
}
}
pub(super) const MAX_PENDING_INPUT: usize = 16 << 20;
pub(super) struct Process {
pub(super) pid: Option<u32>,
child: Mutex<Option<PaneChild>>,
}
impl Process {
pub(super) fn signal_group(&self, signal: i32) {
let Some(pid) = self.pid else { return };
let child = self.child.lock().unwrap_or_else(|e| e.into_inner());
if child.is_none() {
return;
}
#[cfg(unix)]
unsafe {
let pid = pid as libc::pid_t;
libc::kill(pid, signal);
libc::kill(-pid, signal);
}
let _ = (child, signal);
}
pub(super) fn hang_up_session(&self) {
let Some(pid) = self.pid else { return };
let child = self.child.lock().unwrap_or_else(|e| e.into_inner());
if child.is_none() {
return;
}
#[cfg(unix)]
for member in crate::process::session_members(pid as libc::pid_t) {
unsafe {
libc::kill(member, libc::SIGHUP);
}
}
drop(child);
}
}
#[derive(Clone)]
pub struct ProcessHold(Arc<Process>);
impl ProcessHold {
pub(super) fn new(process: &Arc<Process>) -> ProcessHold {
ProcessHold(process.clone())
}
pub fn is(&self, other: &ProcessHold) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
pub fn group_running(&self) -> bool {
self.0.pid.is_some_and(|pid| crate::process::group_running(pid as libc::pid_t))
}
pub fn signal(&self, signal: i32) {
self.0.signal_group(signal);
}
}
impl Drop for Process {
fn drop(&mut self) {
let Some(mut child) = self.child.get_mut().unwrap_or_else(|e| e.into_inner()).take() else { return };
if !matches!(child.try_wait(), Ok(Some(_)))
&& let Some(reaper) = REAPER.get()
{
let _ = reaper.send(child);
}
}
}
type PaneChild = Box<dyn Child + Send + Sync>;
static REAPER: std::sync::OnceLock<crossbeam_channel::Sender<PaneChild>> = std::sync::OnceLock::new();
fn start_reaper() -> Result<()> {
if REAPER.get().is_some() {
return Ok(());
}
let (tx, rx) = crossbeam_channel::unbounded::<PaneChild>();
std::thread::Builder::new().name("pane-reaper".into()).spawn(move || {
let mut waiting: Vec<PaneChild> = Vec::new();
loop {
let next = if waiting.is_empty() {
rx.recv().map_err(|_| crossbeam_channel::RecvTimeoutError::Disconnected)
} else {
rx.recv_timeout(std::time::Duration::from_millis(200))
};
match next {
Ok(child) => waiting.push(child),
Err(crossbeam_channel::RecvTimeoutError::Disconnected) if waiting.is_empty() => return,
Err(_) => {}
}
waiting.retain_mut(|c| matches!(c.try_wait(), Ok(None)));
}
})?;
let _ = REAPER.set(tx);
Ok(())
}
pub struct TermPane {
pub term: Term<Listener>,
pub(super) events: Rc<RefCell<Vec<TermEvent>>>,
pub(super) parser: Processor,
pub(super) master: Box<dyn MasterPty + Send>,
pub(super) input: Sender<Vec<u8>>,
pub(super) input_pending: Arc<AtomicUsize>,
pub input_dropped: bool,
pub(super) output: crossbeam_channel::Receiver<Vec<u8>>,
pub(super) process: Arc<Process>,
reader_stop: Arc<AtomicBool>,
pub pid: Option<u32>,
pub(super) osc: OscScanner,
pub(super) guard: KeyboardStackGuard,
pub(super) commands: Commands,
pub graphics: crate::graphics::PaneGraphics,
}
pub(super) fn pty_size(cols: u16, rows: u16, graphics: &crate::graphics::Shared) -> PtySize {
let (cw, ch) = graphics.cell();
PtySize { rows, cols, pixel_width: cols.saturating_mul(cw), pixel_height: rows.saturating_mul(ch) }
}
impl Drop for TermPane {
fn drop(&mut self) {
self.reader_stop.store(true, Ordering::Relaxed);
}
}
impl TermPane {
pub(super) fn spawn(
id: PaneId,
spec: &PaneSpec,
cols: u16,
rows: u16,
env: &PaneEnv,
tx: &Sender<Event>,
) -> Result<TermPane> {
let strict = !spec.command.is_empty() || spec.startup_input.is_some();
let (cwd, own_dir) = spawn_dir(spec.cwd.as_deref(), strict)?;
start_reaper().context("starting the thread that reaps panes' programs")?;
let pty = portable_pty::native_pty_system();
let pair = pty.openpty(pty_size(cols, rows, &env.graphics)).context("opening a pty")?;
let program: Vec<&OsStr> = if spec.command.is_empty() {
vec![OsStr::new(&env.shell)]
} else {
spec.command.iter().map(OsStr::new).collect()
};
if own_dir {
let path = spec.env.iter().rev().find(|(k, _)| k == "PATH").map(|(_, v)| v.into());
let path = if spec.env_remove.iter().any(|k| k == "PATH") {
None
} else {
path.or_else(|| std::env::var_os("PATH"))
};
if !findable(program[0], &cwd, path.as_deref()) {
anyhow::bail!("starting {:?}: not found", program[0]);
}
}
let mut cmd = command_in(&program, &cwd, own_dir);
for (k, _) in std::env::vars_os() {
let k = k.to_string_lossy();
if k.starts_with("ZELLIJ") || k == "TMUX" || k == "TMUX_PANE" || k == "TERM_PROGRAM_VERSION" {
cmd.env_remove(k.as_ref());
}
}
cmd.env("TERM", "xterm-256color");
cmd.env("COLORTERM", "truecolor");
cmd.env("TERM_PROGRAM", "kraal");
cmd.env("KRAAL", "1");
cmd.env("KRAAL_SOCKET", &env.socket);
cmd.env("KRAAL_SESSION", &env.session);
cmd.env("KRAAL_PANE", id.to_string());
for k in &spec.env_remove {
cmd.env_remove(k);
}
for (k, v) in spec.env.iter().chain(&spec.secret_env.0) {
cmd.env(k, v);
}
let child: PaneChild = pair
.slave
.spawn_command(cmd)
.with_context(|| format!("starting {:?}", spec.command.first().unwrap_or(&env.shell)))?;
drop(pair.slave);
let pid = child.process_id();
let process = Arc::new(Process { pid, child: Mutex::new(Some(child)) });
let io = match start_io(id, &*pair.master, tx) {
Ok(io) => io,
Err(e) => {
process.signal_group(libc::SIGKILL);
return Err(e);
}
};
let wait_tx = tx.clone();
let waited = process.clone();
let eof_rx = io.eof;
let started = std::thread::Builder::new().name(format!("pane-{id}-wait")).spawn(move || {
let code = wait_exited(waited.pid);
let _ = eof_rx.recv_timeout(std::time::Duration::from_secs(2));
let _ = wait_tx.send(Event::PaneExited(id, waited.pid, code));
});
if let Err(e) = started {
io.stop.store(true, Ordering::Relaxed);
process.signal_group(libc::SIGKILL);
return Err(e.into());
}
let events = Rc::new(RefCell::new(Vec::new()));
let config = TermConfig { scrolling_history: env.scrollback, kitty_keyboard: true, ..TermConfig::default() };
let term = Term::new(config, &Size { cols, rows }, Listener(events.clone()));
let mut pane = TermPane {
term,
events,
parser: Processor::new(),
master: pair.master,
input: io.input,
input_pending: io.input_pending,
input_dropped: false,
output: io.output,
process,
reader_stop: io.stop,
pid,
osc: OscScanner::default(),
guard: KeyboardStackGuard::default(),
commands: Commands::new(env.scrollback),
graphics: crate::graphics::PaneGraphics::new(env.graphics.clone()),
};
if let Some(text) = &spec.startup_input {
pane.write(text.clone().into_bytes());
}
Ok(pane)
}
pub(super) fn hang_up(&self) {
self.process.signal_group(libc::SIGHUP);
self.process.hang_up_session();
#[cfg(unix)]
if let Some(fg) = self.master.process_group_leader()
&& Some(fg as u32) != self.pid
{
unsafe {
libc::kill(-fg, libc::SIGHUP);
}
}
}
pub(super) fn write(&mut self, bytes: Vec<u8>) {
if self.input_pending.load(Ordering::Relaxed) + bytes.len() > MAX_PENDING_INPUT {
self.input_dropped = true;
return;
}
self.input_pending.fetch_add(bytes.len(), Ordering::Relaxed);
let _ = self.input.send(bytes);
}
pub(super) fn viewport_point(&self, col: u16, row: u16) -> Point {
let offset = self.term.grid().display_offset() as i32;
let cols = self.term.columns();
let row = (row as usize).min(self.term.screen_lines().saturating_sub(1)) as i32;
Point::new(Line(row - offset), Column((col as usize).min(cols.saturating_sub(1))))
}
}
fn spawn_dir(cwd: Option<&Path>, strict: bool) -> Result<(PathBuf, bool)> {
match (cwd.and_then(|d| std::fs::canonicalize(d).ok()).filter(|d| d.is_dir()), cwd) {
(Some(dir), _) => Ok((dir, strict)),
(None, Some(dir)) if strict => anyhow::bail!("{} is not a directory it can run in", dir.display()),
_ => Ok((dirs::home_dir().unwrap_or_else(|| PathBuf::from("/")), false)),
}
}
fn command_in(program: &[&OsStr], dir: &Path, strict: bool) -> CommandBuilder {
let mut cmd = if strict {
let mut c = CommandBuilder::new("/bin/sh");
c.args(["-c", "cd -P -- \"$1\" || exit 126; shift; exec \"$@\"", "kraal"]);
c.arg(dir);
c.args(program);
c
} else {
let mut c = CommandBuilder::new(program[0]);
c.args(&program[1..]);
c
};
cmd.cwd(dir);
cmd
}
fn findable(program: &OsStr, dir: &Path, path: Option<&OsStr>) -> bool {
use std::os::unix::ffi::OsStrExt;
let executable = |p: &Path| {
let Ok(c) = std::ffi::CString::new(p.as_os_str().as_bytes()) else { return false };
!p.is_dir() && unsafe { libc::access(c.as_ptr(), libc::X_OK) } == 0
};
if program.as_bytes().contains(&b'/') {
return executable(&dir.join(program));
}
path.is_some_and(|path| std::env::split_paths(path).any(|d| executable(&dir.join(d).join(program))))
}
struct Io {
input: Sender<Vec<u8>>,
input_pending: Arc<AtomicUsize>,
output: crossbeam_channel::Receiver<Vec<u8>>,
eof: crossbeam_channel::Receiver<()>,
stop: Arc<AtomicBool>,
}
fn start_io(id: PaneId, master: &(dyn MasterPty + Send), tx: &Sender<Event>) -> Result<Io> {
let reader = PtyReader::new(master)?;
let mut writer = master.take_writer()?;
let stop = Arc::new(AtomicBool::new(false));
let ready_tx = tx.clone();
let (out_tx, out_rx) = crossbeam_channel::bounded::<Vec<u8>>(4);
let (eof_tx, eof_rx) = crossbeam_channel::bounded::<()>(0);
let stopped = stop.clone();
std::thread::Builder::new().name(format!("pane-{id}-read")).spawn(move || {
let _eof = eof_tx;
let mut reader = reader;
let mut buf = vec![0u8; 64 * 1024];
loop {
match reader.read(&mut buf, &stopped) {
Ok(0) | Err(_) => break,
Ok(n) => {
if out_tx.send(buf[..n].to_vec()).is_err() {
return;
}
let _ = ready_tx.try_send(Event::PaneReady(id));
}
}
}
})?;
let (in_tx, in_rx) = crossbeam_channel::unbounded::<Vec<u8>>();
let input_pending = Arc::new(AtomicUsize::new(0));
let pending = input_pending.clone();
let started = std::thread::Builder::new().name(format!("pane-{id}-write")).spawn(move || {
for data in in_rx {
if writer.write_all(&data).and_then(|_| writer.flush()).is_err() {
break;
}
pending.fetch_sub(data.len(), Ordering::Relaxed);
}
});
if let Err(e) = started {
stop.store(true, Ordering::Relaxed);
return Err(e.into());
}
Ok(Io { input: in_tx, input_pending, output: out_rx, eof: eof_rx, stop })
}
struct PtyReader {
file: std::fs::File,
}
impl PtyReader {
fn new(master: &(dyn MasterPty + Send)) -> Result<PtyReader> {
use std::os::fd::FromRawFd;
let fd = master.as_raw_fd().context("the pty has no descriptor")?;
let copy = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) };
if copy < 0 {
return Err(std::io::Error::last_os_error()).context("duplicating the pty");
}
Ok(PtyReader { file: unsafe { std::fs::File::from_raw_fd(copy) } })
}
fn read(&mut self, buf: &mut [u8], stop: &AtomicBool) -> std::io::Result<usize> {
use std::os::fd::AsRawFd;
loop {
if stop.load(Ordering::Relaxed) {
return Ok(0);
}
let mut fds = libc::pollfd { fd: self.file.as_raw_fd(), events: libc::POLLIN, revents: 0 };
match unsafe { libc::poll(&mut fds, 1, 200) } {
0 => continue,
n if n < 0 => {
let e = std::io::Error::last_os_error();
if e.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(e);
}
_ => return self.file.read(buf),
}
}
}
}
fn wait_exited(pid: Option<u32>) -> Option<i32> {
#[cfg(unix)]
if let Some(pid) = pid {
loop {
let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
let r = unsafe { libc::waitid(libc::P_PID, pid as libc::id_t, &mut info, libc::WEXITED | libc::WNOWAIT) };
if r == 0 {
return exit_code(&info);
}
if std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted {
return None;
}
}
}
None
}
#[cfg(unix)]
fn exit_code(info: &libc::siginfo_t) -> Option<i32> {
match info.si_code {
#[cfg(any(target_os = "linux", target_os = "macos"))]
libc::CLD_EXITED => Some(unsafe { info.si_status() }),
libc::CLD_KILLED | libc::CLD_DUMPED => Some(1),
_ => None,
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use super::*;
#[test]
fn a_reader_stops_while_the_program_is_silent() {
let pair = portable_pty::native_pty_system()
.openpty(PtySize { rows: 5, cols: 20, pixel_width: 0, pixel_height: 0 })
.unwrap();
let mut reader = PtyReader::new(&*pair.master).unwrap();
let stop = Arc::new(AtomicBool::new(false));
let stopped = stop.clone();
let (done_tx, done_rx) = crossbeam_channel::bounded(1);
std::thread::spawn(move || {
let mut buf = [0u8; 64];
let _ = done_tx.send(reader.read(&mut buf, &stopped).map_err(|e| e.kind()));
});
std::thread::sleep(Duration::from_millis(50));
stop.store(true, Ordering::Relaxed);
assert_eq!(done_rx.recv_timeout(Duration::from_secs(2)), Ok(Ok(0)));
}
#[test]
fn waiting_for_the_end_leaves_the_reaping_to_the_owner() {
let mut child = std::process::Command::new("sh").arg("-c").arg("exit 3").spawn().unwrap();
let start = Instant::now();
assert_eq!(wait_exited(Some(child.id())), Some(3));
assert!(start.elapsed() < Duration::from_secs(5));
assert_eq!(child.wait().unwrap().code(), Some(3));
let mut killed = std::process::Command::new("sleep").arg("10").spawn().unwrap();
killed.kill().unwrap();
assert_eq!(wait_exited(Some(killed.id())), Some(1));
killed.wait().unwrap();
}
#[test]
fn a_hold_keeps_an_ended_process_unreaped_after_its_pane_lets_it_go() {
let pair = portable_pty::native_pty_system()
.openpty(PtySize { rows: 5, cols: 20, pixel_width: 0, pixel_height: 0 })
.unwrap();
let mut cmd = CommandBuilder::new("sh");
cmd.args(["-c", "trap '' TERM HUP; sleep 30 & exit 0"]);
let child = pair.slave.spawn_command(cmd).unwrap();
let pid = child.process_id().unwrap();
let process = Arc::new(Process { pid: Some(pid), child: Mutex::new(Some(child)) });
let hold = ProcessHold::new(&process);
let taken = || unsafe { libc::kill(pid as libc::pid_t, 0) == 0 };
assert_eq!(wait_exited(Some(pid)), Some(0));
assert!(hold.group_running(), "what it left runs on");
drop(process);
assert!(taken());
hold.signal(libc::SIGTERM);
std::thread::sleep(Duration::from_millis(200));
assert!(hold.group_running(), "SIGTERM is ignored");
hold.signal(libc::SIGKILL);
let start = Instant::now();
while hold.group_running() && start.elapsed() < Duration::from_secs(5) {
std::thread::sleep(Duration::from_millis(10));
}
assert!(!hold.group_running());
assert!(taken(), "still not reaped while held");
drop(hold);
let start = Instant::now();
while taken() && start.elapsed() < Duration::from_secs(5) {
std::thread::sleep(Duration::from_millis(10));
}
assert!(!taken());
}
#[test]
fn an_ended_pane_process_keeps_its_pid_until_the_pane_lets_it_go() {
let pair = portable_pty::native_pty_system()
.openpty(PtySize { rows: 5, cols: 20, pixel_width: 0, pixel_height: 0 })
.unwrap();
let mut cmd = CommandBuilder::new("sh");
cmd.args(["-c", "exit 0"]);
let child = pair.slave.spawn_command(cmd).unwrap();
let pid = child.process_id().unwrap();
let process = Process { pid: Some(pid), child: Mutex::new(Some(child)) };
let taken = || unsafe { libc::kill(pid as libc::pid_t, 0) == 0 };
assert_eq!(wait_exited(Some(pid)), Some(0));
assert!(taken());
process.signal_group(libc::SIGHUP);
drop(process);
let start = Instant::now();
while taken() && start.elapsed() < Duration::from_secs(5) {
std::thread::sleep(Duration::from_millis(10));
}
assert!(!taken());
}
fn wait_for(child: &mut Box<dyn portable_pty::Child + Send + Sync>, what: &str) -> portable_pty::ExitStatus {
let start = Instant::now();
loop {
if let Some(status) = child.try_wait().unwrap() {
return status;
}
if start.elapsed() > Duration::from_secs(10) {
let pid = child.process_id().map(|p| p.to_string()).unwrap_or_default();
let ps = std::process::Command::new("ps")
.args(["-o", "pid,ppid,pgid,sess,stat,wchan,command", "-p", &pid])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
.unwrap_or_default();
let _ = child.kill();
panic!("{what}: still running after 10 s:\n{ps}");
}
std::thread::sleep(Duration::from_millis(20));
}
}
#[test]
fn a_command_whose_directory_went_runs_nowhere() {
let home = std::env::temp_dir().join(format!("kraal-gone-{}", std::process::id()));
std::fs::create_dir_all(&home).unwrap();
let pair = portable_pty::native_pty_system()
.openpty(PtySize { rows: 5, cols: 20, pixel_width: 0, pixel_height: 0 })
.unwrap();
let mut reader = pair.master.try_clone_reader().unwrap();
std::thread::spawn(move || {
let mut buf = [0u8; 1024];
while matches!(std::io::Read::read(&mut reader, &mut buf), Ok(n) if n > 0) {}
});
let program = [OsStr::new("sh"), OsStr::new("-c"), OsStr::new("touch ran-here")];
let mut cmd = command_in(&program, &home.join("gone"), true);
cmd.env("HOME", &home);
let mut child = pair.slave.spawn_command(cmd).unwrap();
assert_eq!(wait_for(&mut child, "in a gone directory").exit_code(), 126);
assert!(!home.join("ran-here").exists(), "the command ran in HOME");
let pair = portable_pty::native_pty_system()
.openpty(PtySize { rows: 5, cols: 20, pixel_width: 0, pixel_height: 0 })
.unwrap();
let mut reader = pair.master.try_clone_reader().unwrap();
std::thread::spawn(move || {
let mut buf = [0u8; 1024];
while matches!(std::io::Read::read(&mut reader, &mut buf), Ok(n) if n > 0) {}
});
let mut child = pair.slave.spawn_command(command_in(&program, &home, true)).unwrap();
assert!(wait_for(&mut child, "in its directory").success());
assert!(home.join("ran-here").exists());
let _ = std::fs::remove_dir_all(&home);
assert!(!findable(OsStr::new("no-such-program-kraal"), &home, std::env::var_os("PATH").as_deref()));
assert!(findable(OsStr::new("sh"), &home, std::env::var_os("PATH").as_deref()));
}
#[test]
fn a_directory_is_resolved_once_as_the_kernel_would() {
let base = std::env::temp_dir().join(format!("kraal-dirs-{}", std::process::id()));
let real = base.join("real/inner");
std::fs::create_dir_all(&real).unwrap();
let link = base.join("link");
let _ = std::fs::remove_file(&link);
std::os::unix::fs::symlink(&real, &link).unwrap();
let (dir, strict) = spawn_dir(Some(&link.join("..")), true).unwrap();
assert_eq!(dir, base.join("real").canonicalize().unwrap());
assert!(strict);
let (dir, _) = spawn_dir(Some(Path::new(".")), true).unwrap();
assert_eq!(dir, std::env::current_dir().unwrap().canonicalize().unwrap());
assert!(spawn_dir(Some(&base.join("gone")), true).is_err());
let (_, strict) = spawn_dir(Some(&base.join("gone")), false).unwrap();
assert!(!strict, "a plain shell starts at home");
let _ = std::fs::remove_dir_all(&base);
}
#[test]
#[cfg(target_os = "linux")]
fn closing_hangs_up_background_jobs_in_groups_of_their_own() {
let dir = std::env::temp_dir().join(format!("kraal-jobs-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let pidfile = dir.join("job");
let pair = portable_pty::native_pty_system()
.openpty(PtySize { rows: 5, cols: 20, pixel_width: 0, pixel_height: 0 })
.unwrap();
let mut cmd = CommandBuilder::new("sh");
cmd.args(["-c", &format!("set -m; sleep 30 & echo $! > {}; wait", pidfile.display())]);
let child = pair.slave.spawn_command(cmd).unwrap();
let pid = child.process_id().unwrap();
let process = Process { pid: Some(pid), child: Mutex::new(Some(child)) };
let start = Instant::now();
let job: libc::pid_t = loop {
if let Some(j) = std::fs::read_to_string(&pidfile).ok().and_then(|s| s.trim().parse().ok()) {
break j;
}
assert!(start.elapsed() < Duration::from_secs(5), "the job never started");
std::thread::sleep(Duration::from_millis(20));
};
assert_ne!(unsafe { libc::getpgid(job) }, pid as libc::pid_t);
process.signal_group(libc::SIGHUP);
std::thread::sleep(Duration::from_millis(300));
assert!(crate::process::running(job));
process.hang_up_session();
let start = Instant::now();
while crate::process::running(job) && start.elapsed() < Duration::from_secs(5) {
std::thread::sleep(Duration::from_millis(20));
}
assert!(!crate::process::running(job));
drop(process);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_program_let_go_while_it_runs_is_reaped_when_it_ends() {
start_reaper().unwrap();
let pair = portable_pty::native_pty_system()
.openpty(PtySize { rows: 5, cols: 20, pixel_width: 0, pixel_height: 0 })
.unwrap();
let mut cmd = CommandBuilder::new("sh");
cmd.args(["-c", "sleep 0.3"]);
let child = pair.slave.spawn_command(cmd).unwrap();
let pid = child.process_id().unwrap();
drop(Process { pid: Some(pid), child: Mutex::new(Some(child)) });
let taken = || unsafe { libc::kill(pid as libc::pid_t, 0) == 0 };
assert!(taken());
let start = Instant::now();
while taken() && start.elapsed() < Duration::from_secs(5) {
std::thread::sleep(Duration::from_millis(20));
}
assert!(!taken(), "ended, and not left a zombie");
}
}