use embers_core::{BufferId, new_request_id};
use embers_protocol::{
BufferRecord, BufferRecordState, BufferRequest, ClientMessage, ServerResponse,
};
use embers_test_support::{TestConnection, TestServer};
use tokio::time::{Duration, Instant, sleep};
use crate::support::{run_cli, session_snapshot_by_name, stdout};
const LONG_RUNNING: &str = "sleep 60";
fn process_alive(pid: u32) -> bool {
let output = std::process::Command::new("ps")
.args(["-o", "stat=", "-p", &pid.to_string()])
.output()
.expect("run ps");
let stat = String::from_utf8_lossy(&output.stdout);
let stat = stat.trim();
!stat.is_empty() && !stat.starts_with('Z')
}
async fn get_buffer(connection: &mut TestConnection, buffer_id: BufferId) -> Option<BufferRecord> {
match connection
.request(&ClientMessage::Buffer(BufferRequest::Get {
request_id: new_request_id(),
buffer_id,
}))
.await
.expect("get buffer round-trips")
{
ServerResponse::Buffer(response) => Some(response.buffer),
ServerResponse::Error(_) => None,
other => panic!("expected buffer response, got {other:?}"),
}
}
async fn session_processes(connection: &mut TestConnection, name: &str) -> Vec<(BufferId, u32)> {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let snapshot = session_snapshot_by_name(connection, name).await;
let processes = snapshot
.buffers
.iter()
.filter_map(|buffer| buffer.pid.map(|pid| (buffer.id, pid)))
.collect::<Vec<_>>();
if processes.len() == snapshot.buffers.len() {
return processes;
}
assert!(Instant::now() < deadline, "buffers did not report pids");
sleep(Duration::from_millis(25)).await;
}
}
async fn assert_killed(connection: &mut TestConnection, processes: &[(BufferId, u32)]) {
let deadline = Instant::now() + Duration::from_secs(10);
for &(buffer_id, pid) in processes {
while process_alive(pid) || get_buffer(connection, buffer_id).await.is_some() {
assert!(
Instant::now() < deadline,
"buffer {buffer_id} (pid {pid}) survived the kill"
);
sleep(Duration::from_millis(25)).await;
}
}
}
fn new_window(server: &TestServer, title: &str) {
run_cli(
server,
[
"new-window",
"-t",
"alpha",
"--title",
title,
"--",
"/bin/sh",
"-c",
LONG_RUNNING,
],
);
}
fn split(server: &TestServer) -> String {
let split = run_cli(
server,
["split-window", "--", "/bin/sh", "-c", LONG_RUNNING],
);
stdout(&split).trim().to_owned()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn kill_pane_ends_the_pane_program() {
let server = TestServer::start().await.expect("start server");
let mut connection = TestConnection::connect(server.socket_path())
.await
.expect("connect protocol client");
run_cli(&server, ["new-session", "alpha"]);
new_window(&server, "work");
let before = session_processes(&mut connection, "alpha").await;
let pane = split(&server);
let killed = session_processes(&mut connection, "alpha")
.await
.into_iter()
.filter(|process| !before.contains(process))
.collect::<Vec<_>>();
assert_eq!(killed.len(), 1);
run_cli(&server, ["kill-pane", "-t", &pane]);
assert_killed(&mut connection, &killed).await;
let (_, survivor) = before[0];
assert!(process_alive(survivor), "the other pane keeps running");
server.shutdown().await.expect("shutdown server");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn kill_pane_detach_keeps_the_program_running_detached() {
let server = TestServer::start().await.expect("start server");
let mut connection = TestConnection::connect(server.socket_path())
.await
.expect("connect protocol client");
run_cli(&server, ["new-session", "alpha"]);
new_window(&server, "work");
let before = session_processes(&mut connection, "alpha").await;
let pane = split(&server);
let (buffer_id, pid) = session_processes(&mut connection, "alpha")
.await
.into_iter()
.find(|process| !before.contains(process))
.expect("split pane has a process");
run_cli(&server, ["kill-pane", "--detach", "-t", &pane]);
sleep(Duration::from_millis(200)).await;
let record = get_buffer(&mut connection, buffer_id)
.await
.expect("detached buffer is kept");
assert_eq!(record.attachment_node_id, None);
assert_eq!(record.state, BufferRecordState::Running);
assert!(process_alive(pid));
server.shutdown().await.expect("shutdown server");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn kill_window_ends_every_program_in_the_window() {
let server = TestServer::start().await.expect("start server");
let mut connection = TestConnection::connect(server.socket_path())
.await
.expect("connect protocol client");
run_cli(&server, ["new-session", "alpha"]);
new_window(&server, "keep");
let kept = session_processes(&mut connection, "alpha").await;
new_window(&server, "work");
split(&server);
let killed = session_processes(&mut connection, "alpha")
.await
.into_iter()
.filter(|process| !kept.contains(process))
.collect::<Vec<_>>();
assert_eq!(killed.len(), 2);
run_cli(&server, ["kill-window", "-t", "alpha:work"]);
assert_killed(&mut connection, &killed).await;
assert!(process_alive(kept[0].1), "the other window keeps running");
server.shutdown().await.expect("shutdown server");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn kill_session_ends_every_program_in_the_session() {
let server = TestServer::start().await.expect("start server");
let mut connection = TestConnection::connect(server.socket_path())
.await
.expect("connect protocol client");
run_cli(&server, ["new-session", "alpha"]);
new_window(&server, "one");
split(&server);
new_window(&server, "two");
run_cli(
&server,
[
"display-popup",
"--no-wait",
"-t",
"alpha",
"--",
"/bin/sh",
"-c",
LONG_RUNNING,
],
);
let killed = session_processes(&mut connection, "alpha").await;
assert_eq!(killed.len(), 4);
run_cli(&server, ["kill-session", "-t", "alpha"]);
assert_killed(&mut connection, &killed).await;
server.shutdown().await.expect("shutdown server");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn kill_popup_ends_the_popup_program() {
let server = TestServer::start().await.expect("start server");
let mut connection = TestConnection::connect(server.socket_path())
.await
.expect("connect protocol client");
run_cli(&server, ["new-session", "alpha"]);
new_window(&server, "work");
let before = session_processes(&mut connection, "alpha").await;
let created = run_cli(
&server,
[
"display-popup",
"--no-wait",
"-t",
"alpha",
"--",
"/bin/sh",
"-c",
LONG_RUNNING,
],
);
let popup_id = stdout(&created).trim().to_owned();
let killed = session_processes(&mut connection, "alpha")
.await
.into_iter()
.filter(|process| !before.contains(process))
.collect::<Vec<_>>();
assert_eq!(killed.len(), 1);
run_cli(&server, ["kill-popup", "-t", &popup_id]);
assert_killed(&mut connection, &killed).await;
assert!(process_alive(before[0].1));
server.shutdown().await.expect("shutdown server");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn kill_server_stops_the_server_and_every_program() {
let server = TestServer::start().await.expect("start server");
let mut connection = TestConnection::connect(server.socket_path())
.await
.expect("connect protocol client");
run_cli(&server, ["new-session", "alpha"]);
new_window(&server, "work");
split(&server);
let killed = session_processes(&mut connection, "alpha").await;
assert_eq!(killed.len(), 2);
drop(connection);
run_cli(&server, ["kill-server"]);
assert!(
TestConnection::connect(server.socket_path()).await.is_err(),
"server still accepts connections after kill-server"
);
let deadline = Instant::now() + Duration::from_secs(10);
for &(_, pid) in &killed {
while process_alive(pid) {
assert!(Instant::now() < deadline, "pid {pid} survived kill-server");
sleep(Duration::from_millis(25)).await;
}
}
server.shutdown().await.expect("shutdown server");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn kill_server_ends_a_detached_server_process() {
let tempdir = tempfile::tempdir().expect("tempdir");
let socket = tempdir.path().join("mux.sock");
let cli = |args: &[&str]| {
let output = embers_test_support::cargo_bin("embers")
.arg("--socket")
.arg(&socket)
.args(args)
.output()
.expect("cli runs");
assert!(
output.status.success(),
"embers {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
};
cli(&["new-session", "alpha"]);
let server_pid = std::fs::read_to_string(socket.with_extension("pid"))
.expect("server writes its pid file")
.trim()
.parse::<u32>()
.expect("pid file holds a pid");
cli(&["kill-server"]);
let deadline = Instant::now() + Duration::from_secs(10);
while process_alive(server_pid) {
if Instant::now() >= deadline {
let _ = std::process::Command::new("kill")
.arg(server_pid.to_string())
.status();
panic!("server process {server_pid} survived kill-server");
}
sleep(Duration::from_millis(25)).await;
}
}
#[test]
fn kill_server_without_a_server_fails_without_starting_one() {
let tempdir = tempfile::tempdir().expect("tempdir");
let socket = tempdir.path().join("mux.sock");
let output = embers_test_support::cargo_bin("embers")
.arg("--socket")
.arg(&socket)
.arg("kill-server")
.output()
.expect("cli runs");
assert!(!output.status.success());
assert!(
String::from_utf8_lossy(&output.stderr).contains("no embers server"),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(!socket.exists(), "kill-server must not start a server");
}