Skip to main content

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}