use notify_debouncer_full::{new_debouncer, notify::RecommendedWatcher, DebounceEventResult};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{mpsc::channel, Arc, Mutex, RwLock};
use crate::builder::builder_options::BuilderOptions;
use crate::builder::{OptionsRegistryBuilder, OptionsWatcherBuilder};
use crate::provider::{
CacheOptions, Features, GetOptionsPreferences, OptionsProvider, OptionsRegistry, WatcherOptions,
};
use crate::schema::metadata::OptionsMetadata;
pub type OptionsWatcherListener = Arc<dyn Fn(&HashSet<PathBuf>) + Send + Sync>;
pub struct OptionsWatcher {
current_provider: Arc<RwLock<OptionsProvider>>,
last_modified: Arc<Mutex<std::time::SystemTime>>,
watched_directories: Vec<PathBuf>,
#[allow(dead_code)]
debouncer_watcher: notify_debouncer_full::Debouncer<
RecommendedWatcher,
notify_debouncer_full::RecommendedCache,
>,
listeners: Arc<Mutex<Vec<OptionsWatcherListener>>>,
}
impl OptionsWatcher {
pub fn build_with_options(
directory: impl AsRef<Path>,
watcher_options: WatcherOptions,
) -> Result<Self, String> {
let mut builder = OptionsWatcherBuilder::new();
builder.with_watcher_options(watcher_options);
builder.add_directory(directory.as_ref())?;
builder.build()
}
pub fn build_with_schema_and_options(
directory: impl AsRef<Path>,
schema_path: impl AsRef<Path>,
watcher_options: WatcherOptions,
) -> Result<Self, String> {
let mut builder = OptionsWatcherBuilder::new();
builder.with_schema(schema_path.as_ref())?;
builder.add_directory(directory.as_ref())?;
builder.with_watcher_options(watcher_options);
builder.build()
}
pub fn build_from_directories_with_options(
directories: &[impl AsRef<Path>],
watcher_options: WatcherOptions,
) -> Result<Self, String> {
let mut builder = OptionsWatcherBuilder::new();
builder.add_directories(directories)?;
builder.with_watcher_options(watcher_options);
builder.build()
}
pub fn build_from_directories_with_schema_and_options(
directories: &[impl AsRef<Path>],
schema_path: impl AsRef<Path>,
watcher_options: WatcherOptions,
) -> Result<Self, String> {
let mut builder = OptionsWatcherBuilder::new();
builder.with_watcher_options(watcher_options);
builder.with_schema(schema_path.as_ref())?;
builder.add_directories(directories)?;
builder.build()
}
pub(crate) fn new(
watched_directories: &[impl AsRef<Path>],
watcher_options: WatcherOptions,
builder_options: BuilderOptions,
) -> Result<Self, String> {
let (tx, rx) = channel();
let mut debouncer_watcher = new_debouncer(
watcher_options.debounce_duration,
None,
move |result: DebounceEventResult| match result {
Ok(events) => {
let paths = events
.iter()
.filter(|event| !event.kind.is_access())
.filter(|event| {
match event.kind {
notify::EventKind::Modify(modify_kind) => {
!matches!(modify_kind, notify::event::ModifyKind::Metadata(_))
}
_ => true,
}
})
.flat_map(|event| event.paths.clone())
.collect::<HashSet<_>>();
if paths.is_empty() {
return;
}
eprintln!(
"[optify] Rebuilding OptionsProvider because contents at these path(s) changed: {paths:?}"
);
tx.send(paths).unwrap();
}
Err(errors) => errors
.iter()
.for_each(|error| eprintln!("\x1b[31m[optify] {error:?}\x1b[0m")),
},
)
.map_err(|e| format!("Failed to create debouncer: {e}"))?;
for dir in watched_directories {
debouncer_watcher
.watch(dir, notify::RecursiveMode::Recursive)
.map_err(|e| format!("Failed to watch directory {:?}: {e}", dir.as_ref()))?;
}
let provider = OptionsProvider::build_from_directories_with_options(
watched_directories,
builder_options.clone(),
)
.map_err(|e| format!("Failed to build provider: {e}"))?;
let last_modified = Arc::new(Mutex::new(std::time::SystemTime::now()));
let self_ = Self {
current_provider: Arc::new(RwLock::new(provider)),
last_modified,
watched_directories: watched_directories
.iter()
.map(|dir| dir.as_ref().to_path_buf())
.collect(),
debouncer_watcher,
listeners: Arc::new(Mutex::new(Vec::new())),
};
let current_provider = self_.current_provider.clone();
let watched_directories = self_.watched_directories.clone();
let last_modified = self_.last_modified.clone();
let listeners = self_.listeners.clone();
std::thread::spawn(move || {
for paths in rx {
let result = std::panic::catch_unwind(|| {
match OptionsProvider::build_from_directories_with_options(
&watched_directories,
builder_options.clone(),
) {
Ok(new_provider) => match current_provider.write() {
Ok(mut provider) => {
*provider = new_provider;
*last_modified.lock().unwrap() = std::time::SystemTime::now();
eprintln!("\x1b[32m[optify] Successfully rebuilt the OptionsProvider.\x1b[0m");
let listeners_guard = listeners.lock().unwrap();
for listener in listeners_guard.iter() {
listener(&paths);
}
}
Err(err) => {
eprintln!(
"\x1b[31m[optify] Error rebuilding provider: {err}\nWill not change the provider until the files are fixed.\x1b[0m"
);
}
},
Err(err) => {
eprintln!("\x1b[31m[optify] Error rebuilding provider: {err}\x1b[0m");
}
}
});
if result.is_err() {
eprintln!("\x1b[31m[optify] Error rebuilding the provider. Will not change the provider until the files are fixed.\x1b[0m");
}
}
});
Ok(self_)
}
pub fn add_listener(&mut self, listener: OptionsWatcherListener) {
self.listeners.lock().unwrap().push(listener);
}
pub fn last_modified(&self) -> std::time::SystemTime {
*self.last_modified.lock().unwrap()
}
}
impl OptionsRegistry for OptionsWatcher {
fn build(directory: impl AsRef<Path>) -> Result<OptionsWatcher, String> {
let mut builder = OptionsWatcherBuilder::new();
builder.add_directory(directory.as_ref())?;
builder.build()
}
fn build_from_directories(directories: &[impl AsRef<Path>]) -> Result<OptionsWatcher, String> {
let mut builder = OptionsWatcherBuilder::new();
builder.add_directories(directories)?;
builder.build()
}
fn build_from_directories_with_options(
directories: &[impl AsRef<Path>],
options: BuilderOptions,
) -> Result<Self, String> {
let mut builder = OptionsWatcherBuilder::new();
builder.with_options(options)?;
builder.add_directories(directories)?;
builder.build()
}
fn build_with_options(
directory: impl AsRef<Path>,
options: BuilderOptions,
) -> Result<Self, String> {
let mut builder = OptionsWatcherBuilder::new();
builder.with_options(options)?;
builder.add_directory(directory.as_ref())?;
builder.build()
}
fn get_aliases(&self) -> Vec<String> {
self.current_provider.read().unwrap().get_aliases()
}
fn get_features_and_aliases(&self) -> Vec<String> {
self.current_provider
.read()
.unwrap()
.get_features_and_aliases()
}
fn get_all_options(
&self,
feature_names: &[impl AsRef<str>],
cache_options: Option<&CacheOptions>,
preferences: Option<&GetOptionsPreferences>,
) -> std::result::Result<serde_json::Value, String> {
self.current_provider.read().unwrap().get_all_options(
feature_names,
cache_options,
preferences,
)
}
fn get_canonical_feature_name(
&self,
feature_name: &str,
) -> std::result::Result<String, String> {
self.current_provider
.read()
.unwrap()
.get_canonical_feature_name(feature_name)
}
fn get_canonical_feature_names(
&self,
feature_names: &[impl AsRef<str>],
) -> std::result::Result<Vec<String>, String> {
self.current_provider
.read()
.unwrap()
.get_canonical_feature_names(feature_names)
}
fn get_feature_metadata(&self, canonical_feature_name: &str) -> Option<OptionsMetadata> {
self.current_provider
.read()
.unwrap()
.get_feature_metadata(canonical_feature_name)
}
fn get_features(&self) -> Vec<String> {
self.current_provider.read().unwrap().get_features()
}
fn get_features_referencing_file(&self, relative_path: &str) -> Option<Vec<String>> {
self.current_provider
.read()
.unwrap()
.get_features_referencing_file(relative_path)
}
fn get_features_with_metadata(&self) -> Features {
self.current_provider
.read()
.unwrap()
.get_features_with_metadata()
}
fn get_filtered_feature_names(
&self,
feature_names: &[impl AsRef<str>],
preferences: Option<&GetOptionsPreferences>,
) -> std::result::Result<Vec<String>, String> {
self.current_provider
.read()
.unwrap()
.get_filtered_feature_names(feature_names, preferences)
}
fn get_options(
&self,
key: &str,
feature_names: &[impl AsRef<str>],
) -> std::result::Result<serde_json::Value, String> {
self.current_provider
.read()
.unwrap()
.get_options(key, feature_names)
}
fn get_options_with_preferences(
&self,
key: &str,
feature_names: &[impl AsRef<str>],
cache_options: Option<&CacheOptions>,
preferences: Option<&GetOptionsPreferences>,
) -> std::result::Result<serde_json::Value, String> {
self.current_provider
.read()
.unwrap()
.get_options_with_preferences(key, feature_names, cache_options, preferences)
}
fn has_conditions(&self, canonical_feature_name: &str) -> bool {
self.current_provider
.read()
.unwrap()
.has_conditions(canonical_feature_name)
}
fn map_feature_names(
&self,
feature_names: &[impl AsRef<str>],
preferences: Option<&GetOptionsPreferences>,
) -> std::result::Result<Vec<Option<String>>, String> {
self.current_provider
.read()
.unwrap()
.map_feature_names(feature_names, preferences)
}
}