Bug 1759175 pt3 - Crashreporter business logic r=gsvelto,fluent-reviewers,flod,eemeli,cmartin

Differential Revision: https://phabricator.services.mozilla.com/D185942
This commit is contained in:
Alex Franchuk 2024-03-20 14:59:44 +00:00
Родитель f3ea01d520
Коммит acc91ba868
19 изменённых файлов: 3005 добавлений и 3 удалений

Просмотреть файл

@ -0,0 +1,127 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
import fluent.syntax.ast as FTL
from fluent.migrate.helpers import transforms_from, VARIABLE_REFERENCE, TERM_REFERENCE
from fluent.migrate.transforms import (
CONCAT,
REPLACE,
REPLACE_IN_TEXT,
LegacySource,
Transform,
)
class SPLIT_AND_REPLACE(LegacySource):
"""Split sentence on '\n\n', return a specific segment, and perform replacements if needed"""
def __init__(self, path, key, index, replacements=None, **kwargs):
super(SPLIT_AND_REPLACE, self).__init__(path, key, **kwargs)
self.index = index
self.replacements = replacements
def __call__(self, ctx):
element = super(SPLIT_AND_REPLACE, self).__call__(ctx)
segments = element.value.split(r"\n\n")
element.value = segments[self.index]
if self.replacements is None:
return Transform.pattern_of(element)
else:
return REPLACE_IN_TEXT(element, self.replacements)(ctx)
def migrate(ctx):
"""Bug 1759175 - Migrate Crash Reporter to Fluent, part {index}."""
target_file = "toolkit/crashreporter/crashreporter.ftl"
source_file = "toolkit/crashreporter/crashreporter.ini"
ctx.add_transforms(
target_file,
target_file,
transforms_from(
"""
crashreporter-title = { COPY(from_path, "CrashReporterTitle") }
crashreporter-no-run-message = { COPY(from_path, "CrashReporterDefault") }
crashreporter-button-details = { COPY(from_path, "Details") }
crashreporter-view-report-title = { COPY(from_path, "ViewReportTitle") }
crashreporter-comment-prompt = { COPY(from_path, "CommentGrayText") }
crashreporter-report-info = { COPY(from_path, "ExtraReportInfo") }
crashreporter-submit-status = { COPY(from_path, "ReportPreSubmit2") }
crashreporter-submit-in-progress = { COPY(from_path, "ReportDuringSubmit2") }
crashreporter-submit-success = { COPY(from_path, "ReportSubmitSuccess") }
crashreporter-submit-failure = { COPY(from_path, "ReportSubmitFailed") }
crashreporter-resubmit-status = { COPY(from_path, "ReportResubmit") }
crashreporter-button-ok = { COPY(from_path, "Ok") }
crashreporter-button-close = { COPY(from_path, "Close") }
""",
from_path=source_file,
),
)
ctx.add_transforms(
target_file,
target_file,
[
FTL.Message(
id=FTL.Identifier("crashreporter-crash-message"),
value=SPLIT_AND_REPLACE(
source_file,
"CrashReporterDescriptionText2",
index=0,
replacements={
"%s": TERM_REFERENCE("brand-short-name"),
},
),
),
FTL.Message(
id=FTL.Identifier("crashreporter-plea"),
value=SPLIT_AND_REPLACE(
source_file,
"CrashReporterDescriptionText2",
index=1,
),
),
FTL.Message(
id=FTL.Identifier("crashreporter-error-details"),
value=SPLIT_AND_REPLACE(
source_file,
"CrashReporterProductErrorText2",
index=2,
replacements={
"%s": VARIABLE_REFERENCE("details"),
},
),
),
FTL.Message(
id=FTL.Identifier("crashreporter-button-quit"),
value=REPLACE(
source_file,
"Quit2",
{
"%s": TERM_REFERENCE("brand-short-name"),
},
),
),
FTL.Message(
id=FTL.Identifier("crashreporter-button-restart"),
value=REPLACE(
source_file,
"Restart",
{
"%s": TERM_REFERENCE("brand-short-name"),
},
),
),
FTL.Message(
id=FTL.Identifier("crashreporter-crash-identifier"),
value=REPLACE(
source_file,
"CrashID",
{
"%s": VARIABLE_REFERENCE("id"),
},
),
),
],
)

Просмотреть файл

@ -0,0 +1,31 @@
/* 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 http://mozilla.org/MPL/2.0/. */
//! Manage work across multiple threads.
//!
//! Each thread has thread-bound data which can be accessed in queued task functions.
pub type TaskFn<T> = Box<dyn FnOnce(&T) + Send + 'static>;
pub struct AsyncTask<T> {
send: Box<dyn Fn(TaskFn<T>) + Send + Sync>,
}
impl<T> AsyncTask<T> {
pub fn new<F: Fn(TaskFn<T>) + Send + Sync + 'static>(send: F) -> Self {
AsyncTask {
send: Box::new(send),
}
}
pub fn push<F: FnOnce(&T) + Send + 'static>(&self, f: F) {
(self.send)(Box::new(f));
}
pub fn wait<R: Send + 'static, F: FnOnce(&T) -> R + Send + 'static>(&self, f: F) -> R {
let (tx, rx) = std::sync::mpsc::sync_channel(0);
self.push(move |v| tx.send(f(v)).unwrap());
rx.recv().unwrap()
}
}

Просмотреть файл

