jdwp_client/eventrequest.rs
1// EventRequest command implementations
2//
3// Set up event requests (breakpoints, steps, exceptions, etc.)
4
5use crate::commands::{command_sets, event_commands, event_kinds};
6use crate::connection::JdwpConnection;
7use crate::protocol::{CommandPacket, JdwpResult};
8use crate::reader::read_i32;
9use crate::types::{FieldId, MethodId, ObjectId, ReferenceTypeId, ThreadId};
10use bytes::BufMut;
11
12/// The three per-request modifiers every `EventRequest.Set` here can carry, in one value.
13///
14/// Bundled rather than passed as a trailing trio because they travel together on all four request
15/// builders and are the same concept in each — `CONTEXT.md` calls them **filters**: modifiers the
16/// *debuggee* applies, so a non-match produces no event at all. Keeping them in one place is also where
17/// the surprise lives, and it is per-kind rather than per-modifier: `HotSpot` accepts every one of these on
18/// every kind below and does not always **apply** them. See ADR-0027 for the measured table — an
19/// `InstanceOnly` on a `METHOD_EXIT` is accepted and ignored, on an `EXCEPTION` it works — and note that
20/// `canUseInstanceFilters` reads `true` either way, so the capability bit does not settle it.
21#[derive(Debug, Clone, Copy, Default)]
22pub struct EventFilters {
23 /// `Count` (1): report only the Nth occurrence, after which the **debuggee** deletes the request.
24 pub count: Option<i32>,
25 /// `ThreadOnly` (10): restrict to hits on one thread.
26 pub thread: Option<ThreadId>,
27 /// `InstanceOnly` (11): restrict to hits whose `this` is one specific object. An armed one **pins**
28 /// that object in the debuggee until the request is cleared (measured; ADR-0027).
29 pub instance: Option<ObjectId>,
30}
31
32/// Suspend policy for events
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[repr(u8)]
35pub enum SuspendPolicy {
36 None = 0,
37 EventThread = 1,
38 All = 2,
39}
40
41/// `EventRequest.Set` modifier kinds, in the order the JDWP spec numbers them. These are easy to
42/// misremember — `ClassOnly` and `FieldOnly` are four apart, and passing the wrong one gets an
43/// unhelpful `INTERNAL` (113) back rather than a complaint about the modifier — so name them.
44mod mod_kinds {
45 pub const COUNT: u8 = 1;
46 pub const THREAD_ONLY: u8 = 3;
47 /// `ClassOnly` (4): restrict to one reference type **and its subtypes**, by id rather than by pattern.
48 ///
49 /// **What it restricts is per event kind, and the monitor kinds are the exception the spec calls out.**
50 /// For most events it tests the *location*'s class; for `MONITOR_WAIT` and `MONITOR_WAITED` it tests
51 /// the class of the **monitor object**; for `CLASS_PREPARE` it tests the type being prepared. So the
52 /// same modifier on `MONITOR_CONTENDED_ENTER` and on `MONITOR_WAIT` answers two different questions —
53 /// which is why `set_monitor_request` documents what its caller is actually narrowing rather than
54 /// calling it "a filter on the lock's type" for all four (DUMP-7, #96, ADR-0035).
55 pub const CLASS_ONLY: u8 = 4;
56 pub const CLASS_MATCH: u8 = 5;
57 /// `ClassExclude` (6): drop events from classes matching a pattern. One modifier per pattern —
58 /// JDWP carries a single string each, so N exclusions occupy N of the request's modifier slots.
59 pub const CLASS_EXCLUDE: u8 = 6;
60 /// `InstanceOnly` (11): restrict the request to hits whose `this` is one specific object.
61 ///
62 /// Filters **inside the JVM**, so an excluded hit costs no packet and no thread suspension — the
63 /// distinction that matters on a shared instance, where every other narrowing this crate offers
64 /// happens after the event has already crossed the wire.
65 pub const INSTANCE_ONLY: u8 = 11;
66 pub const LOCATION_ONLY: u8 = 7;
67 pub const EXCEPTION_ONLY: u8 = 8;
68 pub const FIELD_ONLY: u8 = 9;
69}
70
71impl JdwpConnection {
72 /// Set a breakpoint at a specific location (EventRequest.Set command)
73 /// Returns the request ID for this breakpoint
74 ///
75 /// # Errors
76 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
77 pub async fn set_breakpoint(
78 &mut self,
79 class_id: ReferenceTypeId,
80 method_id: MethodId,
81 bytecode_index: u64,
82 suspend_policy: SuspendPolicy,
83 ) -> JdwpResult<i32> {
84 let id = self.next_id();
85 let mut packet = CommandPacket::new(id, command_sets::EVENT_REQUEST, event_commands::SET);
86
87 // Event kind: BREAKPOINT (2)
88 packet.data.put_u8(event_kinds::BREAKPOINT);
89
90 // Suspend policy
91 packet.data.put_u8(suspend_policy as u8);
92
93 // Number of modifiers (1 - location only)
94 packet.data.put_i32(1);
95
96 // Modifier kind: LocationOnly
97 packet.data.put_u8(mod_kinds::LOCATION_ONLY);
98
99 // Location:
100 // - type tag (1 = class)
101 packet.data.put_u8(1);
102 // - class ID
103 packet.data.put_u64(class_id);
104 // - method ID
105 packet.data.put_u64(method_id);
106 // - index (bytecode position)
107 packet.data.put_u64(bytecode_index);
108
109 let reply = self.send_command(packet).await?;
110 reply.check_error()?;
111
112 let mut data = reply.data();
113 let request_id = read_i32(&mut data)?;
114
115 Ok(request_id)
116 }
117
118 /// Clear a breakpoint by request ID (EventRequest.Clear command)
119 ///
120 /// # Errors
121 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
122 pub async fn clear_breakpoint(&mut self, request_id: i32) -> JdwpResult<()> {
123 let id = self.next_id();
124 let mut packet = CommandPacket::new(id, command_sets::EVENT_REQUEST, event_commands::CLEAR);
125
126 // Event kind: BREAKPOINT
127 packet.data.put_u8(event_kinds::BREAKPOINT);
128
129 // Request ID
130 packet.data.put_i32(request_id);
131
132 let reply = self.send_command(packet).await?;
133 reply.check_error()?;
134
135 Ok(())
136 }
137
138 /// Request notification when a class matching `class_pattern` is prepared/loaded
139 /// (EventRequest.Set, eventKind `CLASS_PREPARE`, with a `ClassMatch` modifier). The pattern is a
140 /// dotted class name, optionally with a leading/trailing `*` wildcard (e.g.
141 /// `br.com.infotravel.service.PontoVendaSrv`). Returns the request id. This is the primitive
142 /// behind deferred ("class not loaded yet") breakpoints: register it, then arm the real
143 /// breakpoint when the matching `ClassPrepare` event arrives.
144 ///
145 /// # Errors
146 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
147 pub async fn set_class_prepare(
148 &mut self,
149 class_pattern: &str,
150 suspend_policy: SuspendPolicy,
151 ) -> JdwpResult<i32> {
152 let id = self.next_id();
153 let mut packet = CommandPacket::new(id, command_sets::EVENT_REQUEST, event_commands::SET);
154
155 packet.data.put_u8(event_kinds::CLASS_PREPARE);
156 packet.data.put_u8(suspend_policy as u8);
157
158 // One modifier: ClassMatch with the dotted class pattern.
159 packet.data.put_i32(1);
160 packet.data.put_u8(mod_kinds::CLASS_MATCH);
161 let pat = class_pattern.as_bytes();
162 packet.data.put_u32(u32::try_from(pat.len()).unwrap_or(u32::MAX));
163 packet.data.extend_from_slice(pat);
164
165 let reply = self.send_command(packet).await?;
166 reply.check_error()?;
167
168 let mut data = reply.data();
169 let request_id = read_i32(&mut data)?;
170 Ok(request_id)
171 }
172
173 /// Clear a `CLASS_PREPARE` request by id (EventRequest.Clear command).
174 ///
175 /// # Errors
176 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
177 pub async fn clear_class_prepare(&mut self, request_id: i32) -> JdwpResult<()> {
178 let id = self.next_id();
179 let mut packet = CommandPacket::new(id, command_sets::EVENT_REQUEST, event_commands::CLEAR);
180 packet.data.put_u8(event_kinds::CLASS_PREPARE);
181 packet.data.put_i32(request_id);
182 let reply = self.send_command(packet).await?;
183 reply.check_error()?;
184 Ok(())
185 }
186
187 /// Break when an exception is thrown (EventRequest.Set, eventKind EXCEPTION, with an
188 /// `ExceptionOnly` modifier). `ref_type` restricts to a single exception class *and its
189 /// subclasses*; pass `None` (or 0) to catch every exception — noisy, since a live JVM throws
190 /// and catches exceptions internally all the time, so prefer a concrete type. `caught` /
191 /// `uncaught` select which throws to report (at least one should be true). Returns the request
192 /// id. This is the primitive behind `debug.set_exception_stop`.
193 ///
194 /// # Errors
195 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
196 pub async fn set_exception_request(
197 &mut self,
198 ref_type: Option<ReferenceTypeId>,
199 caught: bool,
200 uncaught: bool,
201 suspend_policy: SuspendPolicy,
202 ) -> JdwpResult<i32> {
203 self.set_exception_request_ex(ref_type, caught, uncaught, suspend_policy, EventFilters::default())
204 .await
205 }
206
207 /// As [`set_exception_request`](Self::set_exception_request), plus optional `ThreadOnly` (report
208 /// only throws on one thread — the single biggest noise reduction on a busy app server, FILT-1)
209 /// and `Count`.
210 ///
211 /// `Count` reports **only the Nth throw** and then the JVM deletes the request — it is not a
212 /// sampler, so `count: 5` gives you throw #5 and nothing before or after it.
213 ///
214 /// **It is not what bounds trace mode**, and no caller in this workspace passes it: every call site
215 /// gives `None`. The trace-hit budget is counted *server-side* by `decrement_trace_budget`, because
216 /// the requirement is "record the first N hits, then stop" and `Count` cannot express that — it
217 /// would silently record one trace instead of N. See ADR-0002, which rejected `Count` for exactly
218 /// this and notes the JVM-side expiry is attractive enough that it was nearly re-proposed after
219 /// being turned down once. This doc comment previously claimed the opposite; a maintainer who
220 /// believed it might remove the server-side counter as redundant.
221 ///
222 /// `Count` *is* the right tool for `hit_count` ("stop on the Nth hit"), which is what it means, and
223 /// that is where [`set_breakpoint_ex`](Self::set_breakpoint_ex) uses it.
224 ///
225 /// # Errors
226 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
227 pub async fn set_exception_request_ex(
228 &mut self,
229 ref_type: Option<ReferenceTypeId>,
230 caught: bool,
231 uncaught: bool,
232 suspend_policy: SuspendPolicy,
233 filters: EventFilters,
234 ) -> JdwpResult<i32> {
235 let id = self.next_id();
236 let mut packet = CommandPacket::new(id, command_sets::EVENT_REQUEST, event_commands::SET);
237
238 packet.data.put_u8(event_kinds::EXCEPTION);
239 packet.data.put_u8(suspend_policy as u8);
240
241 // ExceptionOnly is always present; ThreadOnly, Count and InstanceOnly are added when asked for.
242 let n_mods = 1
243 + i32::from(filters.count.is_some())
244 + i32::from(filters.thread.is_some())
245 + i32::from(filters.instance.is_some());
246 packet.data.put_i32(n_mods);
247
248 // ExceptionOnly — refType (0 = all), caught flag, uncaught flag.
249 packet.data.put_u8(mod_kinds::EXCEPTION_ONLY);
250 packet.data.put_u64(ref_type.unwrap_or(0));
251 packet.data.put_u8(u8::from(caught));
252 packet.data.put_u8(u8::from(uncaught));
253
254 write_count_thread(&mut packet, filters.count, filters.thread);
255 write_instance_only(&mut packet, filters.instance);
256
257 let reply = self.send_command(packet).await?;
258 reply.check_error()?;
259
260 let mut data = reply.data();
261 let request_id = read_i32(&mut data)?;
262 Ok(request_id)
263 }
264
265 /// Clear an EXCEPTION request by id (EventRequest.Clear command).
266 ///
267 /// # Errors
268 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
269 pub async fn clear_exception_request(&mut self, request_id: i32) -> JdwpResult<()> {
270 let id = self.next_id();
271 let mut packet = CommandPacket::new(id, command_sets::EVENT_REQUEST, event_commands::CLEAR);
272 packet.data.put_u8(event_kinds::EXCEPTION);
273 packet.data.put_i32(request_id);
274 let reply = self.send_command(packet).await?;
275 reply.check_error()?;
276 Ok(())
277 }
278
279 /// Watch one field (EventRequest.Set with a `FieldOnly` modifier) — the primitive behind
280 /// `debug.set_field_stop`, answering "who touches this field?".
281 ///
282 /// `kind` picks [`WatchKind::Modify`] (`FIELD_MODIFICATION` — fires *before* the store commits,
283 /// so the field still reads as its old value) or [`WatchKind::Access`] (`FIELD_ACCESS`, every
284 /// read — far noisier). `ref_type` must be the type that *declares* the field, and `field_id`
285 /// one of its fields; a field id from a subclass is rejected by the JVM. Returns the request id.
286 ///
287 /// The JVM must report `canWatchFieldModification` / `canWatchFieldAccess`; `HotSpot` does, but
288 /// watchpoints disable JIT optimisation of that field, so expect the debuggee to slow down.
289 ///
290 /// # Errors
291 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed. A JVM
292 /// without the capability answers `NOT_IMPLEMENTED` (99).
293 pub async fn set_field_watch(
294 &mut self,
295 ref_type: ReferenceTypeId,
296 field_id: FieldId,
297 kind: WatchKind,
298 suspend_policy: SuspendPolicy,
299 ) -> JdwpResult<i32> {
300 self.set_field_watch_ex(ref_type, field_id, kind, suspend_policy, EventFilters::default()).await
301 }
302
303 /// As [`set_field_watch`](Self::set_field_watch), plus optional `ThreadOnly` (report only touches
304 /// from one thread, FILT-1) and `Count` — which reports **only the Nth touch** before the JVM
305 /// deletes the request, and which no caller here passes. See
306 /// [`set_exception_request_ex`](Self::set_exception_request_ex) for why the trace budget is counted
307 /// server-side instead (ADR-0002).
308 ///
309 /// # Errors
310 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed. A JVM
311 /// without the capability answers `NOT_IMPLEMENTED` (99).
312 pub async fn set_field_watch_ex(
313 &mut self,
314 ref_type: ReferenceTypeId,
315 field_id: FieldId,
316 kind: WatchKind,
317 suspend_policy: SuspendPolicy,
318 filters: EventFilters,
319 ) -> JdwpResult<i32> {
320 let id = self.next_id();
321 let mut packet = CommandPacket::new(id, command_sets::EVENT_REQUEST, event_commands::SET);
322
323 packet.data.put_u8(kind.event_kind());
324 packet.data.put_u8(suspend_policy as u8);
325
326 // FieldOnly is always present; ThreadOnly, Count and InstanceOnly are added when asked for.
327 let n_mods = 1
328 + i32::from(filters.count.is_some())
329 + i32::from(filters.thread.is_some())
330 + i32::from(filters.instance.is_some());
331 packet.data.put_i32(n_mods);
332
333 // FieldOnly — the declaring type plus the field itself.
334 packet.data.put_u8(mod_kinds::FIELD_ONLY);
335 packet.data.put_u64(ref_type);
336 packet.data.put_u64(field_id);
337
338 write_count_thread(&mut packet, filters.count, filters.thread);
339 write_instance_only(&mut packet, filters.instance);
340
341 let reply = self.send_command(packet).await?;
342 reply.check_error()?;
343
344 let mut data = reply.data();
345 let request_id = read_i32(&mut data)?;
346 Ok(request_id)
347 }
348
349 /// Report every return from a method of a class matching `class_pattern` (EventRequest.Set with a
350 /// `ClassMatch` modifier) — the primitive behind `debug.set_method_exit_stop`, answering "what did
351 /// this method actually return?" without having to guess which `return` statement runs.
352 ///
353 /// `with_return_value` picks `METHOD_EXIT_WITH_RETURN_VALUE` (kind 42), which carries the returned
354 /// value, over a plain `METHOD_EXIT` (kind 41), which only says a return happened. Kind 42 needs
355 /// JDWP ≥ 1.6 — ask [`can_get_method_return_values`](Self::can_get_method_return_values), because
356 /// unlike the monitor features this is **not** a capability bit, so an old JVM answers with a
357 /// protocol error rather than `NOT_IMPLEMENTED`.
358 ///
359 /// `class_pattern` is a dotted class name, optionally with a leading/trailing `*`. JDWP has **no
360 /// method-name modifier**, so a request on a class fires on every method of it; narrowing to one
361 /// method is the caller's job. `count` and `thread` add the `Count` and `ThreadOnly` modifiers, and
362 /// this event needs them more than any other: a suspending method exit on a hot method is the
363 /// fastest way to freeze a shared JVM this crate offers.
364 ///
365 /// Returns the request id.
366 ///
367 /// # Errors
368 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
369 pub async fn set_method_exit_request(
370 &mut self,
371 class_pattern: &str,
372 with_return_value: bool,
373 suspend_policy: SuspendPolicy,
374 count: Option<i32>,
375 thread: Option<ThreadId>,
376 ) -> JdwpResult<i32> {
377 self.set_method_exit_request_ex(
378 class_pattern,
379 with_return_value,
380 suspend_policy,
381 &[],
382 EventFilters { count, thread, instance: None },
383 )
384 .await
385 }
386
387 /// [`set_method_exit_request`](Self::set_method_exit_request) with `ClassExclude` patterns (STEP-1).
388 ///
389 /// The exclusions are what make a *wildcard* `ClassMatch` usable on a framework-heavy JVM: the match
390 /// itself is done by the JVM, so a broad pattern sweeps in every proxy and interceptor the container
391 /// generates, and each unwanted exit costs a real event before this side can discard it. An exclusion
392 /// stops the event being generated at all.
393 ///
394 /// **One modifier per pattern**, so the count written into the packet has to include all of them.
395 /// A wrong count is not diagnosed as such: the JVM reads the following bytes as another modifier and
396 /// answers `INTERNAL` (113), which says nothing about the cause.
397 ///
398 /// # Errors
399 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
400 pub async fn set_method_exit_request_ex(
401 &mut self,
402 class_pattern: &str,
403 with_return_value: bool,
404 suspend_policy: SuspendPolicy,
405 exclude: &[String],
406 filters: EventFilters,
407 ) -> JdwpResult<i32> {
408 let id = self.next_id();
409 let mut packet = CommandPacket::new(id, command_sets::EVENT_REQUEST, event_commands::SET);
410
411 packet.data.put_u8(method_exit_kind(with_return_value));
412 packet.data.put_u8(suspend_policy as u8);
413
414 // ClassMatch is always present; ThreadOnly, Count and one ClassExclude per pattern are added
415 // when asked for.
416 let n_mods = 1
417 + i32::from(filters.count.is_some())
418 + i32::from(filters.thread.is_some())
419 + i32::try_from(exclude.len()).unwrap_or(0);
420 packet.data.put_i32(n_mods);
421
422 packet.data.put_u8(mod_kinds::CLASS_MATCH);
423 let pat = class_pattern.as_bytes();
424 packet.data.put_u32(u32::try_from(pat.len()).unwrap_or(u32::MAX));
425 packet.data.extend_from_slice(pat);
426
427 for p in exclude {
428 packet.data.put_u8(mod_kinds::CLASS_EXCLUDE);
429 let b = p.as_bytes();
430 packet.data.put_u32(u32::try_from(b.len()).unwrap_or(u32::MAX));
431 packet.data.extend_from_slice(b);
432 }
433
434 write_count_thread(&mut packet, filters.count, filters.thread);
435 write_instance_only(&mut packet, filters.instance);
436
437 let reply = self.send_command(packet).await?;
438 reply.check_error()?;
439
440 let mut data = reply.data();
441 let request_id = read_i32(&mut data)?;
442 Ok(request_id)
443 }
444
445 /// Clear a method-exit request by id (EventRequest.Clear command). `with_return_value` must match
446 /// what the request was armed with — JDWP keys requests by (eventKind, requestID), and kinds 41 and
447 /// 42 are different keys, so clearing with the wrong one silently leaves the request armed.
448 ///
449 /// # Errors
450 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
451 pub async fn clear_method_exit_request(
452 &mut self,
453 request_id: i32,
454 with_return_value: bool,
455 ) -> JdwpResult<()> {
456 let id = self.next_id();
457 let mut packet = CommandPacket::new(id, command_sets::EVENT_REQUEST, event_commands::CLEAR);
458 packet.data.put_u8(method_exit_kind(with_return_value));
459 packet.data.put_i32(request_id);
460 let reply = self.send_command(packet).await?;
461 reply.check_error()?;
462 Ok(())
463 }
464
465 /// Whether this JVM can report a method's return value (`METHOD_EXIT_WITH_RETURN_VALUE`).
466 ///
467 /// A JDWP **version** check, not a capability bit: JDI's `canGetMethodReturnValues()` is defined as
468 /// JDWP ≥ 1.6, and neither `Capabilities` nor `CapabilitiesNew` carries a flag for it. Getting this
469 /// wrong means looking for a bit that does not exist and concluding the JVM can't do it.
470 ///
471 /// # Errors
472 /// Returns a [`JdwpError`](crate::JdwpError) if the version request fails or the reply cannot be parsed.
473 pub async fn can_get_method_return_values(&mut self) -> JdwpResult<bool> {
474 let v = self.get_version().await?;
475 Ok(v.jdwp_major > 1 || (v.jdwp_major == 1 && v.jdwp_minor >= 6))
476 }
477
478 /// Report lock contention as it happens (EventRequest.Set, one of the four `MONITOR_*` kinds) — the
479 /// primitive behind `debug.set_monitor_stop`, answering "what are these threads blocked on?" **without
480 /// suspending anything** (DUMP-7, #96).
481 ///
482 /// The event-driven counterpart to [`owned_monitors`](Self::owned_monitors) /
483 /// [`current_contended_monitor`](Self::current_contended_monitor), which can only be asked of a thread
484 /// that is already suspended. That is the whole point: "requests are hanging on a lock" was the one
485 /// wedged-app-server question that forced a freeze of a shared instance.
486 ///
487 /// **Ask [`capabilities_new`](Self::capabilities_new) for `can_request_monitor_events` first.** Unlike
488 /// `METHOD_EXIT_WITH_RETURN_VALUE` this *is* a capability bit, so a JVM without it answers
489 /// `NOT_IMPLEMENTED` (99) — which is exactly the bare error code the capability rule exists to improve
490 /// on.
491 ///
492 /// `filters.thread` (`ThreadOnly`) is the cheap narrowing and acts inside the JVM. `monitor_class`
493 /// adds a `ClassOnly`, and **what it narrows depends on the kind**, per the JDWP spec's own wording for
494 /// modKind 4: for [`MonitorKind::Wait`] and [`MonitorKind::Waited`] it tests the class of the *monitor
495 /// object*, and for [`MonitorKind::Blocked`] and [`MonitorKind::Acquired`] it tests the class of the
496 /// *location* — the code that blocked, not the lock it blocked on. See `mod_kinds::CLASS_ONLY`.
497 ///
498 /// `filters.count` and `filters.instance` are accepted by the signature because [`EventFilters`] is one
499 /// value, but see `mcp-server`'s arming path: `InstanceOnly` is refused there rather than passed
500 /// through, on the ADR-0027 rule that acceptance is not application.
501 ///
502 /// # Errors
503 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed. `NOT_IMPLEMENTED`
504 /// (99) when the JVM lacks `canRequestMonitorEvents`.
505 pub async fn set_monitor_request(
506 &mut self,
507 kind: MonitorKind,
508 suspend_policy: SuspendPolicy,
509 monitor_class: Option<ReferenceTypeId>,
510 filters: EventFilters,
511 ) -> JdwpResult<i32> {
512 let id = self.next_id();
513 let mut packet = CommandPacket::new(id, command_sets::EVENT_REQUEST, event_commands::SET);
514
515 packet.data.put_u8(kind.event_kind());
516 packet.data.put_u8(suspend_policy as u8);
517
518 // Every modifier here is optional — a monitor request with none is the honest "report all
519 // contention", unlike a breakpoint, which cannot exist without a `LocationOnly`.
520 let n_mods = i32::from(monitor_class.is_some())
521 + i32::from(filters.count.is_some())
522 + i32::from(filters.thread.is_some())
523 + i32::from(filters.instance.is_some());
524 packet.data.put_i32(n_mods);
525
526 if let Some(t) = monitor_class {
527 packet.data.put_u8(mod_kinds::CLASS_ONLY);
528 packet.data.put_u64(t);
529 }
530 write_count_thread(&mut packet, filters.count, filters.thread);
531 write_instance_only(&mut packet, filters.instance);
532
533 let reply = self.send_command(packet).await?;
534 reply.check_error()?;
535
536 let mut data = reply.data();
537 let request_id = read_i32(&mut data)?;
538 Ok(request_id)
539 }
540
541 /// Clear a monitor request by id (EventRequest.Clear command). `kind` must match the one the request
542 /// was armed with — JDWP keys requests by (eventKind, requestID), and the four monitor kinds are four
543 /// separate keys, so clearing with the wrong one silently leaves the request armed.
544 ///
545 /// # Errors
546 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
547 pub async fn clear_monitor_request(&mut self, request_id: i32, kind: MonitorKind) -> JdwpResult<()> {
548 let id = self.next_id();
549 let mut packet = CommandPacket::new(id, command_sets::EVENT_REQUEST, event_commands::CLEAR);
550 packet.data.put_u8(kind.event_kind());
551 packet.data.put_i32(request_id);
552 let reply = self.send_command(packet).await?;
553 reply.check_error()?;
554 Ok(())
555 }
556
557 /// Clear a field watch by id (EventRequest.Clear command). `kind` must match the one the
558 /// request was created with — JDWP keys requests by (eventKind, requestID).
559 ///
560 /// # Errors
561 /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
562 pub async fn clear_field_watch(&mut self, request_id: i32, kind: WatchKind) -> JdwpResult<()> {
563 let id = self.next_id();
564 let mut packet = CommandPacket::new(id, command_sets::EVENT_REQUEST, event_commands::CLEAR);
565 packet.data.put_u8(kind.event_kind());
566 packet.data.put_i32(request_id);
567 let reply = self.send_command(packet).await?;
568 reply.check_error()?;
569 Ok(())
570 }
571}
572
573/// Append the optional `Count` and `ThreadOnly` modifiers to an `EventRequest.Set` packet, in that
574/// order. The count of modifiers must already have been written to account for whichever are present.
575/// `Count` is written before `ThreadOnly` to match the numbering the JVM expects, though the spec
576/// leaves modifier order free.
577fn write_count_thread(packet: &mut CommandPacket, count: Option<i32>, thread: Option<ThreadId>) {
578 if let Some(c) = count {
579 packet.data.put_u8(mod_kinds::COUNT);
580 packet.data.put_i32(c);
581 }
582 if let Some(t) = thread {
583 packet.data.put_u8(mod_kinds::THREAD_ONLY);
584 packet.data.put_u64(t);
585 }
586}
587
588/// Write an `InstanceOnly` modifier (FILT-9), when one was asked for.
589///
590/// Kept beside [`write_count_thread`] and separate from it because it is not universal: the modifier
591/// tests the event's `this`, so it is meaningless where there is none, and which kinds the JVM will
592/// actually accept it on is measured rather than assumed — see `mcp-server`'s arming paths, which refuse
593/// the combinations that do not work instead of letting the JVM answer `INTERNAL` (113).
594fn write_instance_only(packet: &mut CommandPacket, instance: Option<ObjectId>) {
595 if let Some(o) = instance {
596 packet.data.put_u8(mod_kinds::INSTANCE_ONLY);
597 packet.data.put_u64(o);
598 }
599}
600
601/// The JDWP event kind a method-exit request uses, for both Set and Clear. Kinds 41 and 42 are separate
602/// request keys, so the same answer has to serve both commands or a clear can miss its request.
603const fn method_exit_kind(with_return_value: bool) -> u8 {
604 if with_return_value {
605 event_kinds::METHOD_EXIT_WITH_RETURN_VALUE
606 } else {
607 event_kinds::METHOD_EXIT
608 }
609}
610
611/// Which of the four monitor events a request fires on (DUMP-7, #96).
612///
613/// **They are two pairs, not four independent kinds, and the names say which.** `Blocked` → `Acquired`
614/// brackets one *contended entry* (a thread queued on a lock somebody else held, then got it), and `Wait`
615/// → `Waited` brackets one `Object.wait()`. Arming only one half of a pair is legitimate — it answers "is
616/// anything blocking at all" for the price of one request — but it can never yield a duration, because
617/// [neither half carries a
618/// timing](crate::events::EventKind::MonitorContendedEntered) and the elapsed is measured across the two.
619///
620/// The labels are deliberately not the JDWP constant names. `MONITOR_CONTENDED_ENTER` and
621/// `MONITOR_CONTENDED_ENTERED` differ by two letters and mean opposite ends of the same block, which is a
622/// reading mistake waiting to happen in a reply a human has to act on.
623#[derive(Debug, Clone, Copy, PartialEq, Eq)]
624pub enum MonitorKind {
625 /// `MONITOR_CONTENDED_ENTER` (43): a thread began blocking on a monitor another thread owns.
626 Blocked,
627 /// `MONITOR_CONTENDED_ENTERED` (44): a thread that was blocking has acquired the monitor.
628 Acquired,
629 /// `MONITOR_WAIT` (45): a thread is about to `Object.wait()`, which **releases** the monitor.
630 Wait,
631 /// `MONITOR_WAITED` (46): a thread's `Object.wait()` returned, either notified or timed out.
632 Waited,
633}
634
635impl MonitorKind {
636 /// Every kind, in the order the protocol numbers them — so a caller arming "all of them" arms them in
637 /// a stable, reportable order rather than a hash order.
638 pub const ALL: [Self; 4] = [Self::Blocked, Self::Acquired, Self::Wait, Self::Waited];
639
640 /// The JDWP event kind this registers as, used for both Set and Clear.
641 #[must_use]
642 pub const fn event_kind(self) -> u8 {
643 match self {
644 Self::Blocked => event_kinds::MONITOR_CONTENDED_ENTER,
645 Self::Acquired => event_kinds::MONITOR_CONTENDED_ENTERED,
646 Self::Wait => event_kinds::MONITOR_WAIT,
647 Self::Waited => event_kinds::MONITOR_WAITED,
648 }
649 }
650
651 /// Lowercase label used in tool arguments and output.
652 #[must_use]
653 pub const fn label(self) -> &'static str {
654 match self {
655 Self::Blocked => "blocked",
656 Self::Acquired => "acquired",
657 Self::Wait => "wait",
658 Self::Waited => "waited",
659 }
660 }
661
662 /// The other half of this kind's pair — the one an elapsed measurement needs armed as well.
663 #[must_use]
664 pub const fn partner(self) -> Self {
665 match self {
666 Self::Blocked => Self::Acquired,
667 Self::Acquired => Self::Blocked,
668 Self::Wait => Self::Waited,
669 Self::Waited => Self::Wait,
670 }
671 }
672
673 /// Whether a `ClassOnly` modifier on this kind tests the **monitor object**'s class rather than the
674 /// location's — true for the wait pair only, per `mod_kinds::CLASS_ONLY`.
675 #[must_use]
676 pub const fn class_filter_tests_monitor(self) -> bool {
677 matches!(self, Self::Wait | Self::Waited)
678 }
679}
680
681/// Which kind of field touch a watchpoint fires on.
682#[derive(Debug, Clone, Copy, PartialEq, Eq)]
683pub enum WatchKind {
684 /// Every read of the field (`FIELD_ACCESS`) — noisy on a hot field.
685 Access,
686 /// Every write to the field (`FIELD_MODIFICATION`), reported before the store commits.
687 Modify,
688}
689
690impl WatchKind {
691 /// The JDWP event kind this watch registers as, used for both Set and Clear.
692 #[must_use]
693 pub const fn event_kind(self) -> u8 {
694 match self {
695 Self::Access => event_kinds::FIELD_ACCESS,
696 Self::Modify => event_kinds::FIELD_MODIFICATION,
697 }
698 }
699
700 /// Lowercase label used in tool output and arguments (`"access"` / `"modify"`).
701 #[must_use]
702 pub const fn label(self) -> &'static str {
703 match self {
704 Self::Access => "access",
705 Self::Modify => "modify",
706 }
707 }
708}