diff options
Diffstat (limited to 'toolkit/components/glean/src')
-rw-r--r-- | toolkit/components/glean/src/lib.rs | 364 | ||||
-rw-r--r-- | toolkit/components/glean/src/viaduct_uploader.rs | 54 |
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, + } + } +} |