use std::io::Read;
use std::process::{Child, Command, Stdio};
use std::time::Duration;
struct WorkerGuard(Option<Child>);
impl Drop for WorkerGuard {
fn drop(&mut self) {
if let Some(mut c) = self.0.take() {
let _ = c.kill();
let _ = c.wait();
}
}
}
fn wait_for_worker_ready(pid: i32, timeout: Duration) -> Result<(), String> {
let marker = ktstr::worker_ready::worker_ready_marker_path(pid as u32);
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if std::path::Path::new(&marker).exists() {
let _ = std::fs::remove_file(&marker);
return Ok(());
}
std::thread::sleep(Duration::from_millis(20));
}
Err(format!(
"worker pid {pid} never wrote ready marker {marker}"
))
}
fn yama_blocks_cross_descendant_attach() -> bool {
let Ok(s) = std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope") else {
return false;
};
let scope: i32 = match s.trim().parse() {
Ok(v) => v,
Err(_) => return false,
};
if scope <= 0 {
return false;
}
let euid = unsafe { libc::geteuid() };
euid != 0
}
fn skip_with_reason(reason: &str) {
eprintln!("================================================================");
eprintln!("SKIP: probe_sigint_mid_multi_snapshot_produces_partial_output");
eprintln!("SKIP reason: {reason}");
eprintln!("================================================================");
}
#[test]
#[ignore] fn probe_sigint_mid_multi_snapshot_produces_partial_output() {
let worker_bin = env!("CARGO_BIN_EXE_ktstr-jemalloc-alloc-worker");
let probe_bin = env!("CARGO_BIN_EXE_ktstr-jemalloc-probe");
if yama_blocks_cross_descendant_attach() {
skip_with_reason(
"kernel.yama.ptrace_scope > 0 and caller lacks CAP_SYS_PTRACE — \
probe cannot attach to a sibling worker under this YAMA policy",
);
return;
}
let worker = Command::new(worker_bin)
.arg(format!("{}", 16 * 1024 * 1024))
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn alloc-worker");
let worker_pid = worker.id() as i32;
let _guard = WorkerGuard(Some(worker));
if let Err(e) = wait_for_worker_ready(worker_pid, Duration::from_secs(5)) {
panic!(
"worker ready marker wait failed: {e}. The worker may have \
crashed before writing the marker — see jemalloc_alloc_worker \
exit codes for diagnostics"
);
}
let mut probe = Command::new(probe_bin)
.arg("--pid")
.arg(worker_pid.to_string())
.arg("--snapshots")
.arg("20")
.arg("--interval-ms")
.arg("300")
.arg("--json")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn probe");
let probe_pid = probe.id() as i32;
const INTERVAL_MS: u64 = 300;
let sleep_before_sigint = Duration::from_millis(2 * INTERVAL_MS + 400);
std::thread::sleep(sleep_before_sigint);
unsafe {
if libc::kill(probe_pid, libc::SIGINT) != 0 {
panic!(
"failed to SIGINT probe pid {probe_pid}: {}",
std::io::Error::last_os_error(),
);
}
}
let mut stdout_buf = String::new();
probe
.stdout
.as_mut()
.expect("probe stdout piped")
.read_to_string(&mut stdout_buf)
.expect("read probe stdout");
let mut stderr_buf = String::new();
let _ = probe
.stderr
.as_mut()
.expect("probe stderr piped")
.read_to_string(&mut stderr_buf);
let status = probe.wait().expect("probe wait");
if !status.success()
&& (stderr_buf.contains("Operation not permitted")
|| stderr_buf.contains("permission")
|| stderr_buf.contains("ptrace")
|| stderr_buf.contains("PTRACE_SEIZE"))
{
skip_with_reason(&format!(
"probe could not attach to worker — likely YAMA ptrace_scope > 0 \
or missing CAP_SYS_PTRACE (pre-check did not detect it, \
/proc/sys/kernel/yama/ptrace_scope may be unreadable). \
probe stderr:\n{stderr_buf}"
));
return;
}
assert!(
status.success(),
"probe exited non-zero after SIGINT; interrupt path should \
exit 0 with partial output. status={:?}, stderr:\n{stderr_buf}",
status.code(),
);
let out: serde_json::Value = serde_json::from_str(&stdout_buf).unwrap_or_else(|e| {
panic!(
"probe stdout is not valid JSON after SIGINT: {e}. \
stdout:\n{stdout_buf}\nstderr:\n{stderr_buf}",
)
});
assert_eq!(
out.get("interrupted").and_then(|v| v.as_bool()),
Some(true),
"SIGINT mid-run must set interrupted:true. stdout:\n{stdout_buf}",
);
let snapshots = out
.get("snapshots")
.and_then(|v| v.as_array())
.unwrap_or_else(|| panic!("probe output missing snapshots array: {stdout_buf}"));
assert!(
!snapshots.is_empty(),
"at least one snapshot must land before the SIGINT (sent after \
{sleep_ms}ms); got zero. stdout:\n{stdout_buf}",
sleep_ms = sleep_before_sigint.as_millis(),
);
assert!(
snapshots.len() < 20,
"SIGINT at {sleep_ms}ms into a 6s run must produce fewer than \
the requested 20 snapshots; got {}. stdout:\n{stdout_buf}",
snapshots.len(),
sleep_ms = sleep_before_sigint.as_millis(),
);
}