@ -0,0 +1,526 @@
/* 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/. */
//! Application configuration.
use crate::std::borrow::Cow;
use crate::std::ffi::{OsStr, OsString};
use crate::std::path::{Path, PathBuf};
use crate::{lang, logging::LogTarget, std};
use anyhow::Context;
/// The number of the most recent minidump files to retain when pruning.
const MINIDUMP_PRUNE_SAVE_COUNT: usize = 10;
#[cfg(test)]
pub mod test {
pub const MINIDUMP_PRUNE_SAVE_COUNT: usize = super::MINIDUMP_PRUNE_SAVE_COUNT;
}
const VENDOR_KEY: &str = "Vendor";
const PRODUCT_KEY: &str = "ProductName";
const DEFAULT_VENDOR: &str = "Mozilla";
const DEFAULT_PRODUCT: &str = "Firefox";
#[derive(Default)]
pub struct Config {
/// Whether reports should be automatically submitted.
pub auto_submit: bool,
/// Whether all threads of the process should be dumped (versus just the crashing thread).
pub dump_all_threads: bool,
/// Whether to delete the dump files after submission.
pub delete_dump: bool,
/// The data directory.
pub data_dir: Option<PathBuf>,
/// The events directory.
pub events_dir: Option<PathBuf>,
/// The ping directory.
pub ping_dir: Option<PathBuf>,
/// The dump file.
///
/// If missing, an error dialog is displayed.
pub dump_file: Option<PathBuf>,
/// The XUL_APP_FILE to define if restarting the application.
pub app_file: Option<OsString>,
/// The path to the application to use when restarting the crashed process.
pub restart_command: Option<OsString>,
/// The arguments to pass if restarting the application.
pub restart_args: Vec<OsString>,
/// The URL to which to send reports.
pub report_url: Option<OsString>,
/// The localized strings to use.
pub strings: Option<lang::LangStrings>,
/// The log target.
pub log_target: Option<LogTarget>,
}
pub struct ConfigStringBuilder<'a>(lang::LangStringBuilder<'a>);
impl<'a> ConfigStringBuilder<'a> {
/// Set an argument for the string.
pub fn arg<V: Into<Cow<'a, str>>>(self, key: &'a str, value: V) -> Self {
ConfigStringBuilder(self.0.arg(key, value))
}
/// Get the localized string.
pub fn get(self) -> String {
self.0
.get()
.context("failed to get localized string")
.unwrap()
}
}
impl Config {
/// Return a configuration with no values set, and all bool values false.
pub fn new() -> Self {
Self::default()
}
/// Load a configuration from the application environment.
pub fn read_from_environment(&mut self) -> anyhow::Result<()> {
/// Most environment variables are prefixed with `MOZ_CRASHREPORTER_`.
macro_rules! ekey {
( $name:literal ) => {
concat!("MOZ_CRASHREPORTER_", $name)
};
}
self.auto_submit = env_bool(ekey!("AUTO_SUBMIT"));
self.dump_all_threads = env_bool(ekey!("DUMP_ALL_THREADS"));
self.delete_dump = !env_bool(ekey!("NO_DELETE_DUMP"));
self.data_dir = env_path(ekey!("DATA_DIRECTORY"));
self.events_dir = env_path(ekey!("EVENTS_DIRECTORY"));
self.ping_dir = env_path(ekey!("PING_DIRECTORY"));
self.app_file = std::env::var_os(ekey!("RESTART_XUL_APP_FILE"));
// Only support `MOZ_APP_LAUNCHER` on linux and macos.
if cfg!(not(target_os = "windows")) {
self.restart_command = std::env::var_os("MOZ_APP_LAUNCHER");
}
if self.restart_command.is_none() {
self.restart_command = Some(
self.sibling_program_path(mozbuild::config::MOZ_APP_NAME)
.into(),
)
}
// We no longer use don't use `MOZ_CRASHREPORTER_RESTART_ARG_0`, see bug 1872920.
self.restart_args = (1..)
.into_iter()
.map_while(|arg_num| std::env::var_os(format!("{}_{}", ekey!("RESTART_ARG"), arg_num)))
// Sometimes these are empty, in which case they should be ignored.
.filter(|s| !s.is_empty())
.collect();
self.report_url = std::env::var_os(ekey!("URL"));
let mut args = std::env::args_os()
// skip program name
.skip(1);
self.dump_file = args.next().map(|p| p.into());
while let Some(arg) = args.next() {
log::warn!("ignoring extraneous argument: {}", arg.to_string_lossy());
}
self.strings = Some(lang::load().context("failed to load localized strings")?);
Ok(())
}
/// Get the localized string for the given index.
pub fn string(&self, index: &str) -> String {
self.build_string(index).get()
}
/// Build the localized string for the given index.
pub fn build_string<'a>(&'a self, index: &'a str) -> ConfigStringBuilder<'a> {
ConfigStringBuilder(
self.strings
.as_ref()
.expect("strings not set")
.builder(index),
)
}
/// Whether the configured language has right-to-left text flow.
pub fn is_rtl(&self) -> bool {
self.strings
.as_ref()
.map(|s| s.is_rtl())
.unwrap_or_default()
}
/// Load the extra file, updating configuration.
pub fn load_extra_file(&mut self) -> anyhow::Result<serde_json::Value> {
let extra_file = self.extra_file().unwrap();
// Load the extra file (which minidump-analyzer just updated).
let extra: serde_json::Value =
serde_json::from_reader(std::fs::File::open(&extra_file).with_context(|| {
self.build_string("crashreporter-error-opening-file")
.arg("path", extra_file.display().to_string())
.get()
})?)
.with_context(|| {
self.build_string("crashreporter-error-loading-file")
.arg("path", extra_file.display().to_string())
.get()
})?;
// Set report url if not already set.
if self.report_url.is_none() {
if let Some(url) = extra["ServerURL"].as_str() {
self.report_url = Some(url.into());
}
}
// Set the data dir if not already set.
if self.data_dir.is_none() {
let vendor = extra[VENDOR_KEY].as_str().unwrap_or(DEFAULT_VENDOR);
let product = extra[PRODUCT_KEY].as_str().unwrap_or(DEFAULT_PRODUCT);
self.data_dir = Some(self.get_data_dir(vendor, product)?);
}
// Clear the restart command if WER handled the crash. This prevents restarting the
// program. See bug 1872920.
if extra.get("WindowsErrorReporting").is_some() {
self.restart_command = None;
}
Ok(extra)
}
/// Get the path to the extra file.
///
/// Returns None if no dump_file is set.
pub fn extra_file(&self) -> Option<PathBuf> {
self.dump_file.clone().map(extra_file_for_dump_file)
}
/// Get the path to the memory file.
///
/// Returns None if no dump_file is set or if the memory file does not exist.
pub fn memory_file(&self) -> Option<PathBuf> {
self.dump_file.clone().and_then(|p| {
let p = memory_file_for_dump_file(p);
p.exists().then_some(p)
})
}
/// The path to the data directory.
///
/// Panics if no data directory is set.
pub fn data_dir(&self) -> &Path {
self.data_dir.as_deref().unwrap()
}
/// The path to the dump file.
///
/// Panics if no dump file is set.
pub fn dump_file(&self) -> &Path {
self.dump_file.as_deref().unwrap()
}
/// The id of the local dump file (the base filename without extension).
///
/// Panics if no dump file is set.
pub fn local_dump_id(&self) -> Cow<str> {
self.dump_file().file_stem().unwrap().to_string_lossy()
}
/// Move crash data to the pending folder.
pub fn move_crash_data_to_pending(&mut self) -> anyhow::Result<()> {
let pending_crashes_dir = self.data_dir().join("pending");
std::fs::create_dir_all(&pending_crashes_dir).with_context(|| {
self.build_string("crashreporter-error-creating-dir")
.arg("path", pending_crashes_dir.display().to_string())
.get()
})?;
let move_file = |from: &Path| -> anyhow::Result<PathBuf> {
let to = pending_crashes_dir.join(from.file_name().unwrap());
std::fs::rename(from, &to).with_context(|| {
self.build_string("crashreporter-error-moving-path")
.arg("from", from.display().to_string())
.arg("to", to.display().to_string())
.get()
})?;
Ok(to)
};
let new_dump_file = move_file(self.dump_file())?;
move_file(self.extra_file().unwrap().as_ref())?;
// Failing to move the memory file is recoverable.
if let Some(memory_file) = self.memory_file() {
if let Err(e) = move_file(memory_file.as_ref()) {
log::warn!("failed to move memory file: {e}");
if let Err(e) = std::fs::remove_file(&memory_file) {
log::warn!("failed to remove {}: {e}", memory_file.display());
}
}
}
self.dump_file = Some(new_dump_file);
Ok(())
}
/// Form the path which signals EOL for a particular version.
pub fn version_eol_file(&self, version: &str) -> PathBuf {
self.data_dir().join(format!("EndOfLife{version}"))
}
/// Return the path used to store submitted crash ids.
pub fn submitted_crash_dir(&self) -> PathBuf {
self.data_dir().join("submitted")
}
/// Delete files related to the crash report.
pub fn delete_files(&self) {
if !self.delete_dump {
return;
}
for file in [&self.dump_file, &self.extra_file(), &self.memory_file()]
.into_iter()
.flatten()
{
if let Err(e) = std::fs::remove_file(file) {
log::warn!("failed to remove {}: {e}", file.display());
}
}
}
/// Prune old minidump files adjacent to the dump file.
pub fn prune_files(&self) -> anyhow::Result<()> {
log::info!("pruning minidump files to the {MINIDUMP_PRUNE_SAVE_COUNT} most recent");
let Some(file) = &self.dump_file else {
anyhow::bail!("no dump file")
};
let Some(dir) = file.parent() else {
anyhow::bail!("no parent directory for dump file")
};
log::debug!("pruning {} directory", dir.display());
let read_dir = dir.read_dir().with_context(|| {
format!(
"failed to read dump file parent directory {}",
dir.display()
)
})?;
let mut minidump_files = Vec::new();
for entry in read_dir {
match entry {
Err(e) => log::error!(
"error while iterating over {} directory entry: {e}",
dir.display()
),
Ok(e) if e.path().extension() == Some("dmp".as_ref()) => {
// Return if the metadata can't be read, since not being able to get metadata
// for any file could make the selection of minidumps to delete incorrect.
let meta = e.metadata().with_context(|| {
format!("failed to read metadata for {}", e.path().display())
})?;
if meta.is_file() {
let modified_time =
meta.modified().expect(
"file modification time should be available on all crashreporter platforms",
);
minidump_files.push((modified_time, e.path()));
}
}
_ => (),
}
}
// Sort by modification time first, then path (just to have a defined behavior in the case
// of identical times). The reverse leaves the files in order from newest to oldest.
minidump_files.sort_unstable_by(|a, b| a.cmp(b).reverse());
// Delete files, skipping the most recent MINIDUMP_PRUNE_SAVE_COUNT.
for dump_file in minidump_files
.into_iter()
.skip(MINIDUMP_PRUNE_SAVE_COUNT)
.map(|v| v.1)
{
log::debug!("pruning {} and related files", dump_file.display());
if let Err(e) = std::fs::remove_file(&dump_file) {
log::warn!("failed to delete {}: {e}", dump_file.display());
}
// Ignore errors for the extra file and the memory file: they may not exist.
let _ = std::fs::remove_file(extra_file_for_dump_file(dump_file.clone()));
let _ = std::fs::remove_file(memory_file_for_dump_file(dump_file));
}
Ok(())
}
/// Get the path of a program that is a sibling of the crashreporter.
///
/// On MacOS, this assumes that the crashreporter is its own application bundle within the main
/// program bundle. On other platforms this assumes siblings reside in the same directory as
/// the crashreporter.
///
/// The returned path isn't guaranteed to exist.
// This method could be standalone rather than living in `Config`; it's here because it makes
// sense that if it were to rely on anything, it would be the `Config` (and that may change in
// the future).
pub fn sibling_program_path<N: AsRef<OsStr>>(&self, program: N) -> PathBuf {
// Expect shouldn't ever panic here because we need more than one argument to run
// the program in the first place (we've already previously iterated args).
//
// We use argv[0] rather than `std::env::current_exe` because `current_exe` doesn't define
// how symlinks are treated, and we want to support running directly from the local build
// directory (which uses symlinks on linux and macos).
let self_path = PathBuf::from(std::env::args_os().next().expect("failed to get argv[0]"));
let exe_extension = self_path.extension().unwrap_or_default();
let mut program_path = self_path.clone();
// Pop the executable off to get the parent directory.
program_path.pop();
program_path.push(program.as_ref());
program_path.set_extension(exe_extension);
if !program_path.exists() && cfg!(all(not(mock), target_os = "macos")) {
// On macOS the crash reporter client is shipped as an application bundle contained
// within Firefox's main application bundle. So when it's invoked its current working
// directory looks like:
// Firefox.app/Contents/MacOS/crashreporter.app/Contents/MacOS/
// The other applications we ship with Firefox are stored in the main bundle
// (Firefox.app/Contents/MacOS/) so we we need to go back three directories
// to reach them.
// 4 pops: 1 for the path that was just pushed, and 3 more for
// `crashreporter.app/Contents/MacOS`.
for _ in 0..4 {
program_path.pop();
}
program_path.push(program.as_ref());
program_path.set_extension(exe_extension);
}
program_path
}
cfg_if::cfg_if! {
if #[cfg(mock)] {
fn get_data_dir(&self, vendor: &str, product: &str) -> anyhow::Result<PathBuf> {
let mut path = PathBuf::from("data_dir");
path.push(vendor);
path.push(product);
path.push("Crash Reports");
Ok(path)
}
} else if #[cfg(target_os = "linux")] {
fn get_data_dir(&self, vendor: &str, product: &str) -> anyhow::Result<PathBuf> {
// home_dir is deprecated due to incorrect behavior on windows, but we only use it on linux
#[allow(deprecated)]
let mut data_path =
std::env::home_dir().with_context(|| self.string("crashreporter-error-no-home-dir"))?;
data_path.push(format!(".{}", vendor.to_lowercase()));
data_path.push(product.to_lowercase());
data_path.push("Crash Reports");
Ok(data_path)
}
} else if #[cfg(target_os = "macos")] {
fn get_data_dir(&self, _vendor: &str, product: &str) -> anyhow::Result<PathBuf> {
use objc::{
rc::autoreleasepool,
runtime::{Object, BOOL, YES},
*,
};
#[link(name = "Foundation", kind = "framework")]
extern "system" {
fn NSSearchPathForDirectoriesInDomains(
directory: usize,
domain_mask: usize,
expand_tilde: BOOL,
) -> *mut Object /* NSArray<NSString*>* */;
}
#[allow(non_upper_case_globals)]
const NSApplicationSupportDirectory: usize = 14;
#[allow(non_upper_case_globals)]
const NSUserDomainMask: usize = 1;
let mut data_path = autoreleasepool(|| {
let paths /* NSArray<NSString*>* */ = unsafe {
NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES)
};
if paths.is_null() {
anyhow::bail!("NSSearchPathForDirectoriesInDomains returned nil");
}
let path: *mut Object /* NSString* */ = unsafe { msg_send![paths, firstObject] };
if path.is_null() {
anyhow::bail!("NSSearchPathForDirectoriesInDomains returned no paths");
}
let str_pointer: *const i8 = unsafe { msg_send![path, UTF8String] };
// # Safety
// The pointer is a readable C string with a null terminator.
let Ok(s) = unsafe { std::ffi::CStr::from_ptr(str_pointer) }.to_str() else {
anyhow::bail!("NSString wasn't valid UTF8");
};
Ok(PathBuf::from(s))
})?;
data_path.push(product);
std::fs::create_dir_all(&data_path).with_context(|| {
self.build_string("crashreporter-error-creating-dir")
.arg("path", data_path.display().to_string())
.get()
})?;
data_path.push("Crash Reports");
Ok(data_path)
}
} else if #[cfg(target_os = "windows")] {
fn get_data_dir(&self, vendor: &str, product: &str) -> anyhow::Result<PathBuf> {
use crate::std::os::windows::ffi::OsStringExt;
use windows_sys::{
core::PWSTR,
Win32::{
Globalization::lstrlenW,
System::Com::CoTaskMemFree,
UI::Shell::{FOLDERID_RoamingAppData, SHGetKnownFolderPath},
},
};
let mut path: PWSTR = std::ptr::null_mut();
let result = unsafe { SHGetKnownFolderPath(&FOLDERID_RoamingAppData, 0, 0, &mut path) };
if result != 0 {
unsafe { CoTaskMemFree(path as _) };
anyhow::bail!("failed to get known path for roaming appdata");
}
let length = unsafe { lstrlenW(path) };
let slice = unsafe { std::slice::from_raw_parts(path, length as usize) };
let osstr = OsString::from_wide(slice);
unsafe { CoTaskMemFree(path as _) };
let mut path = PathBuf::from(osstr);
path.push(vendor);
path.push(product);
path.push("Crash Reports");
Ok(path)
}
}
}
}
fn env_bool<K: AsRef<OsStr>>(name: K) -> bool {
std::env::var(name).map(|s| !s.is_empty()).unwrap_or(false)
}
fn env_path<K: AsRef<OsStr>>(name: K) -> Option<PathBuf> {
std::env::var_os(name).map(PathBuf::from)
}
fn extra_file_for_dump_file(mut dump_file: PathBuf) -> PathBuf {
dump_file.set_extension("extra");
dump_file
}
fn memory_file_for_dump_file(mut dump_file: PathBuf) -> PathBuf {
dump_file.set_extension("memory.json.gz");
dump_file
}

