use crate::call_tree::{CallTreeManager, NodeId, ThreadCallTree};
use crate::error::{CallTraceError, Result};
use crate::register_reader::{ArgumentValue, CapturedArgument};
use libc;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::Write;
#[derive(Debug, Serialize, Deserialize)]
pub struct TraceSession {
pub metadata: SessionMetadata,
pub threads: Vec<ThreadTrace>,
pub thread_relationships: Vec<ThreadRelationship>,
pub statistics: SessionStatistics,
#[serde(skip_serializing_if = "Option::is_none")]
pub crash: Option<CrashInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CrashInfo {
pub signal: i32,
pub signal_name: String,
pub thread_id: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub fault_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instruction_pointer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stack_pointer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub register_context: Option<crate::register_reader::RegisterContext>,
pub backtrace: Vec<StackFrame>,
pub crash_time: String,
pub crash_timestamp: f64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct StackFrame {
pub address: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub function_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub library_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SessionMetadata {
pub start_time: String,
pub end_time: String,
pub duration_ms: f64,
pub process_info: ProcessInfo,
pub calltrace_version: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ProcessInfo {
pub pid: u32,
pub architecture: String,
pub executable_path: Option<String>,
pub environment_variables: std::collections::HashMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ThreadTrace {
pub thread_id: u64,
pub thread_name: String,
pub is_main_thread: bool,
pub start_time: String,
pub end_time: Option<String>,
pub parent_thread_id: Option<u64>,
pub creation_function: Option<String>,
pub statistics: ThreadStatistics,
pub call_tree: Option<CallNodeJson>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ThreadRelationship {
pub parent_thread_id: u64,
pub child_thread_id: u64,
pub creation_function: String,
pub creation_time: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CallNodeJson {
pub id: u32,
pub function: String,
pub address: String,
pub call_site: String,
pub start_time: String,
pub end_time: Option<String>,
pub duration_us: Option<u64>,
pub call_depth: usize,
pub signature: Option<String>,
pub arguments: Vec<ArgumentJson>,
pub return_value: Option<ArgumentValueJson>,
pub children: Vec<CallNodeJson>,
pub is_pthread_create: bool,
pub created_thread_id: Option<u64>,
pub source_file: Option<String>,
pub line_number: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ArgumentJson {
pub name: String,
pub type_name: String,
pub value: ArgumentValueJson,
pub location: ArgumentLocationJson,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum ArgumentValueJson {
Integer(u64),
Float(f32),
Double(f64),
Pointer {
address: String,
string_value: Option<String>,
},
String(String),
Raw(String), Object(String), Error(String),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ArgumentLocationJson {
pub class: String, pub register_index: Option<usize>,
pub stack_offset: Option<usize>,
pub size: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ThreadStatistics {
pub total_function_calls: u64,
pub max_call_depth: usize,
pub total_errors: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SessionStatistics {
pub total_threads: u64,
pub total_function_calls: u64,
pub total_nodes: u64,
pub session_duration_us: u64,
}
pub struct JsonOutputGenerator {
pretty_print: bool,
include_arguments: bool,
include_source_info: bool,
}
impl JsonOutputGenerator {
pub fn new() -> Self {
Self {
pretty_print: true,
include_arguments: true,
include_source_info: true,
}
}
pub fn configure(
&mut self,
pretty_print: bool,
include_arguments: bool,
include_source_info: bool,
) {
self.pretty_print = pretty_print;
self.include_arguments = include_arguments;
self.include_source_info = include_source_info;
}
pub fn generate_output(&self, manager: &CallTreeManager) -> Result<TraceSession> {
let stats = manager.get_statistics();
let current_time_us = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
let session_start_us = current_time_us.saturating_sub(stats.session_duration_us);
let metadata = SessionMetadata {
start_time: format_timestamp(session_start_us),
end_time: format_timestamp(current_time_us),
duration_ms: stats.session_duration_us as f64 / 1000.0,
process_info: ProcessInfo {
pid: std::process::id(),
architecture: crate::string_constants::X86_64.to_string(),
executable_path: None, environment_variables: collect_environment_variables(),
},
calltrace_version: env!("CARGO_PKG_VERSION").to_string(),
};
let mut threads = Vec::new();
let thread_ids = manager.get_thread_ids();
for thread_id in thread_ids {
if let Some(thread_tree) = manager.get_thread_tree(thread_id) {
let tree = thread_tree.read().unwrap();
let thread_trace = self.generate_thread_trace(&tree, manager)?;
threads.push(thread_trace);
}
}
let thread_relationships = Vec::new();
let session_stats = SessionStatistics {
total_threads: stats.total_threads,
total_function_calls: 0, total_nodes: stats.total_nodes,
session_duration_us: stats.session_duration_us,
};
Ok(TraceSession {
metadata,
threads,
thread_relationships,
statistics: session_stats,
crash: None,
})
}
fn generate_thread_trace(
&self,
thread_tree: &ThreadCallTree,
manager: &CallTreeManager,
) -> Result<ThreadTrace> {
let call_tree = if let Some(root_id) = thread_tree.root_node {
self.generate_call_node_json(root_id, manager)?
} else {
None
};
Ok(ThreadTrace {
thread_id: thread_tree.thread_id,
thread_name: thread_tree.thread_name.clone(),
is_main_thread: thread_tree.is_main_thread,
start_time: format_timestamp(thread_tree.start_time),
end_time: thread_tree.end_time.map(format_timestamp),
parent_thread_id: thread_tree.parent_thread_id,
creation_function: thread_tree.creation_function.clone(),
statistics: ThreadStatistics {
total_function_calls: thread_tree.total_calls,
max_call_depth: thread_tree.max_depth,
total_errors: thread_tree.total_errors,
},
call_tree,
})
}
fn generate_call_node_json(
&self,
node_id: NodeId,
manager: &CallTreeManager,
) -> Result<Option<CallNodeJson>> {
let node_ref = manager.get_any_node(node_id).ok_or_else(|| {
CallTraceError::InvalidArgument(format!("Node {} not found", node_id))
})?;
let node = node_ref.read().unwrap();
let arguments = if self.include_arguments {
node.arguments
.iter()
.map(|arg| self.convert_argument_to_json(arg))
.collect()
} else {
Vec::new()
};
let return_value = if self.include_arguments {
node.return_value
.as_ref()
.map(|rv| self.convert_return_value_to_json(rv))
} else {
None
};
let mut children = Vec::new();
for &child_id in &node.children {
if let Some(child_json) = self.generate_call_node_json(child_id, manager)? {
children.push(child_json);
}
}
let (source_file, line_number) = if self.include_source_info {
node.function_info.as_ref().map_or((None, None), |info| {
(info.source_file.clone(), info.line_number)
})
} else {
(None, None)
};
let function_display_name = if !node.function_name.starts_with("0x") {
node.function_name.clone()
} else {
crate::format_address_with_prefix("func", node.function_address & 0xFFFF)
};
Ok(Some(CallNodeJson {
id: node.id,
function: function_display_name,
address: crate::format_address(node.function_address),
call_site: crate::format_address(node.call_site),
start_time: format_timestamp(node.start_time),
end_time: node.end_time.map(format_timestamp),
duration_us: node.duration_us,
call_depth: node.call_depth,
signature: node.function_info.as_ref().map(|_| "TODO".to_string()), arguments,
return_value,
children,
is_pthread_create: node.is_pthread_create,
created_thread_id: node.created_thread_id,
source_file,
line_number,
}))
}
fn convert_return_value_to_json(&self, return_value: &ArgumentValue) -> ArgumentValueJson {
match return_value {
ArgumentValue::Integer(val) => ArgumentValueJson::Integer(*val),
ArgumentValue::Float(val) => ArgumentValueJson::Float(*val),
ArgumentValue::Double(val) => ArgumentValueJson::Double(*val),
ArgumentValue::Pointer(addr) => ArgumentValueJson::Pointer {
address: crate::format_address(*addr),
string_value: None, },
ArgumentValue::String(s) => ArgumentValueJson::String(s.clone()),
ArgumentValue::Raw(bytes) => ArgumentValueJson::Raw(hex::encode(bytes)),
ArgumentValue::Struct {
type_name,
members: _,
size,
} => {
ArgumentValueJson::Object(format!("struct {} (size: {})", type_name, size))
}
ArgumentValue::Array {
element_type,
elements: _,
count,
element_size,
} => {
ArgumentValueJson::Object(format!(
"{}[{}] (element_size: {})",
element_type, count, element_size
))
}
ArgumentValue::Union {
type_name,
raw_data: _,
size,
} => ArgumentValueJson::Object(format!("union {} (size: {})", type_name, size)),
ArgumentValue::Null => ArgumentValueJson::Pointer {
address: crate::string_constants::NULL_ADDRESS.to_string(),
string_value: Some(crate::string_constants::NULL_STRING.to_string()),
},
ArgumentValue::Unknown {
type_name,
raw_data,
error,
} => {
let error_info = error
.as_ref()
.map(|e| format!(": {}", e))
.unwrap_or_default();
ArgumentValueJson::Object(format!(
"unknown {} (size: {}){}",
type_name,
raw_data.len(),
error_info
))
}
}
}
fn convert_argument_to_json(&self, arg: &CapturedArgument) -> ArgumentJson {
let value_json = if arg.valid {
match &arg.value {
ArgumentValue::Integer(val) => ArgumentValueJson::Integer(*val),
ArgumentValue::Float(val) => ArgumentValueJson::Float(*val),
ArgumentValue::Double(val) => ArgumentValueJson::Double(*val),
ArgumentValue::Pointer(addr) => ArgumentValueJson::Pointer {
address: crate::format_address(*addr),
string_value: None, },
ArgumentValue::String(s) => ArgumentValueJson::String(s.clone()),
ArgumentValue::Raw(bytes) => ArgumentValueJson::Raw(hex::encode(bytes)),
ArgumentValue::Struct {
type_name,
members: _,
size,
} => {
ArgumentValueJson::Object(format!("struct {} (size: {})", type_name, size))
}
ArgumentValue::Array {
element_type,
elements: _,
count,
element_size,
} => {
ArgumentValueJson::Object(format!(
"{}[{}] (element_size: {})",
element_type, count, element_size
))
}
ArgumentValue::Union {
type_name,
raw_data: _,
size,
} => ArgumentValueJson::Object(format!("union {} (size: {})", type_name, size)),
ArgumentValue::Null => ArgumentValueJson::Pointer {
address: crate::string_constants::NULL_ADDRESS.to_string(),
string_value: Some(crate::string_constants::NULL_STRING.to_string()),
},
ArgumentValue::Unknown {
type_name,
raw_data,
error,
} => {
let error_info = error
.as_ref()
.map(|e| format!(": {}", e))
.unwrap_or_default();
ArgumentValueJson::Object(format!(
"unknown {} (size: {}){}",
type_name,
raw_data.len(),
error_info
))
}
}
} else {
ArgumentValueJson::Error(
arg.error
.clone()
.unwrap_or_else(|| crate::string_constants::UNKNOWN_ERROR.to_string()),
)
};
ArgumentJson {
name: arg.name.clone(),
type_name: arg.type_name.clone(),
value: value_json,
location: ArgumentLocationJson {
class: format!("{:?}", arg.location.class).to_lowercase(),
register_index: arg.location.register_index,
stack_offset: arg.location.stack_offset,
size: arg.location.size,
},
}
}
pub fn write_to_file(&self, trace_session: &TraceSession, file_path: &str) -> Result<()> {
let mut file = File::create(file_path)
.map_err(|e| CallTraceError::JsonError(serde_json::Error::io(e)))?;
if self.pretty_print {
let json_string = serde_json::to_string_pretty(trace_session)?;
file.write_all(json_string.as_bytes())
.map_err(|e| CallTraceError::JsonError(serde_json::Error::io(e)))?;
} else {
serde_json::to_writer(&mut file, trace_session)?;
}
Ok(())
}
pub fn to_string(&self, trace_session: &TraceSession) -> Result<String> {
if self.pretty_print {
serde_json::to_string_pretty(trace_session).map_err(CallTraceError::JsonError)
} else {
serde_json::to_string(trace_session).map_err(CallTraceError::JsonError)
}
}
}
impl Default for JsonOutputGenerator {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
fn get_function_name_from_address(address: u64) -> Option<String> {
use libc::{dladdr, Dl_info};
use std::ffi::{c_void, CStr};
let mut info: Dl_info = unsafe { std::mem::zeroed() };
let result = unsafe { dladdr(address as *const c_void, &mut info) };
if result != 0 && !info.dli_sname.is_null() {
unsafe {
let name = CStr::from_ptr(info.dli_sname)
.to_string_lossy()
.into_owned();
Some(demangle_function_name(&name))
}
} else {
None
}
}
#[allow(dead_code)]
fn demangle_function_name(mangled: &str) -> String {
if mangled.starts_with("_Z") {
mangled.to_string() } else {
mangled.to_string()
}
}
fn format_timestamp(timestamp_us: u64) -> String {
if timestamp_us == 0 {
return "0".to_string();
}
let seconds = timestamp_us / 1_000_000;
let microseconds = timestamp_us % 1_000_000;
if microseconds == 0 {
format!("{}", seconds)
} else {
format!("{}.{:06}", seconds, microseconds)
}
}
fn collect_environment_variables() -> std::collections::HashMap<String, String> {
let mut env_vars = std::collections::HashMap::new();
let important_vars = [
"PATH",
"LD_LIBRARY_PATH",
"LD_PRELOAD",
"USER",
"HOME",
"SHELL",
"LANG",
"LC_ALL",
"LC_CTYPE",
"TERM",
"DISPLAY",
"PWD",
"TMPDIR",
"TMP",
"TEMP",
"CALLTRACE_OUTPUT",
"CALLTRACE_CAPTURE_ARGS",
"CALLTRACE_MAX_DEPTH",
"CALLTRACE_PRETTY_JSON",
"CALLTRACE_DEBUG",
"CALLTRACE_FILTER",
"CARGO_TARGET_DIR",
"RUST_LOG",
"RUST_BACKTRACE",
"DEBUG",
"VERBOSE",
"CC",
"CXX",
"CFLAGS",
"CXXFLAGS",
"LDFLAGS",
];
for var_name in &important_vars {
if let Ok(value) = std::env::var(var_name) {
env_vars.insert(var_name.to_string(), value);
}
}
for (key, value) in std::env::vars() {
if key.starts_with("CALLTRACE_") && !env_vars.contains_key(&key) {
env_vars.insert(key, value);
}
}
const MAX_ENV_VARS: usize = 50;
if env_vars.len() > MAX_ENV_VARS {
let mut limited_vars = std::collections::HashMap::new();
for (key, value) in env_vars.into_iter().take(MAX_ENV_VARS) {
limited_vars.insert(key, value);
}
env_vars = limited_vars;
}
env_vars
}
#[allow(dead_code)]
mod hex {
pub fn encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
}