/* 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 { 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( self, f: F, ) -> std::io::Result { 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(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) + 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 = 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: 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, message: &'static str, } impl SoftAssert { fn new(failed: bool, message: &'static str) -> Self { SoftAssert { failed: Arc::new(AtomicBool::new(failed)), message, } } fn assert(&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 = 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": [ "resource://gre/modules/addons/AddonRepository.sys.mjs:backgroundUpdateCheck:576", "resource://gre/modules/AddonManager.sys.mjs:backgroundUpdateCheck/buPromise<:1269", ] } ]).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"] } } }