1use std::sync::Arc;
2
3use anyhow::{Context, Error, Result};
4
5use remoteprocess::{Pid, ProcessMemory};
6use serde_derive::Serialize;
7
8use crate::config::{Config, LineNo};
9use crate::python_data_access::{copy_bytes, copy_string};
10use crate::python_interpreters::{
11 CodeObject, FrameObject, InterpreterState, ThreadState, TupleObject,
12};
13
14#[derive(Debug, Clone, Serialize)]
16pub struct StackTrace {
17 pub pid: Pid,
19 pub thread_id: u64,
21 pub thread_name: Option<String>,
23 pub os_thread_id: Option<u64>,
25 pub active: bool,
27 pub owns_gil: bool,
29 pub frames: Vec<Frame>,
31 pub process_info: Option<Arc<ProcessInfo>>,
33}
34
35#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize)]
37pub struct Frame {
38 pub name: String,
40 pub filename: String,
42 pub module: Option<String>,
44 pub short_filename: Option<String>,
46 pub line: i32,
48 pub locals: Option<Vec<LocalVariable>>,
50 pub is_entry: bool,
52 pub is_shim_entry: bool,
54}
55
56#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize)]
57pub struct LocalVariable {
58 pub name: String,
59 pub addr: usize,
60 pub arg: bool,
61 pub repr: Option<String>,
62}
63
64#[derive(Debug, Clone, Serialize)]
65pub struct ProcessInfo {
66 pub pid: Pid,
67 pub command_line: String,
68 pub parent: Option<Box<ProcessInfo>>,
69}
70
71pub fn get_stack_traces<I, P>(
73 interpreter_address: usize,
74 process: &P,
75 threadstate_address: usize,
76 config: Option<&Config>,
77) -> Result<Vec<StackTrace>, Error>
78where
79 I: InterpreterState,
80 P: ProcessMemory,
81{
82 let gil_thread_id = get_gil_threadid::<I, P>(threadstate_address, process)?;
83
84 let threadstate_ptr_ptr = I::threadstate_ptr_ptr(interpreter_address);
85 let mut threads: *const I::ThreadState = process
86 .copy_struct(threadstate_ptr_ptr as usize)
87 .context("Failed to copy PyThreadState head pointer")?;
88
89 let mut ret = Vec::new();
90
91 let lineno = config.map(|c| c.lineno).unwrap_or(LineNo::NoLine);
92 let dump_locals = config.map(|c| c.dump_locals).unwrap_or(0);
93
94 while !threads.is_null() {
95 let thread = process
96 .copy_pointer(threads)
97 .context("Failed to copy PyThreadState")?;
98
99 let mut trace = get_stack_trace(&thread, process, dump_locals > 0, lineno)?;
100 trace.owns_gil = trace.thread_id == gil_thread_id;
101
102 ret.push(trace);
103 if ret.len() > 4096 {
105 return Err(format_err!("Max thread recursion depth reached"));
106 }
107 threads = thread.next();
108 }
109 Ok(ret)
110}
111
112pub fn get_stack_trace<T, P>(
114 thread: &T,
115 process: &P,
116 copy_locals: bool,
117 lineno: LineNo,
118) -> Result<StackTrace, Error>
119where
120 T: ThreadState,
121 P: ProcessMemory,
122{
123 let mut frames = Vec::new();
125
126 let mut frame_address = thread.frame_address();
128 if let Some(addr) = frame_address {
129 frame_address = Some(process.copy_struct(addr)?);
130 }
131
132 let mut frame_ptr = thread.frame(frame_address);
133
134 let set_last_frame_as_shim_entry = &mut |frames: &mut Vec<Frame>| {
138 if let Some(frame) = frames.last_mut() {
139 frame.is_shim_entry = true;
140 }
141 };
142
143 while !frame_ptr.is_null() {
144 let frame = process
145 .copy_pointer(frame_ptr)
146 .context("Failed to copy PyFrameObject")?;
147
148 let code = process
149 .copy_pointer(frame.code())
150 .context("Failed to copy PyCodeObject")?;
151
152 let filename = copy_string(code.filename(), process).context("Failed to copy filename");
153 let name = copy_string(code.name(), process).context("Failed to copy function name");
154
155 if filename.is_err() || name.is_err() {
163 frame_ptr = frame.back();
164 set_last_frame_as_shim_entry(&mut frames);
165 continue;
166 }
167 let filename = filename?;
168 let name = name?;
169
170 if filename.is_empty() || filename == "<shim>" {
173 frame_ptr = frame.back();
174 set_last_frame_as_shim_entry(&mut frames);
175 continue;
176 }
177
178 let line = match lineno {
179 LineNo::NoLine => 0,
180 LineNo::First => code.first_lineno(),
181 LineNo::LastInstruction => match get_line_number(&code, frame.lasti(), process) {
182 Ok(line) => line,
183 Err(e) => {
184 warn!(
189 "Failed to get line number from {}.{}: {}",
190 filename, name, e
191 );
192 0
193 }
194 },
195 };
196
197 let locals = if copy_locals {
198 Some(
199 get_locals(&code, frame_ptr, &frame, process)
200 .context("Failed to get local variables")?,
201 )
202 } else {
203 None
204 };
205
206 let is_entry = frame.is_entry();
207
208 frames.push(Frame {
209 name,
210 filename,
211 line,
212 short_filename: None,
213 module: None,
214 locals,
215 is_entry,
216 is_shim_entry: false,
217 });
218 if frames.len() > 4096 {
219 return Err(format_err!("Max frame recursion depth reached"));
220 }
221
222 frame_ptr = frame.back();
223 }
224
225 set_last_frame_as_shim_entry(&mut frames);
227
228 Ok(StackTrace {
229 pid: 0,
230 frames,
231 thread_id: thread.thread_id(),
232 thread_name: None,
233 owns_gil: false,
234 active: true,
235 os_thread_id: thread.native_thread_id(),
236 process_info: None,
237 })
238}
239
240impl StackTrace {
241 pub fn status_str(&self) -> &str {
242 match (self.owns_gil, self.active) {
243 (_, false) => "idle",
244 (true, true) => "active+gil",
245 (false, true) => "active",
246 }
247 }
248
249 pub fn format_threadid(&self) -> String {
250 #[cfg(target_os = "macos")]
252 return format!("{:#X}", self.thread_id);
253
254 #[cfg(not(target_os = "macos"))]
256 match self.os_thread_id {
257 Some(tid) => format!("{}", tid),
258 None => format!("{:#X}", self.thread_id),
259 }
260 }
261}
262
263fn get_line_number<C: CodeObject, P: ProcessMemory>(
265 code: &C,
266 lasti: i32,
267 process: &P,
268) -> Result<i32, Error> {
269 let table =
270 copy_bytes(code.line_table(), process).context("Failed to copy line number table")?;
271 Ok(code.get_line_number(lasti, &table))
272}
273
274fn get_locals<C: CodeObject, F: FrameObject, P: ProcessMemory>(
275 code: &C,
276 frameptr: *const F,
277 frame: &F,
278 process: &P,
279) -> Result<Vec<LocalVariable>, Error> {
280 let local_count = code.nlocals() as usize;
281 let argcount = code.argcount() as usize;
282 let varnames = process
283 .copy_pointer(code.varnames())
284 .context("Failed to get varnames from PyCodeObject")?;
285
286 let ptr_size = std::mem::size_of::<*const i32>();
287 let locals_addr = frameptr as usize + std::mem::size_of_val(frame) - ptr_size;
288
289 let mut ret = Vec::new();
290
291 for i in 0..local_count {
292 let nameptr: *const C::StringObject =
293 process.copy_struct(varnames.address(code.varnames() as usize, i))?;
294
295 let name = copy_string(nameptr, process).context("Failed to copy local variable name")?;
296 let addr: usize = process.copy_struct(locals_addr + i * ptr_size)?;
297
298 let addr = if addr & 1 == 1 { addr - 1 } else { addr };
300
301 if addr == 0 {
302 continue;
303 }
304 ret.push(LocalVariable {
305 name,
306 addr,
307 arg: i < argcount,
308 repr: None,
309 });
310 }
311 Ok(ret)
312}
313
314pub fn get_gil_threadid<I: InterpreterState, P: ProcessMemory>(
315 threadstate_address: usize,
316 process: &P,
317) -> Result<u64, Error> {
318 if threadstate_address == 0 {
320 return Ok(0);
321 }
322
323 let addr = if I::HAS_GIL_RUNTIME_STATE {
324 let gil_state: crate::python_bindings::v3_13_0::_gil_runtime_state =
326 process.copy_struct(threadstate_address)?;
327 if gil_state.locked != 0 {
329 gil_state.last_holder as usize
330 } else {
331 0
332 }
333 } else {
334 process.copy_struct::<usize>(threadstate_address)?
335 };
336
337 let threadid = if addr != 0 {
339 let threadstate: I::ThreadState = process.copy_struct(addr)?;
340 threadstate.thread_id()
341 } else {
342 0
343 };
344
345 Ok(threadid)
346}
347
348impl ProcessInfo {
349 pub fn to_frame(&self) -> Frame {
350 Frame {
351 name: format!("process {}:\"{}\"", self.pid, self.command_line),
352 filename: String::from(""),
353 module: None,
354 short_filename: None,
355 line: 0,
356 locals: None,
357 is_entry: true,
358 is_shim_entry: true,
359 }
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::python_bindings::v3_7_0::PyCodeObject;
367 use crate::python_data_access::tests::to_byteobject;
368 use remoteprocess::LocalProcess;
369
370 #[test]
371 fn test_get_line_number() {
372 let mut lnotab = to_byteobject(&[0u8, 1, 10, 1, 8, 1, 4, 1]);
373 let code = PyCodeObject {
374 co_firstlineno: 3,
375 co_lnotab: &mut lnotab.base.ob_base.ob_base,
376 ..Default::default()
377 };
378 let lineno = get_line_number(&code, 30, &LocalProcess).unwrap();
379 assert_eq!(lineno, 7);
380 }
381}