diff options
Diffstat (limited to 'toolkit/crashreporter/client/app/src/main.rs')
-rw-r--r-- | toolkit/crashreporter/client/app/src/main.rs | 229 |
1 files changed, 229 insertions, 0 deletions
diff --git a/toolkit/crashreporter/client/app/src/main.rs b/toolkit/crashreporter/client/app/src/main.rs new file mode 100644 index 0000000000..07e1b04cb8 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/main.rs @@ -0,0 +1,229 @@ +/* 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; +use anyhow::Context; +use config::Config; + +/// cc is short for Clone Capture, a shorthand way to clone a bunch of values before an expression +/// (particularly useful for closures). +/// +/// It is defined here to allow it to be used in all submodules (textual scope lookup). +macro_rules! cc { + ( ($($c:ident),*) $e:expr ) => { + { + $(let $c = $c.clone();)* + $e + } + } +} + +mod async_task; +mod config; +mod data; +mod lang; +mod logging; +mod logic; +mod net; +mod process; +mod settings; +mod std; +mod thread_bound; +mod ui; + +#[cfg(test)] +mod test; + +#[cfg(not(mock))] +fn main() { + let log_target = logging::init(); + + let mut config = Config::new(); + let config_result = config.read_from_environment(); + config.log_target = Some(log_target); + + let mut config = Arc::new(config); + + let result = config_result.and_then(|()| { + let attempted_send = try_run(&mut config)?; + if !attempted_send { + // Exited without attempting to send the crash report; delete files. + config.delete_files(); + } + Ok(()) + }); + + if let Err(message) = result { + // 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 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": { + "status": "OK" + }, + "Version": "100.0", + "ServerURL": "https://reports.example", + "TelemetryServerURL": "https://telemetry.example", + "TelemetryClientId": "telemetry_client", + "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_PING_UUID: uuid::Uuid = uuid::Uuid::nil(); + const MOCK_REMOTE_CRASH_ID: &str = "8cbb847c-def2-4f68-be9e-000000000000"; + + // 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/minidump-analyzer"), + Box::new(|_| Ok(crate::std::process::success_output())), + ) + .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::time::MockCurrentTime, + time::OffsetDateTime::parse( + MOCK_CURRENT_TIME, + &time::format_description::well_known::Rfc3339, + ) + .unwrap() + .into(), + ) + .set(mock::MockHook::new("ping_uuid"), MOCK_PING_UUID); + + let result = mock.run(|| { + let mut cfg = Config::new(); + cfg.data_dir = Some("data_dir".into()); + cfg.events_dir = Some("events_dir".into()); + cfg.ping_dir = Some("ping_dir".into()); + cfg.dump_file = Some("minidump.dmp".into()); + cfg.restart_command = Some("mockfox".into()); + cfg.strings = Some(lang::load().unwrap()); + 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 { + // Run minidump-analyzer to gather stack traces. + { + let analyzer_path = config.sibling_program_path("minidump-analyzer"); + let mut cmd = crate::process::background_command(&analyzer_path); + if config.dump_all_threads { + cmd.arg("--full"); + } + cmd.arg(config.dump_file()); + let output = cmd + .output() + .with_context(|| config.string("crashreporter-error-minidump-analyzer"))?; + if !output.status.success() { + log::warn!( + "minidump-analyzer failed to run ({});\n\nstderr: {}\n\nstdout: {}", + output.status, + String::from_utf8_lossy(&output.stderr), + String::from_utf8_lossy(&output.stdout), + ); + } + } + + let extra = { + // Perform a few things which may change the config, then treat is 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 + }; + + logic::ReportCrash::new(config.clone(), extra)?.run() + } +} |