java-debugging-jdwp-client 0.20.0

JDWP protocol client for Java debugging — implementation detail of jdwp-mcp, not a supported API
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
// VirtualMachine command implementations
//
// These are the fundamental commands for interacting with the JVM

use crate::commands::{command_sets, vm_commands};
use crate::connection::JdwpConnection;
use crate::protocol::{CommandPacket, JdwpResult};
use crate::reader::{read_i32, read_string, read_u8};
use crate::types::ReferenceTypeId;
use bytes::BufMut;
use serde::{Deserialize, Serialize};

/// JVM version information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VmVersion {
    pub description: String,
    pub jdwp_major: i32,
    pub jdwp_minor: i32,
    pub vm_version: String,
    pub vm_name: String,
}

/// What the target JVM says it supports (`VirtualMachine.Capabilities`).
///
/// The seven original capabilities, which is all this command reports. The newer bits — including the
/// ones hot reload depends on — live in [`VmCapabilitiesNew`] behind `CapabilitiesNew` (command 17);
/// until SWAP-1 (#58) nothing here needed them, and this comment said so. Note that JDI's
/// `canGetMethodReturnValues` is **not** a capability bit at all — it is a JDWP *version* check (≥ 1.6),
/// so [`get_version`](JdwpConnection::get_version) answers that one.
///
/// Worth asking before a feature that depends on one: a JVM without the capability answers
/// `NOT_IMPLEMENTED` (99) to the actual command, and "this JVM can't tell us" is a far more useful
/// report than a bare error code.
///
/// **Two features deliberately don't ask, and it is worth knowing which**, so the sentence above is not
/// read as a guarantee it does not make: [`force_early_return`](JdwpConnection::force_early_return)
/// (`canForceEarlyReturn`) and
/// [`get_source_debug_extension`](JdwpConnection::get_source_debug_extension)
/// (`canGetSourceDebugExtension`) both issue their command without checking first, and neither bit is
/// decoded — [`VmCapabilitiesNew`] reads through position 18 and names five of those, so 13
/// (`canGetSourceDebugExtension`) is read past and 21 (`canForceEarlyReturn`) is never reached. Both then
/// surface the raw
/// `NOT_IMPLEMENTED` (99), which is precisely the bare error code this rule exists to improve on.
/// Accepted for now rather than overlooked: adding a bit nothing consults is the mistake `IDSizes` was
/// deleted for (CLEAN-1, #27), so the bits arrive with the check, not before it. Measured values for the
/// whole `CapabilitiesNew` vector on Temurin 17.0.20 are in `docs/heap-query-measurements.md`.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
// Seven bools because the JDWP reply is seven bools, in this order. This is a decoded wire structure,
// not a parameter bag that wants splitting up — grouping them differently would only make the reader
// map fields back onto the spec by hand.
#[allow(clippy::struct_excessive_bools)]
pub struct VmCapabilities {
    pub can_watch_field_modification: bool,
    pub can_watch_field_access: bool,
    pub can_get_bytecodes: bool,
    pub can_get_synthetic_attribute: bool,
    /// Whether [`owned_monitors`](JdwpConnection::owned_monitors) will work.
    pub can_get_owned_monitor_info: bool,
    /// Whether [`current_contended_monitor`](JdwpConnection::current_contended_monitor) will work.
    pub can_get_current_contended_monitor: bool,
    pub can_get_monitor_info: bool,
}

