| // Copyright 2023 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| use std::collections::HashMap; | |
| use std::str::FromStr; | |
| use std::sync::Arc; | |
| use std::sync::Mutex; | |
| use anyhow::bail; | |
| use anyhow::Context; | |
| use anyhow::Result; | |
| use dbus::nonblock::SyncConnection; | |
| #[cfg(feature = "chromeos")] | |
| use featured::CheckFeature; // Trait CheckFeature is for get_params_and_enabled | |
| use once_cell::sync::OnceCell; | |
| use crate::sync::NoPoison; | |
| type FeatureChangeCallback = Box<dyn Fn(bool) + Send + Sync + 'static>; | |
| type FeatureRegisterInfo = (&'static str, bool, Option<FeatureChangeCallback>); | |
| // The state of a feature that can be changed. | |
| struct FeatureState { | |
| // Whether the feature is enabled. | |
| enabled: bool, | |
| // The params of the feature. If the feature is disabled or has no params | |
| // this map will be empty. | |
| params: HashMap<String, String>, | |
| } | |
| struct Feature { | |
| // The state of the feature. | |
| state: Arc<Mutex<FeatureState>>, | |
| // Callback invoked when the feature enable state changes. | |
| #[cfg(feature = "chromeos")] | |
| cb: Option<FeatureChangeCallback>, | |
| // There must only ever be one struct instance for a given feature name. | |
| // | |
| // Reference: https://chromium.googlesource.com/chromiumos/platform2/+/79195b9779a292e50cef56b609ea089bd92f2175/featured/c_feature_library.h#25 | |
| #[cfg(feature = "chromeos")] | |
| raw: featured::Feature, | |
| } | |
| // Only use featured in ebuild as using featured makes "cargo build" fail. | |
| // | |
| // Reference: https://chromium.googlesource.com/chromiumos/platform2/+/main/featured/README.md | |
| struct FeatureManager { | |
| features: Arc<HashMap<String, Feature>>, | |
| } | |
| impl FeatureManager { | |
| #[cfg_attr(not(feature = "chromeos"), allow(unused_variables))] | |
| fn new(features: Vec<FeatureRegisterInfo>) -> Result<FeatureManager> { | |
| let features = features | |
| .into_iter() | |
| .map(|(name, default_enabled, cb)| -> Result<(String, Feature)> { | |
| cfg_if::cfg_if! { | |
| if #[cfg(feature = "chromeos")] { | |
| let raw = featured::Feature::new(name, default_enabled)?; | |
| // Initialize to default values for now. | |
| // init() below immediately reloads the cache after creating | |
| // the FeatureManager instance which will set the correct actual values. | |
| let feature_state = FeatureState { | |
| enabled: default_enabled, | |
| params: HashMap::new(), | |
| }; | |
| let feature = Feature { | |
| state: Arc::new(Mutex::new(feature_state)), | |
| cb, | |
| raw | |
| }; | |
| Ok((name.to_owned(), feature)) | |
| } else { | |
| Ok((name.to_owned(), Feature { | |
| state: Arc::new(Mutex::new(FeatureState { | |
| enabled: default_enabled, | |
| params: HashMap::new(), | |
| })), | |
| })) | |
| } | |
| } | |
| }) | |
| .collect::<Result<Vec<(String, Feature)>>>() | |
| .context("failed to initialize features")?; | |
| Ok(FeatureManager { | |
| features: Arc::new(HashMap::from_iter(features)), | |
| }) | |
| } | |
| // Returns the cached feature query result. | |
| fn is_feature_enabled(&self, feature_name: &str) -> bool { | |
| match self.features.get(feature_name) { | |
| Some(feature) => { | |
| let state = feature.state.do_lock(); | |
| state.enabled | |
| } | |
| None => false, | |
| } | |
| } | |
| fn get_feature_param_as<T: FromStr>( | |
| &self, | |
| feature_name: &str, | |
| param_name: &str, | |
| ) -> Result<Option<T>> | |
| where | |
| <T as FromStr>::Err: std::error::Error + Send + Sync + 'static, | |
| { | |
| if let Some(feature) = self.features.get(feature_name) { | |
| let state = feature.state.do_lock(); | |
| if let Some(value) = state.params.get(param_name) { | |
| return Ok(Some(value.parse::<T>().with_context(|| { | |
| format!("Failed to parse param {}={}", param_name, value) | |
| })?)); | |
| } | |
| } | |
| Ok(None) | |
| } | |
| fn reload_cache(&self) -> Result<()> { | |
| cfg_if::cfg_if! { | |
| if #[cfg(feature = "chromeos")] { | |
| let features: Vec<&featured::Feature> = | |
| self.features.values().map(|f| &f.raw).collect(); | |
| let resp = featured::PlatformFeatures::get()? | |
| .get_params_and_enabled(&features) | |
| .context("failed to query features")?; | |
| for feature in self.features.values() { | |
| let (run_callback, new_enabled) = { | |
| let mut state = feature.state.do_lock(); | |
| let old_enabled = state.enabled; | |
| let new_params = match resp.get_params(&feature.raw) { | |
| Some(params) => params.clone(), | |
| None => HashMap::new(), | |
| }; | |
| let old_params = std::mem::replace(&mut state.params, new_params); | |
| state.enabled = resp.is_enabled(&feature.raw); | |
| ( | |
| old_enabled != state.enabled || old_params != state.params, | |
| state.enabled | |
| ) | |
| }; | |
| if run_callback { | |
| if let Some(cb) = feature.cb.as_ref() { | |
| cb(new_enabled); | |
| } | |
| } | |
| } | |
| Ok(()) | |
| } else { | |
| Ok(()) | |
| } | |
| } | |
| } | |
| } | |
| // Singleton pattern. | |
| static FEATURE_MANAGER: OnceCell<FeatureManager> = OnceCell::new(); | |
| static PENDING_FEATURES: OnceCell<Mutex<Vec<FeatureRegisterInfo>>> = OnceCell::new(); | |
| /// Register a feature flag to be initialized and monitored by [`init`]. | |
| /// | |
| /// # Arguments | |
| /// * `feature_name` - The name of the feature flag. | |
| /// * `enabled_by_default` - The default value of the flag. | |
| /// * `cb` - An optional callback to be invoked when the feature flag changes. Note | |
| /// that if the initial state of the flag matches `enabled_by_default`, this | |
| /// callback will not be invoked. | |
| pub fn register_feature( | |
| feature_name: &'static str, | |
| enabled_by_default: bool, | |
| cb: Option<FeatureChangeCallback>, | |
| ) { | |
| assert!( | |
| FEATURE_MANAGER.get().is_none(), | |
| "Features cannot be resgistered after FeatureManager initialization" | |
| ); | |
| let pending = PENDING_FEATURES.get_or_init(|| Mutex::new(Vec::new())); | |
| pending | |
| .do_lock() | |
| .push((feature_name, enabled_by_default, cb)) | |
| } | |
| #[cfg_attr(not(feature = "chromeos"), allow(unused_variables))] | |
| pub async fn init(conn: &SyncConnection) -> Result<()> { | |
| let Some(pending) = PENDING_FEATURES.get() else { | |
| return Ok(()); | |
| }; | |
| let pending = std::mem::take(&mut *pending.do_lock()); | |
| let feature_manager = FeatureManager::new(pending)?; | |
| feature_manager | |
| .reload_cache() | |
| .context("failed to load initial feature state")?; | |
| if FEATURE_MANAGER.set(feature_manager).is_err() { | |
| bail!("Double initialization of FEATURE_MANAGER"); | |
| } | |
| #[cfg(feature = "chromeos")] | |
| featured::listen_for_refetch_needed(conn, || { | |
| let feature_manager = FEATURE_MANAGER | |
| .get() | |
| .expect("FEATURE_MANAGER singleton disappeared"); | |
| if let Err(e) = feature_manager.reload_cache() { | |
| log::error!("Error reloading feature cache: {:?}", e); | |
| } | |
| }) | |
| .await | |
| .context("failed to start feature monitoring")?; | |
| Ok(()) | |
| } | |
| #[cfg(not(test))] | |
| pub fn is_feature_enabled(feature_name: &str) -> Result<bool> { | |
| Ok(FEATURE_MANAGER | |
| .get() | |
| .context("FEATURE_MANAGER is not initialized")? | |
| .is_feature_enabled(feature_name)) | |
| } | |
| #[cfg(test)] | |
| pub fn is_feature_enabled(_feature_name: &str) -> Result<bool> { | |
| Ok(false) | |
| } | |
| pub fn get_feature_param_as<T: FromStr>(feature_name: &str, param_name: &str) -> Result<Option<T>> | |
| where | |
| <T as FromStr>::Err: std::error::Error + Send + Sync + 'static, | |
| { | |
| FEATURE_MANAGER | |
| .get() | |
| .context("FEATURE_MANAGER is not initialized")? | |
| .get_feature_param_as(feature_name, param_name) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| fn set_feature_param( | |
| feature_manager: &FeatureManager, | |
| feature_name: &str, | |
| param_name: String, | |
| value: String, | |
| ) { | |
| let mut state = feature_manager | |
| .features | |
| .get(feature_name) | |
| .unwrap() | |
| .state | |
| .lock() | |
| .unwrap(); | |
| state.params.insert(param_name, value); | |
| } | |
| #[test] | |
| fn test_initialize_feature_in_default_state() { | |
| let feature_manager = FeatureManager::new(vec![ | |
| ("FakeFeatureDisabled", false, None), | |
| ("FakeFeatureEnabled", true, None), | |
| ]) | |
| .unwrap(); | |
| assert!(!feature_manager.is_feature_enabled("FakeFeatureDisabled")); | |
| assert!(feature_manager.is_feature_enabled("FakeFeatureEnabled")); | |
| } | |
| #[test] | |
| fn test_feature_param_parsing() { | |
| let feature_manager = FeatureManager::new(vec![("FakeFeature", true, None)]).unwrap(); | |
| set_feature_param( | |
| &feature_manager, | |
| "FakeFeature", | |
| "FakeFeatureParamInteger".to_string(), | |
| "12345".to_string(), | |
| ); | |
| set_feature_param( | |
| &feature_manager, | |
| "FakeFeature", | |
| "FakeFeatureParamString".to_string(), | |
| "abc".to_string(), | |
| ); | |
| assert_eq!( | |
| feature_manager | |
| .get_feature_param_as::<u64>("FakeFeature", "FakeFeatureParamInteger") | |
| .unwrap(), | |
| Some(12345) | |
| ); | |
| assert_eq!( | |
| feature_manager | |
| .get_feature_param_as::<String>("FakeFeature", "FakeFeatureParamString") | |
| .unwrap(), | |
| Some("abc".to_string()) | |
| ); | |
| assert!(feature_manager | |
| .get_feature_param_as::<u64>("FakeFeature", "FakeFeatureParamString") | |
| .is_err()); | |
| assert!(feature_manager | |
| .get_feature_param_as::<u64>("FakeFeature", "NonExistParam") | |
| .unwrap() | |
| .is_none()); | |
| assert!(feature_manager | |
| .get_feature_param_as::<u64>("FakeFeatureNonExist", "FakeFeatureParamInteger") | |
| .unwrap() | |
| .is_none()); | |
| } | |
| } |