// 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/ #[cfg(target_os = "android")] use firefox_on_glean::pings; use firefox_on_glean::{ipc, metrics}; use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK}; use nsstring::{nsACString, nsAString, nsCString}; use std::cell::UnsafeCell; use std::fs; use std::io::ErrorKind; 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; use glean::{AttributionMetrics, DistributionMetrics}; #[no_mangle] pub extern "C" fn fog_shutdown() { glean::shutdown(); } #[no_mangle] pub extern "C" fn fog_register_pings() { #[cfg(not(target_os = "android"))] log::warn!("fog_register_pings on not-Android has no effect."); #[cfg(target_os = "android")] pings::register_pings(Some("gecko")); } // Enough of unstable std::cell::SyncUnsafeCell for our needs, and // compatible enough such that it can just be replaced with // std::cell::SynUnsafeCell when it's stabilized. #[repr(transparent)] pub struct SyncUnsafeCell(UnsafeCell); unsafe impl Sync for SyncUnsafeCell {} impl SyncUnsafeCell { pub const fn new(value: T) -> Self { SyncUnsafeCell(UnsafeCell::new(value)) } pub const fn get(&self) -> *mut T { self.0.get() } } static PENDING_BUF: SyncUnsafeCell> = SyncUnsafeCell::new(Vec::new()); // IPC serialization/deserialization methods // Crucially important that the first two not be called on multiple threads. /// # Safety /// 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 { let pending_buf = &mut *PENDING_BUF.get(); if let Some(buf) = ipc::take_buf() { *pending_buf = buf; pending_buf.len() } else { *pending_buf = vec![]; 0 } } /// # Safety /// 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_buf = &mut *PENDING_BUF.get(); 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 } /// # Safety /// 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 { NS_OK } else { NS_ERROR_FAILURE } } /// Submits a ping by name. #[no_mangle] pub extern "C" fn fog_submit_ping(ping_name: &nsACString) -> nsresult { let ping_name = ping_name.to_string(); #[cfg(feature = "with_gecko")] firefox_on_glean::pings::record_profiler_ping_marker(&ping_name); glean::submit_ping_by_name(&ping_name, 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.is_empty() { 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_apply_server_knobs_config(config_json: &nsACString) { // Normalize null and empty strings to a stringified empty map if config_json == "null" || config_json.is_empty() { glean::glean_apply_server_knobs_config("{}".to_owned()); } glean::glean_apply_server_knobs_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(); } /// Apply a serverknobs config from the given path. #[no_mangle] pub extern "C" fn fog_apply_serverknobs(serverknobs_path: &nsAString) -> bool { let config_json = match fs::read_to_string(serverknobs_path.to_string()) { Ok(c) => c, Err(e) if e.kind() == ErrorKind::NotFound => { // not logging anything if the file is missing. return false; } Err(e) => { log::error!( "Boo, couldn't open serverknobs file at {}, Error: {:?}", serverknobs_path.to_string(), e, ); return false; } }; log::trace!("Loaded serverknobs config. Applying."); glean::glean_apply_server_knobs_config(config_json); true } #[repr(C)] pub struct FogAttributionMetrics { source: nsCString, medium: nsCString, campaign: nsCString, term: nsCString, content: nsCString, } impl FogAttributionMetrics { fn take(&mut self, other: AttributionMetrics) { if let Some(source) = other.source { self.source = source.into(); } if let Some(medium) = other.medium { self.medium = medium.into(); } if let Some(campaign) = other.campaign { self.campaign = campaign.into(); } if let Some(term) = other.term { self.term = term.into(); } if let Some(content) = other.content { self.content = content.into(); } } } impl From<&FogAttributionMetrics> for AttributionMetrics { fn from(value: &FogAttributionMetrics) -> Self { let to_opt_string = |s: &nsCString| { if s.is_empty() { None } else { Some(s.to_utf8().into_owned()) } }; AttributionMetrics { source: to_opt_string(&value.source), medium: to_opt_string(&value.medium), campaign: to_opt_string(&value.campaign), term: to_opt_string(&value.term), content: to_opt_string(&value.content), } } } #[repr(C)] pub struct FogDistributionMetrics { name: nsCString, } impl FogDistributionMetrics { fn take(&mut self, other: DistributionMetrics) { if let Some(name) = other.name { self.name = name.into(); } } } impl From<&FogDistributionMetrics> for DistributionMetrics { fn from(value: &FogDistributionMetrics) -> Self { let name = if value.name.is_empty() { None } else { Some(value.name.to_utf8().into_owned()) }; DistributionMetrics { name } } } #[no_mangle] pub extern "C" fn fog_update_attribution(attr: &FogAttributionMetrics) { glean::update_attribution(attr.into()); } #[no_mangle] pub extern "C" fn fog_test_get_attribution(value: &mut FogAttributionMetrics) { value.take(glean::test_get_attribution()); } #[no_mangle] pub extern "C" fn fog_update_distribution(dist: &FogDistributionMetrics) { glean::update_distribution(dist.into()); } #[no_mangle] pub extern "C" fn fog_test_get_distribution(value: &mut FogDistributionMetrics) { value.take(glean::test_get_distribution()); }