blob: 6b52d6473031e23ceb21d8f46a97463a48a13e9b [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{format_err, Error};
use fuchsia_async as fasync;
use fuchsia_syslog::fx_log_err;
use futures::lock::Mutex;
use serde::de::DeserializeOwned;
use std::fmt::{Debug, Display};
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::sync::Arc;
use crate::config;
use crate::config::base::ConfigLoadInfo;
use crate::inspect::config_logger::{InspectConfigLogger, InspectConfigLoggerHandle};
pub struct DefaultSetting<T, P>
where
T: DeserializeOwned + Clone + Debug,
P: AsRef<Path> + Display,
{
default_value: Option<T>,
config_file_path: P,
cached_value: Option<Option<T>>,
config_logger: Arc<Mutex<InspectConfigLogger>>,
}
impl<T, P> DefaultSetting<T, P>
where
T: DeserializeOwned + Clone + std::fmt::Debug,
P: AsRef<Path> + Display,
{
pub fn new(default_value: Option<T>, config_file_path: P) -> Self {
let inspect_config_logger_handle = InspectConfigLoggerHandle::new();
DefaultSetting {
default_value,
config_file_path,
cached_value: None,
config_logger: inspect_config_logger_handle.logger,
}
}
/// Returns the value of this setting. Loads the value from storage if it hasn't been loaded
/// before, otherwise returns a cached value.
pub fn get_cached_value(&mut self) -> Result<Option<T>, Error> {
if self.cached_value.is_none() {
self.cached_value = Some(self.load_default_settings()?);
}
Ok(self.cached_value.as_ref().expect("cached value not present").clone())
}
/// Loads the value of this setting from storage.
///
/// If the value isn't present, returns the default value.
pub fn load_default_value(&mut self) -> Result<Option<T>, Error> {
Ok(self.load_default_settings()?)
}
/// Attempts to load the settings from the given config_file_path.
///
/// Returns the default value if unable to read or parse the file. The returned option will
/// only be None if the default_value was provided as None.
fn load_default_settings(&mut self) -> Result<Option<T>, Error> {
let config_load_info: Option<ConfigLoadInfo>;
let path = self.config_file_path.to_string();
let load_result = match File::open(self.config_file_path.as_ref()) {
Ok(file) => {
match serde_json::from_reader(BufReader::new(file)) {
Ok(config) => {
// Success path.
config_load_info = Some(ConfigLoadInfo {
status: config::base::ConfigLoadStatus::Success,
contents: if let Some(ref payload) = config {
Some(format!("{:?}", payload))
} else {
None
},
});
Ok(config)
}
Err(e) => {
// Found file, but failed to parse.
let err_msg = format!("unable to parse config: {:?}", e);
config_load_info = Some(ConfigLoadInfo {
status: config::base::ConfigLoadStatus::ParseFailure(err_msg.clone()),
contents: None,
});
Err(format_err!("{:?}", err_msg))
}
}
}
Err(..) => {
// No file found.
config_load_info = Some(ConfigLoadInfo {
status: config::base::ConfigLoadStatus::UsingDefaults(
"File not found, using defaults".to_string(),
),
contents: None,
});
Ok(self.default_value.clone())
}
};
if let Some(config_load_info) = config_load_info {
self.write_config_load_to_inspect(path, config_load_info);
} else {
fx_log_err!("Could not load config for {:?}", path);
}
load_result
}
/// Attempts to write the config load to inspect.
fn write_config_load_to_inspect(
&mut self,
path: String,
config_load_info: config::base::ConfigLoadInfo,
) {
let config_logger = self.config_logger.clone();
fasync::Task::spawn(async move {
config_logger.lock().await.write_config_load_to_inspect(path, config_load_info);
})
.detach();
}
}
#[cfg(test)]
pub(crate) mod testing {
use super::*;
use crate::clock;
use crate::inspect::config_logger::InspectConfigLoggerHandle;
use crate::tests::helpers::move_executor_forward_and_get;
use assert_matches::assert_matches;
use fuchsia_async::TestExecutor;
use fuchsia_inspect::assert_data_tree;
use fuchsia_inspect::testing::AnyProperty;
use fuchsia_zircon::Time;
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
struct TestConfigData {
value: u32,
}
#[fuchsia_async::run_until_stalled(test)]
async fn test_load_valid_config_data() {
let mut setting = DefaultSetting::new(
Some(TestConfigData { value: 3 }),
"/config/data/fake_config_data.json",
);
assert_eq!(
setting.load_default_value().expect("Failed to get default value").unwrap().value,
10
);
}
#[fuchsia_async::run_until_stalled(test)]
async fn test_load_invalid_config_data() {
let mut setting = DefaultSetting::new(
Some(TestConfigData { value: 3 }),
"/config/data/fake_invalid_config_data.json",
);
assert!(setting.load_default_value().is_err());
}
#[fuchsia_async::run_until_stalled(test)]
async fn test_load_invalid_config_file_path() {
let mut setting = DefaultSetting::new(Some(TestConfigData { value: 3 }), "nuthatch");
assert_eq!(
setting.load_default_value().expect("Failed to get default value").unwrap().value,
3
);
}
#[fuchsia_async::run_until_stalled(test)]
async fn test_load_default_none() {
let mut setting = DefaultSetting::<TestConfigData, &str>::new(None, "nuthatch");
assert!(setting.load_default_value().expect("Failed to get default value").is_none());
}
#[fuchsia_async::run_until_stalled(test)]
async fn test_no_inspect_write() {
let mut setting = DefaultSetting::<TestConfigData, &str>::new(None, "nuthatch");
assert!(setting.load_default_value().expect("Failed to get default value").is_none());
}
#[test]
fn test_config_inspect_write() {
clock::mock::set(Time::from_nanos(0));
let mut executor = TestExecutor::new_with_fake_time().expect("Failed to create executor");
let mut setting = DefaultSetting::new(Some(TestConfigData { value: 3 }), "nuthatch");
let load_result = move_executor_forward_and_get(
&mut executor,
async { setting.load_default_value() },
"Unable to get default value",
);
assert_matches!(load_result, Ok(Some(TestConfigData { value: 3 })));
let logger_handle = InspectConfigLoggerHandle::new();
let lock_future = logger_handle.logger.lock();
let inspector = move_executor_forward_and_get(
&mut executor,
lock_future,
"Couldn't get inspect logger lock",
)
.inspector;
assert_data_tree!(inspector, root: {
config_loads: {
"nuthatch": {
"count": AnyProperty,
"timestamp": "0.000000000",
"value": "ConfigLoadInfo {\n status: UsingDefaults(\n \"File not found, using defaults\",\n ),\n contents: None,\n}",
}
}
});
}
}