summaryrefslogtreecommitdiffstats
path: root/toolkit/components/glean/src
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/glean/src')
-rw-r--r--toolkit/components/glean/src/lib.rs364
-rw-r--r--toolkit/components/glean/src/viaduct_uploader.rs54
2 files changed, 418 insertions, 0 deletions
diff --git a/toolkit/components/glean/src/lib.rs b/toolkit/components/glean/src/lib.rs
new file mode 100644
index 0000000000..11bf1dd58c
--- /dev/null
+++ b/toolkit/components/glean/src/lib.rs
@@ -0,0 +1,364 @@
+// 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/
+
+// No one is currently using the Glean SDK, so let's export it, so we know it gets
+// compiled.
+pub extern crate fog;
+
+#[macro_use]
+extern crate cstr;
+#[macro_use]
+extern crate xpcom;
+
+use std::ffi::CStr;
+use std::os::raw::c_char;
+
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK};
+use nsstring::{nsACString, nsAString, nsCStr, nsCString, nsStr, nsString};
+use xpcom::interfaces::{
+ mozIViaduct, nsIFile, nsIObserver, nsIPrefBranch, nsIPropertyBag2, nsISupports, nsIXULAppInfo,
+};
+use xpcom::{RefPtr, XpCom};
+
+use glean::{ClientInfoMetrics, Configuration};
+
+mod viaduct_uploader;
+
+use crate::viaduct_uploader::ViaductUploader;
+
+/// Project FOG's entry point.
+///
+/// This assembles client information and the Glean configuration and then initializes the global
+/// Glean instance.
+#[no_mangle]
+pub unsafe extern "C" fn fog_init() -> nsresult {
+ fog::metrics::fog::initialization.start();
+
+ log::debug!("Initializing FOG.");
+
+ let data_path = match get_data_path() {
+ Ok(dp) => dp,
+ Err(e) => return e,
+ };
+
+ let (app_build, app_display_version, channel) = match get_app_info() {
+ Ok(ai) => ai,
+ Err(e) => return e,
+ };
+
+ let (os_version, _architecture) = match get_system_info() {
+ Ok(si) => si,
+ Err(e) => return e,
+ };
+
+ fog::metrics::fog_validation::os_version.set(os_version);
+
+ let client_info = ClientInfoMetrics {
+ app_build,
+ app_display_version,
+ };
+ log::debug!("Client Info: {:#?}", client_info);
+
+ let pref_observer = UploadPrefObserver::allocate(InitUploadPrefObserver {});
+ if let Err(e) = pref_observer.begin_observing() {
+ log::error!(
+ "Could not observe data upload pref. Abandoning FOG init due to {:?}",
+ e
+ );
+ return e;
+ }
+
+ 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 upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled");
+ let data_path = data_path.to_string();
+ let configuration = Configuration {
+ upload_enabled,
+ data_path,
+ application_id: "firefox.desktop".to_string(),
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ channel: Some(channel),
+ server_endpoint: Some(server),
+ uploader: Some(Box::new(crate::ViaductUploader) as Box<dyn glean::net::PingUploader>),
+ };
+
+ log::debug!("Configuration: {:#?}", configuration);
+
+ // Ensure Viaduct is initialized for networking unconditionally so we don't
+ // need to check again if upload is later enabled.
+ if let Some(viaduct) =
+ xpcom::create_instance::<mozIViaduct>(cstr!("@mozilla.org/toolkit/viaduct;1"))
+ {
+ let result = viaduct.EnsureInitialized();
+ if result.failed() {
+ log::error!("Failed to ensure viaduct was initialized due to {}. Ping upload may not be available.", result.error_name());
+ }
+ } else {
+ log::error!("Failed to create Viaduct via XPCOM. Ping upload may not be available.");
+ }
+
+ if configuration.data_path.len() > 0 {
+ glean::initialize(configuration, client_info);
+
+ // Register all custom pings before we initialize.
+ fog::pings::register_pings();
+
+ fog::metrics::fog::initialization.stop();
+ schedule_fog_validation_ping();
+ }
+
+ NS_OK
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn fog_shutdown() {
+ glean::shutdown();
+}
+
+/// Construct and return the data_path from the profile dir, or return an error.
+fn get_data_path() -> Result<String, nsresult> {
+ let dir_svc = match xpcom::services::get_DirectoryService() {
+ Some(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, and build channel.
+/// 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), nsresult> {
+ let xul = xpcom::services::get_XULRuntime().ok_or(NS_ERROR_FAILURE)?;
+
+ let mut channel = nsCString::new();
+ unsafe {
+ xul.GetDefaultUpdateChannel(&mut *channel).to_result()?;
+ }
+
+ 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(),
+ ))
+ }
+ };
+
+ 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()?;
+ }
+
+ Ok((
+ build_id.to_string(),
+ version.to_string(),
+ channel.to_string(),
+ ))
+}
+
+/// Return a tuple of os_version and architecture, or an error.
+fn get_system_info() -> Result<(String, String), nsresult> {
+ let info_service = xpcom::get_service::<nsIPropertyBag2>(cstr!("@mozilla.org/system-info;1"))
+ .ok_or(NS_ERROR_FAILURE)?;
+
+ let os_version_key: Vec<u16> = "version".encode_utf16().collect();
+ let os_version_key = &nsStr::from(&os_version_key) as &nsAString;
+ let mut os_version = nsCString::new();
+ unsafe {
+ info_service
+ .GetPropertyAsACString(os_version_key, &mut *os_version)
+ .to_result()?;
+ }
+
+ let arch_key: Vec<u16> = "arch".encode_utf16().collect();
+ let arch_key = &nsStr::from(&arch_key) as &nsAString;
+ let mut arch = nsCString::new();
+ unsafe {
+ info_service
+ .GetPropertyAsACString(arch_key, &mut *arch)
+ .to_result()?;
+ }
+
+ Ok((os_version.to_string(), arch.to_string()))
+}
+
+// Partially cargo-culted from https://searchfox.org/mozilla-central/rev/598e50d2c3cd81cd616654f16af811adceb08f9f/security/manager/ssl/cert_storage/src/lib.rs#1192
+#[derive(xpcom)]
+#[xpimplements(nsIObserver)]
+#[refcnt = "atomic"]
+struct InitUploadPrefObserver {}
+
+#[allow(non_snake_case)]
+impl UploadPrefObserver {
+ unsafe fn begin_observing(&self) -> Result<(), nsresult> {
+ let pref_service = xpcom::services::get_PrefService().ok_or(NS_ERROR_FAILURE)?;
+ let pref_branch: RefPtr<nsIPrefBranch> =
+ (*pref_service).query_interface().ok_or(NS_ERROR_FAILURE)?;
+ let pref_nscstr = &nsCStr::from("datareporting.healthreport.uploadEnabled") as &nsACString;
+ (*pref_branch)
+ .AddObserverImpl(pref_nscstr, self.coerce::<nsIObserver>(), false)
+ .to_result()?;
+ Ok(())
+ }
+
+ unsafe fn Observe(
+ &self,
+ _subject: *const nsISupports,
+ topic: *const c_char,
+ pref_name: *const i16,
+ ) -> nserror::nsresult {
+ let topic = CStr::from_ptr(topic).to_str().unwrap();
+ // Conversion utf16 to utf8 is messy.
+ // We should only ever observe changes to the one pref 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 as *const u16, 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" && pref_name == "datareporting.healthreport.uploadEnabled"
+ );
+
+ let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled");
+ glean::set_upload_enabled(upload_enabled);
+ NS_OK
+ }
+}
+
+static mut PENDING_BUF: Vec<u8> = 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) = fog::ipc::take_buf() {
+ PENDING_BUF = buf;
+ PENDING_BUF.len()
+ } else {
+ PENDING_BUF = vec![];
+ 0
+ }
+}
+
+#[no_mangle]
+/// 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.
+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
+}
+
+#[no_mangle]
+/// 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.
+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 = fog::ipc::replay_from_buf(slice);
+ if res.is_err() {
+ log::warn!("Unable to replay ipc buffer. This will result in data loss.");
+ fog::metrics::fog_ipc::replay_failures.add(1);
+ }
+}
+
+#[no_mangle]
+/// Sets the debug tag for pings assembled in the future.
+/// Returns an error result if the provided value is not a valid tag.
+pub unsafe 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;
+ }
+}
+
+#[no_mangle]
+/// Submits a ping by name.
+pub unsafe extern "C" fn fog_submit_ping(ping_name: &nsACString) -> nsresult {
+ glean::submit_ping_by_name(&ping_name.to_string(), None);
+ NS_OK
+}
+
+#[no_mangle]
+/// Turns ping logging on or off.
+/// Returns an error if the logging failed to be configured.
+pub unsafe extern "C" fn fog_set_log_pings(value: bool) -> nsresult {
+ glean::set_log_pings(value);
+ NS_OK
+}
+
+fn schedule_fog_validation_ping() {
+ std::thread::spawn(|| {
+ loop {
+ // Sleep for an hour before and between submissions.
+ std::thread::sleep(std::time::Duration::from_secs(60 * 60));
+ fog::pings::fog_validation.submit(None);
+ }
+ });
+}
diff --git a/toolkit/components/glean/src/viaduct_uploader.rs b/toolkit/components/glean/src/viaduct_uploader.rs
new file mode 100644
index 0000000000..3d01441cb4
--- /dev/null
+++ b/toolkit/components/glean/src/viaduct_uploader.rs
@@ -0,0 +1,54 @@
+// 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::Request;
+
+/// 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 || {
+ let localhost_port = static_prefs::pref!("telemetry.fog.test.localhost_port");
+ if localhost_port < 0 {
+ log::info!("FOG Ping uploader faking success");
+ return Ok(UploadResult::HttpStatus(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::HttpStatus(res.status.into()))
+ })();
+ log::trace!(
+ "FOG Ping Uploader completed uploading to {} (Result {:?})",
+ url,
+ result
+ );
+ match result {
+ Ok(result) => result,
+ _ => UploadResult::UnrecoverableFailure,
+ }
+ }
+}