use std::fs::File;
use std::io::Write;
use std::ops::Deref;
use std::panic;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use minidump::*;
use minidump_processor::{
PendingProcessorStatSubscriptions, PendingProcessorStats, ProcessorOptions,
};
use minidump_unwind::{
debuginfo::DebugInfoSymbolProvider, http_symbol_supplier, simple_symbol_supplier,
MultiSymbolProvider, SymbolProvider, Symbolizer,
};
use clap::{
builder::{PossibleValuesParser, TypedValueParser},
ArgGroup, CommandFactory, Parser,
};
use tracing::error;
use tracing::level_filters::LevelFilter;
#[derive(Parser)]
#[clap(version, about, long_about = None)]
#[clap(propagate_version = true)]
#[clap(group(ArgGroup::new("output-format").args(&[
"json",
"human",
"cyborg",
"dump",
"help_markdown",
])))]
#[clap(override_usage("minidump-stackwalk [FLAGS] [OPTIONS] <minidump> [--] [symbols-path]..."))]
#[clap(verbatim_doc_comment)]
struct Cli {
#[arg(long)]
human: bool,
#[arg(long)]
json: bool,
#[arg(long)]
cyborg: Option<PathBuf>,
#[arg(long)]
dump: bool,
#[arg(long, hide = true)]
help_markdown: bool,
#[arg(long, default_value = "stable-basic")]
#[arg(value_parser = ["stable-basic", "stable-all", "unstable-all"])]
#[arg(verbatim_doc_comment)]
features: String,
#[arg(long)]
#[arg(default_value = "error")]
#[arg(value_parser = PossibleValuesParser::new(["off", "error", "warn", "info", "debug", "trace"]).map(|v| LevelFilter::from_str(&v).unwrap()))]
verbose: LevelFilter,
#[arg(long)]
output_file: Option<PathBuf>,
#[arg(long)]
log_file: Option<PathBuf>,
#[arg(long)]
no_color: bool,
#[arg(long)]
pretty: bool,
#[arg(long)]
brief: bool,
#[arg(long)]
no_interactive: bool,
#[arg(long)]
evil_json: Option<PathBuf>,
#[arg(long)]
recover_function_args: bool,
#[arg(long)]
use_local_debuginfo: bool,
#[arg(long)]
#[arg(verbatim_doc_comment)]
symbols_url: Vec<String>,
#[arg(long)]
symbols_cache: Option<PathBuf>,
#[arg(long)]
symbols_tmp: Option<PathBuf>,
#[arg(long, default_value_t = 1000)]
symbols_download_timeout_secs: u64,
minidump: PathBuf,
#[arg(long)]
symbols_path: Vec<PathBuf>,
symbols_path_legacy: Vec<PathBuf>,
}
#[tokio::main]
async fn main() {
if let Err(e) = main_result().await {
if e.kind() != std::io::ErrorKind::BrokenPipe {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
}
#[cfg_attr(test, allow(dead_code))]
async fn main_result() -> std::io::Result<()> {
let cli = Cli::parse();
if let Some(log_path) = &cli.log_file {
let log_file = File::create(log_path)?;
tracing_subscriber::fmt::fmt()
.with_max_level(cli.verbose)
.with_target(false)
.without_time()
.with_ansi(false)
.with_writer(log_file)
.init();
} else {
tracing_subscriber::fmt::fmt()
.with_max_level(cli.verbose)
.with_target(false)
.without_time()
.with_ansi(!cli.no_color)
.with_writer(std::io::stderr)
.init();
}
panic::set_hook(Box::new(|panic_info| {
let (filename, line) = panic_info
.location()
.map(|loc| (loc.file(), loc.line()))
.unwrap_or(("<unknown>", 0));
let cause = panic_info
.payload()
.downcast_ref::<String>()
.map(String::deref)
.unwrap_or_else(|| {
panic_info
.payload()
.downcast_ref::<&str>()
.copied()
.unwrap_or("<cause unknown>")
});
error!(
"Panic - A panic occurred at {}:{}: {}",
filename, line, cause
);
}));
if cli.help_markdown {
print_help_markdown(&mut std::io::stdout()).expect("help-markdown failed");
return Ok(());
}
let temp_dir = std::env::temp_dir();
let mut symbols_paths = cli.symbols_path;
symbols_paths.extend(cli.symbols_path_legacy);
let symbols_cache = cli
.symbols_cache
.unwrap_or_else(|| temp_dir.join("rust-minidump-cache"));
let symbols_tmp = cli.symbols_tmp.unwrap_or(temp_dir);
let timeout = Duration::from_secs(cli.symbols_download_timeout_secs);
let raw_dump = cli.dump;
let mut json = cli.json;
let mut human = !json && !raw_dump;
if cli.cyborg.is_some() {
human = true;
json = true;
}
if cli.pretty && !json {
error!("Humans must be hideous! (The --pretty and --human flags cannot both be set)");
std::process::exit(1);
}
if cli.brief && !(human || raw_dump) {
error!("Robots cannot be brief! (The --brief flag is only valid for --human, --cyborg, and --dump)");
std::process::exit(1);
}
let mut options = match &*cli.features {
"stable-basic" => ProcessorOptions::stable_basic(),
"stable-all" => ProcessorOptions::stable_all(),
"unstable-all" => ProcessorOptions::unstable_all(),
_ => unimplemented!("unknown --features value"),
};
options.evil_json = cli.evil_json.as_deref();
options.recover_function_args = cli.recover_function_args;
let interactive_enabled = !json && !cli.no_interactive && cli.output_file.is_none();
let mut processor_stats = None;
if interactive_enabled {
let mut subscriptions = PendingProcessorStatSubscriptions::default();
subscriptions.frame_count = true;
subscriptions.thread_count = true;
processor_stats = Some(PendingProcessorStats::new(subscriptions));
options.stat_reporter = processor_stats.as_ref();
}
match Minidump::read_path(cli.minidump) {
Ok(dump) => {
let mut stdout;
let mut output_f;
let cyborg_output_f = cli.cyborg.map(File::create).transpose()?;
let mut output: &mut dyn Write = if let Some(output_path) = cli.output_file {
output_f = File::create(output_path)?;
&mut output_f
} else {
stdout = std::io::stdout();
&mut stdout
};
if raw_dump {
return print_minidump_dump(&dump, &mut output, cli.brief);
}
let mut provider = MultiSymbolProvider::new();
let modules = dump.get_stream::<MinidumpModuleList>().unwrap_or_default();
if cli.use_local_debuginfo {
let system_info = match dump.get_stream::<MinidumpSystemInfo>() {
Err(e) => {
error!("Error getting system info stream from dump (required for local debug info): {}", e);
std::process::exit(1);
}
Ok(s) => s,
};
provider.add(Box::new(
DebugInfoSymbolProvider::new(&system_info, &modules).await,
));
}
if !cli.symbols_url.is_empty() {
provider.add(Box::new(Symbolizer::new(http_symbol_supplier(
symbols_paths,
cli.symbols_url,
symbols_cache,
symbols_tmp,
timeout,
))));
} else if !symbols_paths.is_empty() {
provider.add(Box::new(Symbolizer::new(simple_symbol_supplier(
symbols_paths,
))));
}
let interactive_ui = processor_stats
.as_ref()
.map(|processor_stats| InterativeUi {
all: MultiProgress::new(),
symbol_progress: ProgressBar::hidden(),
thread_progress: ProgressBar::hidden(),
frame_progress: ProgressBar::hidden(),
total_progress: ProgressBar::hidden(),
needed_stats: AtomicBool::new(false),
symbol_stats: &provider,
processor_stats,
});
let update_state = || async {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
loop {
if let Some(interactive_ui) = &interactive_ui {
update_status(interactive_ui, false);
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
};
let result = tokio::select! {
result = minidump_processor::process_minidump_with_options(&dump, &provider, options) => result,
_ = update_state() => unreachable!(),
};
if let Some(interactive_ui) = &interactive_ui {
update_status(interactive_ui, true);
}
match result {
Ok(state) => {
if human {
if cli.brief {
state.print_brief(&mut output)?;
} else {
state.print(&mut output)?;
}
}
if json {
if let Some(mut cyborg_output_f) = cyborg_output_f {
state.print_json(&mut cyborg_output_f, cli.pretty)?;
} else {
state.print_json(&mut output, cli.pretty)?;
}
}
Ok(())
}
Err(err) => {
error!("{} - Error processing dump: {}", err.name(), err);
std::process::exit(1);
}
}
}
Err(err) => {
error!("{} - Error reading dump: {}", err.name(), err);
std::process::exit(1);
}
}
}
fn print_help_markdown(out: &mut dyn Write) -> std::io::Result<()> {
let app_name = "minidump-stackwalk";
let pretty_app_name = "minidump-stackwalk";
writeln!(out, "# {pretty_app_name} CLI manual")?;
writeln!(out)?;
writeln!(
out,
"> This manual can be regenerated with `{pretty_app_name} --help-markdown please`"
)?;
writeln!(out)?;
let mut cli = Cli::command().term_width(0);
let full_command = &mut cli;
full_command.build();
let mut todo = vec![full_command];
let mut is_full_command = true;
while let Some(command) = todo.pop() {
let mut help_buf = Vec::new();
command.write_long_help(&mut help_buf)?;
let help = String::from_utf8(help_buf).unwrap();
let lines = help.lines();
let subcommand_name = command.get_name();
if is_full_command {
} else {
writeln!(out, "<br><br><br>")?;
writeln!(out, "## {pretty_app_name} {subcommand_name}")?;
}
let mut in_subcommands_listing = false;
let mut in_global_options = false;
for line in lines {
if let Some(usage) = line.strip_prefix("Usage: ") {
writeln!(out, "### Usage:")?;
writeln!(out)?;
writeln!(out, "```")?;
writeln!(out, "{usage}")?;
writeln!(out, "```")?;
continue;
}
if let Some(heading) = line.strip_suffix(':') {
if !line.starts_with(' ') {
in_subcommands_listing = heading == "Subcommands";
in_global_options = heading == "GLOBAL OPTIONS";
writeln!(out, "### {heading}")?;
if in_global_options && !is_full_command {
writeln!(
out,
"This subcommand accepts all the [global options](#global-options)"
)?;
}
continue;
}
}
if in_global_options && !is_full_command {
continue;
}
if in_subcommands_listing && !line.starts_with(" ") {
let own_subcommand_name = line.trim();
if !own_subcommand_name.is_empty() {
write!(
out,
"* [{own_subcommand_name}](#{app_name}-{own_subcommand_name}): "
)?;
continue;
}
}
let line = line.trim();
if line.starts_with('-') || line.starts_with('<') {
writeln!(out, "#### `{line}`")?;
continue;
}
if line == "[SYMBOLS_PATH_LEGACY]..." {
writeln!(out, "#### `{line}`")?;
continue;
}
if line.starts_with('[') {
writeln!(out, "\\{line} ")?;
continue;
}
writeln!(out, "{line}")?;
}
writeln!(out)?;
todo.extend(
command
.get_subcommands_mut()
.filter(|cmd| !cmd.is_hide_set())
.collect::<Vec<_>>()
.into_iter()
.rev(),
);
is_full_command = false;
}
Ok(())
}
fn print_minidump_dump<'a, T, W>(
dump: &Minidump<'a, T>,
output: &mut W,
brief: bool,
) -> std::io::Result<()>
where
T: Deref<Target = [u8]> + 'a,
W: Write,
{
dump.print(output)?;
let system_info = dump.get_stream::<MinidumpSystemInfo>().ok();
let mut memory_list = dump.get_stream::<MinidumpMemoryList<'_>>().ok();
let mut memory64_list = dump.get_stream::<MinidumpMemory64List<'_>>().ok();
let misc_info = dump.get_stream::<MinidumpMiscInfo>().ok();
let unified_memory = memory64_list
.take()
.map(UnifiedMemoryList::Memory64)
.or_else(|| memory_list.take().map(UnifiedMemoryList::Memory));
if let Ok(thread_list) = dump.get_stream::<MinidumpThreadList<'_>>() {
thread_list.print(
output,
unified_memory.as_ref(),
system_info.as_ref(),
misc_info.as_ref(),
brief,
)?;
}
if let Ok(module_list) = dump.get_stream::<MinidumpModuleList>() {
module_list.print(output)?;
}
if let Ok(module_list) = dump.get_stream::<MinidumpUnloadedModuleList>() {
module_list.print(output)?;
}
if let Ok(handles) = dump.get_stream::<MinidumpHandleDataStream>() {
handles.print(output)?;
}
if let Some(memory_list) = unified_memory {
memory_list.print(output, brief)?;
}
if let Some(memory_list) = memory_list {
memory_list.print(output, brief)?;
}
if let Some(memory64_list) = memory64_list {
memory64_list.print(output, brief)?;
}
if let Ok(memory_info_list) = dump.get_stream::<MinidumpMemoryInfoList<'_>>() {
memory_info_list.print(output)?;
}
if let Ok(exception) = dump.get_stream::<MinidumpException>() {
exception.print(output, system_info.as_ref(), misc_info.as_ref())?;
}
if let Ok(assertion) = dump.get_stream::<MinidumpAssertion>() {
assertion.print(output)?;
}
if let Some(system_info) = system_info {
system_info.print(output)?;
}
if let Some(misc_info) = misc_info {
misc_info.print(output)?;
}
if let Ok(thread_names) = dump.get_stream::<MinidumpThreadNames>() {
thread_names.print(output)?;
}
if let Ok(breakpad_info) = dump.get_stream::<MinidumpBreakpadInfo>() {
breakpad_info.print(output)?;
}
match dump.get_stream::<MinidumpCrashpadInfo>() {
Ok(crashpad_info) => crashpad_info.print(output)?,
Err(Error::StreamNotFound) => (),
Err(_) => write!(output, "MinidumpCrashpadInfo cannot print invalid data")?,
}
if let Ok(mac_info) = dump.get_stream::<MinidumpMacCrashInfo>() {
mac_info.print(output)?;
}
if let Ok(mac_bootargs) = dump.get_stream::<MinidumpMacBootargs>() {
mac_bootargs.print(output)?;
}
if let Ok(stability_report) = dump.get_stream::<StabilityReport>() {
stability_report.print(output)?;
}
macro_rules! streams {
( $( $x:ident ),* ) => {
&[$( ( minidump_common::format::MINIDUMP_STREAM_TYPE::$x, stringify!($x) ) ),*]
};
}
fn print_raw_stream<T: Write>(name: &str, contents: &[u8], out: &mut T) -> std::io::Result<()> {
writeln!(out, "Stream {name}:")?;
let s = contents
.split(|&v| v == 0)
.map(String::from_utf8_lossy)
.collect::<Vec<_>>()
.join("\\0\n");
write!(out, "{s}\n\n")
}
for &(stream, name) in streams!(
LinuxCmdLine,
LinuxEnviron,
LinuxLsbRelease,
LinuxProcStatus,
LinuxCpuInfo,
LinuxMaps,
MozLinuxLimits,
MozSoftErrors
) {
if let Ok(contents) = dump.get_raw_stream(stream as u32) {
print_raw_stream(name, contents, output)?;
}
}
Ok(())
}
struct InterativeUi<'a> {
all: MultiProgress,
symbol_progress: ProgressBar,
thread_progress: ProgressBar,
frame_progress: ProgressBar,
total_progress: ProgressBar,
needed_stats: AtomicBool,
symbol_stats: &'a MultiSymbolProvider,
processor_stats: &'a PendingProcessorStats,
}
fn update_status(ui: &InterativeUi, finished: bool) {
if finished && !ui.needed_stats.load(Ordering::Relaxed) {
return;
}
let symbol_stats = ui.symbol_stats.pending_stats();
let (t_done, t_pending) = ui.processor_stats.get_thread_count();
let frames_walked = ui.processor_stats.get_frame_count();
let progress = if finished {
100
} else if t_pending == 0 {
0
} else {
let estimated_frames_per_thread = 20;
let estimate = 100 * frames_walked / (estimated_frames_per_thread * t_pending);
estimate.min(80)
};
ui.symbol_progress
.set_length(symbol_stats.symbols_requested);
ui.symbol_progress
.set_position(symbol_stats.symbols_processed);
ui.thread_progress.set_length(t_pending);
ui.thread_progress.set_position(t_done);
ui.frame_progress.set_length(frames_walked);
ui.total_progress.set_position(progress);
if !ui.needed_stats.load(Ordering::Relaxed) {
ui.thread_progress
.set_style(ProgressStyle::with_template("{msg:>17} {pos}/{len}").unwrap());
ui.symbol_progress
.set_style(ProgressStyle::with_template("{msg:>17} {pos}/{len}").unwrap());
ui.frame_progress
.set_style(ProgressStyle::with_template("{msg:>17} {len}").unwrap());
ui.total_progress
.set_style(ProgressStyle::with_template("{msg:>17} {pos:>3}% {wide_bar} ").unwrap());
ui.total_progress.set_length(100);
ui.symbol_progress.set_message("symbols fetched");
ui.thread_progress.set_message("threads processed");
ui.frame_progress.set_message("frames walked");
ui.total_progress.set_message("processing...");
ui.all.add(ui.frame_progress.clone());
ui.all.add(ui.symbol_progress.clone());
ui.all.add(ui.thread_progress.clone());
ui.all.add(ui.total_progress.clone());
ui.needed_stats.store(true, Ordering::Relaxed);
}
if finished {
ui.symbol_progress.finish();
ui.thread_progress.finish();
ui.frame_progress.finish();
ui.total_progress.finish();
}
}