зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
f3ea01d520
Коммит
acc91ba868
|
@ -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.
|
Загрузка…
Ссылка в новой задаче