// 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, disable_internal_pings: bool, ) -> 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, // Flipping it around, because no value = defaults to false, // so we take in `disable` but pass on `enable`. !disable_internal_pings, ) .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, disable_internal_pings: bool, ) -> 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, !disable_internal_pings, ) .into() } fn fog_init_internal( data_path_override: &nsACString, app_id_override: &nsACString, upload_enabled: bool, uploader: Option>, enable_internal_pings: bool, ) -> 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; conf.enable_internal_pings = enable_internal_pings; // 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; } // 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()); 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: true, experimentation_id: None, enable_internal_pings: true, }; 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(()) }