jdwp_client/connection.rs
1// JDWP connection management
2//
3// Handles TCP connection, handshake, and event loop startup
4
5use crate::eventloop::{spawn_event_loop, EventLoopHandle, InFlight};
6use crate::events::EventSet;
7use crate::protocol::{CommandPacket, JdwpError, JdwpResult, ReplyPacket, JDWP_HANDSHAKE};
8use crate::reftype::{FieldInfo, MethodInfo};
9use crate::types::{ClassId, ReferenceTypeId};
10use std::collections::{HashMap, VecDeque};
11use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
12use std::sync::{Arc, Mutex};
13use tokio::io::{AsyncReadExt, AsyncWriteExt};
14use tokio::net::TcpStream;
15use tracing::{debug, info, warn};
16
17/// Default budget for a debuggee invocation, in milliseconds.
18///
19/// A `toString()` that cannot answer in two seconds is not worth freezing a shared JVM for, and the
20/// alternative measured 30-40s against a real `WildFly` before the event loop's generic reply timeout gave
21/// up. Deliberately far below that timeout so an invocation is bounded by *this* budget, not by it.
22pub const DEFAULT_INVOKE_TIMEOUT_MS: u64 = 2000;
23
24/// How many commands [`JdwpConnection::read_independently`] may leave unanswered at once (PERF-1, #100).
25///
26/// **Named for [`InFlight`] rather than for a window, and it was `INDEPENDENT_READ_WINDOW` first.** Every
27/// other "… window" in this codebase is a span of TIME — a capture window, a suspension window, an
28/// observation window, the escalation window, the window in which a watchpoint's old value is still readable
29/// — and this is a count of concurrent commands. Two axes on one word, in a `pub` constant, is the collision
30/// `batch` already cost this project once (see the **independent reads** entry's `_Avoid_`). Renamed while
31/// nothing was pinned to it: it went public unreleased, and `CONTEXT.md`'s own VOCAB-1 passage is that the
32/// window for doing this cheaply does not reopen.
33///
34/// **It is a safety bound before it is a tuning knob, and the thing it makes impossible is a deadlock.**
35/// The cycle to rule out: the event loop blocks writing a command because the JVM has stopped reading;
36/// the JVM has stopped reading because it is blocked writing replies; it is blocked writing because our
37/// receive buffer is full and the reader task is parked on a full [`PACKET_CHANNEL_DEPTH`](
38/// crate::eventloop) channel; and the reader is parked because the loop — blocked in that write — is not
39/// draining it. Every arrow there is real. What breaks it is that the loop can only block in a write once
40/// the *send* buffer fills, and a JDWP command is 11-43 bytes: sixteen of them is under a kilobyte
41/// against a send buffer of at least sixteen. A window of one thousand — which a caller expanding a
42/// thousand-element collection would otherwise ask for — is a different conversation.
43///
44/// It is also the memory bound on buffered replies, which is the other reason it is not the caller's
45/// list length: a reply may be up to `MAX_PACKET_SIZE`, so the window is the ceiling on how much of the
46/// debuggee's heap can be sitting in oneshot channels at once. Sixteen small reads is nothing; sixteen
47/// `AllClasses` replies would be 160MB, and nothing converted to this path reads anything of that shape.
48///
49/// **Sixteen also caps the win**, since `n` reads cost `ceil(n / 16)` round trips rather than one. That is
50/// the trade and it is deliberately on the safe side of it: sixteen-fold is already most of the available
51/// fan-out on the reads PERF-1 names, and the numbers above stop being reassuring well before a window
52/// large enough to matter more.
53///
54/// **Public because a caller with a deadline has to chunk by it.** A dump checks its suspension budget
55/// between threads, and nothing can interrupt one call to `read_independently`; chunking the caller's list
56/// by this hands the budget back every window at no cost in time, since a window takes about as long as one
57/// sequential read. That is the only reason a tuning constant is on the public surface.
58pub const MAX_READS_IN_FLIGHT: usize = 16;
59
60#[derive(Clone, Debug)]
61pub struct JdwpConnection {
62 event_loop: EventLoopHandle,
63 next_id: Arc<AtomicU32>,
64 /// Shared across clones on purpose — the event-pump clone and the request path describe the same
65 /// JVM, so they should warm one cache rather than two.
66 types: Arc<TypeCache>,
67 /// Read-only guard: when set, every primitive that mutates the debuggee refuses instead of sending.
68 /// `Arc` so it is shared with every clone — including the event pump's, which is what evaluates a
69 /// breakpoint condition or a `trace_expr` on a hit.
70 read_only: Arc<AtomicBool>,
71 /// How long a debuggee invocation may take before it is abandoned, in milliseconds.
72 ///
73 /// Separate from the event loop's generic reply timeout, which is 30s and swept every 10s — far too
74 /// long for a `toString()` used to render a value, and measured freezing a real `WildFly` for 30-40s
75 /// when the invoked method could never complete. `Arc` for the same reason as `read_only`: the event
76 /// pump's clone renders trace snapshots and must obey the same budget.
77 invoke_timeout_ms: Arc<AtomicU64>,
78 /// How many round trips this connection has waited for — see [`round_trips`](Self::round_trips).
79 round_trips: Arc<AtomicU32>,
80}
81
82impl JdwpConnection {
83 /// Connect to a JVM via JDWP
84 ///
85 /// # Errors
86 /// Returns a [`JdwpError`] if the TCP connection or JDWP handshake fails.
87 pub async fn connect(host: &str, port: u16) -> JdwpResult<Self> {
88 info!("Connecting to JDWP at {}:{}", host, port);
89
90 let mut stream = TcpStream::connect((host, port)).await?;
91
92 // Perform JDWP handshake
93 Self::handshake(&mut stream).await?;
94
95 // Split stream and spawn event loop
96 let (reader, writer) = stream.into_split();
97 let event_loop = spawn_event_loop(reader, writer);
98
99 Ok(Self {
100 event_loop,
101 next_id: Arc::new(AtomicU32::new(1)),
102 types: Arc::new(TypeCache::default()),
103 read_only: Arc::new(AtomicBool::new(false)),
104 invoke_timeout_ms: Arc::new(AtomicU64::new(DEFAULT_INVOKE_TIMEOUT_MS)),
105 round_trips: Arc::new(AtomicU32::new(0)),
106 })
107 }
108
109 /// Refuse every mutation of the debuggee on this connection from now on (and on every clone of it).
110 ///
111 /// This is the enforcement point for read-only debugging, and per ADR-0001 it is the **only** one:
112 /// the MCP layer above does not decide what counts as mutation, the wire does. Every primitive that
113 /// changes the debuggee returns [`JdwpError::ReadOnly`] instead of sending its packet — the two
114 /// invocations, the four writes, a forced early return, and, since SAFE-9, a class redefinition and
115 /// a frame pop. It deliberately does **not** restrict reads — fields, locals, arrays and type
116 /// metadata are all plain JDWP reads and keep working.
117 ///
118 /// "Mutation" here is wider than "runs code". A class redefinition invokes nothing, writes no field
119 /// and forces no return, yet replaces the running program — and unlike every other entry on that
120 /// list it outlives the connection, so it is the one that least tolerates being missed.
121 ///
122 /// A guard against accident, **not** a security boundary: anyone who can reach the JDWP port can
123 /// open their own connection without it.
124 pub fn set_read_only(&self, read_only: bool) {
125 self.read_only.store(read_only, Ordering::SeqCst);
126 }
127
128 /// Whether this connection refuses to mutate the debuggee.
129 #[must_use]
130 pub fn is_read_only(&self) -> bool {
131 self.read_only.load(Ordering::SeqCst)
132 }
133
134 /// Fail with [`JdwpError::ReadOnly`] if mutating the debuggee is not allowed. `what` is a noun phrase
135 /// naming the operation for the message (e.g. `"an instance method invocation"`, `"a class
136 /// redefinition"`), because five of this guard's call sites are writes rather than calls and a
137 /// message built around "invoke" was already wrong for them.
138 ///
139 /// Named for mutation rather than invocation on purpose: the narrower name is what let SAFE-9's two
140 /// primitives be added without anyone noticing they had skipped the guard entirely.
141 pub(crate) fn guard_mutation(&self, what: &str) -> JdwpResult<()> {
142 if self.is_read_only() {
143 return Err(JdwpError::ReadOnly(what.to_string()));
144 }
145 Ok(())
146 }
147
148 /// Set how long a debuggee invocation may take before it is abandoned. `0` disables the budget.
149 pub fn set_invoke_timeout_ms(&self, ms: u64) {
150 self.invoke_timeout_ms.store(ms, Ordering::SeqCst);
151 }
152
153 /// The current invocation budget in milliseconds; `0` means unbounded.
154 #[must_use]
155 pub fn invoke_timeout_ms(&self) -> u64 {
156 self.invoke_timeout_ms.load(Ordering::SeqCst)
157 }
158
159 /// Send an invocation command under the invocation budget.
160 ///
161 /// On expiry this returns [`JdwpError::InvokeTimeout`] and stops waiting — it does **not** cancel the
162 /// call, because JDWP has no way to. The debuggee thread stays where it is until something resumes the
163 /// VM. What this buys is that the *caller* gets control back in a bounded time and can say why, instead
164 /// of blocking for 30-40s and then reporting a value that looks like it cost nothing.
165 pub(crate) async fn send_invoke(&mut self, packet: CommandPacket) -> JdwpResult<ReplyPacket> {
166 let ms = self.invoke_timeout_ms();
167 if ms == 0 {
168 return self.send_command(packet).await;
169 }
170 tokio::time::timeout(std::time::Duration::from_millis(ms), self.send_command(packet))
171 .await
172 .map_or(Err(JdwpError::InvokeTimeout(ms)), |reply| reply)
173 }
174
175 /// Perform JDWP handshake
176 async fn handshake(stream: &mut TcpStream) -> JdwpResult<()> {
177 debug!("Performing JDWP handshake");
178
179 // Send handshake
180 stream.write_all(JDWP_HANDSHAKE).await?;
181 stream.flush().await?;
182
183 // Receive handshake response
184 let mut buf = vec![0u8; JDWP_HANDSHAKE.len()];
185 stream.read_exact(&mut buf).await?;
186
187 if buf != JDWP_HANDSHAKE {
188 warn!("Invalid handshake response: {:?}", buf);
189 return Err(JdwpError::InvalidHandshake);
190 }
191
192 info!("JDWP handshake successful");
193 Ok(())
194 }
195
196 /// Send a command and wait for reply
197 ///
198 /// # Errors
199 /// Returns a [`JdwpError`] if the JDWP request fails or the reply cannot be parsed.
200 pub async fn send_command(&mut self, packet: CommandPacket) -> JdwpResult<ReplyPacket> {
201 debug!("Sending command packet id={}", packet.id);
202 self.round_trips.fetch_add(1, Ordering::SeqCst);
203 self.event_loop.send_command(packet).await
204 }
205
206 /// How many round trips this connection has waited for.
207 ///
208 /// **The second cost figure, and PERF-1 (#100) is why there are two.** A packet count says what was put
209 /// on the wire; this says how many times the wire was *waited on*, and until independent reads existed
210 /// the two were the same number. They are not any more: a wave of sixteen reads is sixteen packets and
211 /// about one round trip, so on a remote JVM this is the figure that predicts the wait and the packet
212 /// count is the figure that predicts nothing about it.
213 ///
214 /// **Derived from the window, not observed on the socket, and the difference is worth knowing.** A
215 /// single read counts one. A wave of `n` counts `ceil(n / MAX_READS_IN_FLIGHT)`, because at most a
216 /// window's worth can be outstanding at once — so `n` reads cannot take fewer sequential batches than
217 /// that, and the sliding window reaches the bound within one. It is therefore a **tight lower bound**
218 /// rather than a measurement, which is why every reply that prints it prints it with a `~`.
219 #[must_use]
220 pub fn round_trips(&self) -> u32 {
221 self.round_trips.load(Ordering::SeqCst)
222 }
223
224 /// Issue **independent reads** together and return one result per command, in the order given.
225 ///
226 /// The term is `CONTEXT.md`'s and it names a *licence*, not a mechanism: these commands' requests must
227 /// not depend on each other's replies. That is a property of the sequence and has to be established at
228 /// the call site — nothing here can check it, and this doc comment is not permission. ADR-0038 records
229 /// what the licence rests on; three real sequences in this server do **not** have it.
230 ///
231 /// **What it buys is round trips, not packets.** Every command still gets its own id from the same
232 /// counter, so [`packets_sent`](Self::packets_sent) is unchanged and the packet-bound tests are
233 /// unaffected by construction. What changes is that `n` reads cost about one round trip instead of
234 /// `n`, and — where the reads happen under a suspension — the suspension is shorter by the difference.
235 /// On loopback that difference is nearly nothing; it is a remote JVM this exists for.
236 ///
237 /// **Every reply is awaited, including after one has failed.** There is no first-error-wins arm and
238 /// that is deliberate: the commands are already on the wire and JDWP has no way to recall one, so
239 /// abandoning the wait would abandon only the *answer* while the JVM did the work anyway. A caller
240 /// wanting to stop at the first failure can do that to the returned `Vec` at no cost to the wire. A
241 /// failed command therefore cannot desynchronise its siblings — see
242 /// [`InFlight`] for why it cannot desynchronise the stream either.
243 ///
244 /// Results are positional: `result[i]` answers `packets[i]`, whether it succeeded, failed at the JVM,
245 /// or was never written. An error reply is `Ok` here and carries its error code, exactly as
246 /// [`send_command`](Self::send_command) returns it; the caller still owes it a
247 /// [`check_error`](ReplyPacket::check_error).
248 pub async fn read_independently(&self, packets: Vec<CommandPacket>) -> Vec<JdwpResult<ReplyPacket>> {
249 // Counted before anything is issued, from the window rather than from the socket — see
250 // [`round_trips`](Self::round_trips) for why that is a bound and not a measurement.
251 let waves = packets.len().div_ceil(MAX_READS_IN_FLIGHT);
252 self.round_trips.fetch_add(u32::try_from(waves).unwrap_or(u32::MAX), Ordering::SeqCst);
253 let mut results: Vec<JdwpResult<ReplyPacket>> = Vec::with_capacity(packets.len());
254 // Awaited in issue order, which costs nothing: each reply has its own channel, so a reply that
255 // arrives out of order is already sitting there when its turn comes. The window is what bounds
256 // how far ahead of `results` this may run.
257 let mut window: VecDeque<InFlight> = VecDeque::with_capacity(MAX_READS_IN_FLIGHT);
258
259 for packet in packets {
260 if window.len() >= MAX_READS_IN_FLIGHT {
261 if let Some(oldest) = window.pop_front() {
262 results.push(oldest.reply().await);
263 }
264 }
265 match self.event_loop.issue(packet).await {
266 Ok(in_flight) => window.push_back(in_flight),
267 // The loop is gone, so nothing after this will be issued either — but the window still
268 // holds commands that were, and they are owed their answers. Draining it before pushing
269 // the error is what keeps `results[i]` answering `packets[i]`: every command ahead of
270 // this one in the list is also ahead of it in the window.
271 Err(e) => {
272 while let Some(in_flight) = window.pop_front() {
273 results.push(in_flight.reply().await);
274 }
275 results.push(Err(e));
276 }
277 }
278 }
279
280 while let Some(in_flight) = window.pop_front() {
281 results.push(in_flight.reply().await);
282 }
283
284 results
285 }
286
287 /// Try to receive an event without blocking.
288 ///
289 /// Returns `None` immediately if no events are available in the queue.
290 /// This is useful for polling events without blocking the current task.
291 ///
292 /// # Example
293 /// ```no_run
294 /// # async fn demo(connection: jdwp_client::JdwpConnection) {
295 /// if let Some(event) = connection.try_recv_event().await {
296 /// // Handle event
297 /// }
298 /// # }
299 /// ```
300 pub async fn try_recv_event(&self) -> Option<EventSet> {
301 self.event_loop.try_recv_event().await
302 }
303
304 /// Wait for the next event (blocking).
305 ///
306 /// This method blocks until an event is available or the event channel is closed.
307 /// Use this when you want to wait for events like breakpoints or exceptions.
308 ///
309 /// Returns `None` if the event loop has shut down.
310 ///
311 /// # Example
312 /// ```no_run
313 /// # async fn demo(connection: jdwp_client::JdwpConnection) {
314 /// while let Some(event) = connection.recv_event().await {
315 /// // Process event
316 /// }
317 /// # }
318 /// ```
319 pub async fn recv_event(&self) -> Option<EventSet> {
320 self.event_loop.recv_event().await
321 }
322
323 /// Generate next packet ID
324 #[must_use]
325 pub fn next_id(&self) -> u32 {
326 self.next_id.fetch_add(1, Ordering::SeqCst)
327 }
328
329 /// How many command packets this connection has issued.
330 ///
331 /// The measurement instrument for anything that claims to cut JVM round trips. Wall-clock is the
332 /// wrong tool over loopback, where a round trip is sub-millisecond and noise swamps the signal —
333 /// that mistake is why the type cache first looked like it did nothing (0.98s → 0.95s). Packet
334 /// count is what actually differs on a remote JVM, and it is deterministic.
335 ///
336 /// Every command takes exactly one id from the same counter, so the difference across an operation
337 /// is its packet cost. Events are pushed by the JVM and never counted here.
338 #[must_use]
339 pub fn packets_sent(&self) -> u32 {
340 // -1 because ids start at 1: nothing has been sent when the next id is still 1.
341 self.next_id.load(Ordering::SeqCst).saturating_sub(1)
342 }
343
344 /// This connection's type-metadata cache. Used by the `get_signature` / `get_fields` /
345 /// `get_methods` / `get_superclass` implementations in the sibling modules.
346 pub(crate) fn types(&self) -> &TypeCache {
347 &self.types
348 }
349}
350
351/// Per-connection cache of a loaded type's **immutable** metadata: its signature, declared fields,
352/// declared methods, and superclass.
353///
354/// Worth caching because object inspection asks the same questions over and over — walking a
355/// superclass chain to find one field, or scoring method overloads, re-reads the same field and method
356/// lists for every object of that type. Expanding a collection of 20 elements asked the JVM for the
357/// same element type's fields 20 times.
358///
359/// Values are deliberately **not** cached: a field's contents change as the program runs, so a cached
360/// value would be a lie. Only the shape of the type is cached, and a loaded type's shape is fixed.
361///
362/// Two ways this could go stale, and they are no longer treated the same:
363/// - **Class unload** — the type id becomes invalid and we would serve metadata for a type that no
364/// longer exists. Any actual *use* of it (reading a field by its id) fails at the JVM anyway.
365/// - **`RedefineClasses` / `HotSwap`** — changes methods, and could change fields. This crate used to
366/// say it "never calls it, but another debugger attached to the same JVM could"; SWAP-1 (#58) makes it
367/// the caller, so [`redefine_classes`](JdwpConnection::redefine_classes) now [`invalidate`](TypeCache::invalidate)s
368/// each redefined type on success. A *second* debugger swapping classes underneath us is still
369/// unhandled and still only fixed by reattaching, since the cache belongs to the connection.
370#[derive(Debug, Default)]
371pub(crate) struct TypeCache {
372 signatures: Mutex<HashMap<ReferenceTypeId, String>>,
373 fields: Mutex<HashMap<ReferenceTypeId, Vec<FieldInfo>>>,
374 methods: Mutex<HashMap<ReferenceTypeId, Vec<MethodInfo>>>,
375 /// `None` means "this type has no superclass" (`java.lang.Object`), which is itself worth caching —
376 /// it's the terminator of every superclass walk in the crate.
377 superclasses: Mutex<HashMap<ClassId, Option<ClassId>>>,
378 /// **Direct** superinterfaces, as JDWP reports them. The transitive set is derived by walking these
379 /// (an interface extends interfaces), and each step is a cache hit after the first visit — which is
380 /// what makes an `instanceof`-style check affordable enough to use during overload resolution.
381 interfaces: Mutex<HashMap<ReferenceTypeId, Vec<ReferenceTypeId>>>,
382}
383
384/// A superclass cache lookup has three outcomes, and conflating the last two would make every
385/// superclass walk re-query the JVM at `java.lang.Object` forever.
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub(crate) enum CachedSuperclass {
388 /// Not cached — ask the JVM.
389 Unknown,
390 /// Cached: this type has no superclass.
391 Root,
392 /// Cached: this is the parent.
393 Parent(ClassId),
394}
395
396// A poisoned lock would mean another thread panicked while holding it. The cache is pure derived data,
397// so recovering the guard and carrying on is strictly better than propagating the panic into a
398// debugging session — the worst case is a stale-free cache miss.
399macro_rules! guard {
400 ($lock:expr) => {
401 $lock.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
402 };
403}
404
405impl TypeCache {
406 pub(crate) fn signature(&self, id: ReferenceTypeId) -> Option<String> {
407 guard!(self.signatures).get(&id).cloned()
408 }
409
410 pub(crate) fn put_signature(&self, id: ReferenceTypeId, sig: &str) {
411 guard!(self.signatures).insert(id, sig.to_string());
412 }
413
414 pub(crate) fn fields(&self, id: ReferenceTypeId) -> Option<Vec<FieldInfo>> {
415 guard!(self.fields).get(&id).cloned()
416 }
417
418 pub(crate) fn put_fields(&self, id: ReferenceTypeId, fields: &[FieldInfo]) {
419 guard!(self.fields).insert(id, fields.to_vec());
420 }
421
422 pub(crate) fn methods(&self, id: ReferenceTypeId) -> Option<Vec<MethodInfo>> {
423 guard!(self.methods).get(&id).cloned()
424 }
425
426 pub(crate) fn put_methods(&self, id: ReferenceTypeId, methods: &[MethodInfo]) {
427 guard!(self.methods).insert(id, methods.to_vec());
428 }
429
430 pub(crate) fn superclass(&self, id: ClassId) -> CachedSuperclass {
431 match guard!(self.superclasses).get(&id) {
432 None => CachedSuperclass::Unknown,
433 Some(None) => CachedSuperclass::Root,
434 Some(&Some(parent)) => CachedSuperclass::Parent(parent),
435 }
436 }
437
438 pub(crate) fn put_superclass(&self, id: ClassId, parent: Option<ClassId>) {
439 guard!(self.superclasses).insert(id, parent);
440 }
441
442 /// Forget everything cached about one type, because its shape may just have changed under us.
443 ///
444 /// Called by [`redefine_classes`](JdwpConnection::redefine_classes). Deliberately drops the
445 /// signature and the superclass/interface entries too, not only the methods a `HotSpot` swap can
446 /// touch: a JVM answering `canUnrestrictedlyRedefineClasses` may change more than `HotSpot` allows,
447 /// and a cache entry that survives *because we assumed the restriction* would be wrong exactly on
448 /// the JVM that lifted it. Dropping four extra entries costs one round trip each if they are asked
449 /// for again.
450 ///
451 /// Note what is NOT here: line tables. ADR-0011 settled that they are cached per dump rather than
452 /// per connection, on this very ground, so there is nothing connection-scoped to invalidate.
453 pub(crate) fn invalidate(&self, id: ReferenceTypeId) {
454 guard!(self.signatures).remove(&id);
455 guard!(self.fields).remove(&id);
456 guard!(self.methods).remove(&id);
457 guard!(self.superclasses).remove(&id);
458 guard!(self.interfaces).remove(&id);
459 }
460
461 pub(crate) fn interfaces(&self, id: ReferenceTypeId) -> Option<Vec<ReferenceTypeId>> {
462 guard!(self.interfaces).get(&id).cloned()
463 }
464
465 pub(crate) fn put_interfaces(&self, id: ReferenceTypeId, ifaces: &[ReferenceTypeId]) {
466 guard!(self.interfaces).insert(id, ifaces.to_vec());
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473 use crate::protocol::{HEADER_SIZE, REPLY_FLAG};
474
475 fn field(name: &str) -> FieldInfo {
476 FieldInfo {
477 field_id: 1,
478 name: name.to_string(),
479 signature: "I".to_string(),
480 generic_signature: None,
481 mod_bits: 0,
482 }
483 }
484
485 // Round-trips every kind the cache holds. Needs no JVM, so it guards the cache's own logic —
486 // notably that a miss and a cached "no superclass" are distinguishable.
487 #[test]
488 fn type_cache_round_trips_each_kind() {
489 let c = TypeCache::default();
490
491 assert_eq!(c.signature(7), None);
492 c.put_signature(7, "Lcom/x/Foo;");
493 assert_eq!(c.signature(7).as_deref(), Some("Lcom/x/Foo;"));
494
495 assert!(c.fields(7).is_none());
496 c.put_fields(7, &[field("a"), field("b")]);
497 assert_eq!(c.fields(7).map(|f| f.len()), Some(2));
498
499 assert!(c.methods(7).is_none());
500 c.put_methods(
501 7,
502 &[MethodInfo {
503 method_id: 2,
504 name: "m".to_string(),
505 signature: "()V".to_string(),
506 generic_signature: None,
507 mod_bits: 0,
508 }],
509 );
510 assert_eq!(c.methods(7).map(|m| m.len()), Some(1));
511
512 // A different type id must not see the first one's entries.
513 assert_eq!(c.signature(8), None);
514 assert!(c.fields(8).is_none());
515
516 // Direct superinterfaces. An empty list is a real answer worth caching — most classes have
517 // none, and the transitive walk asks about every type it passes.
518 assert!(c.interfaces(7).is_none());
519 c.put_interfaces(7, &[11, 12]);
520 assert_eq!(c.interfaces(7), Some(vec![11, 12]));
521 c.put_interfaces(8, &[]);
522 assert_eq!(c.interfaces(8), Some(vec![]), "\"implements nothing\" must cache as Some(empty)");
523 }
524
525 // "cached: no superclass" must not read as "not cached", or the top of every superclass walk would
526 // re-query the JVM forever.
527 #[test]
528 fn type_cache_distinguishes_root_from_uncached() {
529 let c = TypeCache::default();
530 assert_eq!(c.superclass(1), CachedSuperclass::Unknown);
531 c.put_superclass(1, None);
532 assert_eq!(c.superclass(1), CachedSuperclass::Root);
533 c.put_superclass(2, Some(1));
534 assert_eq!(c.superclass(2), CachedSuperclass::Parent(1));
535 }
536
537 #[test]
538 fn test_next_id() {
539 // Test ID counter without creating a real TcpStream
540 let counter = AtomicU32::new(1);
541
542 assert_eq!(counter.fetch_add(1, Ordering::SeqCst), 1);
543 assert_eq!(counter.fetch_add(1, Ordering::SeqCst), 2);
544 assert_eq!(counter.fetch_add(1, Ordering::SeqCst), 3);
545 }
546
547 /// A peer that completes the JDWP handshake and then answers nothing, ever.
548 ///
549 /// That silence is the whole instrument. Every assertion below is that a read-only connection fails
550 /// *before* it sends, so a peer that would reply to a command could not tell a working guard from a
551 /// guard that sent the packet and got an answer. Any test here that hangs has found a missing guard.
552 async fn deaf_jdwp_peer() -> u16 {
553 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind a loopback port");
554 let port = listener.local_addr().expect("read back the bound port").port();
555 tokio::spawn(async move {
556 if let Ok((mut socket, _)) = listener.accept().await {
557 let mut buf = vec![0u8; JDWP_HANDSHAKE.len()];
558 if socket.read_exact(&mut buf).await.is_ok() {
559 let _ = socket.write_all(JDWP_HANDSHAKE).await;
560 let _ = socket.flush().await;
561 }
562 // Hold the socket open and stay silent, so a leaked packet cannot be answered.
563 std::future::pending::<()>().await;
564 }
565 });
566 port
567 }
568
569 /// SAFE-9. These two primitives were gated in the MCP handlers that call them, which ADR-0001
570 /// forbids: the layer above does not decide what counts as mutation. The regression this test exists
571 /// to catch is invisible from an MCP tool test, because the handler's own check would pass it.
572 ///
573 /// `packets_sent()` is the assertion that matters. "Returned an error" would also be satisfied by a
574 /// primitive that sent its packet and then failed; "sent nothing" is the actual contract.
575 /// The timeout is not the assertion — it is what turns a missing guard from a 30-second hang on the
576 /// event loop's reply timeout into an immediate, legible failure. A guard that is present refuses in
577 /// microseconds and never approaches the budget.
578 const REFUSAL_BUDGET: std::time::Duration = std::time::Duration::from_millis(500);
579
580 #[tokio::test]
581 async fn a_read_only_connection_refuses_a_redefinition_without_sending_a_packet() {
582 let port = deaf_jdwp_peer().await;
583 let mut conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
584 conn.set_read_only(true);
585 let before = conn.packets_sent();
586
587 let err =
588 tokio::time::timeout(REFUSAL_BUDGET, conn.redefine_classes(&[(1, vec![0xCA, 0xFE, 0xBA, 0xBE])]))
589 .await
590 .expect("no refusal: the packet went to the peer and this is waiting for a reply")
591 .expect_err("a read-only connection must refuse a class redefinition");
592
593 assert!(matches!(err, JdwpError::ReadOnly(_)), "expected ReadOnly, got {err:?}");
594 assert_eq!(conn.packets_sent(), before, "refused, but the bytes went out anyway");
595 }
596
597 #[tokio::test]
598 async fn a_read_only_connection_refuses_a_frame_pop_without_sending_a_packet() {
599 let port = deaf_jdwp_peer().await;
600 let mut conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
601 conn.set_read_only(true);
602 let before = conn.packets_sent();
603
604 let err = tokio::time::timeout(REFUSAL_BUDGET, conn.pop_frames(1, 2))
605 .await
606 .expect("no refusal: the packet went to the peer and this is waiting for a reply")
607 .expect_err("a read-only connection must refuse a frame pop");
608
609 assert!(matches!(err, JdwpError::ReadOnly(_)), "expected ReadOnly, got {err:?}");
610 assert_eq!(conn.packets_sent(), before, "refused, but the bytes went out anyway");
611 }
612
613 /// The other half of the contract: the flag is what refuses, not the primitive. Without this, a
614 /// primitive hard-wired to fail would pass the two tests above and nobody would notice that read-only
615 /// had stopped being a *mode*.
616 ///
617 /// On a writable connection each primitive gets past the guard, sends, and then waits forever for a
618 /// reply the deaf peer will never send — so the timeout *is* the pass condition, and `packets_sent()`
619 /// proves it timed out with the bytes on the wire rather than somewhere short of it.
620 #[tokio::test]
621 async fn the_same_primitives_send_when_the_connection_is_writable() {
622 let port = deaf_jdwp_peer().await;
623 let conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
624 assert!(!conn.is_read_only(), "a fresh connection must not be read-only");
625 let budget = std::time::Duration::from_millis(250);
626
627 let mut a = conn.clone();
628 let defs = [(1, vec![0xCA, 0xFE, 0xBA, 0xBE])];
629 let before = a.packets_sent();
630 assert!(
631 tokio::time::timeout(budget, a.redefine_classes(&defs)).await.is_err(),
632 "a writable connection must get past the guard and wait for the peer's reply"
633 );
634 assert_eq!(a.packets_sent(), before + 1, "it waited without having sent anything");
635
636 let mut b = conn.clone();
637 let before = b.packets_sent();
638 assert!(
639 tokio::time::timeout(budget, b.pop_frames(1, 2)).await.is_err(),
640 "a writable connection must get past the guard and wait for the peer's reply"
641 );
642 assert_eq!(b.packets_sent(), before + 1, "it waited without having sent anything");
643 }
644
645 /// Which way round [`wave_peer`] answers a wave it has already read in full.
646 #[derive(Clone, Copy, PartialEq, Eq)]
647 enum Answers {
648 InOrder,
649 Backwards,
650 }
651
652 /// `INVALID_OBJECT`, the failure a per-object field read wave actually meets: one element of the
653 /// collection was collected between the read that found it and the read that asked about it.
654 const INVALID_OBJECT: u16 = 20;
655
656 /// Long enough that a working wave is never near it, short enough that a serialised one is reported
657 /// rather than waited on. See [`wave_peer`] for why a hang is the failure mode to guard against.
658 const WAVE_BUDGET: std::time::Duration = std::time::Duration::from_secs(5);
659
660 /// How many commands these tests require to be outstanding at once.
661 ///
662 /// **A literal, and not [`MAX_READS_IN_FLIGHT`], because the negative control failed.** Written
663 /// first as "one window's worth", which reads like the strongest possible demand and is the weakest:
664 /// a peer that withholds a window's worth is satisfied by a window of *one*, and with one command
665 /// outstanding "the replies arrive backwards" describes nothing at all. Setting the window to 1 to
666 /// watch the correlation test fail is how that was found — it passed. Eight is a number the wave tests
667 /// hold the implementation to, rather than a number they read off it.
668 const WITHHELD: usize = 8;
669
670 /// A JDWP peer that reads the first `withhold` commands **before answering any of them**, answers
671 /// those in the order asked for, and then serves anything further one at a time.
672 ///
673 /// Withholding is the instrument rather than a convenience. A client that awaited each reply before
674 /// sending the next command could never get past its first command here, so this peer **cannot be
675 /// satisfied by the serialised path at all**: it does not merely fail to exercise concurrency, it
676 /// withholds every answer until the concurrency is real. That is what makes these tests a control on
677 /// the primitive and not only on the routing table — and it is why `withhold` must never exceed
678 /// [`MAX_READS_IN_FLIGHT`], which is the most the client will leave outstanding.
679 ///
680 /// The tail matters as much as the wave. It is what lets a test send more reads than the window
681 /// holds, and what lets one ask the connection a plain question *afterwards* — the assertion that
682 /// framing survived.
683 ///
684 /// Each reply's payload is its own packet id, four times over. Asserting on the id alone would pass a
685 /// routing table that delivered the right envelope with the wrong letter in it — the conflation
686 /// ADR-0034's decision table met in another form — so the payload has to name its request too.
687 async fn wave_peer(withhold: usize, answers: Answers, fail_nth: Option<usize>) -> u16 {
688 assert!(
689 withhold <= MAX_READS_IN_FLIGHT,
690 "a peer withholding more than the client's window would deadlock rather than fail; \
691 lowering MAX_READS_IN_FLIGHT below {withhold} needs this test rethought, not retimed"
692 );
693 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind a loopback port");
694 let port = listener.local_addr().expect("read back the bound port").port();
695 tokio::spawn(async move {
696 let Ok((mut socket, _)) = listener.accept().await else { return };
697 let mut hs = vec![0u8; JDWP_HANDSHAKE.len()];
698 if socket.read_exact(&mut hs).await.is_err() {
699 return;
700 }
701 let _ = socket.write_all(JDWP_HANDSHAKE).await;
702 let _ = socket.flush().await;
703
704 let mut ids = Vec::with_capacity(withhold);
705 for _ in 0..withhold {
706 match read_command_id(&mut socket).await {
707 Some(id) => ids.push(id),
708 None => return,
709 }
710 }
711
712 let mut order: Vec<usize> = (0..ids.len()).collect();
713 if answers == Answers::Backwards {
714 order.reverse();
715 }
716 for nth in order {
717 if answer(&mut socket, ids[nth], fail_nth == Some(nth)).await.is_none() {
718 return;
719 }
720 }
721
722 // The tail: everything after the withheld wave, answered as it arrives.
723 while let Some(id) = read_command_id(&mut socket).await {
724 if answer(&mut socket, id, false).await.is_none() {
725 return;
726 }
727 }
728 });
729 port
730 }
731
732 /// Read one whole JDWP command and return its packet id, or `None` once the socket is done.
733 async fn read_command_id(socket: &mut tokio::net::TcpStream) -> Option<u32> {
734 let mut header = [0u8; HEADER_SIZE];
735 socket.read_exact(&mut header).await.ok()?;
736 let length = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize;
737 let id = u32::from_be_bytes([header[4], header[5], header[6], header[7]]);
738 // Consumed in full, or the next header would be read out of this command's body — the same
739 // alignment rule the client side lives by (ADR-0018).
740 let mut rest = vec![0u8; length.saturating_sub(HEADER_SIZE)];
741 if !rest.is_empty() {
742 socket.read_exact(&mut rest).await.ok()?;
743 }
744 Some(id)
745 }
746
747 /// Answer one command: its own id as the payload, or [`INVALID_OBJECT`] and no payload — which is
748 /// what a real JVM sends for a failure.
749 async fn answer(socket: &mut tokio::net::TcpStream, id: u32, fail: bool) -> Option<()> {
750 let error: u16 = if fail { INVALID_OBJECT } else { 0 };
751 let payload = if fail { Vec::new() } else { id.to_be_bytes().repeat(4) };
752 let total = u32::try_from(HEADER_SIZE + payload.len()).unwrap_or(u32::MAX);
753 let mut reply = Vec::with_capacity(HEADER_SIZE + payload.len());
754 reply.extend_from_slice(&total.to_be_bytes());
755 reply.extend_from_slice(&id.to_be_bytes());
756 reply.push(REPLY_FLAG);
757 reply.extend_from_slice(&error.to_be_bytes());
758 reply.extend_from_slice(&payload);
759 socket.write_all(&reply).await.ok()?;
760 socket.flush().await.ok()
761 }
762
763 /// The commands a wave test issues. Command set and command are arbitrary — this peer answers by id
764 /// and never looks at either — but they are a real read pair so nothing here implies a write.
765 fn wave(conn: &JdwpConnection, n: usize) -> Vec<CommandPacket> {
766 (0..n).map(|_| CommandPacket::new(conn.next_id(), 9, 1)).collect()
767 }
768
769 /// PERF-1 (#100), the first acceptance criterion: every reply matched to **its own** request, under a
770 /// reply stream deliberately reversed.
771 ///
772 /// The wave is deliberately **larger** than [`MAX_READS_IN_FLIGHT`], so the sliding window's
773 /// pop-oldest path is exercised rather than only the case where everything fits. The peer withholds
774 /// exactly one window's worth and answers those backwards; the four beyond it cannot be outstanding
775 /// at the same time as the first sixteen, and are served by the peer's tail as the window slides.
776 #[tokio::test]
777 async fn every_reply_is_matched_to_its_own_request_when_they_arrive_backwards() {
778 let port = wave_peer(WITHHELD, Answers::Backwards, None).await;
779 let conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
780 let packets = wave(&conn, MAX_READS_IN_FLIGHT + 4);
781 let ids: Vec<u32> = packets.iter().map(|p| p.id).collect();
782
783 let replies = tokio::time::timeout(WAVE_BUDGET, conn.read_independently(packets))
784 .await
785 .expect("a wave the peer answers in full must not need the whole budget");
786
787 assert_eq!(replies.len(), ids.len(), "one result per command, always");
788 for (nth, (reply, id)) in replies.into_iter().zip(ids).enumerate() {
789 let reply = reply.unwrap_or_else(|e| panic!("command {nth} (id {id}) was not answered: {e:?}"));
790 assert_eq!(reply.id, id, "result {nth} carries the wrong reply");
791 assert_eq!(
792 reply.data(),
793 &id.to_be_bytes().repeat(4)[..],
794 "result {nth} carries the right id with another request's payload, which is the \
795 conflation the id alone cannot catch"
796 );
797 }
798 }
799
800 /// PERF-1 (#100), the error-path criterion: one command failing inside a wave must not touch its
801 /// siblings, and must not touch the stream.
802 ///
803 /// Two assertions, and the second is the one that matters. A JDWP error reply is a normal packet, so
804 /// the siblings arriving intact is nearly free; the risk being tested is that the *stream* survives,
805 /// which is asserted by continuing to use the connection afterwards. If a wave could desynchronise
806 /// framing, this last read is where it would surface — and framing failure ends the session, so
807 /// there would be nothing ambiguous about it (ADR-0018).
808 #[tokio::test]
809 async fn a_failure_inside_a_wave_leaves_its_siblings_and_the_stream_intact() {
810 let wave_size = WITHHELD;
811 let failing = 2;
812 let port = wave_peer(wave_size, Answers::Backwards, Some(failing)).await;
813 let conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
814 let packets = wave(&conn, wave_size);
815 let ids: Vec<u32> = packets.iter().map(|p| p.id).collect();
816
817 let replies = tokio::time::timeout(WAVE_BUDGET, conn.read_independently(packets))
818 .await
819 .expect("a failing command must not stall the wave it is in");
820
821 assert_eq!(replies.len(), wave_size, "one result per command, including the failing one");
822 for (nth, (reply, id)) in replies.into_iter().zip(&ids).enumerate() {
823 let reply = reply.unwrap_or_else(|e| panic!("command {nth} was not answered at all: {e:?}"));
824 assert_eq!(reply.id, *id, "result {nth} carries the wrong reply");
825 if nth == failing {
826 let err = reply.check_error().expect_err("the failing command must report its failure");
827 assert!(
828 matches!(err, JdwpError::JdwpErrorCode(code, _) if code == INVALID_OBJECT),
829 "the JVM's own error code is the diagnosis and must survive the wave: {err:?}"
830 );
831 } else {
832 reply.check_error().unwrap_or_else(|e| {
833 panic!("command {nth} was collateral damage from command {failing}'s failure: {e:?}")
834 });
835 }
836 }
837
838 // The stream, after all that.
839 let mut after = conn.clone();
840 let probe = CommandPacket::new(after.next_id(), 9, 1);
841 let id = probe.id;
842 let reply = tokio::time::timeout(WAVE_BUDGET, after.send_command(probe))
843 .await
844 .expect("the connection must still answer after a wave containing a failure")
845 .expect("a desynchronised stream is a dead session, not a slow one");
846 assert_eq!(reply.id, id, "the reply after the wave belongs to the command after the wave");
847 assert_eq!(reply.data(), &id.to_be_bytes().repeat(4)[..], "framing survived the wave");
848 }
849
850 /// The wave is the same number of packets as the loop it replaces, which is what keeps every
851 /// packet-count bound test in `mcp_integration.rs` meaningful (PERF-1's third criterion).
852 ///
853 /// Asserted here rather than trusted from the implementation because the accounting is easy to break
854 /// invisibly: ids come from the caller, so anything that retried, split, or padded a command would
855 /// move this number while every other test in the file still passed.
856 #[tokio::test]
857 async fn a_wave_costs_exactly_one_packet_per_read() {
858 let port = wave_peer(4, Answers::InOrder, None).await;
859 let conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
860 let before = conn.packets_sent();
861
862 let replies = tokio::time::timeout(WAVE_BUDGET, conn.read_independently(wave(&conn, 4)))
863 .await
864 .expect("four reads answered in order must not need the whole budget");
865
866 assert_eq!(replies.len(), 4);
867 assert_eq!(
868 conn.packets_sent() - before,
869 4,
870 "PERF-1 buys round trips, not packets — a different packet count here would invalidate \
871 every bound asserted in mcp_integration.rs"
872 );
873 }
874
875 /// The flag is shared with every clone, including the event pump's — the property ADR-0001 relies on
876 /// for a breakpoint condition evaluated inside the pump. Worth asserting for the new primitives too,
877 /// since a clone that kept its own copy would be refused nowhere.
878 #[tokio::test]
879 async fn read_only_set_on_one_handle_refuses_on_a_clone() {
880 let port = deaf_jdwp_peer().await;
881 let conn = JdwpConnection::connect("127.0.0.1", port).await.expect("handshake with the peer");
882 let mut clone = conn.clone();
883
884 conn.set_read_only(true);
885
886 let err = clone.redefine_classes(&[(1, vec![])]).await.expect_err("the clone must refuse too");
887 assert!(matches!(err, JdwpError::ReadOnly(_)), "expected ReadOnly, got {err:?}");
888 }
889}