Просмотреть файл

@ -0,0 +1,74 @@
/* 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/. */
use super::LangStrings;
use anyhow::Context;
use fluent::{bundle::FluentBundle, FluentResource};
use unic_langid::LanguageIdentifier;
const FALLBACK_FTL_FILE: &str = include_str!(mozbuild::srcdir_path!(
"/toolkit/locales/en-US/crashreporter/crashreporter.ftl"
));
const FALLBACK_BRANDING_FILE: &str = include_str!(mozbuild::srcdir_path!(
"/browser/branding/official/locales/en-US/brand.ftl"
));
/// Localization language information.
#[derive(Debug, Clone)]
pub struct LanguageInfo {
pub identifier: String,
pub ftl_definitions: String,
pub ftl_branding: String,
}
impl Default for LanguageInfo {
fn default() -> Self {
Self::fallback()
}
}
impl LanguageInfo {
/// Get the fallback bundled language information (en-US).
pub fn fallback() -> Self {
LanguageInfo {
identifier: "en-US".to_owned(),
ftl_definitions: FALLBACK_FTL_FILE.to_owned(),
ftl_branding: FALLBACK_BRANDING_FILE.to_owned(),
}
}
/// Load strings from the language info.
pub fn load_strings(self) -> anyhow::Result<LangStrings> {
let Self {
identifier: lang,
ftl_definitions: definitions,
ftl_branding: branding,
} = self;
let langid = lang
.parse::<LanguageIdentifier>()
.with_context(|| format!("failed to parse language identifier ({lang})"))?;
let rtl = langid.character_direction() == unic_langid::CharacterDirection::RTL;
let mut bundle = FluentBundle::new_concurrent(vec![langid]);
fn add_ftl<M>(
bundle: &mut FluentBundle<FluentResource, M>,
ftl: String,
) -> anyhow::Result<()> {
let resource = FluentResource::try_new(ftl)
.ok()
.context("failed to create fluent resource")?;
bundle
.add_resource(resource)
.ok()
.context("failed to add fluent resource to bundle")?;
Ok(())
}
add_ftl(&mut bundle, branding).context("failed to add branding")?;
add_ftl(&mut bundle, definitions).context("failed to add localization")?;
Ok(LangStrings::new(bundle, rtl))
}
}

Просмотреть файл

@ -0,0 +1,89 @@
/* 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 http://mozilla.org/MPL/2.0/. */
mod language_info;
mod omnijar;
use fluent::{bundle::FluentBundle, FluentArgs, FluentResource};
use intl_memoizer::concurrent::IntlLangMemoizer;
#[cfg(test)]
pub use language_info::LanguageInfo;
use std::borrow::Cow;
use std::collections::BTreeMap;
/// Get the localized string bundle.
pub fn load() -> anyhow::Result<LangStrings> {
// TODO support langpacks, bug 1873210
omnijar::read().unwrap_or_else(|e| {
log::warn!("failed to read localization data from the omnijar ({e}), falling back to bundled content");
Default::default()
}).load_strings()
}
/// A bundle of localized strings.
pub struct LangStrings {
bundle: FluentBundle<FluentResource, IntlLangMemoizer>,
rtl: bool,
}
/// Arguments to build a localized string.
pub type LangStringsArgs<'a> = BTreeMap<&'a str, Cow<'a, str>>;
impl LangStrings {
pub fn new(bundle: FluentBundle<FluentResource, IntlLangMemoizer>, rtl: bool) -> Self {
LangStrings { bundle, rtl }
}
/// Return whether the localized language has right-to-left text flow.
pub fn is_rtl(&self) -> bool {
self.rtl
}
pub fn get(&self, index: &str, args: LangStringsArgs) -> anyhow::Result<String> {
let mut fluent_args = FluentArgs::with_capacity(args.len());
for (k, v) in args {
fluent_args.set(k, v);
}
let Some(pattern) = self.bundle.get_message(index).and_then(|m| m.value()) else {
anyhow::bail!("failed to get fluent message for {index}");
};
let mut errs = Vec::new();
let ret = self
.bundle
.format_pattern(pattern, Some(&fluent_args), &mut errs);
if !errs.is_empty() {
anyhow::bail!("errors while formatting pattern: {errs:?}");
}
Ok(ret.into_owned())
}
pub fn builder<'a>(&'a self, index: &'a str) -> LangStringBuilder<'a> {
LangStringBuilder {
strings: self,
index,
args: Default::default(),
}
}
}
/// A localized string builder.
pub struct LangStringBuilder<'a> {
strings: &'a LangStrings,
index: &'a str,
args: LangStringsArgs<'a>,
}
impl<'a> LangStringBuilder<'a> {
/// Set an argument for the string.
pub fn arg<V: Into<Cow<'a, str>>>(mut self, key: &'a str, value: V) -> Self {
self.args.insert(key, value.into());
self
}
/// Get the localized string.
pub fn get(self) -> anyhow::Result<String> {
self.strings.get(self.index, self.args)
}
}

Просмотреть файл

@ -0,0 +1,79 @@
/* 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 http://mozilla.org/MPL/2.0/. */
use super::language_info::LanguageInfo;
use crate::std::{
env::current_exe,
fs::File,
io::{BufRead, BufReader, Read},
path::Path,
};
use anyhow::Context;
use zip::read::ZipArchive;
/// Read the appropriate localization fluent definitions from the omnijar files.
///
/// Returns (locale name, fluent definitions).
pub fn read() -> anyhow::Result<LanguageInfo> {
let mut path = current_exe().context("failed to get current executable")?;
path.pop();
path.push("omni.ja");
let mut zip = read_omnijar_file(&path)?;
let locale = {
let buf = BufReader::new(
zip.by_name("res/multilocale.txt")
.context("failed to read multilocale file in zip archive")?,
);
buf.lines()
.next()
.ok_or(anyhow::anyhow!("multilocale file was empty"))?
.context("failed to read first line of multilocale file")?
};
let mut file = zip
.by_name(&format!(
"localization/{locale}/toolkit/crashreporter/crashreporter.ftl"
))
.with_context(|| format!("failed to locate localization file for {locale}"))?;
let mut ftl_definitions = String::new();
file.read_to_string(&mut ftl_definitions)
.with_context(|| format!("failed to read localization file for {locale}"))?;
// The brand ftl is in the browser omnijar.
path.pop();
path.push("browser");
path.push("omni.ja");
let ftl_branding = read_omnijar_file(&path)
.and_then(|mut zip| {
let mut file = zip
.by_name(&format!("localization/{locale}/branding/brand.ftl"))
.with_context(|| {
format!("failed to locate branding localization file for {locale}")
})?;
let mut s = String::new();
file.read_to_string(&mut s)
.with_context(|| format!("failed to read localization file for {locale}"))?;
Ok(s)
})
.unwrap_or_else(|e| {
log::warn!("failed to read browser omnijar: {e}");
log::info!("using fallback branding info");
LanguageInfo::default().ftl_branding
});
Ok(LanguageInfo {
identifier: locale,
ftl_definitions,
ftl_branding,
})
}
fn read_omnijar_file(path: &Path) -> anyhow::Result<ZipArchive<File>> {
ZipArchive::new(
File::open(&path).with_context(|| format!("failed to open {}", path.display()))?,
)
.with_context(|| format!("failed to read zip archive in {}", path.display()))
}

Просмотреть файл

@ -0,0 +1,85 @@
/* 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/. */
//! Application logging facilities.
use crate::std::{
self,
path::Path,
sync::{Arc, Mutex},
};
/// Initialize logging and return a log target which can be used to change the destination of log
/// statements.
pub fn init() -> LogTarget {
let log_target_inner = LogTargetInner::default();
env_logger::builder()
.parse_env(
env_logger::Env::new()
.filter("MOZ_CRASHEREPORTER")
.write_style("MOZ_CRASHREPORTER_STYLE"),
)
.target(env_logger::fmt::Target::Pipe(Box::new(
log_target_inner.clone(),
)))
.init();
LogTarget {
inner: log_target_inner,
}
}
/// Controls the target of logging.
#[derive(Clone)]
pub struct LogTarget {
inner: LogTargetInner,
}
impl LogTarget {
/// Set the file to which log statements will be written.
pub fn set_file(&self, path: &Path) {
match std::fs::File::create(path) {
Ok(file) => {
if let Ok(mut guard) = self.inner.target.lock() {
*guard = Box::new(file);
}
}
Err(e) => log::error!("failed to retarget log to {}: {e}", path.display()),
}
}
}
/// A private inner class implements Write, allows creation, etc. Externally the `LogTarget` only
/// supports changing the target and nothing else.
#[derive(Clone)]
struct LogTargetInner {
target: Arc<Mutex<Box<dyn std::io::Write + Send + 'static>>>,
}
impl Default for LogTargetInner {
fn default() -> Self {
LogTargetInner {
target: Arc::new(Mutex::new(Box::new(std::io::stderr()))),
}
}
}
impl std::io::Write for LogTargetInner {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let Ok(mut guard) = self.target.lock() else {
// Pretend we wrote successfully.
return Ok(buf.len());
};
guard.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
let Ok(mut guard) = self.target.lock() else {
// Pretend we flushed successfully.
return Ok(());
};
guard.flush()
}
}

Просмотреть файл

