use std::borrow::Borrow;
use std::cmp::Ordering;
use std::collections::BTreeSet;
use std::fs::File;
use std::fs::{self};
use std::io::Read;
use std::io::{self};
use std::ptr;
use libc::c_void;
use libc::pid_t;
use libc::ptrace;
use libc::waitpid;
use libc::__WALL;
use libc::ESRCH;
use libc::PTRACE_ATTACH;
use libc::PTRACE_CONT;
use libc::PTRACE_DETACH;
use libc::PTRACE_INTERRUPT;
use libc::PTRACE_SEIZE;
use libc::SIGSTOP;
use libc::WIFSTOPPED;
use libc::WSTOPSIG;
#[derive(Debug, Clone)]
pub struct Process {
id: u32,
threads: Vec<Thread>,
}
impl Process {
pub fn id(&self) -> u32 {
self.id
}
pub fn threads(&self) -> &[Thread] {
&self.threads
}
}
#[derive(Debug, Clone)]
pub struct Thread {
id: u32,
name: Option<String>,
frames: Vec<Frame>,
}
impl Thread {
#[inline]
pub fn id(&self) -> u32 {
self.id
}
#[inline]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
#[inline]
pub fn frames(&self) -> &[Frame] {
&self.frames
}
}
#[derive(Debug, Clone)]
pub struct Frame {
pub(crate) ip: u64,
pub(crate) is_signal: bool,
pub(crate) is_inline: bool,
pub(crate) symbol: Option<Symbol>,
pub(crate) module: Option<String>,
pub(crate) source: Option<SourceLocation>,
}
impl Frame {
#[inline]
pub fn ip(&self) -> u64 {
self.ip
}
#[inline]
pub fn is_signal(&self) -> bool {
self.is_signal
}
#[inline]
pub fn is_inline(&self) -> bool {
self.is_inline
}
#[inline]
pub fn symbol(&self) -> Option<&Symbol> {
self.symbol.as_ref()
}
#[inline]
pub fn module(&self) -> Option<&str> {
self.module.as_deref()
}
#[inline]
pub fn source(&self) -> Option<&SourceLocation> {
self.source.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct SourceLocation {
pub(crate) file: String,
pub(crate) line: i32,
}
impl SourceLocation {
#[inline]
pub fn file(&self) -> &str {
&self.file
}
#[inline]
pub fn line(&self) -> i32 {
self.line
}
}
#[derive(Debug, Clone)]
pub struct Symbol {
pub(crate) name: String,
pub(crate) offset: u64,
pub(crate) address: u64,
pub(crate) size: u64,
}
impl Symbol {
#[inline]
pub fn name(&self) -> &str {
&self.name
}
#[inline]
pub fn offset(&self) -> u64 {
self.offset
}
#[inline]
pub fn address(&self) -> u64 {
self.address
}
#[inline]
pub fn size(&self) -> u64 {
self.size
}
}
pub fn trace(handle: &crate::proc::ProcHandle) -> io::Result<Process> {
TraceOptions::new()
.thread_names(true)
.symbols(true)
.demangle(true)
.trace(handle)
}
#[derive(Debug, Clone)]
pub struct TraceOptions {
pub(crate) snapshot: bool,
pub(crate) thread_names: bool,
pub(crate) symbols: bool,
pub(crate) demangle: bool,
pub(crate) module: bool,
pub(crate) source: bool,
pub(crate) inlines: bool,
pub(crate) ptrace_attach: bool,
pub(crate) tid: Option<u32>,
pub(crate) max_frames: usize,
}
impl Default for TraceOptions {
fn default() -> TraceOptions {
TraceOptions {
snapshot: false,
thread_names: false,
symbols: false,
demangle: false,
module: false,
source: false,
inlines: false,
ptrace_attach: true,
tid: None,
max_frames: 0,
}
}
}
impl TraceOptions {
pub fn new() -> TraceOptions {
TraceOptions::default()
}
pub fn snapshot(&mut self, snapshot: bool) -> &mut TraceOptions {
self.snapshot = snapshot;
self
}
pub fn thread_names(&mut self, thread_names: bool) -> &mut TraceOptions {
self.thread_names = thread_names;
self
}
pub fn symbols(&mut self, symbols: bool) -> &mut TraceOptions {
self.symbols = symbols;
self
}
pub fn demangle(&mut self, demangle: bool) -> &mut TraceOptions {
self.demangle = demangle;
self
}
pub fn module(&mut self, module: bool) -> &mut TraceOptions {
self.module = module;
self
}
pub fn source(&mut self, source: bool) -> &mut TraceOptions {
self.source = source;
self
}
pub fn inlines(&mut self, inlines: bool) -> &mut TraceOptions {
self.inlines = inlines;
self
}
pub fn ptrace_attach(&mut self, ptrace_attach: bool) -> &mut TraceOptions {
self.ptrace_attach = ptrace_attach;
self
}
pub fn tid(&mut self, tid: u32) -> &mut TraceOptions {
self.tid = Some(tid);
self
}
pub fn max_frames(&mut self, max_frames: usize) -> &mut TraceOptions {
self.max_frames = max_frames;
self
}
pub fn trace(&self, handle: &crate::proc::ProcHandle) -> io::Result<Process> {
let pid = handle.pid() as u32;
let mut threads = Vec::new();
self.trace_each(handle, |thread| threads.push(thread))?;
Ok(Process { id: pid, threads })
}
pub fn trace_each<F>(&self, handle: &crate::proc::ProcHandle, mut each: F) -> io::Result<()>
where
F: FnMut(Thread),
{
let pid = handle.pid() as u32;
if let Some(tid) = self.tid {
if let Some(thread) = open_thread_or_warn(tid, self.ptrace_attach)? {
each(thread.info(pid, self, handle));
}
return Ok(());
}
if self.snapshot {
self.trace_snapshot_each(handle, pid, &mut each)?;
} else {
self.trace_rolling_each(handle, pid, &mut each)?;
}
Ok(())
}
pub fn trace_core(&self, handle: &crate::proc::ProcHandle) -> io::Result<Process> {
let mut pid = 0;
let mut threads = Vec::new();
self.trace_core_each(handle, |p| pid = p, |thread| threads.push(thread))?;
Ok(Process { id: pid, threads })
}
pub fn trace_core_each<H, F>(
&self,
handle: &crate::proc::ProcHandle,
header: H,
mut each: F,
) -> io::Result<()>
where
H: FnOnce(u32),
F: FnMut(Thread),
{
if !handle.is_core() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"ProcHandle has no core file",
));
}
let pid = handle.pid() as u32;
header(pid);
let comm = handle.comm().ok().map(|s| s.to_string_lossy().into_owned());
let tids = handle.tids()?;
for tid in tids {
let tid32 = tid as u32;
if self.tid.is_some_and(|f| f != tid32) {
continue;
}
let frames = handle.trace_thread(tid32, self);
let name = if self.thread_names {
if tid32 == pid {
comm.clone()
} else {
None
}
} else {
None
};
each(Thread {
id: tid32,
name,
frames,
});
}
Ok(())
}
fn trace_snapshot_each<F>(
&self,
handle: &crate::proc::ProcHandle,
pid: u32,
each: &mut F,
) -> io::Result<()>
where
F: FnMut(Thread),
{
for t in snapshot_threads(pid, self.ptrace_attach)?.iter() {
each(t.info(pid, self, handle));
}
Ok(())
}
fn trace_rolling_each<F>(
&self,
handle: &crate::proc::ProcHandle,
pid: u32,
each: &mut F,
) -> io::Result<()>
where
F: FnMut(Thread),
{
each_thread(pid, |tid| {
if let Some(thread) = open_thread_or_warn(tid, self.ptrace_attach)? {
each(thread.info(pid, self, handle));
}
Ok(())
})
}
}
fn open_thread_or_warn(tid: u32, ptrace_attach: bool) -> io::Result<Option<TracedThread>> {
let thread = if ptrace_attach {
TracedThread::attach(tid)
} else {
TracedThread::traced(tid)
};
match thread {
Ok(thread) => Ok(Some(thread)),
Err(ref e) if e.raw_os_error() == Some(ESRCH) => {
eprintln!("warning: error attaching to thread {tid}: {e}");
Ok(None)
}
Err(e) => Err(e),
}
}
fn snapshot_threads(pid: u32, ptrace_attach: bool) -> io::Result<BTreeSet<TracedThread>> {
let mut threads = BTreeSet::new();
for _ in 0..5 {
let prev = threads.len();
add_threads(&mut threads, pid, ptrace_attach)?;
if prev == threads.len() {
break;
}
}
Ok(threads)
}
fn add_threads(
threads: &mut BTreeSet<TracedThread>,
pid: u32,
ptrace_attach: bool,
) -> io::Result<()> {
each_thread(pid, |tid| {
if !threads.contains(&tid) {
if let Some(thread) = open_thread_or_warn(tid, ptrace_attach)? {
threads.insert(thread);
}
}
Ok(())
})
}
fn each_thread<F>(pid: u32, mut f: F) -> io::Result<()>
where
F: FnMut(u32) -> io::Result<()>,
{
let dir = format!("/proc/{pid}/task");
for entry in fs::read_dir(dir)? {
let entry = entry?;
if let Some(tid) = entry
.file_name()
.to_str()
.and_then(|s| s.parse::<u32>().ok())
{
f(tid)?;
}
}
Ok(())
}
struct TracedThread {
id: u32,
should_detach: bool,
}
impl Drop for TracedThread {
fn drop(&mut self) {
if self.should_detach {
unsafe {
ptrace(
PTRACE_DETACH,
self.id as pid_t,
ptr::null_mut::<c_void>(),
ptr::null_mut::<c_void>(),
);
}
}
}
}
impl PartialOrd for TracedThread {
fn partial_cmp(&self, other: &TracedThread) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for TracedThread {
fn cmp(&self, other: &TracedThread) -> Ordering {
self.id.cmp(&other.id)
}
}
impl PartialEq for TracedThread {
fn eq(&self, other: &TracedThread) -> bool {
self.id == other.id
}
}
impl Eq for TracedThread {}
impl Borrow<u32> for TracedThread {
fn borrow(&self) -> &u32 {
&self.id
}
}
impl TracedThread {
fn attach(pid: u32) -> io::Result<TracedThread> {
unsafe {
let ret = ptrace(
PTRACE_SEIZE,
pid as pid_t,
ptr::null_mut::<c_void>(),
ptr::null_mut::<c_void>(),
);
if ret != 0 {
let e = io::Error::last_os_error();
if e.raw_os_error() == Some(ESRCH) {
return TracedThread::new_fallback(pid);
}
return Err(e);
}
let thread = TracedThread {
id: pid,
should_detach: true,
};
let ret = ptrace(
PTRACE_INTERRUPT,
pid as pid_t,
ptr::null_mut::<c_void>(),
ptr::null_mut::<c_void>(),
);
if ret != 0 {
return Err(io::Error::last_os_error());
}
let mut status = 0;
while waitpid(pid as pid_t, &mut status, __WALL) < 0 {
let e = io::Error::last_os_error();
if e.kind() != io::ErrorKind::Interrupted {
return Err(e);
}
}
if !WIFSTOPPED(status) {
return Err(io::Error::other(format!("unexpected wait status {status}")));
}
Ok(thread)
}
}
fn traced(pid: u32) -> io::Result<TracedThread> {
Ok(TracedThread {
id: pid,
should_detach: false,
})
}
fn new_fallback(pid: u32) -> io::Result<TracedThread> {
unsafe {
let ret = ptrace(
PTRACE_ATTACH,
pid as pid_t,
ptr::null_mut::<c_void>(),
ptr::null_mut::<c_void>(),
);
if ret != 0 {
return Err(io::Error::last_os_error());
}
let thread = TracedThread {
id: pid,
should_detach: true,
};
let mut status = 0;
loop {
let ret = waitpid(pid as pid_t, &mut status, __WALL);
if ret < 0 {
let e = io::Error::last_os_error();
if e.kind() != io::ErrorKind::Interrupted {
return Err(e);
}
continue;
}
if !WIFSTOPPED(status) {
return Err(io::Error::other(format!("unexpected wait status {status}")));
}
let sig = WSTOPSIG(status);
if sig == SIGSTOP {
return Ok(thread);
}
let ret = ptrace(
PTRACE_CONT,
pid as pid_t,
ptr::null_mut::<c_void>(),
sig as *const c_void,
);
if ret != 0 {
return Err(io::Error::last_os_error());
}
}
}
}
fn info(&self, pid: u32, options: &TraceOptions, handle: &crate::proc::ProcHandle) -> Thread {
let name = if options.thread_names {
self.name(pid)
} else {
None
};
let frames = handle.trace_thread(self.id, options);
Thread {
id: self.id,
name,
frames,
}
}
fn name(&self, pid: u32) -> Option<String> {
let path = format!("/proc/{}/task/{}/comm", pid, self.id);
let mut name = vec![];
match File::open(path).and_then(|mut f| f.read_to_end(&mut name)) {
Ok(_) => Some(String::from_utf8_lossy(&name).trim().to_string()),
Err(e) => {
eprintln!("warning: error getting name for thread {}: {}", self.id, e);
None
}
}
}
}