/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! The crash reporter application.
//!
//! # Architecture
//! The application uses a simple declarative [UI model](ui::model) to define the UI. This model
//! contains [data bindings](data) which provide the dynamic behaviors of the UI. Separate UI
//! implementations for linux (gtk), macos (cocoa), and windows (win32) exist, as well as a test UI
//! which is virtual (no actual interface is presented) but allows runtime introspection.
//!
//! # Mocking
//! This application contains mock interfaces for all the `std` functions it uses which interact
//! with the host system. You can see their implementation in [`crate::std`]. To enable mocking,
//! use the `mock` feature or build with `MOZ_CRASHREPORTER_MOCK` set (which, in `build.rs`, is
//! translated to a `cfg` option). *Note* that this cfg _must_ be enabled when running tests.
//! Unfortunately it is not possible to detect whether tests are being built in `build.rs, which
//! is why a feature needed to be made in the first place (it is enabled automatically when running
//! `mach rusttests`).
//!
//! Currently the input program configuration which is mocked when running the application is fixed
//! (see the [`main`] implementation in this file). If needed in the future, it would be nice to
//! extend this to allow runtime tweaking.
//!
//! # Development
//! Because of the mocking support previously mentioned, in generally any `std` imports should
//! actually use `crate::std`. If mocked functions/types are missing, they should be added with
//! appropriate mocking hooks.
// Use the WINDOWS windows subsystem. This prevents a console window from opening with the
// application.
#![cfg_attr(windows, windows_subsystem =
"windows")]
use crate::std::sync::Arc;
#[cfg(not(test))]
use anyhow::Context;
use config::Config;
// A few macros are defined here to allow use in all submodules via textual scope lookup.
/// cc is short for Clone Capture, a shorthand way to clone a bunch of values before an expression
/// (particularly useful for closures).
macro_rules! cc {
( ($($c:ident),*) $e:expr ) => {
{
$(
let $c = $c.clone();)*
$e
}
}
}
/// Create a string literal to be used as an environment variable name.
///
/// This adds the application prefix `MOZ_CRASHREPORTER_`.
macro_rules! ekey {
( $name:literal ) => {
concat!(
"MOZ_CRASHREPORTER_", $name)
};
}
mod analyze;
mod async_task;
mod config;
mod data;
mod glean;
mod lang;
mod logging;
mod logic;
mod memory_test;
mod net;
mod prefs_parser;
mod process;
mod send_ping;
mod settings;
mod std;
mod thread_bound;
mod ui;
#[cfg(test)]
mod test;
fn main() {
// Determine the mode in which to run. This is very simplistic, but need not be more permissive
// nor flexible since we control how the program is invoked. We don't use the mocked version
// because we want the actual args.
match ::std::env::args_os().nth(
1) {
Some(s)
if s ==
"--analyze" => analyze::main(),
Some(s)
if s ==
"--memtest" => memory_test::main(),
Some(s)
if s ==
"--send-ping" => send_ping::main(),
Some(s)
if s ==
"--ping-cleanup" => send_ping::cleanup_main(),
_ => report_main(),
}
}
#[cfg(not(mock))]
fn report_main() {
// Close unused fds before doing anything else, which might open some.
#[cfg(unix)]
let fd_cleanup_error = fd_cleanup::cleanup_unused_fds();
let log_target = logging::init();
let mut config = Config::new();
config.log_target = Some(log_target);
config.read_from_environment();
#[cfg(unix)]
if let Err(e) = fd_cleanup_error {
log::warn!(
"fd cleanup failed: {e}");
}
let mut config = Arc::new(config);
match try_run(&
mut config) {
Ok(attempted_send) => {
if !attempted_send {
// Exited without attempting to send the crash report; delete files.
config.delete_files();
}
}
Err(message) => {
// TODO maybe errors should also delete files?
log::error!(
"exiting with error: {message:#}");
if !config.auto_submit {
// Only show a dialog if auto_submit is disabled.
ui::error_dialog(config, message);
}
std::process::exit(
1);
}
}
}
#[cfg(mock)]
fn report_main() {
// TODO it'd be nice to be able to set these values at runtime in some way when running the
// mock application.
use crate::std::{
fs::{MockFS, MockFiles},
mock,
process::Command,
};
const MOCK_MINIDUMP_EXTRA: &str = r
#"{
"Vendor":
"FooCorp",
"ProductName":
"Bar",
"ReleaseChannel":
"release",
"BuildID":
"1234",
"StackTraces":
"{}",
"Version":
"100.0",
"ServerURL":
"https://reports.example",
"TelemetryServerURL":
"https://telemetry.example",
"TelemetryClientId":
"telemetry_client",
"TelemetryProfileGroupId":
"telemetry_profile_group",
"TelemetrySessionId":
"telemetry_session",
"URL":
"https://url.example"
}
"#;
// Actual content doesn't matter, aside from the hash that is generated.
const MOCK_MINIDUMP_FILE: &[u8] = &[
1,
2,
3,
4];
const MOCK_CURRENT_TIME: &str =
"2004-11-09T12:34:56Z";
const MOCK_REMOTE_CRASH_ID: &str =
"8cbb847c-def2-4f68-be9e-000000000000";
// Initialize logging but don't set it in the configuration, so that it won't be redirected to
// a file (only shown on stderr).
logging::init();
// Create a default set of files which allow successful operation.
let mock_files = MockFiles::new();
mock_files
.add_file(
"minidump.dmp", MOCK_MINIDUMP_FILE)
.add_file(
"minidump.extra", MOCK_MINIDUMP_EXTRA);
// Create a default mock environment which allows successful operation.
let mut mock = mock::builder();
mock.set(
Command::mock(
"work_dir/pingsender"),
Box::new(|_| Ok(
crate::std::process::success_output())),
)
.set(
Command::mock(
"curl"),
Box::new(|_| {
let mut output =
crate::std::process::success_output();
output.stdout = format!(
"CrashID={MOCK_REMOTE_CRASH_ID}").into();
// Network latency.
std::thread::sleep(std::time::Duration::from_secs(
2));
Ok(output)
}),
)
.set(MockFS, mock_files.clone())
.set(
crate::std::env::MockCurrentExe,
"work_dir/crashreporter".into(),
)
.set(
crate::std::env::MockTempDir,
"tmp".into())
.set(
crate::std::time::MockCurrentTime,
time::OffsetDateTime::parse(
MOCK_CURRENT_TIME,
&time::format_description::well_known::Iso8601::DEFAULT,
)
.unwrap()
.into(),
)
.set(mock::MockHook::new(
"enable_glean_pings"),
false);
let result = mock.run(|| {
let mut cfg = Config::new();
cfg.data_dir = Some(
"data_dir".into());
cfg.events_dir = Some(
"events_dir".into());
cfg.dump_file = Some(
"minidump.dmp".into());
cfg.restart_command = Some(
"mockfox".into());
cfg.strings = Some(lang::load());
let mut cfg = Arc::new(cfg);
try_run(&
mut cfg)
});
if let Err(e) = result {
log::error!(
"exiting with error: {e}");
std::process::exit(
1);
}
}
fn try_run(config: &
mut Arc<Config>) -> anyhow::Result<bool> {
if config.dump_file.is_none() {
if !config.auto_submit {
Err(anyhow::anyhow!(config.string(
"crashreporter-information")))
}
else {
Ok(
false)
}
}
else if !config.dump_file().exists() {
// Bug 1959875: If the minidump file doesn't exist, it indicates that an error occurred
// when generating the minidump, and we should return a specific error message to make
// things clear to the user.
Err(anyhow::anyhow!(
config.string(
"crashreporter-error-failed-to-generate-minidump")
))
}
else {
// Use minidump-analyzer to gather stack traces.
#[cfg(not(mock))]
{
if let Err(e) = minidump_analyzer::MinidumpAnalyzer::new(config.dump_file())
.all_threads(config.dump_all_threads)
.analyze()
{
// Minidump analysis gives optional additional information; if it fails, we should
// still proceed.
log::warn!(
"minidump analyzer failed: {e}");
}
}
let extra = {
// Perform a few things which may change the config, then treat it as immutable.
let config = Arc::get_mut(config).expect(
"unexpected config references");
let extra = config.load_extra_file()?;
config.move_crash_data_to_pending()?;
extra
};
// Initialize glean here since it relies on the data directory (which will not change after
// this point). We could potentially initialize it even later (only just before we need
// it), however we may use it for more than just the crash ping in the future, in which
// case it makes more sense to do it as early as possible.
//
// When we are testing, glean will already be initialized (if needed).
#[cfg(not(test))]
let _glean_handle = glean::InitOptions::from_config(&config)
.init()
.context(
"failed to acquire Glean store")?;
logic::ReportCrash::new(config.clone(), extra)?.run()
}
}
/// Close inherited fds which we don't need.
///
/// This is important since we may re-launch Firefox (which may crash again, accumulating open fds
/// all the while). See bug 1986095.
///
/// The `close_fds` crate does this in a more comprehensive way, so we may consider vendoring that in
/// the future.
#[cfg(all(unix, not(mock)))]
mod fd_cleanup {
unsafe extern "C" {
fn close(fd: std::ffi::c_int) -> std::ffi::c_int;
}
pub fn cleanup_unused_fds() -> anyhow::Result<()> {
use anyhow::Context;
let fd_dir: &str =
match std::env::consts::OS {
"linux" =>
"/proc/self/fd",
"macos" =>
"/dev/fd",
os => anyhow::bail!(
"unimplemented for target os {os}"),
};
let dir =
std::fs::read_dir(fd_dir).with_context(|| format!(
"failed to enumerate {fd_dir}"))?;
// Aggregate the fds to close so that we don't close the ReadDir fd (we could get this fd by
// using libc/nix, but at the time of this writing those crates aren't dependencies, and the
// workaround is simple enough).
let mut fds_to_close = Vec::new();
for entry_result
in dir {
let entry =
match entry_result {
Ok(entry) => entry,
Err(e) => {
log::warn!(
"failed to enumerate {fd_dir}: {e}");
continue;
}
};
let filename = entry.file_name();
// These should all be valid utf-8
let Some(filename) = filename.to_str()
else {
continue;
};
let fd: std::os::fd::RawFd =
match filename.parse() {
Ok(n) => n,
Err(e) => {
log::warn!(
"failed to parse {filename} as an fd: {e}");
continue;
}
};
// Ignore negative, stdin (0), stdout (1), or stderr(2) fds.
if fd >=
3 {
fds_to_close.push(fd);
}
}
for fd
in fds_to_close {
// We can ignore errors (e.g. inevitably there will be at least one error from closing the
// ReadDir fd which is now closed). We use libc `close` directly since OwnedFd has the
// invariant that the fd is open (and has a debug assert checking this).
unsafe { close(fd) };
}
Ok(())
}
}
// `std` uses `raw-dylib` to link this dll, but that doesn't work properly on x86 MinGW, so we explicitly
// have to link it.
#[cfg(all(target_os =
"windows", target_env =
"gnu"))]
#[link(name =
"bcryptprimitives")]
extern "C" {}
#[cfg(windows)]
#[link(name =
"rpcrt4")]
extern "C" {}