use crate::eventloop::{spawn_event_loop, EventLoopHandle, InFlight};
use crate::events::EventSet;
use crate::protocol::{CommandPacket, JdwpError, JdwpResult, ReplyPacket, JDWP_HANDSHAKE};
use crate::reftype::{FieldInfo, MethodInfo};
use crate::types::{ClassId, ReferenceTypeId};
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tracing::{debug, info, warn};
pub const DEFAULT_INVOKE_TIMEOUT_MS: u64 = 2000;
pub const MAX_READS_IN_FLIGHT: usize = 16;
#[derive(Clone, Debug)]
pub struct JdwpConnection {
event_loop: EventLoopHandle,
next_id: Arc<AtomicU32>,
types: Arc<TypeCache>,
read_only: Arc<AtomicBool>,
invoke_timeout_ms: Arc<AtomicU64>,
round_trips: Arc<AtomicU32>,
}
impl JdwpConnection {
pub async fn connect(host: &str, port: u16) -> JdwpResult<Self> {
info!("Connecting to JDWP at {}:{}", host, port);
let mut stream = TcpStream::connect((host, port)).await?;
Self::handshake(&mut stream).await?;
let (reader, writer) = stream.into_split();
let event_loop = spawn_event_loop(reader, writer);
Ok(Self {
event_loop,
next_id: Arc::new(AtomicU32::new(1)),
types: Arc::new(TypeCache::default()),
read_only: Arc::new(AtomicBool::new(false)),
invoke_timeout_ms: Arc::new(AtomicU64::new(DEFAULT_INVOKE_TIMEOUT_MS)),
round_trips: Arc::new(AtomicU32::new(0)),
})
}
pub fn set_read_only(&self, read_only: bool) {
self.read_only.store(read_only, Ordering::SeqCst);
}
#[must_use]
pub fn is_read_only(&self) -> bool {
self.read_only.load(Ordering::SeqCst)
}
pub(crate) fn guard_mutation(&self, what: &str) -> JdwpResult<()> {
if self.is_read_only() {
return Err(JdwpError::ReadOnly(what.to_string()));
}
Ok(())
}
pub fn set_invoke_timeout_ms(&self, ms: u64) {
self.invoke_timeout_ms.store(ms, Ordering::SeqCst);
}
#[must_use]
pub fn invoke_timeout_ms(&self) -> u64 {
self.invoke_timeout_ms.load(Ordering::SeqCst)
}
pub(crate) async fn send_invoke(&mut self, packet: CommandPacket) -> JdwpResult<ReplyPacket> {
let ms = self.invoke_timeout_ms();
if ms == 0 {
return self.send_command(packet).await;
}
tokio::time::timeout(std::time::Duration::from_millis(ms), self.send_command(packet))
.await
.map_or(Err(JdwpError::InvokeTimeout(ms)), |reply| reply)
}
async fn handshake(stream: &mut TcpStream) -> JdwpResult<()> {
debug!("Performing JDWP handshake");
stream.write_all(JDWP_HANDSHAKE).await?;
stream.flush().await?;
let mut buf = vec![0u8; JDWP_HANDSHAKE.len()];
stream.read_exact(&mut buf).await?;
if buf != JDWP_HANDSHAKE {
warn!("Invalid handshake response: {:?}", buf);
return Err(JdwpError::InvalidHandshake);
}
info!("JDWP handshake successful");
Ok(())
}
pub async fn send_command(&mut self, packet: CommandPacket) -> JdwpResult<ReplyPacket> {
debug!("Sending command packet id={}", packet.id);
self.round_trips.fetch_add(1, Ordering::SeqCst);
self.event_loop.send_command(packet).await
}
#[must_use]
pub fn round_trips(&self) -> u32 {
self.round_trips.load(Ordering::SeqCst)
}
pub async fn read_independently(&self, packets: Vec<CommandPacket>) -> Vec<JdwpResult<ReplyPacket>> {
let waves = packets.len().div_ceil(MAX_READS_IN_FLIGHT);
self.round_trips.fetch_add(u32::try_from(waves).unwrap_or(u32::MAX), Ordering::SeqCst);
let mut results: Vec<JdwpResult<ReplyPacket>> = Vec::with_capacity(packets.len());
let mut window: VecDeque<InFlight> = VecDeque::with_capacity(MAX_READS_IN_FLIGHT);
for packet in packets {
if window.len() >= MAX_READS_IN_FLIGHT {
if let Some(oldest) = window.pop_front() {
results.push(oldest.reply().await);
}
}
match self.event_loop.issue(packet).await {
Ok(in_flight) => window.push_back(in_flight),
Err(e) => {
while let Some(in_flight) = window.pop_front() {
results.push(in_flight.reply().await);
}
results.push(Err(e));
}
}
}
while let Some(in_flight) = window.pop_front() {
results.push(in_flight.reply().await);
}
results
}
pub async fn try_recv_event(&self) -> Option<EventSet> {
self.event_loop.try_recv_event().await
}
pub async fn recv_event(&self) -> Option<EventSet> {
self.event_loop.recv_event().await
}
#[must_use]
pub fn next_id(&self) -> u32 {
self.next_id.fetch_add(1, Ordering::SeqCst)
}
#[must_use]
pub fn packets_sent(&self) -> u32 {
self.next_id.load(Ordering::SeqCst).saturating_sub(1)
}
pub(crate) fn types(&self) -> &TypeCache {
&self.types
}
}
#[derive(Debug, Default)]
pub(crate) struct TypeCache {
signatures: Mutex<HashMap<ReferenceTypeId, String>>,
fields: Mutex<HashMap<ReferenceTypeId, Vec<FieldInfo>>>,
methods: Mutex<HashMap<ReferenceTypeId, Vec<MethodInfo>>>,
superclasses: Mutex<HashMap<ClassId, Option<ClassId>>>,
interfaces: Mutex<HashMap<ReferenceTypeId, Vec<ReferenceTypeId>>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CachedSuperclass {
Unknown,
Root,
Parent(ClassId),
}
macro_rules! guard {
($lock:expr) => {
$lock.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
};
}
impl TypeCache {
pub(crate) fn signature(&self, id: ReferenceTypeId) -> Option<String> {
guard!(self.signatures).get(&id).cloned()
}
pub(crate) fn put_signature(&self, id: ReferenceTypeId, sig: &str) {
guard!(self.signatures).insert(id, sig.to_string());
}
pub(crate) fn fields(&self, id: ReferenceTypeId) -> Option<Vec<FieldInfo>> {
guard!(self.fields).get(&id).cloned()
}
pub(crate) fn put_fields(&self, id: ReferenceTypeId, fields: &[FieldInfo]) {
guard!(self.fields).insert(id, fields.to_vec());
}
pub(crate) fn methods(&self, id: ReferenceTypeId) -> Option<Vec<MethodInfo>> {
guard!(self.methods).get(&id).cloned()
}
pub(crate) fn put_methods(&self, id: ReferenceTypeId, methods: &[MethodInfo]) {
guard!(self.methods).insert(id, methods.to_vec());
}
pub(crate) fn superclass(&self, id: ClassId) -> CachedSuperclass {
match guard!(self.superclasses).get(&id) {
None => CachedSuperclass::Unknown,
Some(None) => CachedSuperclass::Root,
Some(&Some(parent)) => CachedSuperclass::Parent(parent),
}
}
pub(crate) fn put_superclass(&self, id: ClassId, parent: Option<ClassId>) {
guard!(self.superclasses).insert(id, parent);
}
pub(crate) fn invalidate(&self, id: ReferenceTypeId) {
guard!(self.signatures).remove(&id);
guard!(self.fields).remove(&id);
guard!(self.methods).remove(&id);
guard!(self.superclasses).remove(&id);
guard!(self.interfaces).remove(&id);
}
pub(crate) fn interfaces(&self, id: ReferenceTypeId) -> Option<Vec<ReferenceTypeId>> {
guard!(self.interfaces).get(&id).cloned()
}
pub(crate) fn put_interfaces(&self, id: ReferenceTypeId, ifaces: &[ReferenceTypeId]) {
guard!(self.interfaces).insert(id, ifaces.to_vec());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::{HEADER_SIZE, REPLY_FLAG};
fn field(name: &str) -> FieldInfo {
FieldInfo {
field_id: 1,
name: name.to_string(),
signature: "I".to_string(),
generic_signature: None,
mod_bits: 0,
}
}
#[test]
fn type_cache_round_trips_each_kind() {
let c = TypeCache::default();
assert_eq!(c.signature(7), None);
c.put_signature(7, "Lcom/x/Foo;");
assert_eq!(c.signature(7).as_deref(), Some("Lcom/x/Foo;"));
assert!(c.fields(7).is_none());
c.put_fields(7, &[field("a"), field("b")]);
assert_eq!(c.fields(7).map(|f| f.len()), Some(2));
assert!(c.methods(7).is_none());
c.put_methods(
7,
&[MethodInfo {
method_id: 2,
name: "m".to_string(),
signature: "()V".to_string(),
generic_signature: None,
mod_bits: 0,
}],
);
assert_eq!(c.methods(7).map(|m| m.len()), Some(1));
assert_eq!(c.signature(8), None);
assert!(c.fields(8).is_none());
assert!(c.interfaces(7).is_none());
c.put_interfaces(7, &[11, 12]);
assert_eq!(c.interfaces(7), Some(vec![11, 12]));
c.put_interfaces(8, &[]);
assert_eq!(c.interfaces(8), Some(vec![]), "\"implements nothing\" must cache as Some(empty)");
}
#[test]
fn type_cache_distinguishes_root_from_uncached() {
let c = TypeCache::default();
assert_eq!(c.superclass(1), CachedSuperclass::Unknown);
c.put_superclass(1, None);
assert_eq!(c.superclass(1), CachedSuperclass::Root);
c.put_superclass(2, Some(1));
assert_eq!(c.superclass(2), CachedSuperclass::Parent(1));
}
#[test]
fn test_next_id() {
let counter = AtomicU32::new(1);
assert_eq!(counter.fetch_add(1, Ordering::SeqCst), 1);
assert_eq!(counter.fetch_add(1, Ordering::SeqCst), 2);
assert_eq!(counter.fetch_add(1, Ordering::SeqCst), 3);
}
async fn deaf_jdwp_peer() -> u16 {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind a loopback port");
let port = listener.local_addr().expect("read back the bound port").port();
tokio::spawn(async move {
if let Ok((mut socket, _)) = listener.accept().await {
let mut buf = vec![0u8; JDWP_HANDSHAKE.len()];
if socket.read_exact(&mut buf).await.is_ok() {
let _ = socket.write_all(JDWP_HANDSHAKE).await;
let _ = socket.flush().await;
}
std::future::pending::<()>().await;
}
});
port
}
const REFUSAL_BUDGET: std::time::Duration = std::time::Duration::from_millis(500);
#[tokio::test]
async fn a_read_only_connection_refuses_a_redefinition_without_sending_a_packet() {
let port = deaf_jdwp_peer().await;
let mut conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
conn.set_read_only(true);
let before = conn.packets_sent();
let err =
tokio::time::timeout(REFUSAL_BUDGET, conn.redefine_classes(&[(1, vec![0xCA, 0xFE, 0xBA, 0xBE])]))
.await
.expect("no refusal: the packet went to the peer and this is waiting for a reply")
.expect_err("a read-only connection must refuse a class redefinition");
assert!(matches!(err, JdwpError::ReadOnly(_)), "expected ReadOnly, got {err:?}");
assert_eq!(conn.packets_sent(), before, "refused, but the bytes went out anyway");
}
#[tokio::test]
async fn a_read_only_connection_refuses_a_frame_pop_without_sending_a_packet() {
let port = deaf_jdwp_peer().await;
let mut conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
conn.set_read_only(true);
let before = conn.packets_sent();
let err = tokio::time::timeout(REFUSAL_BUDGET, conn.pop_frames(1, 2))
.await
.expect("no refusal: the packet went to the peer and this is waiting for a reply")
.expect_err("a read-only connection must refuse a frame pop");
assert!(matches!(err, JdwpError::ReadOnly(_)), "expected ReadOnly, got {err:?}");
assert_eq!(conn.packets_sent(), before, "refused, but the bytes went out anyway");
}
#[tokio::test]
async fn the_same_primitives_send_when_the_connection_is_writable() {
let port = deaf_jdwp_peer().await;
let conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
assert!(!conn.is_read_only(), "a fresh connection must not be read-only");
let budget = std::time::Duration::from_millis(250);
let mut a = conn.clone();
let defs = [(1, vec![0xCA, 0xFE, 0xBA, 0xBE])];
let before = a.packets_sent();
assert!(
tokio::time::timeout(budget, a.redefine_classes(&defs)).await.is_err(),
"a writable connection must get past the guard and wait for the peer's reply"
);
assert_eq!(a.packets_sent(), before + 1, "it waited without having sent anything");
let mut b = conn.clone();
let before = b.packets_sent();
assert!(
tokio::time::timeout(budget, b.pop_frames(1, 2)).await.is_err(),
"a writable connection must get past the guard and wait for the peer's reply"
);
assert_eq!(b.packets_sent(), before + 1, "it waited without having sent anything");
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Answers {
InOrder,
Backwards,
}
const INVALID_OBJECT: u16 = 20;
const WAVE_BUDGET: std::time::Duration = std::time::Duration::from_secs(5);
const WITHHELD: usize = 8;
async fn wave_peer(withhold: usize, answers: Answers, fail_nth: Option<usize>) -> u16 {
assert!(
withhold <= MAX_READS_IN_FLIGHT,
"a peer withholding more than the client's window would deadlock rather than fail; \
lowering MAX_READS_IN_FLIGHT below {withhold} needs this test rethought, not retimed"
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind a loopback port");
let port = listener.local_addr().expect("read back the bound port").port();
tokio::spawn(async move {
let Ok((mut socket, _)) = listener.accept().await else { return };
let mut hs = vec![0u8; JDWP_HANDSHAKE.len()];
if socket.read_exact(&mut hs).await.is_err() {
return;
}
let _ = socket.write_all(JDWP_HANDSHAKE).await;
let _ = socket.flush().await;
let mut ids = Vec::with_capacity(withhold);
for _ in 0..withhold {
match read_command_id(&mut socket).await {
Some(id) => ids.push(id),
None => return,
}
}
let mut order: Vec<usize> = (0..ids.len()).collect();
if answers == Answers::Backwards {
order.reverse();
}
for nth in order {
if answer(&mut socket, ids[nth], fail_nth == Some(nth)).await.is_none() {
return;
}
}
while let Some(id) = read_command_id(&mut socket).await {
if answer(&mut socket, id, false).await.is_none() {
return;
}
}
});
port
}
async fn read_command_id(socket: &mut tokio::net::TcpStream) -> Option<u32> {
let mut header = [0u8; HEADER_SIZE];
socket.read_exact(&mut header).await.ok()?;
let length = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize;
let id = u32::from_be_bytes([header[4], header[5], header[6], header[7]]);
let mut rest = vec![0u8; length.saturating_sub(HEADER_SIZE)];
if !rest.is_empty() {
socket.read_exact(&mut rest).await.ok()?;
}
Some(id)
}
async fn answer(socket: &mut tokio::net::TcpStream, id: u32, fail: bool) -> Option<()> {
let error: u16 = if fail { INVALID_OBJECT } else { 0 };
let payload = if fail { Vec::new() } else { id.to_be_bytes().repeat(4) };
let total = u32::try_from(HEADER_SIZE + payload.len()).unwrap_or(u32::MAX);
let mut reply = Vec::with_capacity(HEADER_SIZE + payload.len());
reply.extend_from_slice(&total.to_be_bytes());
reply.extend_from_slice(&id.to_be_bytes());
reply.push(REPLY_FLAG);
reply.extend_from_slice(&error.to_be_bytes());
reply.extend_from_slice(&payload);
socket.write_all(&reply).await.ok()?;
socket.flush().await.ok()
}
fn wave(conn: &JdwpConnection, n: usize) -> Vec<CommandPacket> {
(0..n).map(|_| CommandPacket::new(conn.next_id(), 9, 1)).collect()
}
#[tokio::test]
async fn every_reply_is_matched_to_its_own_request_when_they_arrive_backwards() {
let port = wave_peer(WITHHELD, Answers::Backwards, None).await;
let conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
let packets = wave(&conn, MAX_READS_IN_FLIGHT + 4);
let ids: Vec<u32> = packets.iter().map(|p| p.id).collect();
let replies = tokio::time::timeout(WAVE_BUDGET, conn.read_independently(packets))
.await
.expect("a wave the peer answers in full must not need the whole budget");
assert_eq!(replies.len(), ids.len(), "one result per command, always");
for (nth, (reply, id)) in replies.into_iter().zip(ids).enumerate() {
let reply = reply.unwrap_or_else(|e| panic!("command {nth} (id {id}) was not answered: {e:?}"));
assert_eq!(reply.id, id, "result {nth} carries the wrong reply");
assert_eq!(
reply.data(),
&id.to_be_bytes().repeat(4)[..],
"result {nth} carries the right id with another request's payload, which is the \
conflation the id alone cannot catch"
);
}
}
#[tokio::test]
async fn a_failure_inside_a_wave_leaves_its_siblings_and_the_stream_intact() {
let wave_size = WITHHELD;
let failing = 2;
let port = wave_peer(wave_size, Answers::Backwards, Some(failing)).await;
let conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
let packets = wave(&conn, wave_size);
let ids: Vec<u32> = packets.iter().map(|p| p.id).collect();
let replies = tokio::time::timeout(WAVE_BUDGET, conn.read_independently(packets))
.await
.expect("a failing command must not stall the wave it is in");
assert_eq!(replies.len(), wave_size, "one result per command, including the failing one");
for (nth, (reply, id)) in replies.into_iter().zip(&ids).enumerate() {
let reply = reply.unwrap_or_else(|e| panic!("command {nth} was not answered at all: {e:?}"));
assert_eq!(reply.id, *id, "result {nth} carries the wrong reply");
if nth == failing {
let err = reply.check_error().expect_err("the failing command must report its failure");
assert!(
matches!(err, JdwpError::JdwpErrorCode(code, _) if code == INVALID_OBJECT),
"the JVM's own error code is the diagnosis and must survive the wave: {err:?}"
);
} else {
reply.check_error().unwrap_or_else(|e| {
panic!("command {nth} was collateral damage from command {failing}'s failure: {e:?}")
});
}
}
let mut after = conn.clone();
let probe = CommandPacket::new(after.next_id(), 9, 1);
let id = probe.id;
let reply = tokio::time::timeout(WAVE_BUDGET, after.send_command(probe))
.await
.expect("the connection must still answer after a wave containing a failure")
.expect("a desynchronised stream is a dead session, not a slow one");
assert_eq!(reply.id, id, "the reply after the wave belongs to the command after the wave");
assert_eq!(reply.data(), &id.to_be_bytes().repeat(4)[..], "framing survived the wave");
}
#[tokio::test]
async fn a_wave_costs_exactly_one_packet_per_read() {
let port = wave_peer(4, Answers::InOrder, None).await;
let conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
let before = conn.packets_sent();
let replies = tokio::time::timeout(WAVE_BUDGET, conn.read_independently(wave(&conn, 4)))
.await
.expect("four reads answered in order must not need the whole budget");
assert_eq!(replies.len(), 4);
assert_eq!(
conn.packets_sent() - before,
4,
"PERF-1 buys round trips, not packets — a different packet count here would invalidate \
every bound asserted in mcp_integration.rs"
);
}
#[tokio::test]
async fn read_only_set_on_one_handle_refuses_on_a_clone() {
let port = deaf_jdwp_peer().await;
let conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
let mut clone = conn.clone();
conn.set_read_only(true);
let err = clone.redefine_classes(&[(1, vec![])]).await.expect_err("the clone must refuse too");
assert!(matches!(err, JdwpError::ReadOnly(_)), "expected ReadOnly, got {err:?}");
}
}