#![allow(dead_code)]
pub mod cassette;
use std::collections::HashMap;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{channel, Receiver};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant};
pub const EVENT_TIMEOUT: Duration = Duration::from_secs(25);
pub const JDK_BANNER: &str = "JDK in use:";
#[derive(Clone)]
pub struct Jdk {
pub java: PathBuf,
pub javac: PathBuf,
origin: &'static str,
}
type CompiledClasses = Arc<Vec<(PathBuf, Vec<u8>)>>;
#[allow(clippy::type_complexity)]
static PROBE_CLASSES: OnceLock<
Mutex<HashMap<(PathBuf, String, String), Arc<OnceLock<Result<CompiledClasses, String>>>>>,
> = OnceLock::new();
static JAVAC_RUNS: AtomicUsize = AtomicUsize::new(0);
static COMPILE_REQUESTS: AtomicUsize = AtomicUsize::new(0);
fn javac_once(javac: &Path, debug_info: &str, src: &Path) -> Result<CompiledClasses, String> {
let source = std::fs::read_to_string(src)
.map_err(|e| format!("cannot read {} to compile it: {e}", src.display()))?;
let key = (javac.to_path_buf(), debug_info.to_string(), source);
let slot = {
let mut cache = PROBE_CLASSES
.get_or_init(Default::default)
.lock()
.map_err(|e| format!("the probe compile cache was poisoned by another test: {e}"))?;
Arc::clone(cache.entry(key).or_default())
};
let requests = COMPILE_REQUESTS.fetch_add(1, Ordering::Relaxed) + 1;
let result = slot.get_or_init(|| javac_into_memory(javac, debug_info, src));
if std::env::var_os("JDWP_TEST_TRACE_JAVAC").is_some() {
println!(
"probe-compile request #{requests} -> javac run #{} ({} {})",
JAVAC_RUNS.load(Ordering::Relaxed),
debug_info,
src.file_name().unwrap_or(src.as_os_str()).to_string_lossy()
);
}
result.clone()
}
fn javac_into_memory(javac: &Path, debug_info: &str, src: &Path) -> Result<CompiledClasses, String> {
JAVAC_RUNS.fetch_add(1, Ordering::Relaxed);
let staging = tempfile::tempdir()
.map_err(|e| format!("cannot make a staging directory to compile {}: {e}", src.display()))?;
let out = Command::new(javac)
.arg(debug_info)
.arg("-encoding")
.arg("UTF-8")
.arg("-d")
.arg(staging.path())
.arg(src)
.output()
.map_err(|e| format!("failed to run javac: {e}"))?;
if !out.status.success() {
return Err(format!("javac {} failed: {}", src.display(), String::from_utf8_lossy(&out.stderr)));
}
let mut classes = Vec::new();
collect_class_files(staging.path(), staging.path(), &mut classes)?;
if classes.is_empty() {
return Err(format!("javac {} exited 0 but wrote no class files", src.display()));
}
Ok(Arc::new(classes))
}
fn collect_class_files(root: &Path, dir: &Path, into: &mut Vec<(PathBuf, Vec<u8>)>) -> Result<(), String> {
let entries = std::fs::read_dir(dir).map_err(|e| format!("cannot read {}: {e}", dir.display()))?;
for entry in entries {
let entry = entry.map_err(|e| format!("cannot read an entry of {}: {e}", dir.display()))?;
let path = entry.path();
if path.is_dir() {
collect_class_files(root, &path, into)?;
} else if path.extension().is_some_and(|e| e == "class") {
let relative = path
.strip_prefix(root)
.map_err(|e| format!("{} is not under {}: {e}", path.display(), root.display()))?
.to_path_buf();
let bytes = std::fs::read(&path).map_err(|e| format!("cannot read {}: {e}", path.display()))?;
into.push((relative, bytes));
}
}
into.sort_by(|a, b| a.0.cmp(&b.0));
Ok(())
}
fn write_class_files(classes: &CompiledClasses, out_dir: &Path) -> Result<(), String> {
for (relative, bytes) in classes.iter() {
let dest = out_dir.join(relative);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
}
std::fs::write(&dest, bytes).map_err(|e| format!("write {}: {e}", dest.display()))?;
}
Ok(())
}
impl Jdk {
pub fn find() -> Result<Option<Self>, String> {
if let Some(home) = std::env::var_os("JAVA_HOME").filter(|h| !h.is_empty()) {
let home = PathBuf::from(home);
let jdk = Self::in_bin(&home.join("bin"), "JAVA_HOME");
if let Some(shortfall) = jdk.shortfall() {
return Err(format!(
"JAVA_HOME={} is not a usable JDK: {shortfall}.\n\
Refusing to fall back to PATH or the snap JetBrains runtime. Exporting JAVA_HOME is \
a request for a SPECIFIC JDK, and searching on used to answer it with a different \
one in silence — on this very path, a run pinned to JDK 21 ran JDK 25 and said so \
nowhere (TEST-18, #52).\n\
Point JAVA_HOME at a JDK, or unset it entirely to search for any.",
home.display(),
));
}
return Ok(Some(jdk));
}
let on_path = Self { java: PathBuf::from("java"), javac: PathBuf::from("javac"), origin: "PATH" };
if Command::new(&on_path.javac).arg("-version").output().is_ok_and(|o| o.status.success()) {
return Ok(Some(on_path));
}
let mut candidates: Vec<PathBuf> = glob_snap_jbr();
candidates.sort();
candidates.reverse();
Ok(candidates.into_iter().find_map(|bin| {
let jdk = Self::in_bin(&bin, "the snap JetBrains runtime");
jdk.is_usable().then_some(jdk)
}))
}
fn in_bin(bin: &std::path::Path, origin: &'static str) -> Self {
const EXE: &str = if cfg!(windows) { ".exe" } else { "" };
Self { java: bin.join(format!("java{EXE}")), javac: bin.join(format!("javac{EXE}")), origin }
}
fn shortfall(&self) -> Option<String> {
match (self.java.exists(), self.javac.exists()) {
(true, true) => None,
(true, false) => Some(format!(
"there is no javac at {} — only java, so this is a JRE, and the probes in \
examples/probes are COMPILED at test time rather than merely run",
self.javac.display()
)),
(false, true) => Some(format!("there is no java at {}", self.java.display())),
(false, false) => Some(format!(
"neither java nor javac is in {}",
self.java.parent().unwrap_or(&self.java).display()
)),
}
}
fn is_usable(&self) -> bool {
self.shortfall().is_none()
}
fn banner(&self) -> String {
format!("{JDK_BANNER} {} at {} (found via {})", self.version(), self.home().display(), self.origin)
}
fn version(&self) -> String {
Command::new(&self.javac)
.arg("-version")
.output()
.ok()
.and_then(|out| {
let said = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
said.lines().map(str::trim).find(|l| !l.is_empty()).map(ToString::to_string)
})
.unwrap_or_else(|| format!("an unidentified javac ({})", self.javac.display()))
}
pub fn feature_version(&self) -> Option<u32> {
self.version().split_whitespace().nth(1)?.split('.').next()?.parse().ok()
}
pub fn home(&self) -> PathBuf {
Command::new(&self.java)
.args(["-XshowSettings:properties", "-version"])
.output()
.ok()
.and_then(|out| {
let said = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
said.lines()
.find_map(|l| l.split_once("java.home = "))
.map(|(_, home)| PathBuf::from(home.trim()))
})
.unwrap_or_else(|| self.javac.clone())
}
pub fn compile_probe(&self, name: &str, out_dir: &Path) -> Result<(), String> {
self.compile_probe_with_debug_info("-g", name, out_dir)
}
pub fn compile_probe_stripped(&self, name: &str, out_dir: &Path) -> Result<(), String> {
self.compile_probe_with_debug_info("-g:none", name, out_dir)
}
pub fn compile_probe_variant(
&self,
name: &str,
out_dir: &Path,
edit: impl FnOnce(String) -> String,
) -> Result<PathBuf, String> {
let original = std::fs::read_to_string(probe_source_path(name))
.map_err(|e| format!("cannot read the source of probe {name}: {e}"))?;
let modified = edit(original.clone());
assert_ne!(
modified, original,
"the edit for probe {name} changed nothing, so the variant would be identical to what the \
JVM is already running and any assertion over it would pass for the wrong reason"
);
let src_dir = out_dir.join("src");
std::fs::create_dir_all(&src_dir).map_err(|e| format!("mkdir {}: {e}", src_dir.display()))?;
let src = src_dir.join(format!("{name}.java"));
std::fs::write(&src, modified).map_err(|e| format!("write {}: {e}", src.display()))?;
let classes = javac_once(&self.javac, "-g", &src)?;
write_class_files(&classes, out_dir)?;
Ok(out_dir.join(format!("{name}.class")))
}
fn compile_probe_with_debug_info(
&self,
debug_info: &str,
name: &str,
out_dir: &Path,
) -> Result<(), String> {
let classes = javac_once(&self.javac, debug_info, &probe_source_path(name))?;
write_class_files(&classes, out_dir)
}
}
fn glob_snap_jbr() -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir("/snap/intellij-idea-ultimate") else {
return Vec::new();
};
entries.flatten().map(|e| e.path().join("jbr/bin")).filter(|p| p.is_dir()).collect()
}
pub fn probe_source_path(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/probes").join(format!("{name}.java"))
}
pub fn probe_smap_path(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/probes").join(format!("{name}.smap"))
}
pub fn install_source_debug_extension(class_file: &Path, smap: &str) -> Result<(), String> {
const ATTRIBUTE_NAME: &[u8] = b"SourceDebugExtension";
if !smap.is_ascii() {
return Err(format!(
"the SMAP for {} must be ASCII — the attribute body is MODIFIED UTF-8, which is not \
Rust's, and this writes the bytes through unchanged",
class_file.display()
));
}
let bytes =
std::fs::read(class_file).map_err(|e| format!("cannot read {}: {e}", class_file.display()))?;
if be_u32(&bytes, 0)? != 0xCAFE_BABE {
return Err(format!("{} does not begin with 0xCAFEBABE", class_file.display()));
}
let pool_count = be_u16(&bytes, 8)?;
let pool_end = constant_pool_end(&bytes)?;
let attributes_at = class_attributes_count_offset(&bytes, pool_end)?;
let walked = skip_attributes(&bytes, attributes_at)?;
if walked != bytes.len() {
return Err(format!(
"walking {} landed on byte {walked} of {} — its layout is not what this splice assumes",
class_file.display(),
bytes.len()
));
}
let new_pool_count = u16::try_from(pool_count + 1)
.map_err(|_| "constant pool is full — no room for the attribute name".to_string())?;
let name_index = new_pool_count - 1;
let new_attribute_count = u16::try_from(be_u16(&bytes, attributes_at)? + 1)
.map_err(|_| "class attribute table is full".to_string())?;
let name_length =
u16::try_from(ATTRIBUTE_NAME.len()).map_err(|_| "attribute name too long".to_string())?;
let smap_length = u32::try_from(smap.len()).map_err(|_| "SMAP too long for a u4 length".to_string())?;
let mut out = Vec::with_capacity(bytes.len() + smap.len() + ATTRIBUTE_NAME.len() + 16);
out.extend_from_slice(&bytes[..8]);
out.extend_from_slice(&new_pool_count.to_be_bytes());
out.extend_from_slice(&bytes[10..pool_end]);
out.push(CONSTANT_UTF8);
out.extend_from_slice(&name_length.to_be_bytes());
out.extend_from_slice(ATTRIBUTE_NAME);
out.extend_from_slice(&bytes[pool_end..attributes_at]);
out.extend_from_slice(&new_attribute_count.to_be_bytes());
out.extend_from_slice(&bytes[attributes_at + 2..]);
out.extend_from_slice(&name_index.to_be_bytes());
out.extend_from_slice(&smap_length.to_be_bytes());
out.extend_from_slice(smap.as_bytes());
std::fs::write(class_file, &out).map_err(|e| format!("cannot write {}: {e}", class_file.display()))
}
const CONSTANT_UTF8: u8 = 1;
fn class_attributes_count_offset(bytes: &[u8], pool_end: usize) -> Result<usize, String> {
let mut at = pool_end;
at += 6; at += 2 + 2 * be_u16(bytes, at)?; at = skip_members(bytes, at)?; skip_members(bytes, at) }
fn constant_pool_end(bytes: &[u8]) -> Result<usize, String> {
let count = be_u16(bytes, 8)?;
let mut at = 10;
let mut slot = 1;
while slot < count {
let tag = *bytes.get(at).ok_or_else(|| format!("class file ends at pool slot {slot}"))?;
at += 1;
at += match tag {
CONSTANT_UTF8 => 2 + be_u16(bytes, at)?, 7 | 8 | 16 | 19 | 20 => 2, 15 => 3, 3 | 4 | 9 | 10 | 11 | 12 | 17 | 18 => 4, 5 | 6 => 8, other => return Err(format!("constant pool tag {other} at offset {at} is not one this knows")),
};
slot += if matches!(tag, 5 | 6) { 2 } else { 1 };
}
Ok(at)
}
fn skip_members(bytes: &[u8], mut at: usize) -> Result<usize, String> {
let count = be_u16(bytes, at)?;
at += 2;
for _ in 0..count {
at += 6; at = skip_attributes(bytes, at)?;
}
Ok(at)
}
fn skip_attributes(bytes: &[u8], mut at: usize) -> Result<usize, String> {
let count = be_u16(bytes, at)?;
at += 2;
for _ in 0..count {
at += 2; at += 4 + be_u32(bytes, at)?; }
Ok(at)
}
fn be_u16(bytes: &[u8], at: usize) -> Result<usize, String> {
bytes
.get(at..at + 2)
.and_then(|s| <[u8; 2]>::try_from(s).ok())
.map(|b| usize::from(u16::from_be_bytes(b)))
.ok_or_else(|| format!("class file ends inside the u2 at offset {at}"))
}
fn be_u32(bytes: &[u8], at: usize) -> Result<usize, String> {
let raw = bytes
.get(at..at + 4)
.and_then(|s| <[u8; 4]>::try_from(s).ok())
.ok_or_else(|| format!("class file ends inside the u4 at offset {at}"))?;
usize::try_from(u32::from_be_bytes(raw))
.map_err(|_| format!("the u4 at offset {at} does not fit an address on this platform"))
}
pub fn probe_source(name: &str) -> String {
let p = probe_source_path(name);
std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("cannot read {}: {e}", p.display()))
}
pub fn probe_line(source: &str, marker: &str) -> i32 {
source
.lines()
.position(|l| l.contains(marker))
.map_or_else(|| panic!("no `{marker}` marker in probe source"), |i| i32::try_from(i).unwrap_or(0) + 1)
}
const PORT_TAKEN: &str = "PROBE_PORT_TAKEN";
fn free_port() -> u16 {
std::net::TcpListener::bind("127.0.0.1:0").ok().and_then(|l| l.local_addr().ok()).map_or(0, |a| a.port())
}
struct Relay {
port: u16,
stop: Arc<std::sync::atomic::AtomicBool>,
open: Arc<Mutex<Vec<std::net::TcpStream>>>,
}
impl Relay {
fn start(
label: &'static str,
target_port: Option<u16>,
mut wire: impl FnMut(std::net::TcpStream, Option<std::net::TcpStream>) + Send + 'static,
) -> Result<Self, String> {
let listener =
std::net::TcpListener::bind("127.0.0.1:0").map_err(|e| format!("{label} bind: {e}"))?;
let port = listener.local_addr().map_err(|e| format!("{label} addr: {e}"))?.port();
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let open: Arc<Mutex<Vec<std::net::TcpStream>>> = Arc::new(Mutex::new(Vec::new()));
let (acc_stop, acc_open) = (Arc::clone(&stop), Arc::clone(&open));
std::thread::spawn(move || {
for incoming in listener.incoming() {
if acc_stop.load(std::sync::atomic::Ordering::Relaxed) {
return;
}
let Ok(client) = incoming else { return };
let server = match target_port {
Some(p) => match std::net::TcpStream::connect(("127.0.0.1", p)) {
Ok(s) => Some(s),
Err(_) => return,
},
None => None,
};
let _ = client.set_nodelay(true);
if let Some(s) = server.as_ref() {
let _ = s.set_nodelay(true);
}
if let Ok(mut v) = acc_open.lock() {
if let Ok(c) = client.try_clone() {
v.push(c);
}
if let Some(Ok(s)) = server.as_ref().map(std::net::TcpStream::try_clone) {
v.push(s);
}
}
wire(client, server);
}
});
Ok(Self { port, stop, open })
}
}
impl Relay {
fn sever(&self) {
if let Ok(v) = self.open.lock() {
for s in v.iter() {
let _ = s.shutdown(std::net::Shutdown::Both);
}
}
}
}
impl Drop for Relay {
fn drop(&mut self) {
self.stop.store(true, std::sync::atomic::Ordering::Relaxed);
if let Ok(v) = self.open.lock() {
for s in v.iter() {
let _ = s.shutdown(std::net::Shutdown::Both);
}
}
let _ = std::net::TcpStream::connect(("127.0.0.1", self.port));
}
}
pub struct LatencyRelay {
pub port: u16,
one_way_nanos: Arc<std::sync::atomic::AtomicU64>,
_relay: Relay,
}
impl LatencyRelay {
pub fn start(target_port: u16, rtt: Duration) -> Result<Self, String> {
let one_way = Arc::new(std::sync::atomic::AtomicU64::new(one_way_nanos(rtt)));
let wire_delay = Arc::clone(&one_way);
let relay = Relay::start("relay", Some(target_port), move |client, server| {
let Some(server) = server else { return };
if let (Ok(c_read), Ok(s_read)) = (client.try_clone(), server.try_clone()) {
pump_delayed(c_read, server, Arc::clone(&wire_delay));
pump_delayed(s_read, client, Arc::clone(&wire_delay));
}
})?;
Ok(Self { port: relay.port, one_way_nanos: one_way, _relay: relay })
}
pub fn set_rtt(&self, rtt: Duration) {
self.one_way_nanos.store(one_way_nanos(rtt), std::sync::atomic::Ordering::Relaxed);
}
}
fn one_way_nanos(rtt: Duration) -> u64 {
u64::try_from((rtt / 2).as_nanos()).unwrap_or(u64::MAX)
}
fn pump_delayed(
mut from: std::net::TcpStream,
mut to: std::net::TcpStream,
delay: Arc<std::sync::atomic::AtomicU64>,
) {
std::thread::spawn(move || {
let mut buf = vec![0u8; 1 << 16];
loop {
let n = match std::io::Read::read(&mut from, &mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => n,
};
let one_way = Duration::from_nanos(delay.load(std::sync::atomic::Ordering::Relaxed));
if !one_way.is_zero() {
std::thread::sleep(one_way);
}
if std::io::Write::write_all(&mut to, buf.get(..n).unwrap_or_default()).is_err() {
break;
}
}
let _ = to.shutdown(std::net::Shutdown::Both);
});
}
#[derive(Clone, Debug)]
pub enum Fault {
Error(u16),
Payload(Vec<u8>),
}
#[derive(Clone, Debug)]
pub enum EventFault {
DuplicateKind { kind: u8, times: usize },
DelayKind { kind: u8, ms: u64, times: usize },
}
pub const EVENT_KIND_BREAKPOINT: u8 = 2;
fn composite_event_kind(pkt: &[u8]) -> Option<u8> {
pkt.get(JDWP_HEADER + 1 + 4).copied()
}
pub struct FaultRelay {
pub port: u16,
relay: Relay,
duplicated: Arc<std::sync::atomic::AtomicUsize>,
}
const JDWP_HANDSHAKE: &[u8] = b"JDWP-Handshake";
const JDWP_HEADER: usize = 11;
const JDWP_REPLY_FLAG: u8 = 0x80;
impl FaultRelay {
pub fn start(target_port: u16, faults: Vec<(u8, u8, Fault)>) -> Result<Self, String> {
Self::start_with_events(target_port, faults, None)
}
pub fn start_refusing(target_port: u16, refuse: Vec<(u8, u8)>) -> Result<Self, String> {
Self::start_full(target_port, vec![], None, refuse)
}
pub fn start_with_events(
target_port: u16,
faults: Vec<(u8, u8, Fault)>,
on_events: Option<EventFault>,
) -> Result<Self, String> {
Self::start_full(target_port, faults, on_events, vec![])
}
fn start_full(
target_port: u16,
faults: Vec<(u8, u8, Fault)>,
on_events: Option<EventFault>,
refuse: Vec<(u8, u8)>,
) -> Result<Self, String> {
let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let for_relay = Arc::clone(&counter);
let relay = Relay::start("fault relay", Some(target_port), move |client, server| {
let Some(server) = server else { return };
let faults = faults.clone();
let on_events = on_events.clone();
let counter = Arc::clone(&for_relay);
let refuse = refuse.clone();
let duplicated = Arc::clone(&counter);
wire_framed(client, server, refuse, move |seen| {
let (command, reply) = match seen {
FromDebuggee::Event(pkt) => {
if let Some(EventFault::DelayKind { kind, ms, times }) = on_events {
if duplicated.load(std::sync::atomic::Ordering::Relaxed) < times
&& composite_event_kind(pkt) == Some(kind)
{
duplicated.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
std::thread::sleep(Duration::from_millis(ms));
}
return None;
}
let Some(EventFault::DuplicateKind { kind, times }) = on_events else {
return None;
};
if duplicated.load(std::sync::atomic::Ordering::Relaxed) >= times
|| composite_event_kind(pkt) != Some(kind)
{
return None;
}
duplicated.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return Some([pkt, pkt].concat());
}
FromDebuggee::Reply { command, reply, .. } => (command, reply),
};
let id = packet_id(reply)?;
let fault = faults.iter().find(|(s, c, _)| (*s, *c) == command).map(|(_, _, f)| f)?;
Some(match fault {
Fault::Error(code) => reply_packet(id, *code, &[]),
Fault::Payload(p) => reply_packet(id, 0, p),
})
});
})?;
Ok(Self { port: relay.port, relay, duplicated: counter })
}
pub fn sever(&self) {
self.relay.sever();
}
pub fn duplicated(&self) -> usize {
self.duplicated.load(std::sync::atomic::Ordering::Relaxed)
}
}
fn take_packets(buf: &mut Vec<u8>) -> Vec<Vec<u8>> {
let mut out = Vec::new();
while let Some(head) = buf.get(..4).and_then(|h| <[u8; 4]>::try_from(h).ok()) {
let len = u32::from_be_bytes(head) as usize;
if len < JDWP_HEADER || buf.len() < len {
break;
}
out.push(buf.drain(..len).collect());
}
out
}
enum Frame<'a> {
Handshake(&'a [u8]),
Packet(&'a [u8]),
}
fn read_frames(mut from: std::net::TcpStream, mut on_frame: impl FnMut(Frame<'_>) -> bool) {
let mut buf: Vec<u8> = Vec::new();
let mut chunk = vec![0u8; 1 << 16];
let mut shaken = false;
loop {
let n = match std::io::Read::read(&mut from, &mut chunk) {
Ok(0) | Err(_) => break,
Ok(n) => n,
};
buf.extend_from_slice(chunk.get(..n).unwrap_or_default());
if !shaken {
if buf.len() < JDWP_HANDSHAKE.len() {
continue;
}
let shake: Vec<u8> = buf.drain(..JDWP_HANDSHAKE.len()).collect();
if !on_frame(Frame::Handshake(&shake)) {
return;
}
shaken = true;
}
for pkt in take_packets(&mut buf) {
if !on_frame(Frame::Packet(&pkt)) {
return;
}
}
}
}
fn pump_framed(
from: std::net::TcpStream,
mut to: std::net::TcpStream,
mut transform: impl FnMut(&[u8]) -> Option<Vec<u8>> + Send + 'static,
) -> Arc<std::sync::atomic::AtomicBool> {
let finished = Arc::new(std::sync::atomic::AtomicBool::new(false));
let flag = Arc::clone(&finished);
std::thread::spawn(move || {
read_frames(from, |frame| match frame {
Frame::Handshake(b) => std::io::Write::write_all(&mut to, b).is_ok(),
Frame::Packet(p) => {
let out = transform(p);
std::io::Write::write_all(&mut to, out.as_deref().unwrap_or(p)).is_ok()
}
});
let _ = to.shutdown(std::net::Shutdown::Both);
flag.store(true, std::sync::atomic::Ordering::Relaxed);
});
finished
}
type Pending = Arc<Mutex<std::collections::HashMap<u32, (u8, u8, Vec<u8>)>>>;
enum FromDebuggee<'a> {
Reply { command: (u8, u8), request: &'a [u8], reply: &'a [u8] },
Event(&'a [u8]),
}
const JDWP_NOT_IMPLEMENTED: u16 = 99;
fn wire_framed(
client: std::net::TcpStream,
server: std::net::TcpStream,
refuse: Vec<(u8, u8)>,
mut on_reply: impl FnMut(FromDebuggee<'_>) -> Option<Vec<u8>> + Send + 'static,
) -> Arc<std::sync::atomic::AtomicBool> {
let pending: Pending = Arc::new(Mutex::new(std::collections::HashMap::new()));
let (Ok(c_read), Ok(s_read)) = (client.try_clone(), server.try_clone()) else {
return Arc::new(std::sync::atomic::AtomicBool::new(true));
};
let mut refusals_to = client.try_clone().ok();
let outbound = Arc::clone(&pending);
pump_framed(c_read, server, move |pkt| {
let (Some(id), Some(flags), Some(set), Some(cmd)) =
(packet_id(pkt), pkt.get(8).copied(), pkt.get(9).copied(), pkt.get(10).copied())
else {
return None;
};
if flags & JDWP_REPLY_FLAG != 0 {
return None;
}
if !refuse.contains(&(set, cmd)) {
if let Ok(mut m) = outbound.lock() {
m.insert(id, (set, cmd, pkt.get(JDWP_HEADER..).unwrap_or_default().to_vec()));
}
return None;
}
if let Some(w) = refusals_to.as_mut() {
let _ = std::io::Write::write_all(w, &reply_packet(id, JDWP_NOT_IMPLEMENTED, &[]));
}
Some(Vec::new())
});
pump_framed(s_read, client, move |pkt| {
if pkt.get(8).copied().is_some_and(|f| f & JDWP_REPLY_FLAG == 0) {
return on_reply(FromDebuggee::Event(pkt));
}
let id = packet_id(pkt)?;
let (set, cmd, request) = pending.lock().ok()?.remove(&id)?;
on_reply(FromDebuggee::Reply { command: (set, cmd), request: &request, reply: pkt })
})
}
fn reply_packet(id: u32, error: u16, payload: &[u8]) -> Vec<u8> {
let len = u32::try_from(JDWP_HEADER + payload.len()).unwrap_or(u32::MAX);
let mut out = Vec::with_capacity(JDWP_HEADER + payload.len());
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(&id.to_be_bytes());
out.push(JDWP_REPLY_FLAG);
out.extend_from_slice(&error.to_be_bytes());
out.extend_from_slice(payload);
out
}
fn packet_id(pkt: &[u8]) -> Option<u32> {
let b = pkt.get(4..8)?;
Some(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
pub fn jdwp_string(s: &str) -> Vec<u8> {
let mut out = u32::try_from(s.len()).unwrap_or(0).to_be_bytes().to_vec();
out.extend_from_slice(s.as_bytes());
out
}
pub struct Probe {
child: Child,
stdin: Option<ChildStdin>,
pub port: u16,
name: String,
lines: Arc<Mutex<Vec<String>>>,
new_line: Receiver<()>,
_dir: tempfile::TempDir,
}
impl Probe {
const PROBE_LISTEN_TIMEOUT: Duration = Duration::from_secs(90);
pub fn launch(jdk: &Jdk, name: &str) -> Result<Self, String> {
Self::launch_built_by(jdk, name, None, Jdk::compile_probe)
}
pub fn launch_running(jdk: &Jdk, name: &str, ready: impl FnMut(&str) -> bool) -> Result<Self, String> {
let probe = Self::launch(jdk, name)?;
probe.wait_until_running(EVENT_TIMEOUT, ready)?;
Ok(probe)
}
pub fn launch_delayed(jdk: &Jdk, name: &str, delay: Duration) -> Result<Self, String> {
Self::launch_built_by(jdk, name, Some(delay), Jdk::compile_probe)
}
pub fn launch_stripped(jdk: &Jdk, name: &str) -> Result<Self, String> {
Self::launch_built_by(jdk, name, None, Jdk::compile_probe_stripped)
}
pub fn launch_in_package(jdk: &Jdk, name: &str, main_class: &str) -> Result<Self, String> {
Self::launch_built_by_with_main(jdk, name, Some(main_class), None, Jdk::compile_probe)
}
pub fn launch_with_smap(jdk: &Jdk, name: &str) -> Result<Self, String> {
Self::launch_built_by(jdk, name, None, |jdk, name, dir| {
jdk.compile_probe(name, dir)?;
let fixture = probe_smap_path(name);
let smap = std::fs::read_to_string(&fixture)
.map_err(|e| format!("cannot read {}: {e}", fixture.display()))?;
install_source_debug_extension(&dir.join(format!("{name}.class")), &smap)
})
}
fn launch_built_by(
jdk: &Jdk,
name: &str,
start_delay: Option<Duration>,
build: impl Fn(&Jdk, &str, &Path) -> Result<(), String>,
) -> Result<Self, String> {
Self::launch_built_by_with_main(jdk, name, None, start_delay, build)
}
fn launch_built_by_with_main(
jdk: &Jdk,
name: &str,
main_override: Option<&str>,
start_delay: Option<Duration>,
build: impl Fn(&Jdk, &str, &Path) -> Result<(), String>,
) -> Result<Self, String> {
const ATTEMPTS: u32 = 3;
let mut last = String::new();
for attempt in 1..=ATTEMPTS {
match Self::launch_built_by_once(jdk, name, main_override, start_delay, &build) {
Ok(p) => return Ok(p),
Err(e) if e.starts_with(PORT_TAKEN) && attempt < ATTEMPTS => {
eprintln!(
"note: probe {name} lost the port race (attempt {attempt}/{ATTEMPTS}), \
retrying with a fresh port — {e}"
);
last = e;
}
Err(e) => return Err(e),
}
}
Err(format!("probe {name} lost the port race {ATTEMPTS} times running: {last}"))
}
fn launch_built_by_once(
jdk: &Jdk,
name: &str,
main_override: Option<&str>,
start_delay: Option<Duration>,
build: &impl Fn(&Jdk, &str, &Path) -> Result<(), String>,
) -> Result<Self, String> {
let dir = tempfile::tempdir().map_err(|e| format!("tempdir: {e}"))?;
build(jdk, name, dir.path())?;
let runnable = main_override.unwrap_or(name).to_string();
let main_class = match start_delay {
None => vec![runnable],
Some(delay) => {
compile_slow_start(jdk, dir.path())?;
vec![SLOW_START.to_string(), delay.as_millis().to_string(), runnable]
}
};
let port = free_port();
let agent = format!("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:{port}");
let mut child = Command::new(&jdk.java)
.arg(agent)
.args(["-cp", "."])
.args(&main_class)
.current_dir(dir.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("failed to launch probe {name}: {e}"))?;
let stdin = child.stdin.take();
let stdout = child.stdout.take().ok_or("probe has no stdout")?;
let stderr = child.stderr.take().ok_or("probe has no stderr")?;
let lines = Arc::new(Mutex::new(Vec::new()));
let (tx, new_line) = channel();
pump(stdout, Arc::clone(&lines), tx.clone());
pump(stderr, Arc::clone(&lines), tx);
let probe = Self { child, stdin, port, name: name.to_string(), lines, new_line, _dir: dir };
probe.wait_until_listening()?;
Ok(probe)
}
fn wait_until_listening(&self) -> Result<(), String> {
let started = Instant::now();
let port = self.port.to_string();
let banner = |l: &str| l.starts_with("Listening for transport ") && l.trim_end().ends_with(&port);
let lost_race = |l: &str| l.contains("Address already in use") || l.contains("TRANSPORT_INIT");
if self.wait_for_line(Self::PROBE_LISTEN_TIMEOUT, |l| banner(l) || lost_race(l)).is_some() {
if self.output().iter().any(|l| lost_race(l)) {
return Err(format!(
"{PORT_TAKEN}: another process took port {port} before this JVM bound it"
));
}
return Ok(());
}
let captured = self.output();
let tail: Vec<&String> = captured.iter().rev().take(10).rev().collect();
let said = if tail.is_empty() {
"it printed nothing at all".to_string()
} else {
format!("it printed:\n {}", tail.iter().map(|l| l.as_str()).collect::<Vec<_>>().join("\n "))
};
Err(format!(
"probe never announced a JDWP listener on port {} within {:?} (waited {:?}) — {said}\n\
Expected a line like `Listening for transport dt_socket at address: {}` from the agent \
itself.\n\
If it printed nothing, the JVM is probably just slow to start rather than broken: on \
Windows a first run after a JDK is installed or updated can spend longer than this being \
scanned by Defender, and the same probe then launches in ~1s once warm.",
self.port,
Self::PROBE_LISTEN_TIMEOUT,
started.elapsed(),
self.port,
))
}
pub fn wait_until_running(
&self,
timeout: Duration,
ready: impl FnMut(&str) -> bool,
) -> Result<String, String> {
self.wait_for_line(timeout, ready).ok_or_else(|| {
format!(
"{name} accepted a JDWP connection but never printed a readiness line within {timeout:?} \
— it is listening, not running.\n\
This is a RACE in the test, not a wrong answer from the debugger: the JDWP agent binds \
before the main class is loaded, so anything asked now about loaded state \
(debug.list_classes, debug.list_methods, debug.source) is correctly answered \"not \
loaded\". See TEST-17 (#49) — and do not go looking for #46's wrong-answer bug, which is \
what this looks like from the assertion's side.\n\
What {name} printed: {output:?}",
name = self.name,
output = self.output(),
)
})
}
pub fn send_line(&mut self, line: &str) -> Result<(), String> {
let stdin = self.stdin.as_mut().ok_or("probe stdin already closed")?;
writeln!(stdin, "{line}").map_err(|e| format!("probe stdin write: {e}"))?;
stdin.flush().map_err(|e| format!("probe stdin flush: {e}"))
}
pub fn attach(&mut self, server: &mut Server) -> String {
let out = server.call("debug.attach", serde_json::json!({"host": "127.0.0.1", "port": self.port}));
if out.contains("Connected") {
return out;
}
let diagnosis = self.diagnose_refusal(&out);
panic!("{diagnosis}");
}
pub fn diagnose_refusal(&mut self, out: &str) -> String {
let log = self.output();
let announced = log.iter().any(|l| l.contains("Listening for transport"));
let exit = self.child.try_wait();
let listening = std::net::TcpStream::connect_timeout(
&std::net::SocketAddr::from(([127, 0, 0, 1], self.port)),
Duration::from_millis(500),
)
.is_ok();
let verdict = refusal_verdict(
match &exit {
Ok(None) => JvmState::Alive,
Ok(Some(status)) => JvmState::Exited(status.to_string()),
Err(e) => JvmState::Unknown(e.to_string()),
},
listening,
announced,
);
format!(
"attach to {} on port {} failed: {out}\n \
verdict: {verdict}\n \
the facts it was read from — JVM: {}; listening on the port: {}; announced this port: {}\n \
The probe's last 12 lines, as of BEFORE this diagnosis connected to it (#55 made the \
`Listening for transport … at address:` banner come from this JVM and name this port, so \
its absence is itself the finding):\n{}",
self.name,
self.port,
match &exit {
Ok(None) => "alive".to_string(),
Ok(Some(status)) => format!("exited with {status}"),
Err(e) => format!("try_wait failed: {e}"),
},
if listening { "yes" } else { "no" },
if announced { "yes" } else { "no" },
log.iter().rev().take(12).rev().map(|l| format!(" {l}")).collect::<Vec<_>>().join("\n"),
)
}
pub fn kill_and_wait(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
let dialled = std::net::TcpStream::connect_timeout(
&std::net::SocketAddr::from(([127, 0, 0, 1], self.port)),
Duration::from_millis(200),
);
if dialled.is_err() {
return;
}
std::thread::sleep(Duration::from_millis(50));
}
}
pub fn output(&self) -> Vec<String> {
self.lines.lock().map(|v| v.clone()).unwrap_or_default()
}
pub fn wait_for_line(&self, timeout: Duration, mut pred: impl FnMut(&str) -> bool) -> Option<String> {
let deadline = Instant::now() + timeout;
loop {
if let Some(hit) = self.output().into_iter().find(|l| pred(l)) {
return Some(hit);
}
let left = deadline.checked_duration_since(Instant::now())?;
if self.new_line.recv_timeout(left.min(Duration::from_millis(250))).is_err()
&& Instant::now() >= deadline
{
return None;
}
}
}
}
impl Drop for Probe {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
const SLOW_START: &str = "SlowStart";
fn compile_slow_start(jdk: &Jdk, dir: &Path) -> Result<(), String> {
let src = dir.join(format!("{SLOW_START}.java"));
std::fs::write(
&src,
format!(
"public class {SLOW_START} {{\n \
public static void main(String[] args) throws Exception {{\n \
Thread.sleep(Long.parseLong(args[0]));\n \
Class.forName(args[1]).getMethod(\"main\", String[].class)\n \
.invoke(null, (Object) new String[0]);\n \
}}\n}}\n"
),
)
.map_err(|e| format!("cannot write {}: {e}", src.display()))?;
let classes =
javac_once(&jdk.javac, "-g", &src).map_err(|e| format!("javac failed for {SLOW_START}: {e}"))?;
write_class_files(&classes, dir)
}
const SERVER_LOG_TAIL: usize = 60;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum JvmState {
Alive,
Exited(String),
Unknown(String),
}
pub fn refusal_verdict(jvm: JvmState, listening: bool, announced: bool) -> String {
match (jvm, listening, announced) {
(JvmState::Unknown(e), _, _) => {
format!("UNDETERMINED — could not read the JVM's status ({e}); the facts below are all there is.")
}
(JvmState::Exited(status), _, _) => format!(
"THE PROBE JVM IS GONE — it exited with {status}, and the port went with it. This is not a \
port race and `free_port` explains none of it; find out why a JVM that had announced \
itself stopped running."
),
(JvmState::Alive, true, _) => "SOMETHING ELSE HOLDS THE PORT — something is listening, and it is \
not this probe's agent, which stops listening the moment a debugger completes a handshake. A \
stranger won free_port's race after this JVM bound and released it, or never let it bind."
.to_string(),
(JvmState::Alive, false, true) => "THE SESSION IS ALREADY TAKEN — the JVM is alive and its banner \
names this port, so it did bind it. A live handshaked session refuses a second attach *and* \
closes the listener, so \"nothing listening\" is this world's signature rather than a fault. \
Find what is already attached: a leaked session from an earlier test, a `Relay`, or a \
debugger the harness believes it disconnected."
.to_string(),
(JvmState::Alive, false, false) => "THE PORT WAS NEVER BOUND — the JVM is alive but never printed \
the `Listening for transport` banner for this port, which is `free_port`'s documented TOCTOU. \
Its log should carry the bind failure; nothing portable removes this race, so the remedy is \
that this message exists."
.to_string(),
}
}
pub const fn resume_verdict(printed_before: usize, printed_after: usize) -> &'static str {
if printed_after == printed_before {
"it never ran again — read this as a resume/liveness failure (or a dead probe), NOT as \
force_return returning the wrong value"
} else {
"it DID run and still never produced the forced value — force_return reported success \
without changing what the caller received, which is exactly what this test exists to catch"
}
}
fn pump_tail<R: std::io::Read + Send + 'static>(
stream: R,
sink: Arc<Mutex<std::collections::VecDeque<String>>>,
keep: usize,
) {
std::thread::spawn(move || {
for line in BufReader::new(stream).lines().map_while(Result::ok) {
if let Ok(mut v) = sink.lock() {
if v.len() == keep {
v.pop_front();
}
v.push_back(line);
}
}
});
}
fn pump<R: std::io::Read + Send + 'static>(
stream: R,
sink: Arc<Mutex<Vec<String>>>,
tx: std::sync::mpsc::Sender<()>,
) {
std::thread::spawn(move || {
for line in BufReader::new(stream).lines().map_while(Result::ok) {
if let Ok(mut v) = sink.lock() {
v.push(line);
}
if tx.send(()).is_err() {
break; }
}
});
}
pub struct Server {
child: Child,
stdin: Option<ChildStdin>,
stdout: Option<BufReader<ChildStdout>>,
drained: std::collections::VecDeque<String>,
log: Arc<Mutex<std::collections::VecDeque<String>>>,
next_id: i64,
}
impl Server {
pub fn start() -> Result<Self, String> {
Self::start_with_env(&[])
}
pub fn start_with_env(env: &[(&str, &str)]) -> Result<Self, String> {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_jdwp-mcp"));
for (k, v) in env {
cmd.env(k, v);
}
let mut child = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("failed to start jdwp-mcp: {e}"))?;
let stdin = child.stdin.take().ok_or("server has no stdin")?;
let stdout = BufReader::new(child.stdout.take().ok_or("server has no stdout")?);
let stderr = child.stderr.take().ok_or("server has no stderr")?;
let log = Arc::new(Mutex::new(std::collections::VecDeque::new()));
pump_tail(stderr, Arc::clone(&log), SERVER_LOG_TAIL);
let mut server = Self {
child,
stdin: Some(stdin),
stdout: Some(stdout),
drained: std::collections::VecDeque::new(),
log,
next_id: 1,
};
server.request(
"initialize",
serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "integration-test", "version": "0"}
}),
)?;
Ok(server)
}
#[allow(clippy::needless_pass_by_value)]
pub fn request(&mut self, method: &str, params: serde_json::Value) -> Result<serde_json::Value, String> {
let id = self.next_id;
self.next_id += 1;
let req = serde_json::json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params});
let stdin = self.stdin.as_mut().ok_or("server stdin already closed")?;
writeln!(stdin, "{req}").map_err(|e| format!("server stdin: {e}"))?;
stdin.flush().map_err(|e| format!("server flush: {e}"))?;
loop {
let line = self.next_line()?;
let Ok(v) = serde_json::from_str::<serde_json::Value>(line.trim()) else { continue };
if v.get("id").and_then(serde_json::Value::as_i64) == Some(id) {
return Ok(v);
}
}
}
pub fn send_raw(&mut self, line: &str) -> Result<(), String> {
let stdin = self.stdin.as_mut().ok_or("server stdin already closed")?;
writeln!(stdin, "{line}").map_err(|e| format!("server stdin: {e}"))?;
stdin.flush().map_err(|e| format!("server flush: {e}"))
}
pub fn send_raw_unterminated(&mut self, text: &str) -> Result<(), String> {
let stdin = self.stdin.as_mut().ok_or("server stdin already closed")?;
stdin.write_all(text.as_bytes()).map_err(|e| format!("server stdin: {e}"))?;
stdin.flush().map_err(|e| format!("server flush: {e}"))
}
pub fn read_reply(&mut self) -> Result<serde_json::Value, String> {
let line = self.next_line()?;
serde_json::from_str(line.trim()).map_err(|e| format!("server wrote a non-JSON line ({e}): {line:?}"))
}
fn next_line(&mut self) -> Result<String, String> {
if let Some(line) = self.drained.pop_front() {
return Ok(line);
}
let stdout = self.stdout.as_mut().ok_or("server closed stdout without replying")?;
let mut line = String::new();
match stdout.read_line(&mut line) {
Ok(0) => Err("server closed stdout without replying".to_string()),
Ok(_) => Ok(line),
Err(e) => Err(format!("server stdout: {e}")),
}
}
pub fn raw(&mut self, line: &str) -> Result<serde_json::Value, String> {
self.send_raw(line)?;
self.read_reply()
}
pub fn close_stdin_and_wait(&mut self, timeout: Duration) -> Result<std::process::ExitStatus, String> {
drop(self.stdin.take());
let (tx, rx) = channel();
if let Some(mut stdout) = self.stdout.take() {
std::thread::spawn(move || loop {
let mut line = String::new();
match stdout.read_line(&mut line) {
Ok(0) | Err(_) => return,
Ok(_) => {
if tx.send(line).is_err() {
return;
}
}
}
});
}
let deadline = Instant::now() + timeout;
let outcome = loop {
while let Ok(line) = rx.try_recv() {
self.drained.push_back(line);
}
match self.child.try_wait() {
Ok(Some(status)) => break Ok(status),
Ok(None) if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(20)),
Ok(None) => break Err(format!("server still running {timeout:?} after EOF on stdin")),
Err(e) => break Err(format!("waiting for server: {e}")),
}
};
while let Ok(line) = rx.recv_timeout(Duration::from_millis(200)) {
self.drained.push_back(line);
}
outcome
}
#[allow(clippy::needless_pass_by_value)] pub fn call(&mut self, tool: &str, args: serde_json::Value) -> String {
match self.request("tools/call", serde_json::json!({"name": tool, "arguments": args})) {
Ok(resp) => {
if let Some(err) = resp.get("error") {
return format!("<rpc error> {err}");
}
resp["result"]["content"][0]["text"].as_str().unwrap_or("<no text>").to_string()
}
Err(e) => format!("<transport error> {e}{}", self.log_tail()),
}
}
pub fn log_tail(&self) -> String {
let lines = match self.log.lock() {
Ok(l) => l.iter().cloned().collect::<Vec<_>>(),
Err(_) => return String::new(),
};
if lines.is_empty() {
return String::new();
}
format!("\n--- the server's last {} stderr line(s) ---\n{}", lines.len(), lines.join("\n"))
}
pub fn attach(&mut self, port: u16) -> String {
let out = self.call("debug.attach", serde_json::json!({"host": "127.0.0.1", "port": port}));
assert!(out.contains("Connected"), "attach to port {port} failed: {out}{}", self.log_tail());
out
}
pub fn evaluate(&mut self, expr: &str) -> String {
self.call("debug.evaluate", serde_json::json!({"expression": expr}))
}
pub fn last_event(&mut self) -> String {
self.call("debug.get_last_event", serde_json::json!({}))
}
pub fn panic_reset(&mut self) -> String {
self.call("debug.panic", serde_json::json!({}))
}
pub fn wait_for_traces(&mut self, needle: &str, timeout: Duration) -> Option<String> {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
let traces = self.call("debug.get_traces", serde_json::json!({}));
if traces.contains(needle) {
return Some(traces);
}
std::thread::sleep(Duration::from_millis(150));
}
None
}
pub fn wait_for_no_suspended(&mut self, timeout: Duration) -> Result<(), String> {
let deadline = Instant::now() + timeout;
loop {
let reply = self.call("debug.list_threads", serde_json::json!({"only_suspended": true}));
if reply.starts_with("0/") {
return Ok(());
}
if Instant::now() >= deadline {
return Err(reply);
}
std::thread::sleep(Duration::from_millis(50));
}
}
pub fn wait_for_event(&mut self, needle: &str, timeout: Duration) -> Option<String> {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(200));
let ev = self.last_event();
if ev.contains(needle) {
return Some(ev);
}
}
None
}
}
impl Drop for Server {
fn drop(&mut self) {
let _ = self.request("tools/call", serde_json::json!({"name": "debug.panic", "arguments": {}}));
drop(self.stdin.take());
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match self.child.try_wait() {
Ok(Some(_)) => return, Ok(None) if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(20)),
_ => break,
}
}
let _ = self.child.kill();
let _ = self.child.wait();
}
}
fn resolved_jdk() -> &'static Result<Option<Jdk>, String> {
static RESOLVED: OnceLock<Result<Option<Jdk>, String>> = OnceLock::new();
RESOLVED.get_or_init(|| {
let found = Jdk::find();
match &found {
Ok(Some(jdk)) => println!("{}", jdk.banner()),
Ok(None) => {}
Err(why) => println!("error: {why}"),
}
found
})
}
pub fn jdk_or_skip(test: &str) -> Option<Jdk> {
match resolved_jdk() {
Ok(Some(jdk)) => Some(jdk.clone()),
Ok(None) => {
println!("SKIP {test}: no JDK found (set JAVA_HOME or put javac on PATH)");
None
}
Err(why) => panic!("{}", why.lines().next().unwrap_or(why)),
}
}
pub fn assert_contains_all(label: &str, got: &str, wants: &[&str]) {
let missing: Vec<&str> = wants.iter().copied().filter(|w| !got.contains(w)).collect();
assert!(missing.is_empty(), "{label}: missing {missing:?}\n got: {got}");
}