/// The capabilities `VirtualMachine.CapabilitiesNew` (command 17) adds on top of [`VmCapabilities`].
///
/// The reply repeats the original seven booleans and then adds twenty-five more, of which the last
/// eleven are reserved. Only the ones a feature here turns on are named: decoding a bit nothing
/// consults would be the same uncalled-command mistake `IDSizes` was deleted for (CLEAN-1, #27).
///
/// Asked before hot reload rather than after a failure, per the rule [`VmCapabilities`] states: a JVM
/// without `canRedefineClasses` answers `NOT_IMPLEMENTED` (99) to the command, and "this JVM cannot
/// `HotSwap`" is a far more useful report than a bare error code.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
// Same reasoning as `VmCapabilities`: this is a decoded wire structure, in the spec's order.
#[allow(clippy::struct_excessive_bools)]
pub struct VmCapabilitiesNew {
    /// Whether [`redefine_classes`](JdwpConnection::redefine_classes) will work at all. Every `HotSpot`
    /// this project has met says yes; a JVM in the field may not.
    pub can_redefine_classes: bool,
    /// Whether a redefinition may **add** a method. `HotSpot` says no, which is most of why a swap gets
    /// refused: method *bodies* are all it will accept.
    pub can_add_method: bool,
    /// Whether the JVM lifts the method-bodies-only restriction entirely. `HotSpot` says no.
    pub can_unrestrictedly_redefine_classes: bool,
    /// Whether [`pop_frames`](JdwpConnection::pop_frames) will work — the other half of a useful swap,
    /// since a frame already on the stack keeps running the code it entered with.
    pub can_pop_frames: bool,
    /// Whether a request may carry an `InstanceOnly` modifier (modKind 11) — position **12**.
    ///
    /// Decoded in the same change that consults it (FILT-9, #101), per this struct's rule. It matters
    /// more than most bits here because of *how* the JVM refuses a modifier it cannot honour: not with
    /// `NOT_IMPLEMENTED`, but with `INTERNAL` (113), which says nothing about which modifier was the
    /// problem. Reading the bit first turns that into a sentence.
    ///
    /// Measured `true` on Temurin 17.0.20 — `docs/heap-query-measurements.md` has the full vector.
    pub can_use_instance_filters: bool,
    /// Whether [`instances`](JdwpConnection::instances) and
    /// [`instance_counts`](JdwpConnection::instance_counts) will work — position **16**, four bits past
    /// where this decoder used to stop.
    ///
    /// Decoded in the same change that consults it (DISC-10, #84). Positions 13-15
    /// (`canGetSourceDebugExtension`, `canRequestVMDeathEvent`, `canSetDefaultStratum`) are read past
    /// rather than named, for the reason this struct's documentation gives: a bit nothing reads is the
    /// mistake `IDSizes` was deleted for. Position 12 joined the named ones with FILT-9 (#101).
    pub can_get_instance_info: bool,
    /// Whether [`set_monitor_request`](JdwpConnection::set_monitor_request) will work — position **17**,
    /// the very next bit after 16, so DISC-10's decoder needed no positions skipped to reach it.
    ///
    /// Decoded in the same change that consults it (DUMP-7, #96), per this struct's rule, and consulted at
    /// arming time rather than after the fact: a JVM without it answers `NOT_IMPLEMENTED` (99), and "this
    /// JVM cannot report lock contention as it happens, so a lock diagnosis here still needs a suspending
    /// `debug.thread_dump`" is a far more useful report — it names the fallback as well as the refusal.
    pub can_request_monitor_events: bool,
    /// Whether the JVM can say **at which stack depth** a thread acquired each monitor it owns
    /// (`ThreadReference.OwnedMonitorsStackDepthInfo`) — position **18**.
    ///
    /// Named although no command here issues that request, which is a deliberate exception to this
    /// struct's rule and needs its justification stated rather than assumed. What consults it is the
    /// arming reply for a monitor stop point: a snapshot names the lock, the thread and the location that
    /// blocked, and the obvious next question — *where in this thread's stack was the lock taken* — is
    /// answerable on a JVM with this bit and not on one without. Reporting which of the two a caller is on
    /// costs one already-issued command, where leaving it out invites the reading that the tool simply
    /// does not report frame depth on any JVM.
    ///
    /// That is a *consulted* bit rather than an implied capability, which is the line `IDSizes` crossed
    /// (CLEAN-1, #27): nothing here claims the frame-depth query exists. If it is ever built, this is the
    /// bit it gates on.
    pub can_get_monitor_frame_info: bool,
}

/// Class information from `ClassesBySignature`
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassInfo {
    pub ref_type_tag: u8, // 1=class, 2=interface, 3=array
    pub type_id: ReferenceTypeId,
    pub signature: String,
    pub status: i32,
}

