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}