@ -0,0 +1,660 @@
/* 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 http://mozilla.org/MPL/2.0/. */
//! Business logic for the crash reporter.
use crate::std::{
cell::RefCell,
path::PathBuf,
process::Command,
sync::{
atomic::{AtomicBool, Ordering::Relaxed},
Arc, Mutex,
},
};
use crate::{
async_task::AsyncTask,
config::Config,
net,
settings::Settings,
std,
ui::{ReportCrashUI, ReportCrashUIState, SubmitState},
};
use anyhow::Context;
use uuid::Uuid;
/// The main crash reporting logic.
pub struct ReportCrash {
pub settings: RefCell<Settings>,
config: Arc<Config>,
extra: serde_json::Value,
settings_file: PathBuf,
attempted_to_send: AtomicBool,
ui: Option<AsyncTask<ReportCrashUIState>>,
}
impl ReportCrash {
pub fn new(config: Arc<Config>, extra: serde_json::Value) -> anyhow::Result<Self> {
let settings_file = config.data_dir().join("crashreporter_settings.json");
let settings: Settings = match std::fs::File::open(&settings_file) {
Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
anyhow::bail!(
"failed to open settings file ({}): {e}",
settings_file.display()
);
}
Err(_) => Default::default(),
Ok(f) => Settings::from_reader(f)?,
};
Ok(ReportCrash {
config,
extra,
settings_file,
settings: settings.into(),
attempted_to_send: Default::default(),
ui: None,
})
}
/// Returns whether an attempt was made to send the report.
pub fn run(mut self) -> anyhow::Result<bool> {
self.set_log_file();
let hash = self.compute_minidump_hash().map(Some).unwrap_or_else(|e| {
log::warn!("failed to compute minidump hash: {e}");
None
});
let ping_uuid = self.send_crash_ping(hash.as_deref()).unwrap_or_else(|e| {
log::warn!("failed to send crash ping: {e}");
None
});
if let Err(e) = self.update_events_file(hash.as_deref(), ping_uuid) {
log::warn!("failed to update events file: {e}");
}
self.sanitize_extra();
self.check_eol_version()?;
if !self.config.auto_submit {
self.run_ui();
} else {
anyhow::ensure!(self.try_send().unwrap_or(false), "failed to send report");
}
Ok(self.attempted_to_send.load(Relaxed))
}
/// Set the log file based on the current configured paths.
///
/// This is the earliest that this can occur as the configuration data dir may be set based on
/// fields in the extra file.
fn set_log_file(&self) {
if let Some(log_target) = &self.config.log_target {
log_target.set_file(&self.config.data_dir().join("submit.log"));
}
}
/// Compute the SHA256 hash of the minidump file contents, and return it as a hex string.
fn compute_minidump_hash(&self) -> anyhow::Result<String> {
let hash = {
use sha2::{Digest, Sha256};
let mut dump_file = std::fs::File::open(self.config.dump_file())?;
let mut hasher = Sha256::new();
std::io::copy(&mut dump_file, &mut hasher)?;
hasher.finalize()
};
let mut s = String::with_capacity(hash.len() * 2);
for byte in hash {
use crate::std::fmt::Write;
write!(s, "{:02x}", byte).unwrap();
}
Ok(s)
}
/// Send a crash ping to telemetry.
///
/// Returns the crash ping uuid.
fn send_crash_ping(&self, minidump_hash: Option<&str>) -> anyhow::Result<Option<Uuid>> {
if self.config.ping_dir.is_none() {
log::warn!("not sending crash ping because no ping directory configured");
return Ok(None);
}
//TODO support glean crash pings (or change pingsender to do so)
let dump_id = self.config.local_dump_id();
let ping = net::legacy_telemetry::Ping::crash(&self.extra, dump_id.as_ref(), minidump_hash)
.context("failed to create telemetry crash ping")?;
let submission_url = ping
.submission_url(&self.extra)
.context("failed to generate ping submission URL")?;
let target_file = self
.config
.ping_dir
.as_ref()
.unwrap()
.join(format!("{}.json", ping.id()));
let file = std::fs::File::create(&target_file).with_context(|| {
format!(
"failed to open ping file {} for writing",
target_file.display()
)
})?;
serde_json::to_writer(file, &ping).context("failed to serialize telemetry crash ping")?;
let pingsender_path = self.config.sibling_program_path("pingsender");
crate::process::background_command(&pingsender_path)
.arg(submission_url)
.arg(target_file)
.spawn()
.with_context(|| {
format!(
"failed to launch pingsender process at {}",
pingsender_path.display()
)
})?;
// TODO asynchronously get pingsender result and log it?
Ok(Some(ping.id().clone()))
}
/// Remove unneeded entries from the extra file, and add some that indicate from where the data
/// is being sent.
fn sanitize_extra(&mut self) {
if let Some(map) = self.extra.as_object_mut() {
// Remove these entries, they don't need to be sent.
map.remove("ServerURL");
map.remove("StackTraces");
}
self.extra["SubmittedFrom"] = "Client".into();
self.extra["Throttleable"] = "1".into();
}
/// Update the events file with information about the crash ping, minidump hash, and
/// stacktraces.
fn update_events_file(
&self,
minidump_hash: Option<&str>,
ping_uuid: Option<Uuid>,
) -> anyhow::Result<()> {
use crate::std::io::{BufRead, Error, ErrorKind, Write};
struct EventsFile {
event_version: String,
time: String,
uuid: String,
pub data: serde_json::Value,
}
impl EventsFile {
pub fn parse(mut reader: impl BufRead) -> std::io::Result<Self> {
let mut lines = (&mut reader).lines();
let mut read_field = move |name: &str| -> std::io::Result<String> {
lines.next().transpose()?.ok_or_else(|| {
Error::new(ErrorKind::InvalidData, format!("missing {name} field"))
})
};
let event_version = read_field("event version")?;
let time = read_field("time")?;
let uuid = read_field("uuid")?;
let data = serde_json::from_reader(reader)?;
Ok(EventsFile {
event_version,
time,
uuid,
data,
})
}
pub fn write(&self, mut writer: impl Write) -> std::io::Result<()> {
writeln!(writer, "{}", self.event_version)?;
writeln!(writer, "{}", self.time)?;
writeln!(writer, "{}", self.uuid)?;
serde_json::to_writer(writer, &self.data)?;
Ok(())
}
}
let Some(events_dir) = &self.config.events_dir else {
log::warn!("not updating the events file; no events directory configured");
return Ok(());
};
let event_path = events_dir.join(self.config.local_dump_id().as_ref());
// Read events file.
let file = std::fs::File::open(&event_path)
.with_context(|| format!("failed to open event file at {}", event_path.display()))?;
let mut events_file =
EventsFile::parse(std::io::BufReader::new(file)).with_context(|| {
format!(
"failed to parse events file contents in {}",
event_path.display()
)
})?;
// Update events file fields.
if let Some(hash) = minidump_hash {
events_file.data["MinidumpSha256Hash"] = hash.into();
}
if let Some(uuid) = ping_uuid {
events_file.data["CrashPingUUID"] = uuid.to_string().into();
}
events_file.data["StackTraces"] = self.extra["StackTraces"].clone();
// Write altered events file.
let file = std::fs::File::create(&event_path).with_context(|| {
format!("failed to truncate event file at {}", event_path.display())
})?;
events_file
.write(file)
.with_context(|| format!("failed to write event file at {}", event_path.display()))
}
/// Check whether the version of the software that generated the crash is EOL.
fn check_eol_version(&self) -> anyhow::Result<()> {
if let Some(version) = self.extra["Version"].as_str() {
if self.config.version_eol_file(version).exists() {
self.config.delete_files();
anyhow::bail!(self.config.string("crashreporter-error-version-eol"));
}
}
Ok(())
}
/// Save the current settings.
fn save_settings(&self) {
let result: anyhow::Result<()> = (|| {
Ok(self
.settings
.borrow()
.to_writer(std::fs::File::create(&self.settings_file)?)?)
})();
if let Err(e) = result {
log::error!("error while saving settings: {e}");
}
}
/// Handle a response from submitting a crash report.
///
/// Returns the crash ID to use for the recorded submission event. Errors in this function may
/// result in None being returned to consider the crash report submission as a failure even
/// though the server did provide a response.
fn handle_crash_report_response(
&self,
response: net::report::Response,
) -> anyhow::Result<Option<String>> {
if let Some(version) = response.stop_sending_reports_for {
// Create the EOL version file. The content seemingly doesn't matter, but we mimic what
// was written by the old crash reporter.
if let Err(e) = std::fs::write(self.config.version_eol_file(&version), "1\n") {
log::warn!("failed to write EOL file: {e}");
}
}
if response.discarded {
log::debug!("response indicated that the report was discarded");
return Ok(None);
}
let Some(crash_id) = response.crash_id else {
log::debug!("response did not provide a crash id");
return Ok(None);
};
// Write the id to the `submitted` directory
let submitted_dir = self.config.submitted_crash_dir();
std::fs::create_dir_all(&submitted_dir).with_context(|| {
format!(
"failed to create submitted crash directory {}",
submitted_dir.display()
)
})?;
let crash_id_file = submitted_dir.join(format!("{crash_id}.txt"));
let mut file = std::fs::File::create(&crash_id_file).with_context(|| {
format!(
"failed to create submitted crash file for {crash_id} ({})",
crash_id_file.display()
)
})?;
// Shadow `std::fmt::Write` to use the correct trait below.
use crate::std::io::Write;
if let Err(e) = writeln!(
&mut file,
"{}",
self.config
.build_string("crashreporter-crash-identifier")
.arg("id", &crash_id)
.get()
) {
log::warn!(
"failed to write to submitted crash file ({}) for {crash_id}: {e}",
crash_id_file.display()
);
}
if let Some(url) = response.view_url {
if let Err(e) = writeln!(
&mut file,
"{}",
self.config
.build_string("crashreporter-crash-details")
.arg("url", url)
.get()
) {
log::warn!(
"failed to write view url to submitted crash file ({}) for {crash_id}: {e}",
crash_id_file.display()
);
}
}
Ok(Some(crash_id))
}
/// Write the submission event.
///
/// A `None` crash_id indicates that the submission failed.
fn write_submission_event(&self, crash_id: Option<String>) -> anyhow::Result<()> {
let Some(events_dir) = &self.config.events_dir else {
// If there's no events dir, don't do anything.
return Ok(());
};
let local_id = self.config.local_dump_id();
let event_path = events_dir.join(format!("{local_id}-submission"));
let unix_epoch_seconds = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.expect("system time is before the unix epoch")
.as_secs();
std::fs::write(
&event_path,
format!(
"crash.submission.1\n{unix_epoch_seconds}\n{local_id}\n{}\n{}",
crash_id.is_some(),
crash_id.as_deref().unwrap_or("")
),
)
.with_context(|| format!("failed to write event to {}", event_path.display()))
}
/// Restart the program.
fn restart_process(&self) {
if self.config.restart_command.is_none() {
// The restart button should be hidden in this case, so this error should not occur.
log::error!("no process configured for restart");
return;
}
let mut cmd = Command::new(self.config.restart_command.as_ref().unwrap());
cmd.args(&self.config.restart_args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
if let Some(xul_app_file) = &self.config.app_file {
cmd.env("XUL_APP_FILE", xul_app_file);
}
log::debug!("restarting process: {:?}", cmd);
if let Err(e) = cmd.spawn() {
log::error!("failed to restart process: {e}");
}
}
/// Run the crash reporting UI.
fn run_ui(&mut self) {
use crate::std::{sync::mpsc, thread};
let (logic_send, logic_recv) = mpsc::channel();
// Wrap work_send in an Arc so that it can be captured weakly by the work queue and
// drop when the UI finishes, including panics (allowing the logic thread to exit).
//
// We need to wrap in a Mutex because std::mpsc::Sender isn't Sync (until rust 1.72).
let logic_send = Arc::new(Mutex::new(logic_send));
let weak_logic_send = Arc::downgrade(&logic_send);
let logic_remote_queue = AsyncTask::new(move |f| {
if let Some(logic_send) = weak_logic_send.upgrade() {
// This is best-effort: ignore errors.
let _ = logic_send.lock().unwrap().send(f);
}
});
let crash_ui = ReportCrashUI::new(
&*self.settings.borrow(),
self.config.clone(),
logic_remote_queue,
);
// Set the UI remote queue.
self.ui = Some(crash_ui.async_task());
// Spawn a separate thread to handle all interactions with `self`. This prevents blocking
// the UI for any reason.
// Use a barrier to ensure both threads are live before either starts (ensuring they
// can immediately queue work for each other).
let barrier = std::sync::Barrier::new(2);
let barrier = &barrier;
thread::scope(move |s| {
// Move `logic_send` into this scope so that it will drop when the scope completes
// (which will drop the `mpsc::Sender` and cause the logic thread to complete and join
// when the UI finishes so the scope can exit).
let _logic_send = logic_send;
s.spawn(move || {
barrier.wait();
while let Ok(f) = logic_recv.recv() {
f(self);
}
// Clear the UI remote queue, using it after this point is an error.
//
// NOTE we do this here because the compiler can't reason about `self` being safely
// accessible after `thread::scope` returns. This is effectively the same result
// since the above loop will only exit when `logic_send` is dropped at the end of
// the scope.
self.ui = None;
});
barrier.wait();
crash_ui.run()
});
}
}
// These methods may interact with `self.ui`.
impl ReportCrash {
/// Update the submission details shown in the UI.
pub fn update_details(&self) {
use crate::std::fmt::Write;
let extra = self.current_extra_data();
let mut details = String::new();
let mut entries: Vec<_> = extra.as_object().unwrap().into_iter().collect();
entries.sort_unstable_by_key(|(k, _)| *k);
for (key, value) in entries {
let _ = write!(details, "{key}: ");
if let Some(v) = value.as_str() {
details.push_str(v);
} else {
match serde_json::to_string(value) {
Ok(s) => details.push_str(&s),
Err(e) => {
let _ = write!(details, "<serialization error: {e}>");
}
}
}
let _ = writeln!(details);
}
let _ = writeln!(
details,
"{}",
self.config.string("crashreporter-report-info")
);
self.ui().push(move |ui| *ui.details.borrow_mut() = details);
}
/// Restart the application and send the crash report.
pub fn restart(&self) {
self.save_settings();
// Get the program restarted before sending the report.
self.restart_process();
let result = self.try_send();
self.close_window(result.is_some());
}
/// Quit and send the crash report.
pub fn quit(&self) {
self.save_settings();
let result = self.try_send();
self.close_window(result.is_some());
}
fn close_window(&self, report_sent: bool) {
if report_sent && !self.config.auto_submit && !cfg!(test) {
// Add a delay to allow the user to see the result.
std::thread::sleep(std::time::Duration::from_secs(5));
}
self.ui().push(|r| r.close_window.fire(&()));
}
/// Try to send the report.
///
/// This function may be called without a UI active (if auto_submit is true), so it will not
/// panic if `self.ui` is unset.
///
/// Returns whether the report was received (regardless of whether the response was processed
/// successfully), if a report could be sent at all (based on the configuration).
fn try_send(&self) -> Option<bool> {
self.attempted_to_send.store(true, Relaxed);
let send_report = self.settings.borrow().submit_report;
if !send_report {
log::trace!("not sending report due to user setting");
return None;
}
// TODO? load proxy info from libgconf on linux
let Some(url) = &self.config.report_url else {
log::warn!("not sending report due to missing report url");
return None;
};
if let Some(ui) = &self.ui {
ui.push(|r| *r.submit_state.borrow_mut() = SubmitState::InProgress);
}
// Send the report to the server.
let extra = self.current_extra_data();
let memory_file = self.config.memory_file();
let report = net::report::CrashReport {
extra: &extra,
dump_file: self.config.dump_file(),
memory_file: memory_file.as_deref(),
url,
};
let report_response = report
.send()
.map(Some)
.unwrap_or_else(|e| {
log::error!("failed to initialize report transmission: {e}");
None
})
.and_then(|sender| {
// Normally we might want to do the following asynchronously since it will block,
// however we don't really need the Logic thread to do anything else (the UI
// becomes disabled from this point onward), so we just do it here. Same goes for
// the `std::thread::sleep` in close_window() later on.
sender.finish().map(Some).unwrap_or_else(|e| {
log::error!("failed to send report: {e}");
None
})
});
let report_received = report_response.is_some();
let crash_id = report_response.and_then(|response| {
self.handle_crash_report_response(response)
.unwrap_or_else(|e| {
log::error!("failed to handle crash report response: {e}");
None
})
});
if report_received {
// If the response could be handled (indicated by the returned crash id), clean up by
// deleting the minidump files. Otherwise, prune old minidump files.
if crash_id.is_some() {
self.config.delete_files();
} else {
if let Err(e) = self.config.prune_files() {
log::warn!("failed to prune files: {e}");
}
}
}
if let Err(e) = self.write_submission_event(crash_id) {
log::warn!("failed to write submission event: {e}");
}
// Indicate whether the report was sent successfully, regardless of whether the response
// was processed successfully.
//
// FIXME: this is how the old crash reporter worked, but we might want to change this
// behavior.
if let Some(ui) = &self.ui {
ui.push(move |r| {
*r.submit_state.borrow_mut() = if report_received {
SubmitState::Success
} else {
SubmitState::Failure
}
});
}
Some(report_received)
}
/// Form the extra data, taking into account user input.
fn current_extra_data(&self) -> serde_json::Value {
let include_address = self.settings.borrow().include_url;
let comment = if !self.config.auto_submit {
self.ui().wait(|r| r.comment.get())
} else {
Default::default()
};
let mut extra = self.extra.clone();
if !comment.is_empty() {
extra["Comments"] = comment.into();
}
if !include_address {
extra.as_object_mut().unwrap().remove("URL");
}
extra
}
fn ui(&self) -> &AsyncTask<ReportCrashUIState> {
self.ui.as_ref().expect("UI remote queue missing")
}
}