impl JdwpConnection {
    /// Get JVM version information (VirtualMachine.Version command)
    ///
    /// # Errors
    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
    pub async fn get_version(&mut self) -> JdwpResult<VmVersion> {
        let id = self.next_id();
        let packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::VERSION);

        let reply = self.send_command(packet).await?;
        reply.check_error()?;

        let mut data = reply.data();

        let description = read_string(&mut data)?;
        let jdwp_major = read_i32(&mut data)?;
        let jdwp_minor = read_i32(&mut data)?;
        let vm_version = read_string(&mut data)?;
        let vm_name = read_string(&mut data)?;

        Ok(VmVersion { description, jdwp_major, jdwp_minor, vm_version, vm_name })
    }

    // `VirtualMachine.IDSizes` (command 7) used to be wrapped here and was deleted by CLEAN-1 (#27):
    // the #19 coverage run measured it at **0 hits**, the only function in that review never executed at
    // all. Nothing called it and nothing needed to, because the reader assumes 8-byte ids outright —
    // see the note at the top of `reader.rs`. An uncalled wire command that *looks* like it validates
    // that assumption is worse than none, since it makes the assumption read as checked. If the widths
    // are ever worth verifying, that is a check at attach time, built deliberately.

    /// Ask the JVM which optional capabilities it supports (VirtualMachine.Capabilities, command 12).
    ///
    /// Seven booleans, one byte each, in the order the spec lists them. Used to turn "the JVM refused"
    /// into "this JVM cannot do that" — see [`VmCapabilities`].
    ///
    /// # Errors
    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
    pub async fn capabilities(&mut self) -> JdwpResult<VmCapabilities> {
        let id = self.next_id();
        let packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::CAPABILITIES);

        let reply = self.send_command(packet).await?;
        reply.check_error()?;

        let mut data = reply.data();
        let mut flag = || -> JdwpResult<bool> { Ok(read_u8(&mut data)? != 0) };
        Ok(VmCapabilities {
            can_watch_field_modification: flag()?,
            can_watch_field_access: flag()?,
            can_get_bytecodes: flag()?,
            can_get_synthetic_attribute: flag()?,
            can_get_owned_monitor_info: flag()?,
            can_get_current_contended_monitor: flag()?,
            can_get_monitor_info: flag()?,
        })
    }

    /// Ask the JVM for the *newer* capability bits (VirtualMachine.CapabilitiesNew, command 17).
    ///
    /// The reply repeats [`capabilities`](Self::capabilities)' seven booleans before the ones that are
    /// only here, so the first seven bytes are read past rather than decoded twice — the two commands
    /// answer about the same JVM and disagreeing about the overlap is not a state worth representing.
    /// Everything past the eighteenth bit is skipped for the reason [`VmCapabilitiesNew`] gives, and so
    /// are 13-15, which sit between bits that *are* consulted.
    ///
    /// # Errors
    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
    pub async fn capabilities_new(&mut self) -> JdwpResult<VmCapabilitiesNew> {
        let id = self.next_id();
        let packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::CAPABILITIES_NEW);

        let reply = self.send_command(packet).await?;
        reply.check_error()?;

        let mut data = reply.data();
        let mut flag = || -> JdwpResult<bool> { Ok(read_u8(&mut data)? != 0) };
        // The seven `Capabilities` bits, in the same order, first.
        for _ in 0..7 {
            flag()?;
        }
        let can_redefine_classes = flag()?;
        let can_add_method = flag()?;
        let can_unrestrictedly_redefine_classes = flag()?;
        let can_pop_frames = flag()?;
        // 12: canUseInstanceFilters, consulted by FILT-9 (#101).
        let can_use_instance_filters = flag()?;
        // 13-15: canGetSourceDebugExtension, canRequestVMDeathEvent, canSetDefaultStratum. Read past,
        // not named — nothing here consults them yet.
        for _ in 0..3 {
            flag()?;
        }
        Ok(VmCapabilitiesNew {
            can_redefine_classes,
            can_add_method,
            can_unrestrictedly_redefine_classes,
            can_pop_frames,
            can_use_instance_filters,
            // 16, 17, 18 — read in order, which is the only reason these three can be struct-literal
            // fields rather than `let` bindings. Reordering them here reads every bit off the wrong byte.
            can_get_instance_info: flag()?,
            can_request_monitor_events: flag()?,
            can_get_monitor_frame_info: flag()?,
        })
    }

    /// How many live instances each of `ref_types` has (`VirtualMachine.InstanceCounts`, command 21).
    ///
    /// **This stops the world, and JDWP never says so.** It requires no suspend and this client issues
    /// none, yet the JVM holds every application thread for a full live-heap walk: measured at **630 ms
    /// over a 2,000,000-object heap, with a matching 522 ms pause**, against 54 ms on a 20,000-object
    /// heap. The cost tracks the **live heap**, not the answer. See `docs/heap-query-measurements.md`.
    ///
    /// **One walk covers the whole batch** — three types measured at 604 ms, about the price of one — so
    /// this takes a slice rather than being called in a loop, and asking about more types is close to
    /// free.
    ///
    /// Counts are **exact-type**, matching [`instances`](Self::instances): a base class does not count
    /// its subclasses. A `ref_types` entry the JVM does not recognise answers `0` rather than erroring,
    /// which makes a typo look like an absence — the caller has to resolve names first.
    ///
    /// # Errors
    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed. `NOT_IMPLEMENTED`
    /// (99) when the JVM lacks `canGetInstanceInfo` — ask [`capabilities_new`](Self::capabilities_new)
    /// first, so a refusal can be reported as "this JVM cannot answer that" rather than as an error code.
    pub async fn instance_counts(&mut self, ref_types: &[ReferenceTypeId]) -> JdwpResult<Vec<i64>> {
        let id = self.next_id();
        let mut packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::INSTANCE_COUNTS);
        packet.data.put_i32(i32::try_from(ref_types.len()).unwrap_or(i32::MAX));
        for t in ref_types {
            packet.data.put_u64(*t);
        }

        let reply = self.send_command(packet).await?;
        reply.check_error()?;

        let mut data = reply.data();
        let n = read_i32(&mut data)?;
        let mut counts = Vec::with_capacity(usize::try_from(n).unwrap_or(0));
        for _ in 0..n {
            counts.push(crate::reader::read_i64(&mut data)?);
        }
        Ok(counts)
    }

    /// Install new bytecode for already-loaded classes (VirtualMachine.RedefineClasses, command 18) —
    /// `HotSwap`, what an IDE calls "reload changed classes".
    ///
    /// All-or-nothing: the JVM either accepts every definition in the batch or changes nothing, which is
    /// why this takes a slice rather than being called in a loop. On `HotSpot` it accepts **method body
    /// changes only** — add or remove a method or a field, change a signature, a modifier or the
    /// hierarchy, and it refuses with one of the twelve codes at 60-71 in
    /// [`ERROR_MESSAGES`](crate::protocol). Translating those into what the caller should do next is the
    /// MCP layer's job; this reports them as they came.
    ///
    /// **Frames already on the stack keep running the code they entered with.** A method suspended at a
    /// breakpoint is unaffected by its own redefinition until it is re-entered — see
    /// [`pop_frames`](Self::pop_frames), which is how it gets re-entered without re-issuing the request
    /// that got there.
    ///
    /// On success every redefined type is dropped from the [type cache](crate::connection): the cache
    /// holds each type's methods, fields, signature and interfaces, and a redefinition is one of the two
    /// events its own documentation names as making those stale. Method ids for changed methods become
    /// *obsolete* rather than invalid, so a cached list would keep naming code the JVM no longer runs.
    ///
    /// # Errors
    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the JVM refuses the redefinition.
    pub async fn redefine_classes(&mut self, defs: &[(ReferenceTypeId, Vec<u8>)]) -> JdwpResult<()> {
        // SAFE-9: at the wire, not above it (ADR-0001). A redefinition installs code, and it is the one
        // mutation on this connection that outlives the connection — nothing here can undo it.
        self.guard_mutation("a class redefinition")?;

        let id = self.next_id();
        let mut packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::REDEFINE_CLASSES);

        packet.data.put_i32(i32::try_from(defs.len()).unwrap_or(i32::MAX));
        for (type_id, bytes) in defs {
            packet.data.put_u64(*type_id);
            packet.data.put_u32(u32::try_from(bytes.len()).unwrap_or(u32::MAX));
            packet.data.extend_from_slice(bytes);
        }

        let reply = self.send_command(packet).await?;
        reply.check_error()?;

        for (type_id, _) in defs {
            self.types().invalidate(*type_id);
        }
        Ok(())
    }

    /// Dispose of the debugger connection (VirtualMachine.Dispose command).
    ///
    /// The JVM's own clean exit from a debug session: it clears **every** event request this
    /// connection set and resumes **every** thread it suspended, then invalidates the connection.
    /// That "resume everything, leave no request armed" guarantee is exactly what a safe disconnect
    /// needs — a `resume_all` alone would leave breakpoints armed to re-freeze the next request, and
    /// clearing our tracked requests one by one could still miss one the JVM knows about and we don't.
    ///
    /// The connection is unusable afterwards; drop it. Fire-and-forget by design: if the socket is
    /// already half-dead (the case a disconnect most needs to handle), there is nothing better to do
    /// than try and move on.
    ///
    /// # Errors
    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
    pub async fn dispose(&mut self) -> JdwpResult<()> {
        let id = self.next_id();
        let packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::DISPOSE);
        let reply = self.send_command(packet).await?;
        reply.check_error()?;
        Ok(())
    }

    /// Find classes by signature (VirtualMachine.ClassesBySignature command)
    /// Signature format: "Lcom/example/MyClass;" for classes
    ///
    /// # Errors
    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
    pub async fn classes_by_signature(&mut self, signature: &str) -> JdwpResult<Vec<ClassInfo>> {
        let id = self.next_id();
        let mut packet =
            CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::CLASSES_BY_SIGNATURE);

        // Write signature as JDWP string (4-byte length + UTF-8 bytes)
        let sig_bytes = signature.as_bytes();
        packet.data.put_u32(u32::try_from(sig_bytes.len()).unwrap_or(u32::MAX));
        packet.data.extend_from_slice(sig_bytes);

        let reply = self.send_command(packet).await?;
        reply.check_error()?;

        let mut data = reply.data();

        // Read number of classes
        let classes_count = read_i32(&mut data)?;
        let mut classes = Vec::with_capacity(usize::try_from(classes_count).unwrap_or(0));

        for _ in 0..classes_count {
            let ref_type_tag = read_u8(&mut data)?;
            let type_id = crate::reader::read_u64(&mut data)?;
            let status = read_i32(&mut data)?;

            classes.push(ClassInfo { ref_type_tag, type_id, signature: signature.to_string(), status });
        }

        Ok(classes)
    }

    /// List every loaded reference type (VirtualMachine.AllClasses command).
    ///
    /// Heavier than `classes_by_signature` (returns thousands of entries), but lets a caller
    /// resolve a class by *simple* name when the full package isn't known — e.g. match any
    /// signature ending in `/ConfigDefaultUtils;`.
    ///
    /// # Errors
    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
    pub async fn all_classes(&mut self) -> JdwpResult<Vec<ClassInfo>> {
        let id = self.next_id();
        let packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::ALL_CLASSES);

        let reply = self.send_command(packet).await?;
        reply.check_error()?;

        let mut data = reply.data();

        let classes_count = read_i32(&mut data)?;
        let mut classes = Vec::with_capacity(usize::try_from(classes_count).unwrap_or(0));

        for _ in 0..classes_count {
            let ref_type_tag = read_u8(&mut data)?;
            let type_id = crate::reader::read_u64(&mut data)?;
            let signature = read_string(&mut data)?;
            let status = read_i32(&mut data)?;

            classes.push(ClassInfo { ref_type_tag, type_id, signature, status });
        }

        Ok(classes)
    }
}