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}