Просмотреть файл

@ -6,6 +6,10 @@
// 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).
///
@ -19,11 +23,85 @@ macro_rules! cc {
}
}
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;
use ui::*;
fn main() {
todo!();
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);
}
}
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()
}
}

Просмотреть файл

@ -0,0 +1,174 @@
/* 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 http://mozilla.org/MPL/2.0/. */
use anyhow::Context;
use serde::Serialize;
use std::collections::BTreeMap;
use uuid::Uuid;
const TELEMETRY_VERSION: u64 = 4;
const PAYLOAD_VERSION: u64 = 1;
// Generated by `build.rs`.
// static PING_ANNOTATIONS: phf::Set<&'static str>;
include!(concat!(env!("OUT_DIR"), "/ping_annotations.rs"));
#[derive(Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Ping<'a> {
Crash {
id: Uuid,
version: u64,
#[serde(with = "time::serde::rfc3339")]
creation_date: time::OffsetDateTime,
client_id: &'a str,
#[serde(skip_serializing_if = "serde_json::Value::is_null")]
environment: serde_json::Value,
payload: Payload<'a>,
application: Application<'a>,
},
}
time::serde::format_description!(date_format, Date, "[year]-[month]-[day]");
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Payload<'a> {
session_id: &'a str,
version: u64,
#[serde(with = "date_format")]
crash_date: time::Date,
#[serde(with = "time::serde::rfc3339")]
crash_time: time::OffsetDateTime,
has_crash_environment: bool,
crash_id: &'a str,
minidump_sha256_hash: Option<&'a str>,
process_type: &'a str,
#[serde(skip_serializing_if = "serde_json::Value::is_null")]
stack_traces: serde_json::Value,
metadata: BTreeMap<&'a str, &'a str>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Application<'a> {
vendor: &'a str,
name: &'a str,
build_id: &'a str,
display_version: String,
platform_version: String,
version: &'a str,
channel: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
architecture: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
xpcom_abi: Option<String>,
}
impl<'a> Ping<'a> {
pub fn crash(
extra: &'a serde_json::Value,
crash_id: &'a str,
minidump_sha256_hash: Option<&'a str>,
) -> anyhow::Result<Self> {
let now: time::OffsetDateTime = crate::std::time::SystemTime::now().into();
let environment: serde_json::Value = extra["TelemetryEnvironment"]
.as_str()
.and_then(|estr| serde_json::from_str(estr).ok())
.unwrap_or_default();
// The subset of extra file entries (crash annotations) which are allowed in pings.
let metadata = extra
.as_object()
.map(|map| {
map.iter()
.filter_map(|(k, v)| {
PING_ANNOTATIONS
.contains(k)
.then(|| k.as_str())
.zip(v.as_str())
})
.collect()
})
.unwrap_or_default();
let display_version = environment
.pointer("/build/displayVersion")
.and_then(|s| s.as_str())
.unwrap_or_default()
.to_owned();
let platform_version = environment
.pointer("/build/platformVersion")
.and_then(|s| s.as_str())
.unwrap_or_default()
.to_owned();
let architecture = environment
.pointer("/build/architecture")
.and_then(|s| s.as_str())
.map(ToOwned::to_owned);
let xpcom_abi = environment
.pointer("/build/xpcomAbi")
.and_then(|s| s.as_str())
.map(ToOwned::to_owned);
Ok(Ping::Crash {
id: Uuid::new_v4(),
version: TELEMETRY_VERSION,
creation_date: now,
client_id: extra["TelemetryClientId"]
.as_str()
.context("missing TelemetryClientId")?,
environment,
payload: Payload {
session_id: extra["TelemetrySessionId"]
.as_str()
.context("missing TelemetrySessionId")?,
version: PAYLOAD_VERSION,
crash_date: now.date(),
crash_time: now,
has_crash_environment: true,
crash_id,
minidump_sha256_hash,
process_type: "main",
stack_traces: extra["StackTraces"].clone(),
metadata,
},
application: Application {
vendor: extra["Vendor"].as_str().unwrap_or_default(),
name: extra["ProductName"].as_str().unwrap_or_default(),
build_id: extra["BuildID"].as_str().unwrap_or_default(),
display_version,
platform_version,
version: extra["Version"].as_str().unwrap_or_default(),
channel: extra["ReleaseChannel"].as_str().unwrap_or_default(),
architecture,
xpcom_abi,
},
})
}
/// Generate the telemetry URL for submitting this ping.
pub fn submission_url(&self, extra: &serde_json::Value) -> anyhow::Result<String> {
let url = extra["TelemetryServerURL"]
.as_str()
.context("missing TelemetryServerURL")?;
let id = self.id();
let name = extra["ProductName"]
.as_str()
.context("missing ProductName")?;
let version = extra["Version"].as_str().context("missing Version")?;
let channel = extra["ReleaseChannel"]
.as_str()
.context("missing ReleaseChannel")?;
let buildid = extra["BuildID"].as_str().context("missing BuildID")?;
Ok(format!("{url}/submit/telemetry/{id}/crash/{name}/{version}/{channel}/{buildid}?v={TELEMETRY_VERSION}"))
}
/// Get the ping identifier.
pub fn id(&self) -> &Uuid {
match self {
Ping::Crash { id, .. } => id,
}
}
}

