summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/client/app/src/main.rs
blob: 07e1b04cb8330b200f92447f8bcf90fd283e7677 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
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()
    }
}