From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- toolkit/components/glean/src/init/mod.rs | 368 +++++++++++++++++++++ toolkit/components/glean/src/init/upload_pref.rs | 99 ++++++ toolkit/components/glean/src/init/user_activity.rs | 129 ++++++++ .../components/glean/src/init/viaduct_uploader.rs | 73 ++++ toolkit/components/glean/src/lib.rs | 210 ++++++++++++ 5 files changed, 879 insertions(+) create mode 100644 toolkit/components/glean/src/init/mod.rs create mode 100644 toolkit/components/glean/src/init/upload_pref.rs create mode 100644 toolkit/components/glean/src/init/user_activity.rs create mode 100644 toolkit/components/glean/src/init/viaduct_uploader.rs create mode 100644 toolkit/components/glean/src/lib.rs (limited to 'toolkit/components/glean/src') diff --git a/toolkit/components/glean/src/init/mod.rs b/toolkit/components/glean/src/init/mod.rs new file mode 100644 index 0000000000..f430cd7384 --- /dev/null +++ b/toolkit/components/glean/src/init/mod.rs @@ -0,0 +1,368 @@ +// 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 std::env; +use std::ffi::CString; +use std::ops::DerefMut; +use std::path::PathBuf; + +use firefox_on_glean::{metrics, pings}; +use nserror::{nsresult, NS_ERROR_FAILURE}; +use nsstring::{nsACString, nsCString, nsString}; +use xpcom::interfaces::{ + mozILocaleService, nsIFile, nsIPrefService, nsIProperties, nsIXULAppInfo, nsIXULRuntime, +}; +use xpcom::{RefPtr, XpCom}; + +use glean::{ClientInfoMetrics, Configuration}; + +#[cfg(not(target_os = "android"))] +mod upload_pref; +#[cfg(not(target_os = "android"))] +mod user_activity; +mod viaduct_uploader; + +#[cfg(not(target_os = "android"))] +use upload_pref::UploadPrefObserver; +#[cfg(not(target_os = "android"))] +use user_activity::UserActivityObserver; +use viaduct_uploader::ViaductUploader; + +/// Project FOG's entry point. +/// +/// This assembles client information and the Glean configuration and then initializes the global +/// Glean instance. +#[cfg(not(target_os = "android"))] +#[no_mangle] +pub extern "C" fn fog_init( + data_path_override: &nsACString, + app_id_override: &nsACString, +) -> nsresult { + let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled"); + let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0; + let uploader = Some(Box::new(ViaductUploader) as Box); + + fog_init_internal( + data_path_override, + app_id_override, + upload_enabled || recording_enabled, + uploader, + ) + .into() +} + +/// Project FOG's entry point on Android. +/// +/// This assembles client information and the Glean configuration and then initializes the global +/// Glean instance. +/// It always enables upload and set no uploader. +/// This should only be called in test scenarios. +/// In normal use Glean should be initialized and controlled by the Glean Kotlin SDK. +#[cfg(target_os = "android")] +#[no_mangle] +pub extern "C" fn fog_init( + data_path_override: &nsACString, + app_id_override: &nsACString, +) -> nsresult { + // On Android always enable Glean upload. + let upload_enabled = true; + // Don't set up an uploader. + let uploader = None; + + fog_init_internal( + data_path_override, + app_id_override, + upload_enabled, + uploader, + ) + .into() +} + +fn fog_init_internal( + data_path_override: &nsACString, + app_id_override: &nsACString, + upload_enabled: bool, + uploader: Option>, +) -> Result<(), nsresult> { + metrics::fog::initialization.start(); + + log::debug!("Initializing FOG."); + + setup_observers()?; + + let (mut conf, client_info) = build_configuration(data_path_override, app_id_override)?; + + conf.upload_enabled = upload_enabled; + conf.uploader = uploader; + + // If we're operating in automation without any specific source tags to set, + // set the tag "automation" so any pings that escape don't clutter the tables. + // See https://mozilla.github.io/glean/book/user/debugging/index.html#enabling-debugging-features-through-environment-variables + if env::var("MOZ_AUTOMATION").is_ok() && env::var("GLEAN_SOURCE_TAGS").is_err() { + log::info!("In automation, setting 'automation' source tag."); + glean::set_source_tags(vec!["automation".to_string()]); + log::info!("In automation, disabling MPS to avoid 4am issues."); + conf.use_core_mps = false; + } + + log::debug!("Configuration: {:#?}", conf); + + // Register all custom pings before we initialize. + pings::register_pings(Some(&conf.application_id)); + + glean::initialize(conf, client_info); + + metrics::fog::initialization.stop(); + + Ok(()) +} + +fn build_configuration( + data_path_override: &nsACString, + app_id_override: &nsACString, +) -> Result<(Configuration, ClientInfoMetrics), nsresult> { + let data_path_str = if data_path_override.is_empty() { + get_data_path()? + } else { + data_path_override.to_utf8().to_string() + }; + let data_path = PathBuf::from(&data_path_str); + + let (app_build, app_display_version, channel, locale) = get_app_info()?; + + let client_info = ClientInfoMetrics { + app_build, + app_display_version, + channel: Some(channel), + locale: Some(locale), + }; + log::debug!("Client Info: {:#?}", client_info); + + const SERVER: &str = "https://incoming.telemetry.mozilla.org"; + let localhost_port = static_prefs::pref!("telemetry.fog.test.localhost_port"); + let server = if localhost_port > 0 { + format!("http://localhost:{}", localhost_port) + } else { + String::from(SERVER) + }; + + let application_id = if app_id_override.is_empty() { + "firefox.desktop".to_string() + } else { + app_id_override.to_utf8().to_string() + }; + + extern "C" { + fn FOG_MaxPingLimit() -> u32; + fn FOG_EventTimestampsEnabled() -> bool; + } + + // SAFETY NOTE: Safe because it returns a primitive by value. + let pings_per_interval = unsafe { FOG_MaxPingLimit() }; + metrics::fog::max_pings_per_minute.set(pings_per_interval.into()); + + // SAFETY NOTE: Safe because it returns a primitive by value. + let enable_event_timestamps = unsafe { FOG_EventTimestampsEnabled() }; + + let rate_limit = Some(glean::PingRateLimit { + seconds_per_interval: 60, + pings_per_interval, + }); + + let configuration = Configuration { + upload_enabled: false, + data_path, + application_id, + max_events: None, + delay_ping_lifetime_io: true, + server_endpoint: Some(server), + uploader: None, + use_core_mps: true, + trim_data_to_registered_pings: true, + log_level: None, + rate_limit, + enable_event_timestamps, + experimentation_id: None, + }; + + Ok((configuration, client_info)) +} + +#[cfg(not(target_os = "android"))] +fn setup_observers() -> Result<(), nsresult> { + if let Err(e) = UploadPrefObserver::begin_observing() { + log::error!( + "Could not observe data upload pref. Abandoning FOG init due to {:?}", + e + ); + return Err(e); + } + + if let Err(e) = UserActivityObserver::begin_observing() { + log::error!( + "Could not observe user activity. Abandoning FOG init due to {:?}", + e + ); + return Err(e); + } + + Ok(()) +} + +#[cfg(target_os = "android")] +fn setup_observers() -> Result<(), nsresult> { + // No observers are set up on Android. + Ok(()) +} + +/// Construct and return the data_path from the profile dir, or return an error. +fn get_data_path() -> Result { + let dir_svc: RefPtr = match xpcom::components::Directory::service() { + Ok(ds) => ds, + _ => return Err(NS_ERROR_FAILURE), + }; + let mut profile_dir = xpcom::GetterAddrefs::::new(); + unsafe { + dir_svc + .Get( + cstr!("ProfD").as_ptr(), + &nsIFile::IID, + profile_dir.void_ptr(), + ) + .to_result()?; + } + let profile_dir = profile_dir.refptr().ok_or(NS_ERROR_FAILURE)?; + let mut profile_path = nsString::new(); + unsafe { + (*profile_dir).GetPath(&mut *profile_path).to_result()?; + } + let profile_path = String::from_utf16(&profile_path[..]).map_err(|_| NS_ERROR_FAILURE)?; + let data_path = profile_path + "/datareporting/glean"; + Ok(data_path) +} + +/// Return a tuple of the build_id, app version, build channel, and locale. +/// If the XUL Runtime isn't a XULAppInfo (e.g. in xpcshell), +/// build_id ad app_version will be "unknown". +/// Other problems result in an error being returned instead. +fn get_app_info() -> Result<(String, String, String, String), nsresult> { + let xul: RefPtr = + xpcom::components::XULRuntime::service().map_err(|_| NS_ERROR_FAILURE)?; + + let pref_service: RefPtr = + xpcom::components::Preferences::service().map_err(|_| NS_ERROR_FAILURE)?; + let locale_service: RefPtr = + xpcom::components::Locale::service().map_err(|_| NS_ERROR_FAILURE)?; + let branch = xpcom::getter_addrefs(|p| { + // Safe because: + // * `null` is explicitly allowed per documentation + // * `p` is a valid outparam guaranteed by `getter_addrefs` + unsafe { pref_service.GetDefaultBranch(std::ptr::null(), p) } + })?; + let pref_name = CString::new("app.update.channel").map_err(|_| NS_ERROR_FAILURE)?; + let mut channel = nsCString::new(); + // Safe because: + // * `branch` is non-null (otherwise `getter_addrefs` would've been `Err` + // * `pref_name` exists so a pointer to it is valid for the life of the function + // * `channel` exists so a pointer to it is valid, and it can be written to + unsafe { + if (*branch) + .GetCharPref(pref_name.as_ptr(), channel.deref_mut() as *mut nsACString) + .to_result() + .is_err() + { + channel = "unknown".into(); + } + } + + let app_info = match xul.query_interface::() { + Some(ai) => ai, + // In e.g. xpcshell the XULRuntime isn't XULAppInfo. + // We still want to return sensible values so tests don't explode. + _ => { + return Ok(( + "unknown".to_owned(), + "unknown".to_owned(), + channel.to_string(), + "unknown".to_owned(), + )) + } + }; + + let mut build_id = nsCString::new(); + unsafe { + app_info.GetAppBuildID(&mut *build_id).to_result()?; + } + + let mut version = nsCString::new(); + unsafe { + app_info.GetVersion(&mut *version).to_result()?; + } + + let mut locale = nsCString::new(); + unsafe { + locale_service + .GetAppLocaleAsBCP47(&mut *locale) + .to_result()?; + } + + Ok(( + build_id.to_string(), + version.to_string(), + channel.to_string(), + locale.to_string(), + )) +} + +/// **TEST-ONLY METHOD** +/// Resets FOG and the underlying Glean SDK, clearing stores. +#[no_mangle] +pub extern "C" fn fog_test_reset( + data_path_override: &nsACString, + app_id_override: &nsACString, +) -> nsresult { + fog_test_reset_internal(data_path_override, app_id_override).into() +} + +// Split out into its own function so I could use `?` +#[cfg(not(target_os = "android"))] +fn fog_test_reset_internal( + data_path_override: &nsACString, + app_id_override: &nsACString, +) -> Result<(), nsresult> { + let (mut conf, client_info) = build_configuration(data_path_override, app_id_override)?; + + let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled"); + let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0; + conf.upload_enabled = upload_enabled || recording_enabled; + + // Don't accidentally send "main" pings during tests. + conf.use_core_mps = false; + + // I'd prefer to reuse the uploader, but it gets moved into Glean so we build anew. + conf.uploader = Some(Box::new(ViaductUploader) as Box); + + glean::test_reset_glean(conf, client_info, true); + Ok(()) +} + +#[cfg(target_os = "android")] +fn fog_test_reset_internal( + data_path_override: &nsACString, + app_id_override: &nsACString, +) -> Result<(), nsresult> { + let (mut conf, client_info) = build_configuration(data_path_override, app_id_override)?; + + // On Android always enable Glean upload. + conf.upload_enabled = true; + + // Don't accidentally send "main" pings during tests. + conf.use_core_mps = false; + + // Same as before, would prefer to reuse, but it gets moved into Glean so we build anew. + conf.uploader = Some(Box::new(ViaductUploader) as Box); + + glean::test_reset_glean(conf, client_info, true); + Ok(()) +} diff --git a/toolkit/components/glean/src/init/upload_pref.rs b/toolkit/components/glean/src/init/upload_pref.rs new file mode 100644 index 0000000000..737230c16c --- /dev/null +++ b/toolkit/components/glean/src/init/upload_pref.rs @@ -0,0 +1,99 @@ +// 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 std::ffi::CStr; +use std::os::raw::c_char; +use std::sync::atomic::{AtomicBool, Ordering}; + +use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK}; +use nsstring::{nsACString, nsCStr}; +use xpcom::{ + interfaces::{nsIPrefBranch, nsISupports}, + RefPtr, +}; + +/// Whether the current value of the localhost testing pref is permitting +/// metric recording (even if upload is disabled). +static RECORDING_ENABLED: AtomicBool = AtomicBool::new(false); + +// Partially cargo-culted from https://searchfox.org/mozilla-central/rev/598e50d2c3cd81cd616654f16af811adceb08f9f/security/manager/ssl/cert_storage/src/lib.rs#1192 +#[xpcom(implement(nsIObserver), atomic)] +pub(crate) struct UploadPrefObserver {} + +#[allow(non_snake_case)] +impl UploadPrefObserver { + pub(crate) fn begin_observing() -> Result<(), nsresult> { + // Ensure we begin with the correct current value of RECORDING_ENABLED. + let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0; + RECORDING_ENABLED.store(recording_enabled, Ordering::SeqCst); + + // SAFETY: Everything here is self-contained. + // + // * We allocate the pref observer, created by the xpcom macro + // * We query the pref service and bail out if it doesn't exist. + // * We create a nsCStr from a static string. + // * We control all input to `AddObserverImpl` + unsafe { + let pref_obs = Self::allocate(InitUploadPrefObserver {}); + let pref_branch: RefPtr = + xpcom::components::Preferences::service().map_err(|_| NS_ERROR_FAILURE)?; + let pref_nscstr = + &nsCStr::from("datareporting.healthreport.uploadEnabled") as &nsACString; + (*pref_branch) + .AddObserverImpl(pref_nscstr, pref_obs.coerce(), false) + .to_result()?; + let pref_nscstr = &nsCStr::from("telemetry.fog.test.localhost_port") as &nsACString; + (*pref_branch) + .AddObserverImpl(pref_nscstr, pref_obs.coerce(), false) + .to_result()?; + } + + Ok(()) + } + + unsafe fn Observe( + &self, + _subject: *const nsISupports, + topic: *const c_char, + pref_name: *const u16, + ) -> nserror::nsresult { + let topic = CStr::from_ptr(topic).to_str().unwrap(); + // Conversion utf16 to utf8 is messy. + // We should only ever observe changes to one of the two prefs we want, + // but just to be on the safe side let's assert. + + // cargo-culted from https://searchfox.org/mozilla-central/rev/598e50d2c3cd81cd616654f16af811adceb08f9f/security/manager/ssl/cert_storage/src/lib.rs#1606-1612 + // (with a little transformation) + let len = (0..).take_while(|&i| *pref_name.offset(i) != 0).count(); // find NUL. + let slice = std::slice::from_raw_parts(pref_name, len); + let pref_name = match String::from_utf16(slice) { + Ok(name) => name, + Err(_) => return NS_ERROR_FAILURE, + }; + log::info!("Observed {:?}, {:?}", topic, pref_name); + debug_assert!(topic == "nsPref:changed"); + debug_assert!( + pref_name == "datareporting.healthreport.uploadEnabled" + || pref_name == "telemetry.fog.test.localhost_port" + ); + + let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled"); + let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0; + log::info!( + "New upload_enabled {}, recording_enabled {}", + upload_enabled, + recording_enabled + ); + if RECORDING_ENABLED.load(Ordering::SeqCst) && !recording_enabled { + // Whenever the test pref goes from permitting recording to forbidding it, + // ensure Glean is told to wipe the stores. + // This may send a "deletion-request" ping for a client_id that's never sent + // any other pings. + glean::set_upload_enabled(false); + } + RECORDING_ENABLED.store(recording_enabled, Ordering::SeqCst); + glean::set_upload_enabled(upload_enabled || recording_enabled); + NS_OK + } +} diff --git a/toolkit/components/glean/src/init/user_activity.rs b/toolkit/components/glean/src/init/user_activity.rs new file mode 100644 index 0000000000..dbafeca4e8 --- /dev/null +++ b/toolkit/components/glean/src/init/user_activity.rs @@ -0,0 +1,129 @@ +// 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 std::ffi::CStr; +use std::os::raw::c_char; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + RwLock, +}; +use std::time::{Duration, Instant}; + +use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK}; +use xpcom::{ + interfaces::{nsIObserverService, nsISupports}, + RefPtr, +}; + +// Partially cargo-culted from UploadPrefObserver. +#[xpcom(implement(nsIObserver), atomic)] +pub(crate) struct UserActivityObserver { + last_edge: RwLock, + was_active: AtomicBool, +} + +/// Listens to Firefox Desktop's `user-interaction-(in)active` topics, +/// debouncing them before calling into the Glean SDK Client Activity API. +/// See +/// [the docs](https://firefox-source-docs.mozilla.org/toolkit/components/glean/builtin_pings.html) +/// for more info. +#[allow(non_snake_case)] +impl UserActivityObserver { + pub(crate) fn begin_observing() -> Result<(), nsresult> { + // First and foremost, even if we can't get the ObserverService, + // init always means client activity. + glean::handle_client_active(); + + // SAFETY: Everything here is self-contained. + // + // * We allocate the activity observer, created by the xpcom macro + // * We create cstr from a static string. + // * We control all input to `AddObserver` + unsafe { + let activity_obs = Self::allocate(InitUserActivityObserver { + last_edge: RwLock::new(Instant::now()), + was_active: AtomicBool::new(false), + }); + let obs_service: RefPtr = + xpcom::components::Observer::service().map_err(|_| NS_ERROR_FAILURE)?; + let rv = obs_service.AddObserver( + activity_obs.coerce(), + cstr!("user-interaction-active").as_ptr(), + false, + ); + if !rv.succeeded() { + return Err(rv); + } + let rv = obs_service.AddObserver( + activity_obs.coerce(), + cstr!("user-interaction-inactive").as_ptr(), + false, + ); + if !rv.succeeded() { + return Err(rv); + } + } + Ok(()) + } + + unsafe fn Observe( + &self, + _subject: *const nsISupports, + topic: *const c_char, + _data: *const u16, + ) -> nserror::nsresult { + match CStr::from_ptr(topic).to_str() { + Ok("user-interaction-active") => self.handle_active(), + Ok("user-interaction-inactive") => self.handle_inactive(), + _ => NS_OK, + } + } + + fn handle_active(&self) -> nserror::nsresult { + let was_active = self.was_active.swap(true, Ordering::SeqCst); + if !was_active { + let inactivity = self + .last_edge + .read() + .expect("Edge lock poisoned.") + .elapsed(); + // We only care after a certain period of inactivity (default 20min). + let limit = static_prefs::pref!("telemetry.fog.test.inactivity_limit"); + if inactivity >= Duration::from_secs(limit.into()) { + log::info!( + "User triggers core activity after {}s!", + inactivity.as_secs() + ); + glean::handle_client_active(); + } + let mut edge = self.last_edge.write().expect("Edge lock poisoned."); + *edge = Instant::now(); + } + NS_OK + } + + fn handle_inactive(&self) -> nserror::nsresult { + let was_active = self.was_active.swap(false, Ordering::SeqCst); + // This is actually always so. Inactivity is only notified once. + if was_active { + let activity = self + .last_edge + .read() + .expect("Edge lock poisoned.") + .elapsed(); + // We only care after a certain period of activity (default 2min). + let limit = static_prefs::pref!("telemetry.fog.test.activity_limit"); + if activity >= Duration::from_secs(limit.into()) { + log::info!( + "User triggers core inactivity after {}s!", + activity.as_secs() + ); + glean::handle_client_inactive(); + } + let mut edge = self.last_edge.write().expect("Edge lock poisoned."); + *edge = Instant::now(); + } + NS_OK + } +} diff --git a/toolkit/components/glean/src/init/viaduct_uploader.rs b/toolkit/components/glean/src/init/viaduct_uploader.rs new file mode 100644 index 0000000000..d9ce4e0488 --- /dev/null +++ b/toolkit/components/glean/src/init/viaduct_uploader.rs @@ -0,0 +1,73 @@ +// 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 glean::net::{PingUploader, UploadResult}; +use url::Url; +use viaduct::{Error::*, Request}; + +extern "C" { + fn FOG_TooLateToSend() -> bool; +} + +/// An uploader that uses [Viaduct](https://github.com/mozilla/application-services/tree/main/components/viaduct). +#[derive(Debug)] +pub(crate) struct ViaductUploader; + +impl PingUploader for ViaductUploader { + /// Uploads a ping to a server. + /// + /// # Arguments + /// + /// * `url` - the URL path to upload the data to. + /// * `body` - the serialized text data to send. + /// * `headers` - a vector of tuples containing the headers to send with + /// the request, i.e. (Name, Value). + fn upload(&self, url: String, body: Vec, headers: Vec<(String, String)>) -> UploadResult { + log::trace!("FOG Ping Uploader uploading to {}", url); + let url_clone = url.clone(); + let result: std::result::Result = (move || { + // SAFETY NOTE: Safe because it returns a primitive by value. + if unsafe { FOG_TooLateToSend() } { + log::trace!("Attempted to send ping too late into shutdown."); + return Ok(UploadResult::done()); + } + let debug_tagged = headers.iter().any(|(name, _)| name == "X-Debug-ID"); + let localhost_port = static_prefs::pref!("telemetry.fog.test.localhost_port"); + if localhost_port < 0 + || (localhost_port == 0 && !debug_tagged && cfg!(feature = "disable_upload")) + { + log::info!("FOG Ping uploader faking success"); + return Ok(UploadResult::http_status(200)); + } + let parsed_url = Url::parse(&url_clone)?; + + log::info!("FOG Ping uploader uploading to {:?}", parsed_url); + + let mut req = Request::post(parsed_url.clone()).body(body.clone()); + for (header_key, header_value) in &headers { + req = req.header(header_key.to_owned(), header_value)?; + } + + log::trace!("FOG Ping Uploader sending ping to {}", parsed_url); + let res = req.send()?; + Ok(UploadResult::http_status(res.status as i32)) + })(); + log::trace!( + "FOG Ping Uploader completed uploading to {} (Result {:?})", + url, + result + ); + match result { + Ok(result) => result, + Err(NonTlsUrl | UrlError(_)) => UploadResult::unrecoverable_failure(), + Err( + RequestHeaderError(_) + | BackendError(_) + | NetworkError(_) + | BackendNotInitialized + | SetBackendError, + ) => UploadResult::recoverable_failure(), + } + } +} diff --git a/toolkit/components/glean/src/lib.rs b/toolkit/components/glean/src/lib.rs new file mode 100644 index 0000000000..79f3258bb7 --- /dev/null +++ b/toolkit/components/glean/src/lib.rs @@ -0,0 +1,210 @@ +// 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/. + +//! Firefox on Glean (FOG) is the name of the layer that integrates the [Glean SDK][glean-sdk] into Firefox Desktop. +//! It is currently being designed and implemented. +//! +//! The [Glean SDK][glean-sdk] is a data collection library built by Mozilla for use in its products. +//! Like [Telemetry][telemetry], it can be used to +//! (in accordance with our [Privacy Policy][privacy-policy]) +//! send anonymous usage statistics to Mozilla in order to make better decisions. +//! +//! Documentation can be found online in the [Firefox Source Docs][docs]. +//! +//! [glean-sdk]: https://github.com/mozilla/glean/ +//! [book-of-glean]: https://mozilla.github.io/glean/book/index.html +//! [privacy-policy]: https://www.mozilla.org/privacy/ +//! [docs]: https://firefox-source-docs.mozilla.org/toolkit/components/glean/ + +use firefox_on_glean::{ipc, metrics, pings}; +use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK}; +use nsstring::{nsACString, nsCString}; +use thin_vec::ThinVec; + +#[macro_use] +extern crate cstr; +#[cfg_attr(not(target_os = "android"), macro_use)] +extern crate xpcom; + +mod init; + +pub use init::fog_init; + +#[no_mangle] +pub extern "C" fn fog_shutdown() { + glean::shutdown(); +} + +#[no_mangle] +pub extern "C" fn fog_register_pings() { + pings::register_pings(None); +} + +static mut PENDING_BUF: Vec = Vec::new(); + +// IPC serialization/deserialization methods +// Crucially important that the first two not be called on multiple threads. + +/// Only safe if only called on a single thread (the same single thread you call +/// fog_give_ipc_buf on). +#[no_mangle] +pub unsafe extern "C" fn fog_serialize_ipc_buf() -> usize { + if let Some(buf) = ipc::take_buf() { + PENDING_BUF = buf; + PENDING_BUF.len() + } else { + PENDING_BUF = vec![]; + 0 + } +} + +/// Only safe if called on a single thread (the same single thread you call +/// fog_serialize_ipc_buf on), and if buf points to an allocated buffer of at +/// least buf_len bytes. +#[no_mangle] +pub unsafe extern "C" fn fog_give_ipc_buf(buf: *mut u8, buf_len: usize) -> usize { + let pending_len = PENDING_BUF.len(); + if buf.is_null() || buf_len < pending_len { + return 0; + } + std::ptr::copy_nonoverlapping(PENDING_BUF.as_ptr(), buf, pending_len); + PENDING_BUF = Vec::new(); + pending_len +} + +/// Only safe if buf points to an allocated buffer of at least buf_len bytes. +/// No ownership is transfered to Rust by this method: caller owns the memory at +/// buf before and after this call. +#[no_mangle] +pub unsafe extern "C" fn fog_use_ipc_buf(buf: *const u8, buf_len: usize) { + let slice = std::slice::from_raw_parts(buf, buf_len); + let res = ipc::replay_from_buf(slice); + if res.is_err() { + log::warn!("Unable to replay ipc buffer. This will result in data loss."); + metrics::fog_ipc::replay_failures.add(1); + } +} + +/// Sets the debug tag for pings assembled in the future. +/// Returns an error result if the provided value is not a valid tag. +#[no_mangle] +pub extern "C" fn fog_set_debug_view_tag(value: &nsACString) -> nsresult { + let result = glean::set_debug_view_tag(&value.to_string()); + if result { + return NS_OK; + } else { + return NS_ERROR_FAILURE; + } +} + +/// Submits a ping by name. +#[no_mangle] +pub extern "C" fn fog_submit_ping(ping_name: &nsACString) -> nsresult { + glean::submit_ping_by_name(&ping_name.to_string(), None); + NS_OK +} + +/// Turns ping logging on or off. +/// Returns an error if the logging failed to be configured. +#[no_mangle] +pub extern "C" fn fog_set_log_pings(value: bool) -> nsresult { + glean::set_log_pings(value); + NS_OK +} + +/// Flushes ping-lifetime data to the db when delay_ping_lifetime_io is true. +#[no_mangle] +pub extern "C" fn fog_persist_ping_lifetime_data() -> nsresult { + glean::persist_ping_lifetime_data(); + NS_OK +} + +/// Indicate that an experiment is running. +/// Glean will add an experiment annotation which is sent with pings. +/// This information is not persisted between runs. +/// +/// See [`glean_core::Glean::set_experiment_active`]. +#[no_mangle] +pub extern "C" fn fog_set_experiment_active( + experiment_id: &nsACString, + branch: &nsACString, + extra_keys: &ThinVec, + extra_values: &ThinVec, +) { + assert_eq!( + extra_keys.len(), + extra_values.len(), + "Experiment extra keys and values differ in length." + ); + let extra = if extra_keys.len() == 0 { + None + } else { + Some( + extra_keys + .iter() + .zip(extra_values.iter()) + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + ) + }; + glean::set_experiment_active(experiment_id.to_string(), branch.to_string(), extra); +} + +/// Indicate that an experiment is no longer running. +/// +/// See [`glean_core::Glean::set_experiment_inactive`]. +#[no_mangle] +pub extern "C" fn fog_set_experiment_inactive(experiment_id: &nsACString) { + glean::set_experiment_inactive(experiment_id.to_string()); +} + +/// TEST ONLY FUNCTION +/// +/// Returns true if the identified experiment is active. +#[no_mangle] +pub extern "C" fn fog_test_is_experiment_active(experiment_id: &nsACString) -> bool { + glean::test_is_experiment_active(experiment_id.to_string()) +} + +/// TEST ONLY FUNCTION +/// +/// Fills `branch`, `extra_keys`, and `extra_values` with the identified experiment's data. +/// Panics if the identified experiment isn't active. +#[no_mangle] +pub extern "C" fn fog_test_get_experiment_data( + experiment_id: &nsACString, + branch: &mut nsACString, + extra_keys: &mut ThinVec, + extra_values: &mut ThinVec, +) { + let data = glean::test_get_experiment_data(experiment_id.to_string()); + if let Some(data) = data { + branch.assign(&data.branch); + if let Some(extra) = data.extra { + let (data_keys, data_values): (Vec<_>, Vec<_>) = extra.iter().unzip(); + extra_keys.extend(data_keys.into_iter().map(|key| key.into())); + extra_values.extend(data_values.into_iter().map(|value| value.into())); + } + } +} + +/// Sets the remote feature configuration. +/// +/// See [`glean_core::Glean::set_metrics_disabled_config`]. +#[no_mangle] +pub extern "C" fn fog_set_metrics_feature_config(config_json: &nsACString) { + // Normalize null and empty strings to a stringified empty map + if config_json == "null" || config_json.is_empty() { + glean::glean_set_metrics_enabled_config("{}".to_owned()); + } + glean::glean_set_metrics_enabled_config(config_json.to_string()); +} + +/// Performs Glean tasks when client state changes to inactive +/// +/// See [`glean_core::Glean::handle_client_inactive`]. +#[no_mangle] +pub extern "C" fn fog_internal_glean_handle_client_inactive() { + glean::handle_client_inactive(); +} -- cgit v1.2.3