Просмотреть файл

@ -0,0 +1,408 @@
/* 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/. */
//! Partial libcurl bindings with some wrappers for safe cleanup.
use crate::std::path::Path;
use libloading::{Library, Symbol};
use std::ffi::{c_char, c_long, c_uint, CStr, CString};
// Constants lifted from `curl.h`
const CURLE_OK: CurlCode = 0;
const CURL_ERROR_SIZE: usize = 256;
const CURLOPTTYPE_LONG: CurlOption = 0;
const CURLOPTTYPE_OBJECTPOINT: CurlOption = 10000;
const CURLOPTTYPE_FUNCTIONPOINT: CurlOption = 20000;
const CURLOPTTYPE_STRINGPOINT: CurlOption = CURLOPTTYPE_OBJECTPOINT;
const CURLOPTTYPE_CBPOINT: CurlOption = CURLOPTTYPE_OBJECTPOINT;
const CURLOPT_WRITEDATA: CurlOption = CURLOPTTYPE_CBPOINT + 1;
const CURLOPT_URL: CurlOption = CURLOPTTYPE_STRINGPOINT + 2;
const CURLOPT_ERRORBUFFER: CurlOption = CURLOPTTYPE_OBJECTPOINT + 10;
const CURLOPT_WRITEFUNCTION: CurlOption = CURLOPTTYPE_FUNCTIONPOINT + 11;
const CURLOPT_USERAGENT: CurlOption = CURLOPTTYPE_STRINGPOINT + 18;
const CURLOPT_MIMEPOST: CurlOption = CURLOPTTYPE_OBJECTPOINT + 269;
const CURLOPT_MAXREDIRS: CurlOption = CURLOPTTYPE_LONG + 68;
const CURLINFO_LONG: CurlInfo = 0x200000;
const CURLINFO_RESPONSE_CODE: CurlInfo = CURLINFO_LONG + 2;
const CURL_LIB_NAMES: &[&str] = if cfg!(target_os = "linux") {
&[
"libcurl.so",
"libcurl.so.4",
// Debian gives libcurl a different name when it is built against GnuTLS
"libcurl-gnutls.so",
"libcurl-gnutls.so.4",
// Older versions in case we find nothing better
"libcurl.so.3",
"libcurl-gnutls.so.3", // See above for Debian
]
} else if cfg!(target_os = "macos") {
&[
"/usr/lib/libcurl.dylib",
"/usr/lib/libcurl.4.dylib",
"/usr/lib/libcurl.3.dylib",
]
} else if cfg!(target_os = "windows") {
&["libcurl.dll", "curl.dll"]
} else {
&[]
};
// Shim until min rust version 1.74 which allows std::io::Error::other
fn error_other<E>(error: E) -> std::io::Error
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
std::io::Error::new(std::io::ErrorKind::Other, error)
}
#[repr(transparent)]
#[derive(Clone, Copy)]
struct CurlHandle(*mut ());
type CurlCode = c_uint;
type CurlOption = c_uint;
type CurlInfo = c_uint;
#[repr(transparent)]
#[derive(Clone, Copy)]
struct CurlMime(*mut ());
#[repr(transparent)]
#[derive(Clone, Copy)]
struct CurlMimePart(*mut ());
macro_rules! library_binding {
( $localname:ident members[$($members:tt)*] load[$($load:tt)*] fn $name:ident $args:tt $( -> $ret:ty )? ; $($rest:tt)* ) => {
library_binding! {
$localname
members[
$($members)*
$name: Symbol<'static, unsafe extern fn $args $(->$ret)?>,
]
load[
$($load)*
$name: unsafe {
let symbol = $localname.get::<unsafe extern fn $args $(->$ret)?>(stringify!($name).as_bytes())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
// All symbols refer to library, so `'static` lifetimes are safe (`library`
// will outlive them).
std::mem::transmute(symbol)
},
]
$($rest)*
}
};
( $localname:ident members[$($members:tt)*] load[$($load:tt)*] ) => {
pub struct Curl {
$($members)*
_library: Library
}
impl Curl {
fn load() -> std::io::Result<Self> {
// Try each of the libraries, debug-logging load failures.
let library = CURL_LIB_NAMES.iter().find_map(|name| {
log::debug!("attempting to load {name}");
match unsafe { Library::new(name) } {
Ok(lib) => {
log::info!("loaded {name}");
Some(lib)
}
Err(e) => {
log::debug!("error when loading {name}: {e}");
None
}
}
});
let $localname = library.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "failed to find curl library")
})?;
Ok(Curl { $($load)* _library: $localname })
}
}
};
( $($rest:tt)* ) => {
library_binding! {
library members[] load[] $($rest)*
}
}
}
library_binding! {
fn curl_easy_init() -> CurlHandle;
fn curl_easy_setopt(CurlHandle, CurlOption, ...) -> CurlCode;
fn curl_easy_perform(CurlHandle) -> CurlCode;
fn curl_easy_getinfo(CurlHandle, CurlInfo, ...) -> CurlCode;
fn curl_easy_cleanup(CurlHandle);
fn curl_mime_init(CurlHandle) -> CurlMime;
fn curl_mime_addpart(CurlMime) -> CurlMimePart;
fn curl_mime_name(CurlMimePart, *const c_char) -> CurlCode;
fn curl_mime_filename(CurlMimePart, *const c_char) -> CurlCode;
fn curl_mime_type(CurlMimePart, *const c_char) -> CurlCode;
fn curl_mime_data(CurlMimePart, *const c_char, usize) -> CurlCode;
fn curl_mime_filedata(CurlMimePart, *const c_char) -> CurlCode;
fn curl_mime_free(CurlMime);
}
lazy_static::lazy_static! {
static ref CURL: std::io::Result<Curl> = Curl::load();
}
/// Load libcurl if possible.
pub fn load() -> std::io::Result<&'static Curl> {
CURL.as_ref().map_err(error_other)
}
#[derive(Debug)]
pub struct Error {
code: CurlCode,
error: Option<String>,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "curl error code {}", self.code)?;
if let Some(e) = &self.error {
write!(f, ": {e}")?;
}
Ok(())
}
}
impl std::error::Error for Error {}
impl From<Error> for std::io::Error {
fn from(e: Error) -> Self {
error_other(e)
}
}
pub type Result<T> = std::result::Result<T, Error>;
fn to_result(code: CurlCode) -> Result<()> {
if code == CURLE_OK {
Ok(())
} else {
Err(Error { code, error: None })
}
}
impl Curl {
pub fn easy(&self) -> std::io::Result<Easy> {
let handle = unsafe { (self.curl_easy_init)() };
if handle.0.is_null() {
Err(error_other("curl_easy_init failed"))
} else {
Ok(Easy {
lib: self,
handle,
mime: Default::default(),
})
}
}
}
struct ErrorBuffer([u8; CURL_ERROR_SIZE]);
impl Default for ErrorBuffer {
fn default() -> Self {
ErrorBuffer([0; CURL_ERROR_SIZE])
}
}
pub struct Easy<'a> {
lib: &'a Curl,
handle: CurlHandle,
mime: Option<Mime<'a>>,
}
impl<'a> Easy<'a> {
pub fn set_url(&mut self, url: &str) -> Result<()> {
let url = CString::new(url.to_string()).unwrap();
to_result(unsafe { (self.lib.curl_easy_setopt)(self.handle, CURLOPT_URL, url.as_ptr()) })
}
pub fn set_user_agent(&mut self, user_agent: &str) -> Result<()> {
let ua = CString::new(user_agent.to_string()).unwrap();
to_result(unsafe {
(self.lib.curl_easy_setopt)(self.handle, CURLOPT_USERAGENT, ua.as_ptr())
})
}
pub fn mime(&self) -> std::io::Result<Mime<'a>> {
let handle = unsafe { (self.lib.curl_mime_init)(self.handle) };
if handle.0.is_null() {
Err(error_other("curl_mime_init failed"))
} else {
Ok(Mime {
lib: self.lib,
handle,
})
}
}
pub fn set_mime_post(&mut self, mime: Mime<'a>) -> Result<()> {
let result = to_result(unsafe {
(self.lib.curl_easy_setopt)(self.handle, CURLOPT_MIMEPOST, mime.handle)
});
if result.is_ok() {
self.mime = Some(mime);
}
result
}
pub fn set_max_redirs(&mut self, redirs: c_long) -> Result<()> {
to_result(unsafe { (self.lib.curl_easy_setopt)(self.handle, CURLOPT_MAXREDIRS, redirs) })
}
/// Returns the response data on success.
pub fn perform(&self) -> Result<Vec<u8>> {
// Set error buffer, but degrade service if it doesn't work.
let mut error_buffer = ErrorBuffer::default();
let error_buffer_set = unsafe {
(self.lib.curl_easy_setopt)(
self.handle,
CURLOPT_ERRORBUFFER,
error_buffer.0.as_mut_ptr() as *mut c_char,
)
} == CURLE_OK;
// Set the write function to fill a Vec. If there is a panic, this might leave stale
// pointers in the curl options, but they won't be used without another perform, at which
// point they'll be overwritten.
let mut data: Vec<u8> = Vec::new();
extern "C" fn write_callback(
data: *const u8,
size: usize,
nmemb: usize,
dest: &mut Vec<u8>,
) -> usize {
let total = size * nmemb;
dest.extend(unsafe { std::slice::from_raw_parts(data, total) });
total
}
unsafe {
to_result((self.lib.curl_easy_setopt)(
self.handle,
CURLOPT_WRITEFUNCTION,
write_callback as extern "C" fn(*const u8, usize, usize, &mut Vec<u8>) -> usize,
))?;
to_result((self.lib.curl_easy_setopt)(
self.handle,
CURLOPT_WRITEDATA,
&mut data as *mut _,
))?;
};
let mut result = to_result(unsafe { (self.lib.curl_easy_perform)(self.handle) });
// Clean up a bit by unsetting the write function and write data, though they won't be used
// anywhere else. Ignore return values.
unsafe {
(self.lib.curl_easy_setopt)(
self.handle,
CURLOPT_WRITEFUNCTION,
std::ptr::null_mut::<()>(),
);
(self.lib.curl_easy_setopt)(self.handle, CURLOPT_WRITEDATA, std::ptr::null_mut::<()>());
}
if error_buffer_set {
unsafe {
(self.lib.curl_easy_setopt)(
self.handle,
CURLOPT_ERRORBUFFER,
std::ptr::null_mut::<()>(),
)
};
if let Err(e) = &mut result {
if let Ok(cstr) = CStr::from_bytes_until_nul(error_buffer.0.as_slice()) {
e.error = Some(cstr.to_string_lossy().into_owned());
}
}
}
result.map(move |()| data)
}
pub fn get_response_code(&self) -> Result<u64> {
let mut code = c_long::default();
to_result(unsafe {
(self.lib.curl_easy_getinfo)(
self.handle,
CURLINFO_RESPONSE_CODE,
&mut code as *mut c_long,
)
})?;
Ok(code.try_into().expect("negative http response code"))
}
}
impl Drop for Easy<'_> {
fn drop(&mut self) {
self.mime.take();
unsafe { (self.lib.curl_easy_cleanup)(self.handle) };
}
}
pub struct Mime<'a> {
lib: &'a Curl,
handle: CurlMime,
}
impl<'a> Mime<'a> {
pub fn add_part(&mut self) -> std::io::Result<MimePart<'a>> {
let handle = unsafe { (self.lib.curl_mime_addpart)(self.handle) };
if handle.0.is_null() {
Err(error_other("curl_mime_addpart failed"))
} else {
Ok(MimePart {
lib: self.lib,
handle,
})
}
}
}
impl Drop for Mime<'_> {
fn drop(&mut self) {
unsafe { (self.lib.curl_mime_free)(self.handle) };
}
}
pub struct MimePart<'a> {
lib: &'a Curl,
handle: CurlMimePart,
}
impl MimePart<'_> {
pub fn set_name(&mut self, name: &str) -> Result<()> {
let name = CString::new(name.to_string()).unwrap();
to_result(unsafe { (self.lib.curl_mime_name)(self.handle, name.as_ptr()) })
}
pub fn set_filename(&mut self, filename: &str) -> Result<()> {
let filename = CString::new(filename.to_string()).unwrap();
to_result(unsafe { (self.lib.curl_mime_filename)(self.handle, filename.as_ptr()) })
}
pub fn set_type(&mut self, mime_type: &str) -> Result<()> {
let mime_type = CString::new(mime_type.to_string()).unwrap();
to_result(unsafe { (self.lib.curl_mime_type)(self.handle, mime_type.as_ptr()) })
}
pub fn set_filedata(&mut self, file: &Path) -> Result<()> {
let file = CString::new(file.display().to_string()).unwrap();
to_result(unsafe { (self.lib.curl_mime_filedata)(self.handle, file.as_ptr()) })
}
pub fn set_data(&mut self, data: &[u8]) -> Result<()> {
to_result(unsafe {
(self.lib.curl_mime_data)(self.handle, data.as_ptr() as *const c_char, data.len())
})
}
}

