Skip to main content

jdwp_client/
vm.rs

1// VirtualMachine command implementations
2//
3// These are the fundamental commands for interacting with the JVM
4
5use crate::commands::{command_sets, vm_commands};
6use crate::connection::JdwpConnection;
7use crate::protocol::{CommandPacket, JdwpResult};
8use crate::reader::{read_i32, read_string, read_u8};
9use crate::types::ReferenceTypeId;
10use bytes::BufMut;
11use serde::{Deserialize, Serialize};
12
13/// JVM version information
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct VmVersion {
16    pub description: String,
17    pub jdwp_major: i32,
18    pub jdwp_minor: i32,
19    pub vm_version: String,
20    pub vm_name: String,
21}
22
23/// What the target JVM says it supports (`VirtualMachine.Capabilities`).
24///
25/// The seven original capabilities, which is all this command reports. The newer bits — including the
26/// ones hot reload depends on — live in [`VmCapabilitiesNew`] behind `CapabilitiesNew` (command 17);
27/// until SWAP-1 (#58) nothing here needed them, and this comment said so. Note that JDI's
28/// `canGetMethodReturnValues` is **not** a capability bit at all — it is a JDWP *version* check (≥ 1.6),
29/// so [`get_version`](JdwpConnection::get_version) answers that one.
30///
31/// Worth asking before a feature that depends on one: a JVM without the capability answers
32/// `NOT_IMPLEMENTED` (99) to the actual command, and "this JVM can't tell us" is a far more useful
33/// report than a bare error code.
34///
35/// **Two features deliberately don't ask, and it is worth knowing which**, so the sentence above is not
36/// read as a guarantee it does not make: [`force_early_return`](JdwpConnection::force_early_return)
37/// (`canForceEarlyReturn`) and
38/// [`get_source_debug_extension`](JdwpConnection::get_source_debug_extension)
39/// (`canGetSourceDebugExtension`) both issue their command without checking first, and neither bit is
40/// decoded — [`VmCapabilitiesNew`] reads through position 18 and names five of those, so 13
41/// (`canGetSourceDebugExtension`) is read past and 21 (`canForceEarlyReturn`) is never reached. Both then
42/// surface the raw
43/// `NOT_IMPLEMENTED` (99), which is precisely the bare error code this rule exists to improve on.
44/// Accepted for now rather than overlooked: adding a bit nothing consults is the mistake `IDSizes` was
45/// deleted for (CLEAN-1, #27), so the bits arrive with the check, not before it. Measured values for the
46/// whole `CapabilitiesNew` vector on Temurin 17.0.20 are in `docs/heap-query-measurements.md`.
47#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
48// Seven bools because the JDWP reply is seven bools, in this order. This is a decoded wire structure,
49// not a parameter bag that wants splitting up — grouping them differently would only make the reader
50// map fields back onto the spec by hand.
51#[allow(clippy::struct_excessive_bools)]
52pub struct VmCapabilities {
53    pub can_watch_field_modification: bool,
54    pub can_watch_field_access: bool,
55    pub can_get_bytecodes: bool,
56    pub can_get_synthetic_attribute: bool,
57    /// Whether [`owned_monitors`](JdwpConnection::owned_monitors) will work.
58    pub can_get_owned_monitor_info: bool,
59    /// Whether [`current_contended_monitor`](JdwpConnection::current_contended_monitor) will work.
60    pub can_get_current_contended_monitor: bool,
61    pub can_get_monitor_info: bool,
62}
63
64/// The capabilities `VirtualMachine.CapabilitiesNew` (command 17) adds on top of [`VmCapabilities`].
65///
66/// The reply repeats the original seven booleans and then adds twenty-five more, of which the last
67/// eleven are reserved. Only the ones a feature here turns on are named: decoding a bit nothing
68/// consults would be the same uncalled-command mistake `IDSizes` was deleted for (CLEAN-1, #27).
69///
70/// Asked before hot reload rather than after a failure, per the rule [`VmCapabilities`] states: a JVM
71/// without `canRedefineClasses` answers `NOT_IMPLEMENTED` (99) to the command, and "this JVM cannot
72/// `HotSwap`" is a far more useful report than a bare error code.
73#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
74// Same reasoning as `VmCapabilities`: this is a decoded wire structure, in the spec's order.
75#[allow(clippy::struct_excessive_bools)]
76pub struct VmCapabilitiesNew {
77    /// Whether [`redefine_classes`](JdwpConnection::redefine_classes) will work at all. Every `HotSpot`
78    /// this project has met says yes; a JVM in the field may not.
79    pub can_redefine_classes: bool,
80    /// Whether a redefinition may **add** a method. `HotSpot` says no, which is most of why a swap gets
81    /// refused: method *bodies* are all it will accept.
82    pub can_add_method: bool,
83    /// Whether the JVM lifts the method-bodies-only restriction entirely. `HotSpot` says no.
84    pub can_unrestrictedly_redefine_classes: bool,
85    /// Whether [`pop_frames`](JdwpConnection::pop_frames) will work — the other half of a useful swap,
86    /// since a frame already on the stack keeps running the code it entered with.
87    pub can_pop_frames: bool,
88    /// Whether a request may carry an `InstanceOnly` modifier (modKind 11) — position **12**.
89    ///
90    /// Decoded in the same change that consults it (FILT-9, #101), per this struct's rule. It matters
91    /// more than most bits here because of *how* the JVM refuses a modifier it cannot honour: not with
92    /// `NOT_IMPLEMENTED`, but with `INTERNAL` (113), which says nothing about which modifier was the
93    /// problem. Reading the bit first turns that into a sentence.
94    ///
95    /// Measured `true` on Temurin 17.0.20 — `docs/heap-query-measurements.md` has the full vector.
96    pub can_use_instance_filters: bool,
97    /// Whether [`instances`](JdwpConnection::instances) and
98    /// [`instance_counts`](JdwpConnection::instance_counts) will work — position **16**, four bits past
99    /// where this decoder used to stop.
100    ///
101    /// Decoded in the same change that consults it (DISC-10, #84). Positions 13-15
102    /// (`canGetSourceDebugExtension`, `canRequestVMDeathEvent`, `canSetDefaultStratum`) are read past
103    /// rather than named, for the reason this struct's documentation gives: a bit nothing reads is the
104    /// mistake `IDSizes` was deleted for. Position 12 joined the named ones with FILT-9 (#101).
105    pub can_get_instance_info: bool,
106    /// Whether [`set_monitor_request`](JdwpConnection::set_monitor_request) will work — position **17**,
107    /// the very next bit after 16, so DISC-10's decoder needed no positions skipped to reach it.
108    ///
109    /// Decoded in the same change that consults it (DUMP-7, #96), per this struct's rule, and consulted at
110    /// arming time rather than after the fact: a JVM without it answers `NOT_IMPLEMENTED` (99), and "this
111    /// JVM cannot report lock contention as it happens, so a lock diagnosis here still needs a suspending
112    /// `debug.thread_dump`" is a far more useful report — it names the fallback as well as the refusal.
113    pub can_request_monitor_events: bool,
114    /// Whether the JVM can say **at which stack depth** a thread acquired each monitor it owns
115    /// (`ThreadReference.OwnedMonitorsStackDepthInfo`) — position **18**.
116    ///
117    /// Named although no command here issues that request, which is a deliberate exception to this
118    /// struct's rule and needs its justification stated rather than assumed. What consults it is the
119    /// arming reply for a monitor stop point: a snapshot names the lock, the thread and the location that
120    /// blocked, and the obvious next question — *where in this thread's stack was the lock taken* — is
121    /// answerable on a JVM with this bit and not on one without. Reporting which of the two a caller is on
122    /// costs one already-issued command, where leaving it out invites the reading that the tool simply
123    /// does not report frame depth on any JVM.
124    ///
125    /// That is a *consulted* bit rather than an implied capability, which is the line `IDSizes` crossed
126    /// (CLEAN-1, #27): nothing here claims the frame-depth query exists. If it is ever built, this is the
127    /// bit it gates on.
128    pub can_get_monitor_frame_info: bool,
129}
130
131/// Class information from `ClassesBySignature`
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ClassInfo {
134    pub ref_type_tag: u8, // 1=class, 2=interface, 3=array
135    pub type_id: ReferenceTypeId,
136    pub signature: String,
137    pub status: i32,
138}
139
140impl JdwpConnection {
141    /// Get JVM version information (VirtualMachine.Version command)
142    ///
143    /// # Errors
144    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
145    pub async fn get_version(&mut self) -> JdwpResult<VmVersion> {
146        let id = self.next_id();
147        let packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::VERSION);
148
149        let reply = self.send_command(packet).await?;
150        reply.check_error()?;
151
152        let mut data = reply.data();
153
154        let description = read_string(&mut data)?;
155        let jdwp_major = read_i32(&mut data)?;
156        let jdwp_minor = read_i32(&mut data)?;
157        let vm_version = read_string(&mut data)?;
158        let vm_name = read_string(&mut data)?;
159
160        Ok(VmVersion { description, jdwp_major, jdwp_minor, vm_version, vm_name })
161    }
162
163    // `VirtualMachine.IDSizes` (command 7) used to be wrapped here and was deleted by CLEAN-1 (#27):
164    // the #19 coverage run measured it at **0 hits**, the only function in that review never executed at
165    // all. Nothing called it and nothing needed to, because the reader assumes 8-byte ids outright —
166    // see the note at the top of `reader.rs`. An uncalled wire command that *looks* like it validates
167    // that assumption is worse than none, since it makes the assumption read as checked. If the widths
168    // are ever worth verifying, that is a check at attach time, built deliberately.
169
170    /// Ask the JVM which optional capabilities it supports (VirtualMachine.Capabilities, command 12).
171    ///
172    /// Seven booleans, one byte each, in the order the spec lists them. Used to turn "the JVM refused"
173    /// into "this JVM cannot do that" — see [`VmCapabilities`].
174    ///
175    /// # Errors
176    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
177    pub async fn capabilities(&mut self) -> JdwpResult<VmCapabilities> {
178        let id = self.next_id();
179        let packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::CAPABILITIES);
180
181        let reply = self.send_command(packet).await?;
182        reply.check_error()?;
183
184        let mut data = reply.data();
185        let mut flag = || -> JdwpResult<bool> { Ok(read_u8(&mut data)? != 0) };
186        Ok(VmCapabilities {
187            can_watch_field_modification: flag()?,
188            can_watch_field_access: flag()?,
189            can_get_bytecodes: flag()?,
190            can_get_synthetic_attribute: flag()?,
191            can_get_owned_monitor_info: flag()?,
192            can_get_current_contended_monitor: flag()?,
193            can_get_monitor_info: flag()?,
194        })
195    }
196
197    /// Ask the JVM for the *newer* capability bits (VirtualMachine.CapabilitiesNew, command 17).
198    ///
199    /// The reply repeats [`capabilities`](Self::capabilities)' seven booleans before the ones that are
200    /// only here, so the first seven bytes are read past rather than decoded twice — the two commands
201    /// answer about the same JVM and disagreeing about the overlap is not a state worth representing.
202    /// Everything past the eighteenth bit is skipped for the reason [`VmCapabilitiesNew`] gives, and so
203    /// are 13-15, which sit between bits that *are* consulted.
204    ///
205    /// # Errors
206    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
207    pub async fn capabilities_new(&mut self) -> JdwpResult<VmCapabilitiesNew> {
208        let id = self.next_id();
209        let packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::CAPABILITIES_NEW);
210
211        let reply = self.send_command(packet).await?;
212        reply.check_error()?;
213
214        let mut data = reply.data();
215        let mut flag = || -> JdwpResult<bool> { Ok(read_u8(&mut data)? != 0) };
216        // The seven `Capabilities` bits, in the same order, first.
217        for _ in 0..7 {
218            flag()?;
219        }
220        let can_redefine_classes = flag()?;
221        let can_add_method = flag()?;
222        let can_unrestrictedly_redefine_classes = flag()?;
223        let can_pop_frames = flag()?;
224        // 12: canUseInstanceFilters, consulted by FILT-9 (#101).
225        let can_use_instance_filters = flag()?;
226        // 13-15: canGetSourceDebugExtension, canRequestVMDeathEvent, canSetDefaultStratum. Read past,
227        // not named — nothing here consults them yet.
228        for _ in 0..3 {
229            flag()?;
230        }
231        Ok(VmCapabilitiesNew {
232            can_redefine_classes,
233            can_add_method,
234            can_unrestrictedly_redefine_classes,
235            can_pop_frames,
236            can_use_instance_filters,
237            // 16, 17, 18 — read in order, which is the only reason these three can be struct-literal
238            // fields rather than `let` bindings. Reordering them here reads every bit off the wrong byte.
239            can_get_instance_info: flag()?,
240            can_request_monitor_events: flag()?,
241            can_get_monitor_frame_info: flag()?,
242        })
243    }
244
245    /// How many live instances each of `ref_types` has (`VirtualMachine.InstanceCounts`, command 21).
246    ///
247    /// **This stops the world, and JDWP never says so.** It requires no suspend and this client issues
248    /// none, yet the JVM holds every application thread for a full live-heap walk: measured at **630 ms
249    /// over a 2,000,000-object heap, with a matching 522 ms pause**, against 54 ms on a 20,000-object
250    /// heap. The cost tracks the **live heap**, not the answer. See `docs/heap-query-measurements.md`.
251    ///
252    /// **One walk covers the whole batch** — three types measured at 604 ms, about the price of one — so
253    /// this takes a slice rather than being called in a loop, and asking about more types is close to
254    /// free.
255    ///
256    /// Counts are **exact-type**, matching [`instances`](Self::instances): a base class does not count
257    /// its subclasses. A `ref_types` entry the JVM does not recognise answers `0` rather than erroring,
258    /// which makes a typo look like an absence — the caller has to resolve names first.
259    ///
260    /// # Errors
261    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed. `NOT_IMPLEMENTED`
262    /// (99) when the JVM lacks `canGetInstanceInfo` — ask [`capabilities_new`](Self::capabilities_new)
263    /// first, so a refusal can be reported as "this JVM cannot answer that" rather than as an error code.
264    pub async fn instance_counts(&mut self, ref_types: &[ReferenceTypeId]) -> JdwpResult<Vec<i64>> {
265        let id = self.next_id();
266        let mut packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::INSTANCE_COUNTS);
267        packet.data.put_i32(i32::try_from(ref_types.len()).unwrap_or(i32::MAX));
268        for t in ref_types {
269            packet.data.put_u64(*t);
270        }
271
272        let reply = self.send_command(packet).await?;
273        reply.check_error()?;
274
275        let mut data = reply.data();
276        let n = read_i32(&mut data)?;
277        let mut counts = Vec::with_capacity(usize::try_from(n).unwrap_or(0));
278        for _ in 0..n {
279            counts.push(crate::reader::read_i64(&mut data)?);
280        }
281        Ok(counts)
282    }
283
284    /// Install new bytecode for already-loaded classes (VirtualMachine.RedefineClasses, command 18) —
285    /// `HotSwap`, what an IDE calls "reload changed classes".
286    ///
287    /// All-or-nothing: the JVM either accepts every definition in the batch or changes nothing, which is
288    /// why this takes a slice rather than being called in a loop. On `HotSpot` it accepts **method body
289    /// changes only** — add or remove a method or a field, change a signature, a modifier or the
290    /// hierarchy, and it refuses with one of the twelve codes at 60-71 in
291    /// [`ERROR_MESSAGES`](crate::protocol). Translating those into what the caller should do next is the
292    /// MCP layer's job; this reports them as they came.
293    ///
294    /// **Frames already on the stack keep running the code they entered with.** A method suspended at a
295    /// breakpoint is unaffected by its own redefinition until it is re-entered — see
296    /// [`pop_frames`](Self::pop_frames), which is how it gets re-entered without re-issuing the request
297    /// that got there.
298    ///
299    /// On success every redefined type is dropped from the [type cache](crate::connection): the cache
300    /// holds each type's methods, fields, signature and interfaces, and a redefinition is one of the two
301    /// events its own documentation names as making those stale. Method ids for changed methods become
302    /// *obsolete* rather than invalid, so a cached list would keep naming code the JVM no longer runs.
303    ///
304    /// # Errors
305    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the JVM refuses the redefinition.
306    pub async fn redefine_classes(&mut self, defs: &[(ReferenceTypeId, Vec<u8>)]) -> JdwpResult<()> {
307        // SAFE-9: at the wire, not above it (ADR-0001). A redefinition installs code, and it is the one
308        // mutation on this connection that outlives the connection — nothing here can undo it.
309        self.guard_mutation("a class redefinition")?;
310
311        let id = self.next_id();
312        let mut packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::REDEFINE_CLASSES);
313
314        packet.data.put_i32(i32::try_from(defs.len()).unwrap_or(i32::MAX));
315        for (type_id, bytes) in defs {
316            packet.data.put_u64(*type_id);
317            packet.data.put_u32(u32::try_from(bytes.len()).unwrap_or(u32::MAX));
318            packet.data.extend_from_slice(bytes);
319        }
320
321        let reply = self.send_command(packet).await?;
322        reply.check_error()?;
323
324        for (type_id, _) in defs {
325            self.types().invalidate(*type_id);
326        }
327        Ok(())
328    }
329
330    /// Dispose of the debugger connection (VirtualMachine.Dispose command).
331    ///
332    /// The JVM's own clean exit from a debug session: it clears **every** event request this
333    /// connection set and resumes **every** thread it suspended, then invalidates the connection.
334    /// That "resume everything, leave no request armed" guarantee is exactly what a safe disconnect
335    /// needs — a `resume_all` alone would leave breakpoints armed to re-freeze the next request, and
336    /// clearing our tracked requests one by one could still miss one the JVM knows about and we don't.
337    ///
338    /// The connection is unusable afterwards; drop it. Fire-and-forget by design: if the socket is
339    /// already half-dead (the case a disconnect most needs to handle), there is nothing better to do
340    /// than try and move on.
341    ///
342    /// # Errors
343    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
344    pub async fn dispose(&mut self) -> JdwpResult<()> {
345        let id = self.next_id();
346        let packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::DISPOSE);
347        let reply = self.send_command(packet).await?;
348        reply.check_error()?;
349        Ok(())
350    }
351
352    /// Find classes by signature (VirtualMachine.ClassesBySignature command)
353    /// Signature format: "Lcom/example/MyClass;" for classes
354    ///
355    /// # Errors
356    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
357    pub async fn classes_by_signature(&mut self, signature: &str) -> JdwpResult<Vec<ClassInfo>> {
358        let id = self.next_id();
359        let mut packet =
360            CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::CLASSES_BY_SIGNATURE);
361
362        // Write signature as JDWP string (4-byte length + UTF-8 bytes)
363        let sig_bytes = signature.as_bytes();
364        packet.data.put_u32(u32::try_from(sig_bytes.len()).unwrap_or(u32::MAX));
365        packet.data.extend_from_slice(sig_bytes);
366
367        let reply = self.send_command(packet).await?;
368        reply.check_error()?;
369
370        let mut data = reply.data();
371
372        // Read number of classes
373        let classes_count = read_i32(&mut data)?;
374        let mut classes = Vec::with_capacity(usize::try_from(classes_count).unwrap_or(0));
375
376        for _ in 0..classes_count {
377            let ref_type_tag = read_u8(&mut data)?;
378            let type_id = crate::reader::read_u64(&mut data)?;
379            let status = read_i32(&mut data)?;
380
381            classes.push(ClassInfo { ref_type_tag, type_id, signature: signature.to_string(), status });
382        }
383
384        Ok(classes)
385    }
386
387    /// List every loaded reference type (VirtualMachine.AllClasses command).
388    ///
389    /// Heavier than `classes_by_signature` (returns thousands of entries), but lets a caller
390    /// resolve a class by *simple* name when the full package isn't known — e.g. match any
391    /// signature ending in `/ConfigDefaultUtils;`.
392    ///
393    /// # Errors
394    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
395    pub async fn all_classes(&mut self) -> JdwpResult<Vec<ClassInfo>> {
396        let id = self.next_id();
397        let packet = CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, vm_commands::ALL_CLASSES);
398
399        let reply = self.send_command(packet).await?;
400        reply.check_error()?;
401
402        let mut data = reply.data();
403
404        let classes_count = read_i32(&mut data)?;
405        let mut classes = Vec::with_capacity(usize::try_from(classes_count).unwrap_or(0));
406
407        for _ in 0..classes_count {
408            let ref_type_tag = read_u8(&mut data)?;
409            let type_id = crate::reader::read_u64(&mut data)?;
410            let signature = read_string(&mut data)?;
411            let status = read_i32(&mut data)?;
412
413            classes.push(ClassInfo { ref_type_tag, type_id, signature, status });
414        }
415
416        Ok(classes)
417    }
418}