Test Ctrl-C, job control and hangup with an interactive shell on a terminal
This commit is contained in:
+171
-2
@@ -1,7 +1,13 @@
|
|||||||
use std::fs;
|
use std::fs::{self, File};
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::process::Command;
|
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@@ -107,3 +113,166 @@ impl std::ops::Deref for ConfigFile {
|
|||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn wait_for_file(path: &Path) {
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(10);
|
||||||
|
while !path.exists() {
|
||||||
|
assert!(Instant::now() < deadline, "timed out waiting for {path:?}");
|
||||||
|
thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Terminal {
|
||||||
|
master: Option<File>,
|
||||||
|
output: String,
|
||||||
|
session_leader: Child,
|
||||||
|
_guard: RwLockReadGuard<'static, ()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminal {
|
||||||
|
pub fn start(command_line: &[&str]) -> Self {
|
||||||
|
let guard = HostGlobsLock::for_scan();
|
||||||
|
let (master, slave) = open_pty();
|
||||||
|
|
||||||
|
let mut command = Command::new(command_line[0]);
|
||||||
|
command
|
||||||
|
.args(&command_line[1..])
|
||||||
|
.env("TERM", "dumb")
|
||||||
|
.stdin(Stdio::from(slave.try_clone().unwrap()))
|
||||||
|
.stdout(Stdio::from(slave.try_clone().unwrap()))
|
||||||
|
.stderr(Stdio::from(slave));
|
||||||
|
unsafe { command.pre_exec(take_terminal) };
|
||||||
|
let session_leader = command.spawn().expect("failed to start session leader");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
master: Some(master),
|
||||||
|
output: String::new(),
|
||||||
|
session_leader,
|
||||||
|
_guard: guard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_text(&mut self, text: &str) {
|
||||||
|
self.master().write_all(text.as_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_line(&mut self, line: &str) {
|
||||||
|
self.type_text(&format!("{line}\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_output(&mut self) -> &str {
|
||||||
|
self.read_available_output();
|
||||||
|
&self.output
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_for_output(&mut self, text: &str) {
|
||||||
|
self.wait_until(&format!("output {text:?}"), |terminal| {
|
||||||
|
terminal.output.contains(text)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_until_shell_owns_terminal(&mut self) {
|
||||||
|
self.wait_until("shell in foreground", Self::is_shell_in_foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_until_job_owns_terminal(&mut self) {
|
||||||
|
self.wait_until("job in foreground", |terminal| {
|
||||||
|
!terminal.is_shell_in_foreground()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_until_job_stopped(&mut self) {
|
||||||
|
self.wait_until("job stopped", Self::has_stopped_job);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hang_up(&mut self) {
|
||||||
|
self.master = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn master(&mut self) -> &mut File {
|
||||||
|
self.master.as_mut().expect("terminal is hung up")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_available_output(&mut self) {
|
||||||
|
let Some(master) = &mut self.master else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut buffer = [0; 4096];
|
||||||
|
while let Ok(count @ 1..) = master.read(&mut buffer) {
|
||||||
|
self.output
|
||||||
|
.push_str(&String::from_utf8_lossy(&buffer[..count]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_until(&mut self, description: &str, condition: impl Fn(&Self) -> bool) {
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(10);
|
||||||
|
loop {
|
||||||
|
self.read_available_output();
|
||||||
|
if condition(self) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
Instant::now() < deadline,
|
||||||
|
"timed out waiting for {description}, terminal output:\n{}",
|
||||||
|
self.output
|
||||||
|
);
|
||||||
|
thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_shell_in_foreground(&self) -> bool {
|
||||||
|
let master = self.master.as_ref().expect("terminal is hung up");
|
||||||
|
let foreground = unsafe { libc::tcgetpgrp(master.as_raw_fd()) };
|
||||||
|
foreground == self.session_leader.id() as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_stopped_job(&self) -> bool {
|
||||||
|
let session = self.session_leader.id().to_string();
|
||||||
|
fs::read_dir("/proc")
|
||||||
|
.unwrap()
|
||||||
|
.filter_map(|entry| fs::read_to_string(entry.ok()?.path().join("stat")).ok())
|
||||||
|
.any(|stat| {
|
||||||
|
let fields_after_comm = &stat[stat.rfind(')').unwrap() + 1..];
|
||||||
|
let fields: Vec<&str> = fields_after_comm.split_whitespace().collect();
|
||||||
|
fields[0] == "T" && fields[3] == session
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_pty() -> (File, OwnedFd) {
|
||||||
|
let flags = libc::O_RDWR | libc::O_NOCTTY | libc::O_CLOEXEC;
|
||||||
|
let master = unsafe { libc::posix_openpt(flags | libc::O_NONBLOCK) };
|
||||||
|
assert!(
|
||||||
|
master >= 0,
|
||||||
|
"posix_openpt failed: {}",
|
||||||
|
io::Error::last_os_error()
|
||||||
|
);
|
||||||
|
let master = unsafe { File::from_raw_fd(master) };
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { libc::unlockpt(master.as_raw_fd()) },
|
||||||
|
0,
|
||||||
|
"unlockpt failed"
|
||||||
|
);
|
||||||
|
let slave = unsafe { libc::ioctl(master.as_raw_fd(), libc::TIOCGPTPEER, flags) };
|
||||||
|
assert!(
|
||||||
|
slave >= 0,
|
||||||
|
"TIOCGPTPEER failed: {}",
|
||||||
|
io::Error::last_os_error()
|
||||||
|
);
|
||||||
|
(master, unsafe { OwnedFd::from_raw_fd(slave) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_terminal() -> io::Result<()> {
|
||||||
|
if unsafe { libc::setsid() } == -1 || unsafe { libc::ioctl(0, libc::TIOCSCTTY, 0) } == -1 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Terminal {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.session_leader.kill();
|
||||||
|
let _ = self.session_leader.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use std::os::unix::process::{CommandExt, ExitStatusExt};
|
use std::os::unix::process::{CommandExt, ExitStatusExt};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::{Child, Stdio};
|
use std::process::{Child, Stdio};
|
||||||
use std::thread;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@@ -125,14 +123,6 @@ fn spawn_sandbox(args: &[&str], dir: &Path, script: &str) -> (Sandbox, Child) {
|
|||||||
(sandbox, child)
|
(sandbox, child)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_file(path: &Path) {
|
|
||||||
let deadline = Instant::now() + Duration::from_secs(10);
|
|
||||||
while !path.exists() {
|
|
||||||
assert!(Instant::now() < deadline, "timed out waiting for {path:?}");
|
|
||||||
thread::sleep(Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_signal(child: &Child, signal: i32) {
|
fn send_signal(child: &Child, signal: i32) {
|
||||||
unsafe { libc::kill(child.id() as i32, signal) };
|
unsafe { libc::kill(child.id() as i32, signal) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ mod namespaces;
|
|||||||
mod sandbox_info;
|
mod sandbox_info;
|
||||||
mod seccomp;
|
mod seccomp;
|
||||||
mod signals;
|
mod signals;
|
||||||
|
mod terminal;
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ fn exit_code_of_entrypoint_is_returned() {
|
|||||||
assert_eq!(status.code(), Some(42));
|
assert_eq!(status.code(), Some(42));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_exit_130_remains_an_exit_code() {
|
||||||
|
let status = Sandbox::new(&[])
|
||||||
|
.args(["--", "sh", "-c", "exit 130"])
|
||||||
|
.status()
|
||||||
|
.expect("agent-sandbox binary failed to execute");
|
||||||
|
|
||||||
|
assert_eq!(status.code(), Some(130));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn signal_death_of_entrypoint_becomes_exit_code() {
|
fn signal_death_of_entrypoint_becomes_exit_code() {
|
||||||
let status = Sandbox::new(&[])
|
let status = Sandbox::new(&[])
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
const BASH: &[&str] = &["bash", "--norc", "--noprofile", "-i"];
|
||||||
|
const ZSH: &[&str] = &["zsh", "-f", "-i"];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_c_lets_entrypoint_clean_up_in_bash() {
|
||||||
|
assert_ctrl_c_lets_entrypoint_clean_up(BASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_c_lets_entrypoint_clean_up_in_zsh() {
|
||||||
|
assert_ctrl_c_lets_entrypoint_clean_up(ZSH);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_ctrl_c_lets_entrypoint_clean_up(shell: &[&str]) {
|
||||||
|
let dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let script = "trap 'sleep 0.5; touch cleaned; exit 0' INT
|
||||||
|
touch ready
|
||||||
|
sleep 60 & wait";
|
||||||
|
|
||||||
|
let mut terminal = Terminal::start(shell);
|
||||||
|
terminal.type_line(&build_sandbox_command(dir.path(), script));
|
||||||
|
wait_for_file(&dir.path().join("ready"));
|
||||||
|
terminal.wait_until_job_owns_terminal();
|
||||||
|
terminal.type_text(CTRL_C);
|
||||||
|
terminal.type_line("echo STATUS-$?");
|
||||||
|
terminal.wait_for_output("STATUS-0");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
dir.path().join("cleaned").exists(),
|
||||||
|
"cleanup did not finish"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CTRL_C: &str = "\x03";
|
||||||
|
const CTRL_Z: &str = "\x1a";
|
||||||
|
|
||||||
|
fn build_sandbox_command(dir: &Path, script: &str) -> String {
|
||||||
|
let args = build_sandbox_args(dir, script);
|
||||||
|
shlex::try_join(args.iter().map(String::as_str)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_sandbox_args(dir: &Path, script: &str) -> Vec<String> {
|
||||||
|
let dir = dir.to_str().unwrap();
|
||||||
|
[
|
||||||
|
env!("CARGO_BIN_EXE_agent-sandbox"),
|
||||||
|
"--no-config",
|
||||||
|
"--blacklist",
|
||||||
|
"--chdir",
|
||||||
|
dir,
|
||||||
|
"--",
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
script,
|
||||||
|
]
|
||||||
|
.map(String::from)
|
||||||
|
.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_z_and_fg_resume_entrypoint_in_bash() {
|
||||||
|
assert_ctrl_z_and_fg_resume_entrypoint(BASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_z_and_fg_resume_entrypoint_in_zsh() {
|
||||||
|
assert_ctrl_z_and_fg_resume_entrypoint(ZSH);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_ctrl_z_and_fg_resume_entrypoint(shell: &[&str]) {
|
||||||
|
let dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let script = "touch ready
|
||||||
|
read line
|
||||||
|
echo \"got-$line\"";
|
||||||
|
|
||||||
|
let mut terminal = Terminal::start(shell);
|
||||||
|
terminal.type_line(&build_sandbox_command(dir.path(), script));
|
||||||
|
wait_for_file(&dir.path().join("ready"));
|
||||||
|
terminal.wait_until_job_owns_terminal();
|
||||||
|
terminal.type_text(CTRL_Z);
|
||||||
|
terminal.wait_until_shell_owns_terminal();
|
||||||
|
terminal.type_line("fg");
|
||||||
|
terminal.wait_until_job_owns_terminal();
|
||||||
|
terminal.type_line("hello");
|
||||||
|
terminal.wait_for_output("got-hello");
|
||||||
|
terminal.type_line("echo STATUS-$?");
|
||||||
|
|
||||||
|
terminal.wait_for_output("STATUS-0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_c_ends_surrounding_bash_script() {
|
||||||
|
let dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let script = ": > ready
|
||||||
|
sleep 60";
|
||||||
|
let surrounding_script = format!(
|
||||||
|
"{}; echo AFTER-$((1 + 1))",
|
||||||
|
build_sandbox_command(dir.path(), script)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut terminal = Terminal::start(BASH);
|
||||||
|
terminal.type_line(&format!(
|
||||||
|
"bash -c {}",
|
||||||
|
shlex::try_quote(&surrounding_script).unwrap()
|
||||||
|
));
|
||||||
|
wait_for_file(&dir.path().join("ready"));
|
||||||
|
terminal.wait_until_job_owns_terminal();
|
||||||
|
terminal.type_text(CTRL_C);
|
||||||
|
terminal.type_line("echo STATUS-$?");
|
||||||
|
terminal.wait_for_output("STATUS-130");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!terminal.read_output().contains("AFTER-2"),
|
||||||
|
"surrounding script continued after Ctrl-C"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_z_and_bg_continue_entrypoint_in_bash() {
|
||||||
|
assert_ctrl_z_and_bg_continue_entrypoint(BASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_z_and_bg_continue_entrypoint_in_zsh() {
|
||||||
|
assert_ctrl_z_and_bg_continue_entrypoint(ZSH);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_ctrl_z_and_bg_continue_entrypoint(shell: &[&str]) {
|
||||||
|
let dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let script = "touch ready
|
||||||
|
while [ ! -e go ]; do sleep 0.01; done
|
||||||
|
touch done";
|
||||||
|
|
||||||
|
let mut terminal = Terminal::start(shell);
|
||||||
|
terminal.type_line(&build_sandbox_command(dir.path(), script));
|
||||||
|
wait_for_file(&dir.path().join("ready"));
|
||||||
|
terminal.wait_until_job_owns_terminal();
|
||||||
|
terminal.type_text(CTRL_Z);
|
||||||
|
terminal.wait_until_shell_owns_terminal();
|
||||||
|
fs::write(dir.path().join("go"), "").unwrap();
|
||||||
|
thread::sleep(Duration::from_millis(200));
|
||||||
|
assert!(!dir.path().join("done").exists(), "entrypoint did not stop");
|
||||||
|
terminal.type_line("bg");
|
||||||
|
|
||||||
|
wait_for_file(&dir.path().join("done"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn background_job_reads_terminal_after_fg_in_bash() {
|
||||||
|
assert_background_job_reads_terminal_after_fg(BASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn background_job_reads_terminal_after_fg_in_zsh() {
|
||||||
|
assert_background_job_reads_terminal_after_fg(ZSH);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_background_job_reads_terminal_after_fg(shell: &[&str]) {
|
||||||
|
let dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let script = "touch ready
|
||||||
|
read line
|
||||||
|
echo \"got-$line\"";
|
||||||
|
|
||||||
|
let mut terminal = Terminal::start(shell);
|
||||||
|
terminal.type_line(&format!("{} &", build_sandbox_command(dir.path(), script)));
|
||||||
|
wait_for_file(&dir.path().join("ready"));
|
||||||
|
terminal.wait_until_job_stopped();
|
||||||
|
terminal.type_line("fg");
|
||||||
|
terminal.wait_until_job_owns_terminal();
|
||||||
|
terminal.type_line("hello");
|
||||||
|
terminal.wait_for_output("got-hello");
|
||||||
|
terminal.type_line("echo STATUS-$?");
|
||||||
|
|
||||||
|
terminal.wait_for_output("STATUS-0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_c_reaches_both_sandboxes_in_a_pipeline() {
|
||||||
|
let first = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let second = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let script = "trap 'touch cleaned; exit 0' INT
|
||||||
|
touch ready
|
||||||
|
sleep 60 & wait";
|
||||||
|
|
||||||
|
let mut terminal = Terminal::start(BASH);
|
||||||
|
terminal.type_line(&format!(
|
||||||
|
"{} | {}",
|
||||||
|
build_sandbox_command(first.path(), script),
|
||||||
|
build_sandbox_command(second.path(), script)
|
||||||
|
));
|
||||||
|
wait_for_file(&first.path().join("ready"));
|
||||||
|
wait_for_file(&second.path().join("ready"));
|
||||||
|
terminal.wait_until_job_owns_terminal();
|
||||||
|
terminal.type_text(CTRL_C);
|
||||||
|
terminal.type_line("echo STATUS-$?");
|
||||||
|
terminal.wait_for_output("STATUS-0");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
first.path().join("cleaned").exists(),
|
||||||
|
"first sandbox did not clean up"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
second.path().join("cleaned").exists(),
|
||||||
|
"second sandbox did not clean up"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hangup_of_session_leader_reaches_entrypoint() {
|
||||||
|
let dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let args = build_sandbox_args(dir.path(), HANGUP_SCRIPT);
|
||||||
|
|
||||||
|
let mut terminal = Terminal::start(&args.iter().map(String::as_str).collect::<Vec<_>>());
|
||||||
|
wait_for_file(&dir.path().join("ready"));
|
||||||
|
terminal.hang_up();
|
||||||
|
|
||||||
|
wait_for_file(&dir.path().join("hungup"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const HANGUP_SCRIPT: &str = "trap 'touch hungup; exit 0' HUP
|
||||||
|
touch ready
|
||||||
|
sleep 60 & wait";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_hangup_reaches_entrypoint_in_bash() {
|
||||||
|
assert_shell_hangup_reaches_entrypoint(BASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_hangup_reaches_entrypoint_in_zsh() {
|
||||||
|
assert_shell_hangup_reaches_entrypoint(ZSH);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_shell_hangup_reaches_entrypoint(shell: &[&str]) {
|
||||||
|
let dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
|
||||||
|
let mut terminal = Terminal::start(shell);
|
||||||
|
terminal.type_line(&build_sandbox_command(dir.path(), HANGUP_SCRIPT));
|
||||||
|
wait_for_file(&dir.path().join("ready"));
|
||||||
|
terminal.hang_up();
|
||||||
|
|
||||||
|
wait_for_file(&dir.path().join("hungup"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nohup_keeps_entrypoint_alive_after_hangup() {
|
||||||
|
let dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let script = "touch ready
|
||||||
|
while [ ! -e go ]; do sleep 0.01; done
|
||||||
|
touch survived";
|
||||||
|
|
||||||
|
let mut terminal = Terminal::start(BASH);
|
||||||
|
terminal.type_line(&format!(
|
||||||
|
"nohup {} >/dev/null 2>&1",
|
||||||
|
build_sandbox_command(dir.path(), script)
|
||||||
|
));
|
||||||
|
wait_for_file(&dir.path().join("ready"));
|
||||||
|
terminal.hang_up();
|
||||||
|
thread::sleep(Duration::from_millis(200));
|
||||||
|
fs::write(dir.path().join("go"), "").unwrap();
|
||||||
|
|
||||||
|
wait_for_file(&dir.path().join("survived"));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user