Просмотреть файл

@ -0,0 +1,12 @@
/* 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 http://mozilla.org/MPL/2.0/. */
pub mod legacy_telemetry;
mod libcurl;
pub mod report;
#[cfg(test)]
pub fn can_load_libcurl() -> bool {
libcurl::load().is_ok()
}

Просмотреть файл

@ -0,0 +1,242 @@
/* 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/. */
use crate::std::{ffi::OsStr, path::Path, process::Child};
use anyhow::Context;
pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
/// A crash report to upload.
///
/// Post a multipart form payload to the report URL.
///
/// The form data contains:
/// | name | filename | content | mime |
/// ====================================
/// | `extra` | `extra.json` | extra json object | `application/json`|
/// | `upload_file_minidump` | dump file name | dump file contents | derived (probably application/binary) |
/// if present:
/// | `memory_report` | memory file name | memory file contents | derived (probably gzipped json) |
pub struct CrashReport<'a> {
pub extra: &'a serde_json::Value,
pub dump_file: &'a Path,
pub memory_file: Option<&'a Path>,
pub url: &'a OsStr,
}
impl CrashReport<'_> {
/// Send the crash report.
pub fn send(&self) -> std::io::Result<CrashReportSender> {
// Windows 10+ and macOS 10.15+ contain `curl` 7.64.1+ as a system-provided binary, so
// `send_with_curl_binary` should not fail.
//
// Linux distros generally do not contain `curl`, but `libcurl` is very likely to be
// incidentally installed (if not outright part of the distro base packages). Based on a
// cursory look at the debian repositories as an examplar, the curl binary is much less
// likely to be incidentally installed.
//
// For uniformity, we always will try the curl binary first, then try libcurl if that
// fails.
let extra_json_data = serde_json::to_string(self.extra)?;
self.send_with_curl_binary(extra_json_data.clone())
.or_else(|e| {
log::info!("failed to invoke curl ({e}), trying libcurl");
self.send_with_libcurl(extra_json_data.clone())
})
}
/// Send the crash report using the `curl` binary.
fn send_with_curl_binary(&self, extra_json_data: String) -> std::io::Result<CrashReportSender> {
let mut cmd = crate::process::background_command("curl");
cmd.args(["--user-agent", USER_AGENT]);
cmd.arg("--form");
// `@-` causes the data to be read from stdin, which is desirable to not have to worry
// about process argument string length limitations (though they are generally pretty high
// limits).
cmd.arg("extra=@-;filename=extra.json;type=application/json");
cmd.arg("--form");
cmd.arg(format!(
"upload_file_minidump=@{}",
CurlQuote(&self.dump_file.display().to_string())
));
if let Some(path) = self.memory_file {
cmd.arg("--form");
cmd.arg(format!(
"memory_report=@{}",
CurlQuote(&path.display().to_string())
));
}
cmd.arg(self.url);
cmd.stdin(std::process::Stdio::piped());
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
cmd.spawn().map(move |child| CrashReportSender::CurlChild {
child,
extra_json_data,
})
}
/// Send the crash report using the `curl` library.
fn send_with_libcurl(&self, extra_json_data: String) -> std::io::Result<CrashReportSender> {
let curl = super::libcurl::load()?;
let mut easy = curl.easy()?;
easy.set_url(&self.url.to_string_lossy())?;
easy.set_user_agent(USER_AGENT)?;
easy.set_max_redirs(30)?;
let mut mime = easy.mime()?;
{
let mut part = mime.add_part()?;
part.set_name("extra")?;
part.set_filename("extra.json")?;
part.set_type("application/json")?;
part.set_data(extra_json_data.as_bytes())?;
}
{
let mut part = mime.add_part()?;
part.set_name("upload_file_minidump")?;
part.set_filename(&self.dump_file.display().to_string())?;
part.set_filedata(self.dump_file)?;
}
if let Some(path) = self.memory_file {
let mut part = mime.add_part()?;
part.set_name("memory_report")?;
part.set_filename(&path.display().to_string())?;
part.set_filedata(path)?;
}
easy.set_mime_post(mime)?;
Ok(CrashReportSender::LibCurl { easy })
}
}
pub enum CrashReportSender {
CurlChild {
child: Child,
extra_json_data: String,
},
LibCurl {
easy: super::libcurl::Easy<'static>,
},
}
impl CrashReportSender {
pub fn finish(self) -> anyhow::Result<Response> {
let response = match self {
Self::CurlChild {
mut child,
extra_json_data,
} => {
{
let mut stdin = child
.stdin
.take()
.context("failed to get curl process stdin")?;
std::io::copy(&mut std::io::Cursor::new(extra_json_data), &mut stdin)
.context("failed to write extra file data to stdin of curl process")?;
// stdin is dropped at the end of this scope so that the stream gets an EOF,
// otherwise curl will wait for more input.
}
let output = child
.wait_with_output()
.context("failed to wait on curl process")?;
anyhow::ensure!(
output.status.success(),
"process failed (exit status {}) with stderr: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).into_owned()
}
Self::LibCurl { easy } => {
let response = easy.perform()?;
let response_code = easy.get_response_code()?;
let response = String::from_utf8_lossy(&response).into_owned();
dbg!(&response, &response_code);
anyhow::ensure!(
response_code == 200,
"unexpected response code ({response_code}): {response}"
);
response
}
};
log::debug!("received response from sending report: {:?}", &*response);
Ok(Response::parse(response))
}
}
/// A parsed response from submitting a crash report.
#[derive(Default, Debug)]
pub struct Response {
pub crash_id: Option<String>,
pub stop_sending_reports_for: Option<String>,
pub view_url: Option<String>,
pub discarded: bool,
}
impl Response {
/// Parse a server response.
///
/// The response should be newline-separated `<key>=<value>` pairs.
fn parse<S: AsRef<str>>(response: S) -> Self {
let mut ret = Self::default();
// Fields may be omitted, and parsing is best-effort but will not produce any errors (just
// a default Response struct).
for line in response.as_ref().lines() {
if let Some((key, value)) = line.split_once('=') {
match key {
"StopSendingReportsFor" => {
ret.stop_sending_reports_for = Some(value.to_owned())
}
"Discarded" => ret.discarded = true,
"CrashID" => ret.crash_id = Some(value.to_owned()),
"ViewURL" => ret.view_url = Some(value.to_owned()),
_ => (),
}
}
}
ret
}
}
/// Quote a string per https://curl.se/docs/manpage.html#-F.
/// That is, add quote characters and escape " and \ with backslashes.
struct CurlQuote<'a>(&'a str);
impl std::fmt::Display for CurlQuote<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::fmt::Write;
f.write_char('"')?;
const ESCAPE_CHARS: [char; 2] = ['"', '\\'];
for substr in self.0.split_inclusive(ESCAPE_CHARS) {
// The last string returned by `split_inclusive` may or may not contain the
// search character, unfortunately.
if substr.ends_with(ESCAPE_CHARS) {
// Safe to use a byte offset rather than a character offset because the
// ESCAPE_CHARS are each 1 byte in utf8.
let (s, escape) = substr.split_at(substr.len() - 1);
f.write_str(s)?;
f.write_char('\\')?;
f.write_str(escape)?;
} else {
f.write_str(substr)?;
}
}
f.write_char('"')
}
}

Просмотреть файл

@ -0,0 +1,22 @@
/* 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 http://mozilla.org/MPL/2.0/. */
//! Process utility functions.
use crate::std::{ffi::OsStr, process::Command};
/// Return a command configured to run in the background.
///
/// This means that no associated console will be opened, when applicable.
pub fn background_command<S: AsRef<OsStr>>(program: S) -> Command {
#[allow(unused_mut)]
let mut cmd = Command::new(program);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
use windows_sys::Win32::System::Threading::CREATE_NO_WINDOW;
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd
}

Просмотреть файл

@ -0,0 +1,39 @@
/* 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/. */
//! Persistent settings of the application.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Settings {
/// Whether a crash report should be sent.
pub submit_report: bool,
/// Whether the URL that was open should be included in a sent report.
pub include_url: bool,
}
impl Default for Settings {
fn default() -> Self {
Settings {
submit_report: true,
include_url: false,
}
}
}
impl Settings {
/// Write the settings to the given writer.
pub fn to_writer<W: std::io::Write>(&self, writer: W) -> anyhow::Result<()> {
Ok(serde_json::to_writer_pretty(writer, self)?)
}
/// Read the settings from the given reader.
pub fn from_reader<R: std::io::Read>(reader: R) -> anyhow::Result<Self> {
Ok(serde_json::from_reader(reader)?)
}
#[cfg(test)]
pub fn to_string(&self) -> String {
serde_json::to_string_pretty(self).unwrap()
}
}

Просмотреть файл

@ -0,0 +1,5 @@
/* 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/. */
pub use std::*;

