#![allow(dead_code)]
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use crate::utils::hooks::hooks_config_snapshot::get_hooks_config_from_snapshot;
#[derive(Debug, Clone)]
pub enum FileEvent {
Change,
Add,
Unlink,
}
impl std::fmt::Display for FileEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FileEvent::Change => write!(f, "change"),
FileEvent::Add => write!(f, "add"),
FileEvent::Unlink => write!(f, "unlink"),
}
}
}
pub struct HookOutsideReplResult {
pub results: Vec<HookResult>,
pub watch_paths: Vec<String>,
pub system_messages: Vec<String>,
}
pub struct HookResult {
pub succeeded: bool,
pub output: Option<String>,
}
struct FileWatcherState {
watched_paths: Vec<String>,
current_cwd: String,
dynamic_watch_paths: Vec<String>,
dynamic_watch_paths_sorted: Vec<String>,
initialized: bool,
has_env_hooks: bool,
notify_callback: Option<Box<dyn Fn(String, bool) + Send + Sync>>,
}
impl FileWatcherState {
fn new() -> Self {
Self {
watched_paths: Vec::new(),
current_cwd: String::new(),
dynamic_watch_paths: Vec::new(),
dynamic_watch_paths_sorted: Vec::new(),
initialized: false,
has_env_hooks: false,
notify_callback: None,
}
}
}
lazy_static::lazy_static! {
static ref FILE_WATCHER_STATE: Arc<Mutex<FileWatcherState>> = Arc::new(Mutex::new(
FileWatcherState::new()
));
}
pub fn set_env_hook_notifier(cb: Option<Box<dyn Fn(String, bool) + Send + Sync>>) {
let mut state = FILE_WATCHER_STATE.lock().unwrap();
state.notify_callback = cb;
}
pub fn initialize_file_changed_watcher(cwd: &str) {
{
let state = FILE_WATCHER_STATE.lock().unwrap();
if state.initialized {
return;
}
}
let config = get_hooks_config_from_snapshot();
let has_env_hooks = {
let cwd_changed_len = config
.as_ref()
.and_then(|c| c.events.get("CwdChanged"))
.map(|m| m.len())
.unwrap_or(0);
let file_changed_len = config
.as_ref()
.and_then(|c| c.events.get("FileChanged"))
.map(|m| m.len())
.unwrap_or(0);
cwd_changed_len > 0 || file_changed_len > 0
};
{
let mut state = FILE_WATCHER_STATE.lock().unwrap();
state.initialized = true;
state.current_cwd = cwd.to_string();
state.has_env_hooks = has_env_hooks;
}
if has_env_hooks {
log_for_debugging("FileChanged: registered cleanup for file watcher");
}
let paths = resolve_watch_paths();
if paths.is_empty() {
return;
}
start_watching(&paths);
}
fn resolve_watch_paths() -> Vec<String> {
let state = FILE_WATCHER_STATE.lock().unwrap();
let cwd = state.current_cwd.clone();
let dynamic_paths = state.dynamic_watch_paths.clone();
drop(state);
let config = get_hooks_config_from_snapshot();
let matchers = config
.as_ref()
.and_then(|c| c.events.get("FileChanged"))
.cloned()
.unwrap_or_default();
let mut static_paths: HashSet<String> = HashSet::new();
for matcher in &matchers {
let matcher_str = matcher.matcher.as_deref().unwrap_or("");
if matcher_str.is_empty() {
continue;
}
for name in matcher_str.split('|').map(|s: &str| s.trim()) {
if name.is_empty() {
continue;
}
let path = Path::new(name);
let full_path = if path.is_absolute() {
path.to_path_buf()
} else {
PathBuf::from(&cwd).join(name)
};
static_paths.insert(full_path.to_string_lossy().to_string());
}
}
let mut all_paths: Vec<String> = static_paths.into_iter().collect();
for p in dynamic_paths {
if !all_paths.contains(&p) {
all_paths.push(p);
}
}
all_paths
}
fn start_watching(paths: &[String]) {
log_for_debugging(&format!(
"FileChanged: watching {} paths (polling mode)",
paths.len()
));
{
let mut state = FILE_WATCHER_STATE.lock().unwrap();
state.watched_paths = paths.to_vec();
}
}
fn handle_file_event(path: &str, event: &FileEvent) {
log_for_debugging(&format!("FileChanged: {} {}", event, path));
let path_clone = path.to_string();
let event_clone = event.clone();
tokio::spawn(async move {
match execute_file_changed_hooks(&path_clone, &event_clone).await {
Ok(result) => {
if !result.watch_paths.is_empty() {
update_watch_paths(&result.watch_paths);
}
for msg in result.system_messages {
notify_callback_inner(&msg, false);
}
for r in result.results {
if !r.succeeded {
if let Some(output) = r.output {
notify_callback_inner(&output, true);
}
}
}
}
Err(e) => {
let msg = format!("FileChanged hook failed: {}", e);
log_for_debugging(&msg);
notify_callback_inner(&msg, true);
}
}
});
}
fn notify_callback_inner(text: &str, is_error: bool) {
let state = FILE_WATCHER_STATE.lock().unwrap();
if let Some(ref cb) = state.notify_callback {
cb(text.to_string(), is_error);
}
}
pub fn update_watch_paths(paths: &[String]) {
let mut state = FILE_WATCHER_STATE.lock().unwrap();
if !state.initialized {
return;
}
let mut sorted = paths.to_vec();
sorted.sort();
if sorted.len() == state.dynamic_watch_paths_sorted.len()
&& sorted
.iter()
.zip(state.dynamic_watch_paths_sorted.iter())
.all(|(a, b)| a == b)
{
return;
}
state.dynamic_watch_paths = paths.to_vec();
state.dynamic_watch_paths_sorted = sorted;
drop(state);
restart_watching();
}
fn restart_watching() {
let paths = resolve_watch_paths();
if !paths.is_empty() {
start_watching(&paths);
}
}
pub async fn on_cwd_changed_for_hooks(
old_cwd: &str,
new_cwd: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if old_cwd == new_cwd {
return Ok(());
}
let config = get_hooks_config_from_snapshot();
let current_has_env_hooks = {
let cwd_changed_len = config
.as_ref()
.and_then(|c| c.events.get("CwdChanged"))
.map(|m| m.len())
.unwrap_or(0);
let file_changed_len = config
.as_ref()
.and_then(|c| c.events.get("FileChanged"))
.map(|m| m.len())
.unwrap_or(0);
cwd_changed_len > 0 || file_changed_len > 0
};
if !current_has_env_hooks {
return Ok(());
}
{
let mut state = FILE_WATCHER_STATE.lock().unwrap();
state.current_cwd = new_cwd.to_string();
}
let hook_result = execute_cwd_changed_hooks(old_cwd, new_cwd)
.await
.unwrap_or_else(|e| {
let msg = format!("CwdChanged hook failed: {}", e);
log_for_debugging(&msg);
notify_callback_inner(&msg, true);
HookOutsideReplResult {
results: Vec::new(),
watch_paths: Vec::new(),
system_messages: Vec::new(),
}
});
{
let mut state = FILE_WATCHER_STATE.lock().unwrap();
state.dynamic_watch_paths = hook_result.watch_paths.clone();
let mut sorted = hook_result.watch_paths.clone();
sorted.sort();
state.dynamic_watch_paths_sorted = sorted;
}
for msg in &hook_result.system_messages {
notify_callback_inner(msg, false);
}
for r in &hook_result.results {
if !r.succeeded {
if let Some(ref output) = r.output {
notify_callback_inner(output, true);
}
}
}
{
let state = FILE_WATCHER_STATE.lock().unwrap();
if state.initialized {
drop(state);
restart_watching();
}
}
Ok(())
}
async fn execute_file_changed_hooks(
_path: &str,
_event: &FileEvent,
) -> Result<HookOutsideReplResult, Box<dyn std::error::Error + Send + Sync>> {
Ok(HookOutsideReplResult {
results: Vec::new(),
watch_paths: Vec::new(),
system_messages: Vec::new(),
})
}
async fn execute_cwd_changed_hooks(
_old_cwd: &str,
_new_cwd: &str,
) -> Result<HookOutsideReplResult, Box<dyn std::error::Error + Send + Sync>> {
Ok(HookOutsideReplResult {
results: Vec::new(),
watch_paths: Vec::new(),
system_messages: Vec::new(),
})
}
fn dispose() {
let mut state = FILE_WATCHER_STATE.lock().unwrap();
state.watched_paths.clear();
state.dynamic_watch_paths.clear();
state.dynamic_watch_paths_sorted.clear();
state.initialized = false;
state.has_env_hooks = false;
state.notify_callback = None;
}
pub fn reset_file_changed_watcher_for_testing() {
dispose();
}
fn log_for_debugging(msg: &str) {
log::debug!("{}", msg);
}