Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! The crashping crate allows populating and sending the crash ping, which contains all
//! ping-scoped crash annotations.
pub use glean::net;
pub use glean::ClientInfoMetrics;
use glean::{Configuration, ConfigurationBuilder};
use std::path::PathBuf;
mod glean_metrics {
// Env variable set to the file generated by glean_rust.py (by build.rs).
include!(env!("GLEAN_METRICS_FILE"));
}
mod annotations;
mod single_instance;
use annotations::ANNOTATIONS;
const TELEMETRY_SERVER: &str = "https://incoming.telemetry.mozilla.org";
/// Initialize the Glean ping. This must be called _before_ Glean initialization.
///
/// Since Glean v63.0.0, custom pings are required to be instantiated prior to Glean init
/// in order to ensure they are enabled and able to collect data. This is due to the data
/// collection state being determined at the ping level now instead of just by the global
/// Glean collection enabled flag. See Bug 1934931 for more information.
pub fn init() {
_ = &*glean_metrics::crash;
}
/// Initialize Glean and the Glean ping.
///
/// If using this type, you do not need to call `init()`. This is intended for use in runtimes that
/// are only using Glean to send the crash ping.
///
/// You should be sure to set an `uploader` on the `configuration` before initializing. It is
/// recommended to set the `ClientInfoMetrics` fields to more useful values as well.
pub struct InitGlean {
pub configuration: Configuration,
pub client_info_metrics: ClientInfoMetrics,
pub clear_uploader_for_tests: bool,
}
/// A handle taking ownership of the Glean store.
pub struct GleanHandle {
single_instance: single_instance::SingleInstance,
}
impl GleanHandle {
/// Own the Glean store for the lifetime of the application.
pub fn application_lifetime(self) {
self.single_instance.retain_until_application_exit();
}
}
impl InitGlean {
/// The data_dir should be a dedicated directory for use by Glean.
pub fn new(data_dir: PathBuf, app_id: &str, client_info_metrics: ClientInfoMetrics) -> Self {
InitGlean {
configuration: ConfigurationBuilder::new(true, data_dir, app_id)
.with_server_endpoint(TELEMETRY_SERVER)
.with_use_core_mps(false)
.with_internal_pings(false)
.build(),
client_info_metrics,
clear_uploader_for_tests: true,
}
}
pub fn initialize(self) -> std::io::Result<GleanHandle> {
self.init_with(glean::initialize)
}
/// Initialize using glean::test_reset_glean.
///
/// This will not do any process locking of the Glean store.
pub fn test_reset_glean(self, clear_stores: bool) {
self.init_with_no_lock(move |c, m| glean::test_reset_glean(c, m, clear_stores))
}
/// Initialize with the given function.
///
/// This will take exclusive ownership of the Glean store for the current process (potentially
/// blocking). The returned GleanHandle tracks ownership.
pub fn init_with<F: FnOnce(Configuration, ClientInfoMetrics)>(
self,
f: F,
) -> std::io::Result<GleanHandle> {
std::fs::create_dir_all(&self.configuration.data_path)?;
let handle = GleanHandle {
single_instance: single_instance::SingleInstance::acquire(
&self.configuration.data_path.join("crashping.pid"),
)?,
};
self.init_with_no_lock(f);
Ok(handle)
}
pub fn init_with_no_lock<F: FnOnce(Configuration, ClientInfoMetrics)>(mut self, f: F) {
// Clear the uploader for tests, if configured.
// No need to check `cfg!(test)`, since we don't set an uploader in unit tests (and if we
// did, it would be test-specific).
let is_test = std::env::var_os("XPCSHELL_TEST_PROFILE_DIR").is_some()
|| std::env::var_os("MOZ_AUTOMATION").is_some();
if self.clear_uploader_for_tests && is_test {
self.configuration.uploader = None;
self.configuration.server_endpoint = None;
}
init();
f(self.configuration, self.client_info_metrics);
}
}
/// Send the Glean crash ping.
pub fn send(annotations: &serde_json::Value, reason: Option<&str>) -> anyhow::Result<()> {
// The crash.time metric may be overwritten if a CrashTime annotation is present.
glean_metrics::crash::time.set(None);
set_metrics_from_annotations(annotations)?;
log::debug!("submitting Glean crash ping");
glean_metrics::crash.submit(reason);
Ok(())
}
/// **Test-only API**
///
/// Register a callback that will be called before the next ping is sent.
pub fn test_before_next_send<F: FnOnce(Option<&str>) + Send + 'static>(cb: F) {
glean_metrics::crash.test_before_next_submit(cb);
}
/// **Test-only API**
///
/// Get all metric values as a JSON object.
pub fn test_get_metric_values() -> serde_json::Value {
let mut ret: serde_json::Map<String, serde_json::Value> = Default::default();
for annotation in ANNOTATIONS {
if let Some(value) = (annotation.test_get_glean_value)() {
ret.insert(annotation.glean_key.into(), value);
}
}
ret.into()
}
/// Set Glean metrics from the given annotations.
fn set_metrics_from_annotations(annotations: &serde_json::Value) -> anyhow::Result<()> {
for annotation in ANNOTATIONS {
if let Some(value) = annotations.get(annotation.key) {
(annotation.set_glean_metric)(value)?;
}
}
Ok(())
}
#[cfg(test)]
mod test {
use super::{send, test_before_next_send, ANNOTATIONS};
use std::sync::{
atomic::{AtomicBool, Ordering::Relaxed},
Arc, Mutex,
};
static TEST_LOCK: Mutex<()> = Mutex::new(());
#[must_use]
fn test_init_glean() -> std::sync::MutexGuard<'static, ()> {
let guard = TEST_LOCK.lock().unwrap();
let glean_path = std::env::temp_dir().join("crashping_glean");
super::InitGlean::new(
glean_path,
"crashping.test",
super::ClientInfoMetrics::unknown(),
)
.test_reset_glean(true);
return guard;
}
/// Run a test that uses Glean.
///
/// This function ensures that Glean tests run sequentially.
fn glean_test<F: FnOnce() + std::panic::UnwindSafe>(f: F) {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Debug)
.is_test(true)
.try_init();
let res = {
let _guard = test_init_glean();
// Catch panics so that we don't poison the mutex (so other tests can run).
std::panic::catch_unwind(f)
};
if let Err(e) = res {
std::panic::resume_unwind(e);
}
}
#[derive(Clone)]
struct SoftAssert {
failed: Arc<AtomicBool>,
message: &'static str,
}
impl SoftAssert {
fn new(failed: bool, message: &'static str) -> Self {
SoftAssert {
failed: Arc::new(AtomicBool::new(failed)),
message,
}
}
fn assert<T: std::fmt::Display>(&self, value: bool, msg: T) {
if !value {
eprintln!("{}", msg);
self.failed.store(true, Relaxed);
}
}
fn clear(&self) {
self.failed.store(false, Relaxed);
}
}
impl Drop for SoftAssert {
fn drop(&mut self) {
assert!(!self.failed.load(Relaxed), "{}", self.message);
}
}
#[test]
fn all_annotations() {
glean_test(|| {
// Set test values here if the default values aren't sufficient nor applicable.
let mut annotations = serde_json::json!({
"AsyncShutdownTimeout": "{\"phase\":\"abcd\",\"conditions\":[{\"foo\":\"bar\"}],\"brokenAddBlockers\":[\"foo\"]}",
"BlockedDllList": "Foo.dll;bar.dll;rawr.dll",
"CrashTime": "1234",
"LastInteractionDuration": "100",
"NimbusEnrollments": "a,b,c,d,e",
"QuotaManagerShutdownTimeout": "line1\nline2\nline3",
"JavaException": r#"{
"exception": {
"values": [{
"stacktrace": {
"value": "something went wrong",
"module": "foo.bar",
"type": "FooBarType",
"frames": [{
"module": "org.mozilla",
"function": "FooBar",
"in_app": true,
"lineno": 42,
"file": "FooBar.java"
}]
}
}]
}
}"#,
"SecondsSinceLastCrash": "50000",
"StackTraces": {
"status": "OK",
// Add extraneous field to ensure it doesn't affect setting the metric
"foobar": "baz",
"crash_info": {
"type": "bad crash",
"address": "0xcafe",
"crashing_thread": 1
},
"main_module": 0,
"modules": [{
"base_addr": "0xcafe",
"end_addr": "0xf000",
"code_id": "CODEID",
"debug_file": "debug_file.so",
"debug_id": "DEBUGID",
"filename": "file.so",
"version": "1.0.0"
}],
"threads": [
{"frames": [
{
"ip": "0xf00",
"trust": "crash",
"module_index": 0
}
]},
{"frames": [
{
"ip": "0x0",
"trust": "crash",
"module_index": 0
},
{
"ip": "0xbadf00d",
"trust": "cfi",
"module_index": 0
}
]}
]
},
"UptimeTS": "400.5",
"UtilityActorsName": "abc,def",
"WindowsFileDialogErrorCode": "40",
});
// For convenience, automatically populate example values for the simple cases.
for annotation in ANNOTATIONS {
if !annotations
.as_object()
.unwrap()
.contains_key(annotation.key)
{
let default_val: Option<serde_json::Value> = match annotation.convert_fn {
"convert_boolean_to_boolean" => Some("1".into()),
"convert_string_to_string" => Some("some_string".into()),
"convert_u64_to_quantity" => Some("42".into()),
_ => None,
};
if let Some(val) = default_val {
annotations
.as_object_mut()
.unwrap()
.insert(annotation.key.to_owned(), val);
}
}
}
let success = SoftAssert::new(false, "one or more failures occurred");
// Ensure all annotations have a test value.
for annotation in ANNOTATIONS {
success.assert(
annotations
.as_object()
.unwrap()
.contains_key(annotation.key),
format!("{} test value is not set", annotation.key),
);
}
// Ensure all metrics are set.
let metrics_tested = SoftAssert::new(true, "test_before_next_send did not run");
{
let success = success.clone();
let metrics_tested = metrics_tested.clone();
test_before_next_send(move |_| {
for annotation in ANNOTATIONS {
success.assert(
(annotation.test_get_glean_value)().is_some(),
format!("{} not set", annotation.glean_key),
);
}
metrics_tested.clear();
});
}
send(&annotations, Some("crash")).expect("failed to set metrics");
});
}
fn test_annotation(
annotation: &'static crate::annotations::Annotation,
value: serde_json::Value,
expected: serde_json::Value,
) {
glean_test(|| {
let annotations = serde_json::json!({annotation.key: value});
let sent = SoftAssert::new(true, "test_before_next_send did not run");
let check = SoftAssert::new(false, "annotation check failed");
let sent_inner = sent.clone();
let check_inner = check.clone();
let input_str = annotations.to_string();
test_before_next_send(move |_| {
sent_inner.clear();
// Use a SoftAssert rather than `assert_eq!` so that we don't panic in the callback
// (which will poison the mutex that Glean uses, making other tests fail
// unnecessarily).
let actual = (annotation.test_get_glean_value)();
if let Some(actual) = actual {
check_inner.assert(actual == expected, "value mismatch");
} else {
check_inner.assert(
false,
format!(
"missing value for {} with input {}",
annotation.glean_key, input_str,
),
);
}
});
send(&annotations, Some("crash")).expect("failed to set metrics");
});
}
macro_rules! test_annotations {
( ) => {};
( $test_name:ident ($annotation:ident) { $($value:tt => $expected:tt),+ } $($rest:tt)* ) => {
#[test]
fn $test_name() {
$(test_annotation(&crate::annotations::$annotation, serde_json::json!($value), serde_json::json!($expected));)*
}
test_annotations! { $($rest)* }
};
}
test_annotations! {
test_async_shutdown_timeout(AsyncShutdownTimeout) {
r#"{"phase":"AddonManager: Waiting to start provider shutdown.","conditions":[{"name":"AddonRepository Background Updater","state":"(none)","filename":"resource://gre/modules/addons/AddonRepository.sys.mjs","lineNumber":576,"stack":["resource://gre/modules/addons/AddonRepository.sys.mjs:backgroundUpdateCheck:576","resource://gre/modules/AddonManager.sys.mjs:backgroundUpdateCheck/buPromise<:1269"]}],"brokenAddBlockers":["JSON store: writing data for 'creditcards' - IOUtils: waiting for profileBeforeChange IO to complete finished","StorageSyncService: shutdown - profile-change-teardown finished"]}"#
=> {
"phase": "AddonManager: Waiting to start provider shutdown.",
"conditions": serde_json::json!([
{
"name": "AddonRepository Background Updater",
"state": "(none)",
"filename": "resource://gre/modules/addons/AddonRepository.sys.mjs",
"lineNumber": 576,
"stack": [
]
}
]).to_string(),
"broken_add_blockers": [
"JSON store: writing data for 'creditcards' - IOUtils: waiting for profileBeforeChange IO to complete finished",
"StorageSyncService: shutdown - profile-change-teardown finished"
]
}
}
test_blocked_dll_list(BlockedDllList) {
"Foo.dll;bar.dll;rawr.dll" => ["Foo.dll", "bar.dll", "rawr.dll"]
}
test_java_exception(JavaException) {
r#"{
"exception": {
"values": [{
"stacktrace": {
"value": "something went wrong",
"module": "foo.bar",
"type": "FooBarType",
"frames": [{
"module": "org.mozilla",
"function": "FooBar",
"in_app": true,
"lineno": 42,
"filename": "FooBar.java"
}]
}
}]
}
}"#
=> {
"throwables": [
{
"message": "something went wrong",
"type_name": "foo.bar.FooBarType",
"stack": [
{
"class_name": "org.mozilla",
"method_name": "FooBar",
"file": "FooBar.java",
"is_native": false,
"line": 42
}
]
}
]
}
}
test_nimbus_enrollments(NimbusEnrollments) {
"foo:control,bar:treatment-a" => ["foo:control", "bar:treatment-a"]
}
test_quota_manager_shutdown_timeout(QuotaManagerShutdownTimeout) {
"foo\nbar\nbaz" => ["foo", "bar", "baz"]
}
test_stack_traces(StackTraces) {
{
"status": "OK",
"crash_info": {
"type": "main",
"address": "0xf001ba11",
"crashing_thread": 1
},
"main_module": 0,
"modules": [
{
"base_addr": "0x00000000",
"end_addr": "0x00004000",
"code_id": "8675309",
"debug_file": "",
"debug_id": "18675309",
"filename": "foo.exe",
"version": "1.0.0"
},
{
"base_addr": "0x00004000",
"end_addr": "0x00008000",
"code_id": "42",
"debug_file": "foo.pdb",
"debug_id": "43",
"filename": "foo.dll",
"version": "1.1.0"
}
],
"threads": [
{
"frames": [
{ "module_index": 0, "ip": "0x10", "trust": "context" },
{ "module_index": 0, "ip": "0x20", "trust": "cfi" }
]
},
{
"frames": [
{ "module_index": 1, "ip": "0x4010", "trust": "context" },
{ "module_index": 0, "ip": "0x30", "trust": "cfi" }
]
}
]
}
=> {
"crash_type": "main",
"crash_address": "0xf001ba11",
"crash_thread": 1,
"main_module": 0,
"modules": [
{
"base_address": "0x00000000",
"end_address": "0x00004000",
"code_id": "8675309",
"debug_file": "",
"debug_id": "18675309",
"filename": "foo.exe",
"version": "1.0.0",
},
{
"base_address": "0x00004000",
"end_address": "0x00008000",
"code_id": "42",
"debug_file": "foo.pdb",
"debug_id": "43",
"filename": "foo.dll",
"version": "1.1.0",
},
],
"threads": [
{
"frames": [
{ "module_index": 0, "ip": "0x10", "trust": "context" },
{ "module_index": 0, "ip": "0x20", "trust": "cfi" },
],
},
{
"frames": [
{ "module_index": 1, "ip": "0x4010", "trust": "context" },
{ "module_index": 0, "ip": "0x30", "trust": "cfi" },
],
},
],
}
}
test_utility_actors_name(UtilityActorsName) {
"audio-decoder-generic,js-oracle" => ["audio-decoder-generic","js-oracle"]
}
}
}