use crate::{Result, TensorError};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver, TryRecvError};
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone)]
pub enum WatchEvent {
Modified(PathBuf),
Error(String),
}
#[derive(Debug)]
pub struct ConfigWatcher {
file_path: PathBuf,
last_modified: Option<SystemTime>,
#[allow(dead_code)]
receiver: Option<Receiver<WatchEvent>>,
poll_interval: Duration,
last_poll: SystemTime,
}
impl ConfigWatcher {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
let file_path = path.as_ref().to_path_buf();
if !file_path.exists() {
return Err(TensorError::invalid_argument(format!(
"Configuration file does not exist: {}",
file_path.display()
)));
}
let last_modified = std::fs::metadata(&file_path)
.map_err(|e| {
TensorError::invalid_argument(format!(
"Failed to get file metadata for {}: {}",
file_path.display(),
e
))
})?
.modified()
.ok();
Ok(Self {
file_path,
last_modified,
receiver: None,
poll_interval: Duration::from_secs(1),
last_poll: SystemTime::now(),
})
}
pub fn with_poll_interval<P: AsRef<Path>>(path: P, interval: Duration) -> Result<Self> {
let mut watcher = Self::new(path)?;
watcher.poll_interval = interval;
Ok(watcher)
}
pub fn path(&self) -> &Path {
&self.file_path
}
pub fn poll_interval(&self) -> Duration {
self.poll_interval
}
pub fn set_poll_interval(&mut self, interval: Duration) {
self.poll_interval = interval;
}
pub fn check_changes(&mut self) -> Result<Option<WatchEvent>> {
let now = SystemTime::now();
if now
.duration_since(self.last_poll)
.unwrap_or(Duration::from_secs(0))
< self.poll_interval
{
return Ok(None);
}
self.last_poll = now;
if !self.file_path.exists() {
return Ok(Some(WatchEvent::Error(format!(
"Configuration file no longer exists: {}",
self.file_path.display()
))));
}
let current_modified = std::fs::metadata(&self.file_path)
.map_err(|e| {
TensorError::invalid_argument(format!(
"Failed to get file metadata for {}: {}",
self.file_path.display(),
e
))
})?
.modified()
.ok();
if current_modified != self.last_modified {
self.last_modified = current_modified;
return Ok(Some(WatchEvent::Modified(self.file_path.clone())));
}
if let Some(ref receiver) = self.receiver {
match receiver.try_recv() {
Ok(event) => return Ok(Some(event)),
Err(TryRecvError::Empty) => {} Err(TryRecvError::Disconnected) => {
return Ok(Some(WatchEvent::Error(
"File watcher disconnected".to_string(),
)));
}
}
}
Ok(None)
}
pub fn start_native_watching(&mut self) -> Result<()> {
self.start_polling_watching()
}
pub fn start_polling_watching(&mut self) -> Result<()> {
Ok(())
}
pub fn stop_watching(&mut self) {
self.receiver = None;
}
pub fn is_watching(&self) -> bool {
self.receiver.is_some()
}
pub fn file_info(&self) -> Result<FileInfo> {
let metadata = std::fs::metadata(&self.file_path).map_err(|e| {
TensorError::invalid_argument(format!(
"Failed to get file metadata for {}: {}",
self.file_path.display(),
e
))
})?;
Ok(FileInfo {
path: self.file_path.clone(),
size: metadata.len(),
modified: metadata.modified().ok(),
is_file: metadata.is_file(),
is_dir: metadata.is_dir(),
})
}
pub fn wait_for_change(&mut self, timeout: Option<Duration>) -> Result<Option<WatchEvent>> {
let start_time = SystemTime::now();
loop {
if let Some(event) = self.check_changes()? {
return Ok(Some(event));
}
if let Some(timeout_duration) = timeout {
if start_time.elapsed().unwrap_or(Duration::from_secs(0)) >= timeout_duration {
return Ok(None);
}
}
std::thread::sleep(Duration::from_millis(100));
}
}
pub fn last_modified(&self) -> Option<SystemTime> {
self.last_modified
}
pub fn force_check(&mut self) -> Result<Option<WatchEvent>> {
let old_poll_time = self.last_poll;
self.last_poll = SystemTime::UNIX_EPOCH; let result = self.check_changes();
self.last_poll = old_poll_time;
result
}
}
#[derive(Debug, Clone)]
pub struct FileInfo {
pub path: PathBuf,
pub size: u64,
pub modified: Option<SystemTime>,
pub is_file: bool,
pub is_dir: bool,
}
impl FileInfo {
pub fn description(&self) -> String {
let file_type = if self.is_file {
"file"
} else if self.is_dir {
"directory"
} else {
"unknown"
};
let size_str = if self.size < 1024 {
format!("{} B", self.size)
} else if self.size < 1024 * 1024 {
format!("{:.1} KB", self.size as f64 / 1024.0)
} else {
format!("{:.1} MB", self.size as f64 / (1024.0 * 1024.0))
};
let modified_str = if let Some(modified) = self.modified {
format!("{:?}", modified)
} else {
"unknown".to_string()
};
format!(
"{} ({}, {}, modified: {})",
self.path.display(),
file_type,
size_str,
modified_str
)
}
}
#[derive(Debug)]
pub struct MultiFileWatcher {
watchers: Vec<ConfigWatcher>,
poll_interval: Duration,
}
impl MultiFileWatcher {
pub fn new() -> Self {
Self {
watchers: Vec::new(),
poll_interval: Duration::from_secs(1),
}
}
pub fn add_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let watcher = ConfigWatcher::with_poll_interval(path, self.poll_interval)?;
self.watchers.push(watcher);
Ok(())
}
pub fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> bool {
let path = path.as_ref();
if let Some(pos) = self.watchers.iter().position(|w| w.path() == path) {
self.watchers.remove(pos);
true
} else {
false
}
}
pub fn check_changes(&mut self) -> Result<Vec<WatchEvent>> {
let mut events = Vec::new();
for watcher in &mut self.watchers {
if let Some(event) = watcher.check_changes()? {
events.push(event);
}
}
Ok(events)
}
pub fn file_count(&self) -> usize {
self.watchers.len()
}
pub fn watched_paths(&self) -> Vec<&Path> {
self.watchers.iter().map(|w| w.path()).collect()
}
pub fn set_poll_interval(&mut self, interval: Duration) {
self.poll_interval = interval;
for watcher in &mut self.watchers {
watcher.set_poll_interval(interval);
}
}
pub fn file_infos(&self) -> Result<Vec<FileInfo>> {
self.watchers
.iter()
.map(|w| w.file_info())
.collect::<Result<Vec<_>>>()
}
}
impl Default for MultiFileWatcher {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_config_watcher_creation() {
let temp_file = NamedTempFile::new().expect("temp file creation should succeed");
let watcher =
ConfigWatcher::new(temp_file.path()).expect("watcher creation should succeed");
assert_eq!(watcher.path(), temp_file.path());
assert_eq!(watcher.poll_interval(), Duration::from_secs(1));
}
#[test]
fn test_nonexistent_file() {
let result = ConfigWatcher::new("/nonexistent/path/config.yaml");
assert!(result.is_err());
}
#[test]
fn test_custom_poll_interval() {
let temp_file = NamedTempFile::new().expect("temp file creation should succeed");
let interval = Duration::from_millis(500);
let watcher = ConfigWatcher::with_poll_interval(temp_file.path(), interval)
.expect("watcher creation should succeed");
assert_eq!(watcher.poll_interval(), interval);
}
#[test]
fn test_file_change_detection() {
let mut temp_file = NamedTempFile::new().expect("test: temp file creation should succeed");
let mut watcher =
ConfigWatcher::with_poll_interval(temp_file.path(), Duration::from_millis(10))
.expect("watcher creation should succeed");
let initial_check = watcher
.check_changes()
.expect("test: operation should succeed");
assert!(initial_check.is_none());
std::thread::sleep(Duration::from_millis(20));
writeln!(temp_file, "new content").expect("test: writeln should succeed");
temp_file.flush().expect("test: flush should succeed");
std::thread::sleep(Duration::from_millis(20));
let change_check = watcher
.force_check()
.expect("test: operation should succeed");
match change_check {
Some(WatchEvent::Modified(path)) => {
assert_eq!(path, temp_file.path());
}
_ => panic!("Expected Modified event"),
}
}
#[test]
fn test_file_info() {
let mut temp_file = NamedTempFile::new().expect("test: temp file creation should succeed");
writeln!(temp_file, "test content").expect("test: writeln should succeed");
temp_file.flush().expect("test: flush should succeed");
let watcher =
ConfigWatcher::new(temp_file.path()).expect("watcher creation should succeed");
let file_info = watcher.file_info().expect("test: operation should succeed");
assert_eq!(file_info.path, temp_file.path());
assert!(file_info.is_file);
assert!(!file_info.is_dir);
assert!(file_info.size > 0);
assert!(file_info.modified.is_some());
let description = file_info.description();
assert!(description.contains("file"));
assert!(description.contains("B")); }
#[test]
fn test_wait_for_change_timeout() {
let temp_file = NamedTempFile::new().expect("temp file creation should succeed");
let mut watcher =
ConfigWatcher::with_poll_interval(temp_file.path(), Duration::from_millis(10))
.expect("watcher creation should succeed");
let start_time = SystemTime::now();
let result = watcher
.wait_for_change(Some(Duration::from_millis(50)))
.expect("operation should succeed");
let elapsed = start_time
.elapsed()
.expect("test: operation should succeed");
assert!(result.is_none()); assert!(elapsed >= Duration::from_millis(50));
assert!(elapsed < Duration::from_millis(200)); }
#[test]
fn test_multi_file_watcher() {
let temp_file1 = NamedTempFile::new().expect("test: temp file creation should succeed");
let temp_file2 = NamedTempFile::new().expect("test: temp file creation should succeed");
let mut multi_watcher = MultiFileWatcher::new();
assert_eq!(multi_watcher.file_count(), 0);
multi_watcher
.add_file(temp_file1.path())
.expect("test: operation should succeed");
multi_watcher
.add_file(temp_file2.path())
.expect("test: operation should succeed");
assert_eq!(multi_watcher.file_count(), 2);
let watched_paths = multi_watcher.watched_paths();
assert!(watched_paths.contains(&temp_file1.path()));
assert!(watched_paths.contains(&temp_file2.path()));
let removed = multi_watcher.remove_file(temp_file1.path());
assert!(removed);
assert_eq!(multi_watcher.file_count(), 1);
let not_removed = multi_watcher.remove_file("/nonexistent/path");
assert!(!not_removed);
}
#[test]
fn test_multi_file_watcher_changes() {
let mut temp_file1 = NamedTempFile::new().expect("test: temp file creation should succeed");
let mut temp_file2 = NamedTempFile::new().expect("test: temp file creation should succeed");
let mut multi_watcher = MultiFileWatcher::new();
multi_watcher.set_poll_interval(Duration::from_millis(10));
multi_watcher
.add_file(temp_file1.path())
.expect("test: operation should succeed");
multi_watcher
.add_file(temp_file2.path())
.expect("test: operation should succeed");
let initial_changes = multi_watcher
.check_changes()
.expect("test: operation should succeed");
assert!(initial_changes.is_empty());
std::thread::sleep(Duration::from_millis(20));
writeln!(temp_file1, "content1").expect("test: writeln should succeed");
temp_file1.flush().expect("test: flush should succeed");
writeln!(temp_file2, "content2").expect("test: writeln should succeed");
temp_file2.flush().expect("test: flush should succeed");
std::thread::sleep(Duration::from_millis(20));
let changes = multi_watcher
.check_changes()
.expect("test: operation should succeed");
assert_eq!(changes.len(), 2);
for change in changes {
match change {
WatchEvent::Modified(path) => {
assert!(path == temp_file1.path() || path == temp_file2.path());
}
_ => panic!("Expected Modified event"),
}
}
}
#[test]
fn test_file_info_descriptions() {
let temp_file = NamedTempFile::new().expect("temp file creation should succeed");
let watcher =
ConfigWatcher::new(temp_file.path()).expect("watcher creation should succeed");
let file_info = watcher.file_info().expect("test: operation should succeed");
let description = file_info.description();
assert!(description.contains(
temp_file
.path()
.to_str()
.expect("test: operation should succeed")
));
assert!(description.contains("file"));
assert!(
description.contains("B") || description.contains("KB") || description.contains("MB")
);
assert!(description.contains("modified:"));
}
#[test]
fn test_watcher_state_management() {
let temp_file = NamedTempFile::new().expect("temp file creation should succeed");
let mut watcher =
ConfigWatcher::new(temp_file.path()).expect("test: operation should succeed");
assert!(!watcher.is_watching());
watcher
.start_polling_watching()
.expect("test: operation should succeed");
watcher.stop_watching();
assert!(!watcher.is_watching());
}
}