py_spy_for_datakit/
stack_trace.rs

1use std;
2use std::sync::Arc;
3
4use anyhow::{Context, Error, Result};
5
6use remoteprocess::{ProcessMemory, Pid, Process};
7use serde_derive::Serialize;
8
9use crate::python_interpreters::{InterpreterState, ThreadState, FrameObject, CodeObject, TupleObject};
10use crate::python_data_access::{copy_string, copy_bytes};
11use crate::config::LineNo;
12
13/// Call stack for a single python thread
14#[derive(Debug, Clone, Serialize)]
15pub struct StackTrace {
16    /// The process id than generated this stack trace
17    pub pid: Pid,
18    /// The python thread id for this stack trace
19    pub thread_id: u64,
20    // The python thread name for this stack trace
21    pub thread_name: Option<String>,
22    /// The OS thread id for this stack tracee
23    pub os_thread_id: Option<u64>,
24    /// Whether or not the thread was active
25    pub active: bool,
26    /// Whether or not the thread held the GIL
27    pub owns_gil: bool,
28    /// The frames
29    pub frames: Vec<Frame>,
30    /// process commandline / parent process info
31    pub process_info: Option<Arc<ProcessInfo>>
32}
33
34/// Information about a single function call in a stack trace
35#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize)]
36pub struct Frame {
37    /// The function name
38    pub name: String,
39    /// The full filename of the file
40    pub filename: String,
41    /// The module/shared library the
42    pub module: Option<String>,
43    /// A short, more readable, representation of the filename
44    pub short_filename: Option<String>,
45    /// The line number inside the file (or 0 for native frames without line information)
46    pub line: i32,
47    /// Local Variables associated with the frame
48    pub locals: Option<Vec<LocalVariable>>,
49}
50
51#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize)]
52pub struct LocalVariable {
53    pub name: String,
54    pub addr: usize,
55    pub arg: bool,
56    pub repr: Option<String>,
57}
58
59#[derive(Debug, Clone, Serialize)]
60pub struct ProcessInfo {
61    pub pid:  Pid,
62    pub command_line: String,
63    pub parent: Option<Box<ProcessInfo>>
64}
65
66/// Given an InterpreterState, this function returns a vector of stack traces for each thread
67pub fn get_stack_traces<I>(interpreter: &I, process: &Process, lineno: LineNo) -> Result<Vec<StackTrace>, Error>
68        where I: InterpreterState {
69    // TODO: deprecate this method
70    let mut ret = Vec::new();
71    let mut threads = interpreter.head();
72    while !threads.is_null() {
73        let thread = process.copy_pointer(threads).context("Failed to copy PyThreadState")?;
74        ret.push(get_stack_trace(&thread, process, false, lineno)?);
75        // This seems to happen occasionally when scanning BSS addresses for valid interpreters
76        if ret.len() > 4096 {
77            return Err(format_err!("Max thread recursion depth reached"));
78        }
79        threads = thread.next();
80    }
81    Ok(ret)
82}
83
84/// Gets a stack trace for an individual thread
85pub fn get_stack_trace<T>(thread: &T, process: &Process, copy_locals: bool, lineno: LineNo) -> Result<StackTrace, Error>
86        where T: ThreadState {
87    // TODO: just return frames here? everything else probably should be returned out of scope
88    let mut frames = Vec::new();
89
90    // python 3.11+ has an extra level of indirection to get the Frame from the threadstate
91    let mut frame_address = thread.frame_address();
92    if let Some(addr) = frame_address {
93        frame_address = Some(process.copy_struct(addr)?);
94    }
95
96    let mut frame_ptr = thread.frame(frame_address);
97    while !frame_ptr.is_null() {
98        let frame = process.copy_pointer(frame_ptr).context("Failed to copy PyFrameObject")?;
99        let code = process.copy_pointer(frame.code()).context("Failed to copy PyCodeObject")?;
100
101        let filename = copy_string(code.filename(), process).context("Failed to copy filename")?;
102        let name = copy_string(code.name(), process).context("Failed to copy function name")?;
103
104        let line = match lineno {
105            LineNo::NoLine => 0,
106            LineNo::FirstLineNo => code.first_lineno(),
107            LineNo::LastInstruction => match get_line_number(&code, frame.lasti(), process) {
108                Ok(line) => line,
109                Err(e) => {
110                    // Failling to get the line number really shouldn't be fatal here, but
111                    // can happen in extreme cases (https://github.com/benfred/py-spy/issues/164)
112                    // Rather than fail set the linenumber to 0. This is used by the native extensions
113                    // to indicate that we can't load a line number and it should be handled gracefully
114                    warn!("Failed to get line number from {}.{}: {}", filename, name, e);
115                    0
116                }
117            }
118        };
119
120        let locals = if copy_locals {
121            Some(get_locals(&code, frame_ptr, &frame, process)?)
122        } else {
123            None
124        };
125
126        frames.push(Frame{name, filename, line, short_filename: None, module: None, locals});
127        if frames.len() > 4096 {
128            return Err(format_err!("Max frame recursion depth reached"));
129        }
130
131        frame_ptr = frame.back();
132    }
133
134    Ok(StackTrace{pid: process.pid, frames, thread_id: thread.thread_id(), thread_name: None, owns_gil: false, active: true, os_thread_id: None, process_info: None})
135}
136
137impl StackTrace {
138    pub fn status_str(&self) -> &str {
139        match (self.owns_gil, self.active) {
140            (_, false) => "idle",
141            (true, true) => "active+gil",
142            (false, true) => "active",
143        }
144    }
145
146    pub fn format_threadid(&self) -> String {
147        // native threadids in osx are kinda useless, use the pthread id instead
148        #[cfg(target_os="macos")]
149        return format!("{:#X}", self.thread_id);
150
151        // otherwise use the native threadid if given
152        #[cfg(not(target_os="macos"))]
153        match self.os_thread_id {
154            Some(tid) => format!("{}", tid),
155            None => format!("{:#X}", self.thread_id)
156        }
157    }
158}
159
160/// Returns the line number from a PyCodeObject (given the lasti index from a PyFrameObject)
161fn get_line_number<C: CodeObject, P: ProcessMemory>(code: &C, lasti: i32, process: &P) -> Result<i32, Error> {
162    let table = copy_bytes(code.line_table(), process).context("Failed to copy line number table")?;
163    Ok(code.get_line_number(lasti, &table))
164}
165
166
167fn get_locals<C: CodeObject, F: FrameObject, P: ProcessMemory>(code: &C, frameptr: *const F, frame: &F, process: &P)
168        -> Result<Vec<LocalVariable>, Error> {
169    let local_count = code.nlocals() as usize;
170    let argcount = code.argcount() as usize;
171    let varnames = process.copy_pointer(code.varnames())?;
172
173    let ptr_size = std::mem::size_of::<*const i32>();
174    let locals_addr = frameptr as usize + std::mem::size_of_val(frame) - ptr_size;
175
176    let mut ret = Vec::new();
177
178    for i in 0..local_count {
179        let nameptr: *const C::StringObject = process.copy_struct(varnames.address(code.varnames() as usize, i))?;
180        let name = copy_string(nameptr, process)?;
181        let addr: usize = process.copy_struct(locals_addr + i * ptr_size)?;
182        if addr == 0 {
183            continue;
184        }
185        ret.push(LocalVariable{name, addr, arg: i < argcount, repr: None});
186    }
187    Ok(ret)
188}
189
190impl ProcessInfo {
191    pub fn to_frame(&self) -> Frame {
192        Frame{name: format!("process {}:\"{}\"", self.pid, self.command_line),
193            filename: String::from(""),
194            module: None, short_filename: None, line: 0, locals: None}
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use remoteprocess::LocalProcess;
202    use crate::python_bindings::v3_7_0::{PyCodeObject};
203    use crate::python_data_access::tests::to_byteobject;
204
205    #[test]
206    fn test_get_line_number() {
207        let mut lnotab = to_byteobject(&[0u8, 1, 10, 1, 8, 1, 4, 1]);
208        let code = PyCodeObject{co_firstlineno: 3,
209                                co_lnotab: &mut lnotab.base.ob_base.ob_base,
210                                ..Default::default()};
211        let lineno = get_line_number(&code, 30, &LocalProcess).unwrap();
212        assert_eq!(lineno, 7);
213    }
214}