Просмотреть файл

@ -0,0 +1,41 @@
/* 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 http://mozilla.org/MPL/2.0/. */
//! Encapsulate thread-bound values in a safe manner.
//!
//! This allows non-`Send`/`Sync` values to be transferred across thread boundaries, checking at
//! runtime at access sites whether it is safe to use them.
pub struct ThreadBound<T> {
data: T,
origin: std::thread::ThreadId,
}
impl<T: Default> Default for ThreadBound<T> {
fn default() -> Self {
ThreadBound::new(Default::default())
}
}
impl<T> ThreadBound<T> {
pub fn new(data: T) -> Self {
ThreadBound {
data,
origin: std::thread::current().id(),
}
}
pub fn borrow(&self) -> &T {
assert!(
std::thread::current().id() == self.origin,
"unsafe access to thread-bound value"
);
&self.data
}
}
// # Safety
// Access to the inner value is only permitted on the originating thread.
unsafe impl<T> Send for ThreadBound<T> {}
unsafe impl<T> Sync for ThreadBound<T> {}

Просмотреть файл

@ -10,6 +10,14 @@
//! * a `fn invoke(&self, f: model::InvokeFn)` method which invokes the given function
//! asynchronously (without blocking) on the UI loop thread.
use crate::std::{rc::Rc, sync::Arc};
use crate::{
async_task::AsyncTask, config::Config, data, logic::ReportCrash, settings::Settings, std,
thread_bound::ThreadBound,
};
use model::{ui, Application};
use ui_impl::UI;
mod model;
#[cfg(test)]
@ -18,3 +26,241 @@ pub mod test {
pub use crate::ui::model::*;
}
}
mod ui_impl {
#[derive(Default)]
pub struct UI;
impl UI {
pub fn run_loop(&self, _app: super::model::Application) {
unimplemented!();
}
pub fn invoke(&self, _f: super::model::InvokeFn) {
unimplemented!();
}
}
}
/// Display an error dialog with the given message.
pub fn error_dialog<M: std::fmt::Display>(config: &Config, message: M) {
let close = data::Event::default();
// Config may not have localized strings
let string_or = |name, fallback: &str| {
if config.strings.is_none() {
fallback.into()
} else {
config.string(name)
}
};
let details = if config.strings.is_none() {
format!("Details: {}", message)
} else {
config
.build_string("crashreporter-error-details")
.arg("details", message.to_string())
.get()
};
let window = ui! {
Window title(string_or("crashreporter-title", "Crash Reporter")) hsize(600) vsize(400)
close_when(&close) halign(Alignment::Fill) valign(Alignment::Fill) {
VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) {
Label text(string_or(
"crashreporter-error",
"The application had a problem and crashed. \
Unfortunately, the crash reporter is unable to submit a report for the crash."
)),
Label text(details),
Button["close"] halign(Alignment::End) on_click(move || close.fire(&())) {
Label text(string_or("crashreporter-button-close", "Close"))
}
}
}
};
UI::default().run_loop(Application {
windows: vec![window],
rtl: config.is_rtl(),
});
}
#[derive(Default, Debug, PartialEq, Eq)]
pub enum SubmitState {
#[default]
Initial,
InProgress,
Success,
Failure,
}
/// The UI for the main crash reporter windows.
pub struct ReportCrashUI {
state: Arc<ThreadBound<ReportCrashUIState>>,
ui: Arc<UI>,
config: Arc<Config>,
logic: Rc<AsyncTask<ReportCrash>>,
}
/// The state of the creash UI.
pub struct ReportCrashUIState {
pub send_report: data::Synchronized<bool>,
pub include_address: data::Synchronized<bool>,
pub show_details: data::Synchronized<bool>,
pub details: data::Synchronized<String>,
pub comment: data::OnDemand<String>,
pub submit_state: data::Synchronized<SubmitState>,
pub close_window: data::Event<()>,
}
impl ReportCrashUI {
pub fn new(
initial_settings: &Settings,
config: Arc<Config>,
logic: AsyncTask<ReportCrash>,
) -> Self {
let send_report = data::Synchronized::new(initial_settings.submit_report);
let include_address = data::Synchronized::new(initial_settings.include_url);
ReportCrashUI {
state: Arc::new(ThreadBound::new(ReportCrashUIState {
send_report,
include_address,
show_details: Default::default(),
details: Default::default(),
comment: Default::default(),
submit_state: Default::default(),
close_window: Default::default(),
})),
ui: Default::default(),
config,
logic: Rc::new(logic),
}
}
pub fn async_task(&self) -> AsyncTask<ReportCrashUIState> {
let state = self.state.clone();
let ui = Arc::downgrade(&self.ui);
AsyncTask::new(move |f| {
let Some(ui) = ui.upgrade() else { return };
ui.invoke(Box::new(cc! { (state) move || {
f(state.borrow());
}}));
})
}
pub fn run(&self) {
let ReportCrashUI {
state,
ui,
config,
logic,
} = self;
let ReportCrashUIState {
send_report,
include_address,
show_details,
details,
comment,
submit_state,
close_window,
} = state.borrow();
send_report.on_change(cc! { (logic) move |v| {
let v = *v;
logic.push(move |s| s.settings.borrow_mut().submit_report = v);
}});
include_address.on_change(cc! { (logic) move |v| {
let v = *v;
logic.push(move |s| s.settings.borrow_mut().include_url = v);
}});
let input_enabled = submit_state.mapped(|s| s == &SubmitState::Initial);
let submit_status_text = submit_state.mapped(cc! { (config) move |s| {
config.string(match s {
SubmitState::Initial => "crashreporter-submit-status",
SubmitState::InProgress => "crashreporter-submit-in-progress",
SubmitState::Success => "crashreporter-submit-success",
SubmitState::Failure => "crashreporter-submit-failure",
})
}});
let progress_visible = submit_state.mapped(|s| s == &SubmitState::InProgress);
let details_window = ui! {
Window["crash-details-window"] title(config.string("crashreporter-view-report-title"))
visible(show_details) modal(true) hsize(600) vsize(400)
halign(Alignment::Fill) valign(Alignment::Fill)
{
VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) {
Scroll halign(Alignment::Fill) valign(Alignment::Fill) {
TextBox["details-text"] content(details) halign(Alignment::Fill) valign(Alignment::Fill)
},
Button["close-details"] halign(Alignment::End) on_click(cc! { (show_details) move || *show_details.borrow_mut() = false }) {
Label text(config.string("crashreporter-button-ok"))
}
}
}
};
let main_window = ui! {
Window title(config.string("crashreporter-title")) hsize(600) vsize(400)
halign(Alignment::Fill) valign(Alignment::Fill) close_when(close_window)
child_window(details_window)
{
VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) {
Label text(config.string("crashreporter-crash-message")),
Label text(config.string("crashreporter-plea")),
Checkbox["send"] checked(send_report) label(config.string("crashreporter-send-report"))
enabled(&input_enabled),
VBox margin_start(20) visible(send_report) spacing(5)
halign(Alignment::Fill) valign(Alignment::Fill) {
Button["details"] enabled(&input_enabled) on_click(cc! { (config, details, show_details, logic) move || {
// Immediately display the window to feel responsive, even if forming
// the details string takes a little while (it really shouldn't
// though).
*details.borrow_mut() = config.string("crashreporter-loading-details");
logic.push(|s| s.update_details());
*show_details.borrow_mut() = true;
}})
{
Label text(config.string("crashreporter-button-details"))
},
Scroll halign(Alignment::Fill) valign(Alignment::Fill) {
TextBox["comment"] placeholder(config.string("crashreporter-comment-prompt"))
content(comment)
editable(true)
enabled(&input_enabled)
halign(Alignment::Fill) valign(Alignment::Fill)
},
Checkbox["include-url"] checked(include_address)
label(config.string("crashreporter-include-url")) enabled(&input_enabled),
Label text(&submit_status_text) margin_top(20),
Progress halign(Alignment::Fill) visible(&progress_visible),
},
HBox valign(Alignment::End) halign(Alignment::End) spacing(10)
{
Button["restart"] visible(config.restart_command.is_some())
on_click(cc! { (logic) move || logic.push(|s| s.restart()) })
enabled(&input_enabled)
{
Label text(config.string("crashreporter-button-restart"))
},
Button["quit"] on_click(cc! { (logic) move || logic.push(|s| s.quit()) })
enabled(&input_enabled)
{
Label text(config.string("crashreporter-button-quit"))
}
}
}
}
};
ui.run_loop(Application {
windows: vec![main_window],
rtl: config.is_rtl(),
});
}
}

Просмотреть файл

@ -0,0 +1,64 @@
# 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 http://mozilla.org/MPL/2.0/.
crashreporter-title = Crash Reporter
crashreporter-crash-message = { -brand-short-name } had a problem and crashed.
crashreporter-plea = To help us diagnose and fix the problem, you can send us a crash report.
crashreporter-information = This application is run after a crash to report the problem to { -vendor-short-name }. It should not be run directly.
crashreporter-error = { -brand-short-name } had a problem and crashed. Unfortunately, the crash reporter is unable to submit a report for this crash.
# $details (String) - the reason that a crash report cannot be submitted
crashreporter-error-details = Details: { $details }
crashreporter-no-run-message = This application is run after a crash to report the problem to the application vendor. It should not be run directly.
crashreporter-button-details = Details…
crashreporter-loading-details = Loading…
crashreporter-view-report-title = Report Contents
crashreporter-comment-prompt = Add a comment (comments are publicly visible)
crashreporter-report-info = This report also contains technical information about the state of the application when it crashed.
crashreporter-send-report = Tell { -vendor-short-name } about this crash so they can fix it.
crashreporter-include-url = Include the address of the page I was on.
crashreporter-submit-status = Your crash report will be submitted before you quit or restart.
crashreporter-submit-in-progress = Submitting your report…
crashreporter-submit-success = Report submitted successfully!
crashreporter-submit-failure = There was a problem submitting your report.
crashreporter-resubmit-status = Resending reports that previously failed to send…
crashreporter-button-quit = Quit { -brand-short-name }
crashreporter-button-restart = Restart { -brand-short-name }
crashreporter-button-ok = OK
crashreporter-button-close = Close
# $id (String) - the crash id from the server, typically a UUID
crashreporter-crash-identifier = Crash ID: { $id }
# $url (String) - the url which the user can use to view the submitted crash report
crashreporter-crash-details = You can view details of this crash at { $url }.
# Error strings
crashreporter-error-minidump-analyzer = Failed to run minidump-analyzer
# $path (String) - the file path
crashreporter-error-opening-file = Failed to open file ({ $path })
# $path (String) - the file path
crashreporter-error-loading-file = Failed to load file ({ $path })
# $path (String) - the path
crashreporter-error-creating-dir = Failed to create directory ({ $path })
crashreporter-error-no-home-dir = Missing home directory
# $from (String) - the source path
# $to (String) - the destination path
crashreporter-error-moving-path = Failed to move { $from } to { $to }
crashreporter-error-version-eol = Version end of life: crash reports are no longer accepted.