#![cfg(all(windows, feature = "blocking"))]
pub mod helpers;
use std::io::{self, Read, Write};
use std::thread;
use std::time::Duration;
use conpty_oxide::blocking::Command;
use conpty_oxide::{ErrorKind, Size};
use helpers::sync::Session;
use helpers::{process_is_running, wait_for_descendant, wait_until, with_timeout};
const BUDGET: Duration = Duration::from_secs(30);
const APPEAR: Duration = Duration::from_secs(10);
const VANISH: Duration = Duration::from_secs(10);
const ROOT_EXE: &str = "cmd.exe";
const GRANDCHILD_EXE: &str = "ping.exe";
const NEVER_ENDING: [&str; 4] = ["/c", "ping", "-t", "127.0.0.1"];
fn assert_tree_terminated(root: u32, grandchild: u32) {
assert!(
wait_until(VANISH, || !process_is_running(grandchild, GRANDCHILD_EXE)),
"{GRANDCHILD_EXE} ({grandchild}) outlived the kill, so only the root \
process was terminated instead of the whole tree"
);
assert!(
wait_until(VANISH, || !process_is_running(root, ROOT_EXE)),
"the root child ({root}) outlived the kill"
);
}
fn kill_terminates_the_whole_tree_in() {
let mut session = Session::start(Command::new(ROOT_EXE).args(NEVER_ENDING));
let root = session.child.id();
let grandchild = wait_for_descendant(root, GRANDCHILD_EXE, APPEAR);
session.child.kill().expect("kill must succeed");
assert_tree_terminated(root, grandchild);
let status = session.child.wait().expect("waiting must succeed");
assert!(!status.success());
assert_eq!(status.code(), 1, "a killed tree reports exit code 1");
let (_output, again) = session.finish();
assert_eq!(again, status, "the exit status must remain cached");
}
#[test]
fn kill_terminates_the_whole_tree() {
with_timeout(BUDGET, || {
kill_terminates_the_whole_tree_in();
});
}
#[test]
fn dropping_a_managed_child_terminates_the_whole_tree() {
with_timeout(BUDGET, || {
let session = Session::start(Command::new(ROOT_EXE).args(NEVER_ENDING));
let Session {
child,
output,
writer,
controller,
} = session;
let root = child.id();
let grandchild = wait_for_descendant(root, GRANDCHILD_EXE, APPEAR);
drop(child);
assert_tree_terminated(root, grandchild);
output.join();
drop(writer);
drop(controller);
});
}
#[test]
fn dropping_a_managed_session_terminates_the_whole_tree() {
with_timeout(BUDGET, || {
let session = Command::new(ROOT_EXE)
.args(NEVER_ENDING)
.spawn()
.expect("managed spawning must succeed");
let root = session.id();
let grandchild = wait_for_descendant(root, GRANDCHILD_EXE, APPEAR);
drop(session);
assert_tree_terminated(root, grandchild);
});
}
#[test]
fn dropping_the_child_from_managed_parts_terminates_the_whole_tree() {
with_timeout(BUDGET, || {
let parts = Command::new(ROOT_EXE)
.args(NEVER_ENDING)
.spawn()
.expect("managed spawning must succeed")
.into_parts();
let root = parts.child.id();
let grandchild = wait_for_descendant(root, GRANDCHILD_EXE, APPEAR);
drop(parts.child);
assert_tree_terminated(root, grandchild);
drop(parts.output);
drop(parts.input);
drop(parts.controller);
});
}
#[test]
fn collecting_output_is_bounded_by_the_root_process() {
with_timeout(BUDGET, || {
const MARKER: &str = "root-bounded-blocking-tail";
let mut session = Command::new(ROOT_EXE)
.args(["/d", "/q"])
.spawn()
.expect("managed spawning must succeed");
let root = session.id();
session
.write_all(
format!(
"start \"\" /b ping -t 127.0.0.1 >nul & echo {MARKER} & \
ping -n 3 127.0.0.1 >nul & exit /b 23\r\n"
)
.as_bytes(),
)
.expect("the root command must reach the session");
let grandchild = wait_for_descendant(root, GRANDCHILD_EXE, APPEAR);
let output = session
.collect_output()
.expect("root-bounded collection must complete");
assert_eq!(output.status().code(), 23);
assert!(
String::from_utf8_lossy(output.as_bytes()).contains(MARKER),
"the root's teardown tail must be preserved"
);
assert_tree_terminated(root, grandchild);
});
}
#[test]
fn root_exit_terminates_descendants_after_into_parts() {
with_timeout(BUDGET, || {
const MARKER: &str = "root-watcher-blocking-tail";
let mut session = Command::new(ROOT_EXE)
.args(["/d", "/q"])
.spawn()
.expect("managed spawning must succeed");
let root = session.id();
session
.write_all(
format!(
"start \"\" /b ping -t 127.0.0.1 >nul & echo {MARKER} & ping -n 3 127.0.0.1 >nul & exit /b 23\r\n"
)
.as_bytes(),
)
.expect("the root command must reach the session");
let grandchild = wait_for_descendant(root, GRANDCHILD_EXE, APPEAR);
let parts = session.into_parts();
let mut child = parts.child;
let mut output = parts.output;
let input = parts.input;
let controller = parts.controller;
let reader = thread::spawn(move || {
let mut bytes = Vec::new();
output
.read_to_end(&mut bytes)
.expect("output must reach EOF after root exit");
bytes
});
let status = child.wait().expect("root wait must succeed");
assert_eq!(status.code(), 23);
assert_tree_terminated(root, grandchild);
let bytes = reader
.join()
.expect("the output reader thread must not panic");
assert!(
String::from_utf8_lossy(&bytes).contains(MARKER),
"the root's final output must survive watcher teardown"
);
let resize = controller
.resize(Size::default())
.expect_err("resize after root teardown must fail");
assert_eq!(resize.kind(), ErrorKind::Resize);
assert_eq!(
resize
.io_error()
.expect("resize retains its I/O source")
.kind(),
io::ErrorKind::NotConnected
);
let clear = controller
.clear()
.expect_err("clear after root teardown must fail");
if controller.supports_clear() {
assert_eq!(clear.kind(), ErrorKind::Clear);
assert_eq!(
clear
.io_error()
.expect("clear retains its I/O source")
.kind(),
io::ErrorKind::NotConnected
);
} else {
assert_eq!(clear.kind(), ErrorKind::UnsupportedFeature);
}
drop(input);
drop(controller);
});
}