blob: 9e875e696ee59093a6770b8f4e896c6fbb3c6233 [file] [log] [blame] [edit]
// 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());
}
}