Skip to main content

jdwp_client/
thread.rs

1// ThreadReference command implementations
2//
3// Commands for working with threads (frames, status, suspend/resume)
4
5use crate::commands::{command_sets, thread_commands};
6use crate::connection::JdwpConnection;
7use crate::protocol::{CommandPacket, JdwpResult};
8use crate::reader::{read_i32, read_string, read_u64};
9use crate::types::{FrameId, Location, ObjectId, ThreadId};
10use bytes::BufMut;
11use serde::{Deserialize, Serialize};
12
13/// Stack frame information
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Frame {
16    pub frame_id: FrameId,
17    pub location: Location,
18}
19
20/// One monitor (lock) object, as JDWP reports it in a tagged-objectID.
21///
22/// The `object_id` is the identity that matters: correlating "thread A holds this" with "thread B is
23/// waiting for this" is a comparison of these ids, and that comparison is the whole of deadlock
24/// detection by eye.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26pub struct Monitor {
27    /// JDWP type tag of the monitor object (`'L'` for a plain object, `'['` for an array, …).
28    pub tag: u8,
29    pub object_id: ObjectId,
30}
31
32impl JdwpConnection {
33    /// Get stack frames for a thread (ThreadReference.Frames command)
34    ///
35    /// # Errors
36    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
37    pub async fn get_frames(
38        &mut self,
39        thread_id: ThreadId,
40        start_frame: i32,
41        length: i32,
42    ) -> JdwpResult<Vec<Frame>> {
43        let id = self.next_id();
44        let mut packet = CommandPacket::new(id, command_sets::THREAD_REFERENCE, thread_commands::FRAMES);
45
46        // Write thread ID
47        packet.data.put_u64(thread_id);
48        // Start frame (0 = current/top frame)
49        packet.data.put_i32(start_frame);
50        // Length (-1 = all frames)
51        packet.data.put_i32(length);
52
53        let reply = self.send_command(packet).await?;
54        reply.check_error()?;
55
56        let mut data = reply.data();
57
58        // Read number of frames
59        let frames_count = read_i32(&mut data)?;
60        let mut frames = Vec::with_capacity(usize::try_from(frames_count).unwrap_or(0));
61
62        for _ in 0..frames_count {
63            let frame_id = read_u64(&mut data)?;
64
65            // Read location
66            let type_tag = crate::reader::read_u8(&mut data)?;
67            let class_id = read_u64(&mut data)?;
68            let method_id = read_u64(&mut data)?;
69            let index = read_u64(&mut data)?;
70
71            frames.push(Frame { frame_id, location: Location { type_tag, class_id, method_id, index } });
72        }
73
74        Ok(frames)
75    }
76
77    /// The monitors this thread currently **holds** (ThreadReference.OwnedMonitors, command 8).
78    ///
79    /// Half of what a deadlock investigation consists of; the other half is
80    /// [`current_contended_monitor`](Self::current_contended_monitor). Cross-referencing the two across
81    /// threads — A holds what B waits for, and vice versa — is what makes a lock cycle visible, which is
82    /// otherwise unanswerable through this tool.
83    ///
84    /// **The thread must be suspended.** A running thread's lock set is not a well-defined thing to
85    /// read, so the JVM answers `THREAD_NOT_SUSPENDED` (13) rather than a snapshot that was never true.
86    /// Requires the JVM's `canGetOwnedMonitorInfo` (see [`capabilities`](Self::capabilities)); without it
87    /// the answer is `NOT_IMPLEMENTED` (99).
88    ///
89    /// # Errors
90    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
91    pub async fn owned_monitors(&mut self, thread_id: ThreadId) -> JdwpResult<Vec<Monitor>> {
92        let id = self.next_id();
93        let mut packet =
94            CommandPacket::new(id, command_sets::THREAD_REFERENCE, thread_commands::OWNED_MONITORS);
95        packet.data.put_u64(thread_id);
96
97        let reply = self.send_command(packet).await?;
98        reply.check_error()?;
99
100        let mut data = reply.data();
101        let count = read_i32(&mut data)?;
102        let mut monitors = Vec::with_capacity(usize::try_from(count).unwrap_or(0));
103        for _ in 0..count {
104            // Each entry is a tagged-objectID: one tag byte then the object id.
105            let tag = crate::reader::read_u8(&mut data)?;
106            let object_id = read_u64(&mut data)?;
107            monitors.push(Monitor { tag, object_id });
108        }
109        Ok(monitors)
110    }
111
112    /// The monitor this thread is **blocked waiting to enter**, if any
113    /// (ThreadReference.CurrentContendedMonitor, command 9).
114    ///
115    /// `None` means the thread is not contending for a lock — the common case, and not an error. A
116    /// thread parked in `Object.wait()` reports the monitor it will re-acquire.
117    ///
118    /// **The thread must be suspended**, and the JVM must report `canGetCurrentContendedMonitor`; see
119    /// [`owned_monitors`](Self::owned_monitors) for why both hold.
120    ///
121    /// # Errors
122    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
123    pub async fn current_contended_monitor(&mut self, thread_id: ThreadId) -> JdwpResult<Option<Monitor>> {
124        let id = self.next_id();
125        let mut packet = CommandPacket::new(
126            id,
127            command_sets::THREAD_REFERENCE,
128            thread_commands::CURRENT_CONTENDED_MONITOR,
129        );
130        packet.data.put_u64(thread_id);
131
132        let reply = self.send_command(packet).await?;
133        reply.check_error()?;
134
135        let mut data = reply.data();
136        let tag = crate::reader::read_u8(&mut data)?;
137        let object_id = read_u64(&mut data)?;
138        // A null objectID (0) is how "not waiting on anything" comes back, whatever the tag says.
139        Ok((object_id != 0).then_some(Monitor { tag, object_id }))
140    }
141
142    /// Get all threads (VirtualMachine.AllThreads)
143    ///
144    /// # Errors
145    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
146    pub async fn get_all_threads(&mut self) -> JdwpResult<Vec<ThreadId>> {
147        let id = self.next_id();
148        let packet =
149            CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, crate::commands::vm_commands::ALL_THREADS);
150
151        let reply = self.send_command(packet).await?;
152        reply.check_error()?;
153
154        let mut data = reply.data();
155
156        let threads_count = read_i32(&mut data)?;
157        let mut threads = Vec::with_capacity(usize::try_from(threads_count).unwrap_or(0));
158
159        for _ in 0..threads_count {
160            threads.push(read_u64(&mut data)?);
161        }
162
163        Ok(threads)
164    }
165
166    /// Get a thread's name (ThreadReference.Name).
167    ///
168    /// # Errors
169    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
170    pub async fn get_thread_name(&mut self, thread_id: ThreadId) -> JdwpResult<String> {
171        let packet = self.thread_read_request(thread_id, thread_commands::NAME);
172        let reply = self.send_command(packet).await?;
173        Self::decode_thread_name(&reply)
174    }
175
176    /// The name of each of `thread_ids`, read as **independent reads** (PERF-1, #100).
177    ///
178    /// **The widest fan-out in the tool.** A dump's triage asks this of every thread the VM has — 306 on a
179    /// production-shaped instance — and used to ask one at a time, under the suspension. A thread's name is
180    /// nothing to do with any other thread's, so this is a wave.
181    ///
182    /// **Chunk by [`MAX_READS_IN_FLIGHT`](crate::MAX_READS_IN_FLIGHT) if you have a deadline to
183    /// honour.** Passing all 306 ids is correct and bounded, but nothing can interrupt the call once it
184    /// starts, and a dump's suspension budget is checked between threads. Chunking hands the budget back
185    /// every window — which costs it nothing in time, because a window of sixteen takes about as long as one
186    /// sequential read.
187    pub async fn read_thread_names_independently(&self, thread_ids: &[ThreadId]) -> Vec<JdwpResult<String>> {
188        let packets =
189            thread_ids.iter().map(|&t| self.thread_read_request(t, thread_commands::NAME)).collect();
190        self.read_independently(packets)
191            .await
192            .into_iter()
193            .map(|reply| reply.and_then(|r| Self::decode_thread_name(&r)))
194            .collect()
195    }
196
197    /// The `(thread_status, suspend_status)` of each of `thread_ids`, read as **independent reads**.
198    ///
199    /// The dump's second per-thread read, and the one that must go out **after** the name filter rather than
200    /// beside it: a thread whose name is filtered out never has its status read on the sequential path, so a
201    /// single wave over both would spend a packet the loop never spent. See `triage_dump_threads`.
202    pub async fn read_thread_statuses_independently(
203        &self,
204        thread_ids: &[ThreadId],
205    ) -> Vec<JdwpResult<(i32, i32)>> {
206        let packets =
207            thread_ids.iter().map(|&t| self.thread_read_request(t, thread_commands::STATUS)).collect();
208        self.read_independently(packets)
209            .await
210            .into_iter()
211            .map(|reply| reply.and_then(|r| Self::decode_thread_status(&r)))
212            .collect()
213    }
214
215    /// The request half of any `ThreadReference` command whose whole payload is the thread id — `Name`,
216    /// `Status`, `SuspendCount` and friends all share that shape.
217    fn thread_read_request(&self, thread_id: ThreadId, command: u8) -> CommandPacket {
218        let id = self.next_id();
219        let mut packet = CommandPacket::new(id, command_sets::THREAD_REFERENCE, command);
220        packet.data.put_u64(thread_id);
221        packet
222    }
223
224    /// The decode half of `ThreadReference.Name`, error check included.
225    fn decode_thread_name(reply: &crate::protocol::ReplyPacket) -> JdwpResult<String> {
226        reply.check_error()?;
227        let mut data = reply.data();
228        read_string(&mut data)
229    }
230
231    /// The decode half of `ThreadReference.Status`, error check included.
232    fn decode_thread_status(reply: &crate::protocol::ReplyPacket) -> JdwpResult<(i32, i32)> {
233        reply.check_error()?;
234        let mut data = reply.data();
235        let thread_status = read_i32(&mut data)?;
236        let suspend_status = read_i32(&mut data)?;
237        Ok((thread_status, suspend_status))
238    }
239
240    /// Get a thread's (`thread_status`, `suspend_status`) (ThreadReference.Status).
241    /// `suspend_status` != 0 means the thread is currently suspended.
242    ///
243    /// # Errors
244    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
245    pub async fn get_thread_status(&mut self, thread_id: ThreadId) -> JdwpResult<(i32, i32)> {
246        let packet = self.thread_read_request(thread_id, thread_commands::STATUS);
247        let reply = self.send_command(packet).await?;
248        Self::decode_thread_status(&reply)
249    }
250
251    /// How many times this thread has been suspended (`ThreadReference.SuspendCount`).
252    ///
253    /// JDWP **counts** suspends: a thread suspended n times must be resumed n times before it runs
254    /// again. That makes this the only way to answer "did my resume actually resume it?" — a single
255    /// `resume_all` against a count of 2 leaves the thread stopped while every command still succeeds.
256    /// Verified against a real JVM: two `Suspend`s then one `Resume` leaves the debuggee stopped.
257    ///
258    /// # Errors
259    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
260    pub async fn suspend_count(&mut self, thread_id: ThreadId) -> JdwpResult<i32> {
261        let id = self.next_id();
262        let mut packet = CommandPacket::new(
263            id,
264            command_sets::THREAD_REFERENCE,
265            crate::commands::thread_commands::SUSPEND_COUNT,
266        );
267        packet.data.put_u64(thread_id);
268
269        let reply = self.send_command(packet).await?;
270        reply.check_error()?;
271
272        let mut data = reply.data();
273        read_i32(&mut data)
274    }
275
276    /// Suspend all threads (VirtualMachine.Suspend)
277    ///
278    /// Suspends are **counted** — calling this twice needs two resumes. Callers that mean "make sure it
279    /// is stopped" should check [`suspend_count`](Self::suspend_count) first rather than suspending
280    /// again, or they will build a depth that a single resume can't undo.
281    ///
282    /// # Errors
283    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
284    pub async fn suspend_all(&mut self) -> JdwpResult<()> {
285        let id = self.next_id();
286        let packet =
287            CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, crate::commands::vm_commands::SUSPEND);
288
289        let reply = self.send_command(packet).await?;
290        reply.check_error()?;
291
292        Ok(())
293    }
294
295    /// Resume all threads (VirtualMachine.Resume) — **one** decrement of every thread's suspend count.
296    ///
297    /// Not the same as "make the VM run": if anything suspended it twice, this leaves it stopped and
298    /// still reports success. Use [`resume_all_fully`](Self::resume_all_fully) when the intent is that
299    /// the application actually continues.
300    ///
301    /// # Errors
302    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
303    pub async fn resume_all(&mut self) -> JdwpResult<()> {
304        let id = self.next_id();
305        let packet =
306            CommandPacket::new(id, command_sets::VIRTUAL_MACHINE, crate::commands::vm_commands::RESUME);
307
308        let reply = self.send_command(packet).await?;
309        reply.check_error()?;
310
311        Ok(())
312    }
313
314    /// Resume until the application is actually running, not just once.
315    ///
316    /// Returns `(resumes issued, remaining suspend count)` — a remaining count of 0 means the VM is
317    /// genuinely going again. `probe_thread` is the thread whose count is checked; any live thread works
318    /// for a VM-wide suspend, since `VirtualMachine.Suspend` increments all of them.
319    ///
320    /// This exists because "resume" and "is it running" are different questions in JDWP, and a caller
321    /// whose job is to un-freeze a shared JVM (a watchdog, a panic button) must not report success on
322    /// the strength of a command that returned OK while the debuggee stayed stopped.
323    ///
324    /// Bounded by `max_resumes` so a pathological count can't spin forever; a thread that is *also*
325    /// suspended individually (an `EventThread`-policy event) may legitimately need more than one.
326    ///
327    /// # Errors
328    /// Returns a [`JdwpError`](crate::JdwpError) if a JDWP request fails or a reply cannot be parsed.
329    pub async fn resume_all_fully(
330        &mut self,
331        probe_thread: ThreadId,
332        max_resumes: u32,
333    ) -> JdwpResult<(u32, i32)> {
334        let mut issued = 0;
335        for _ in 0..max_resumes {
336            self.resume_all().await?;
337            issued += 1;
338            // A dead/invalid thread can't report a count; treat that as "nothing left to resume"
339            // rather than looping, since the thread we were watching has gone.
340            let left = self.suspend_count(probe_thread).await.unwrap_or(0);
341            if left <= 0 {
342                return Ok((issued, 0));
343            }
344        }
345        let left = self.suspend_count(probe_thread).await.unwrap_or(0);
346        Ok((issued, left))
347    }
348
349    /// Suspend **one** thread (`ThreadReference.Suspend`, set 11 command 2) — the counterpart to
350    /// [`resume_thread`](Self::resume_thread), and the cheap alternative to
351    /// [`suspend_all`](Self::suspend_all) on a debuggee other people are using.
352    ///
353    /// This is the only way to obtain an evaluable frame without freezing every in-flight request:
354    /// `VirtualMachine.Suspend` and a `SuspendPolicy::All` stop point both hold the whole VM, and on a
355    /// shared application server that is a cost nobody agreed to pay.
356    ///
357    /// **Counted, exactly like every other suspend here.** This increments *this* thread's suspend count
358    /// by one and nothing else's; a thread already held by a `VirtualMachine.Suspend`, or parked at an
359    /// `EventThread`-policy event, ends up at 2 and needs two decrements before it runs. So a caller
360    /// must read [`suspend_count`](Self::suspend_count) afterwards rather than assume a depth of 1 —
361    /// which is ADR-0003's rule arriving at the per-thread door.
362    ///
363    /// **What the JVM answers for a thread that is not running.** A **finished** thread (`ZOMBIE`) can
364    /// still be named and described while the debugger holds its `Thread` object, but it cannot be
365    /// suspended: `HotSpot` answers `INVALID_THREAD` (10) — which reads as "you passed a bad id" and is
366    /// not what happened. A **vanished** thread, whose id the JVM has already collected, answers
367    /// `INVALID_OBJECT` (20). The two are different findings and callers must not collapse them
368    /// (DUMP-4), so this returns the raw error rather than a sentence.
369    ///
370    /// # Errors
371    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
372    pub async fn suspend_thread(&mut self, thread_id: ThreadId) -> JdwpResult<()> {
373        let id = self.next_id();
374        let mut packet = CommandPacket::new(id, command_sets::THREAD_REFERENCE, thread_commands::SUSPEND);
375        packet.data.put_u64(thread_id);
376
377        let reply = self.send_command(packet).await?;
378        reply.check_error()?;
379
380        Ok(())
381    }
382
383    /// Resume a single thread (ThreadReference.Resume) — decrements just that thread's suspend
384    /// count, leaving other suspended threads alone. Used after arming a deferred breakpoint on the
385    /// thread that a `ClassPrepare` event suspended, so class init proceeds without disturbing any
386    /// thread parked at a real breakpoint.
387    ///
388    /// **One decrement, not "make this thread run".** The distinction is the same one
389    /// [`resume_all`](Self::resume_all) draws against [`resume_all_fully`](Self::resume_all_fully): the
390    /// JVM acknowledges this command whether or not the thread is left suspended underneath, so a caller
391    /// whose intent is that the thread proceeds must verify with
392    /// [`suspend_count`](Self::suspend_count). `debug.resume_thread` does exactly that, and says so when
393    /// the count did not reach zero.
394    ///
395    /// # Errors
396    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
397    pub async fn resume_thread(&mut self, thread_id: ThreadId) -> JdwpResult<()> {
398        let id = self.next_id();
399        let mut packet = CommandPacket::new(id, command_sets::THREAD_REFERENCE, thread_commands::RESUME);
400        packet.data.put_u64(thread_id);
401
402        let reply = self.send_command(packet).await?;
403        reply.check_error()?;
404
405        Ok(())
406    }
407
408    /// Force the topmost frame of a suspended thread to return `value` immediately
409    /// (ThreadReference.ForceEarlyReturn). The thread must be suspended and the value's tag must be
410    /// assignable to the method's declared return type — pass a `Void` value for a `void` method.
411    /// Lets a caller short-circuit a method (e.g. make a rejecting `salvar` return `true`) without
412    /// editing and redeploying code. Requires the JVM's `canForceEarlyReturn` capability.
413    ///
414    /// # Errors
415    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
416    pub async fn force_early_return(
417        &mut self,
418        thread_id: ThreadId,
419        value: &crate::types::Value,
420    ) -> JdwpResult<()> {
421        self.guard_mutation("a forced early return")?;
422        let id = self.next_id();
423        let mut packet =
424            CommandPacket::new(id, command_sets::THREAD_REFERENCE, thread_commands::FORCE_EARLY_RETURN);
425        packet.data.put_u64(thread_id);
426        crate::eval::write_tagged_value(&mut packet.data, value);
427
428        let reply = self.send_command(packet).await?;
429        reply.check_error()?;
430
431        Ok(())
432    }
433}