Skip to main content

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}