diff options
Diffstat (limited to 'toolkit/components/glean/src/init')
-rw-r--r-- | toolkit/components/glean/src/init/mod.rs | 368 | ||||
-rw-r--r-- | toolkit/components/glean/src/init/upload_pref.rs | 99 | ||||
-rw-r--r-- | toolkit/components/glean/src/init/user_activity.rs | 129 | ||||
-rw-r--r-- | toolkit/components/glean/src/init/viaduct_uploader.rs | 73 |
4 files changed, 669 insertions, 0 deletions
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<dyn glean::net::PingUploader>); + + 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<Box<dyn glean::net::PingUploader>>, +) -> 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<String, nsresult> { + let dir_svc: RefPtr<nsIProperties> = match xpcom::components::Directory::service() { + Ok(ds) => ds, + _ => return Err(NS_ERROR_FAILURE), + }; + let mut profile_dir = xpcom::GetterAddrefs::<nsIFile>::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<nsIXULRuntime> = + xpcom::components::XULRuntime::service().map_err(|_| NS_ERROR_FAILURE)?; + + let pref_service: RefPtr<nsIPrefService> = + xpcom::components::Preferences::service().map_err(|_| NS_ERROR_FAILURE)?; + let locale_service: RefPtr<mozILocaleService> = + 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::<nsIXULAppInfo>() { + 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<dyn glean::net::PingUploader>); + + 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<dyn glean::net::PingUploader>); + + 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<nsIPrefBranch> = + 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<Instant>, + 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<nsIObserverService> = + 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<u8>, headers: Vec<(String, String)>) -> UploadResult { + log::trace!("FOG Ping Uploader uploading to {}", url); + let url_clone = url.clone(); + let result: std::result::Result<UploadResult, viaduct::Error> = (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(), + } + } +} |