mockforge_plugin_registry/
hot_reload.rs1use crate::{RegistryError, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::sync::{Arc, RwLock};
8use std::time::{Duration, SystemTime};
9use tracing::error;
10
11pub struct HotReloadManager {
13 plugins: Arc<RwLock<HashMap<String, LoadedPlugin>>>,
15
16 watchers: Arc<RwLock<HashMap<String, FileWatcher>>>,
18
19 config: HotReloadConfig,
21}
22
23#[derive(Debug, Clone)]
25struct LoadedPlugin {
26 name: String,
28
29 path: PathBuf,
31
32 last_modified: SystemTime,
34
35 load_count: u32,
37
38 version: String,
40}
41
42#[derive(Debug)]
44struct FileWatcher {
45 path: PathBuf,
46 last_check: SystemTime,
47 last_modified: SystemTime,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct HotReloadConfig {
53 pub enabled: bool,
55
56 pub check_interval: u64,
58
59 pub debounce_delay: u64,
61
62 pub auto_reload: bool,
64
65 pub watch_recursive: bool,
67
68 pub watch_patterns: Vec<String>,
70
71 pub exclude_patterns: Vec<String>,
73}
74
75impl Default for HotReloadConfig {
76 fn default() -> Self {
77 Self {
78 enabled: true,
79 check_interval: 2,
80 debounce_delay: 500,
81 auto_reload: true,
82 watch_recursive: false,
83 watch_patterns: vec![
84 "*.so".to_string(),
85 "*.dylib".to_string(),
86 "*.dll".to_string(),
87 "*.wasm".to_string(),
88 ],
89 exclude_patterns: vec!["*.tmp".to_string(), "*.swp".to_string(), "*~".to_string()],
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ReloadEvent {
97 pub plugin_name: String,
99
100 pub event_type: ReloadEventType,
102
103 pub timestamp: String,
105
106 pub old_version: Option<String>,
108
109 pub new_version: Option<String>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum ReloadEventType {
117 FileChanged,
118 ReloadStarted,
119 ReloadCompleted,
120 ReloadFailed { error: String },
121 PluginUnloaded,
122}
123
124impl HotReloadManager {
125 pub fn new(config: HotReloadConfig) -> Self {
127 Self {
128 plugins: Arc::new(RwLock::new(HashMap::new())),
129 watchers: Arc::new(RwLock::new(HashMap::new())),
130 config,
131 }
132 }
133
134 pub fn register_plugin(&self, name: &str, path: &Path, version: &str) -> Result<()> {
136 if !self.config.enabled {
137 return Ok(());
138 }
139
140 let last_modified = std::fs::metadata(path)
141 .and_then(|m| m.modified())
142 .map_err(|e| RegistryError::Storage(format!("Failed to get file metadata: {}", e)))?;
143
144 {
146 let mut plugins = self.plugins.write().map_err(|e| {
147 RegistryError::Storage(format!("Failed to acquire write lock: {}", e))
148 })?;
149
150 plugins.insert(
151 name.to_string(),
152 LoadedPlugin {
153 name: name.to_string(),
154 path: path.to_path_buf(),
155 last_modified,
156 load_count: 1,
157 version: version.to_string(),
158 },
159 );
160 }
161
162 {
164 let mut watchers = self.watchers.write().map_err(|e| {
165 RegistryError::Storage(format!("Failed to acquire write lock: {}", e))
166 })?;
167
168 watchers.insert(
169 name.to_string(),
170 FileWatcher {
171 path: path.to_path_buf(),
172 last_check: SystemTime::now(),
173 last_modified,
174 },
175 );
176 }
177
178 Ok(())
179 }
180
181 pub fn unregister_plugin(&self, name: &str) -> Result<()> {
183 {
184 let mut plugins = self.plugins.write().map_err(|e| {
185 RegistryError::Storage(format!("Failed to acquire write lock: {}", e))
186 })?;
187 plugins.remove(name);
188 }
189
190 {
191 let mut watchers = self.watchers.write().map_err(|e| {
192 RegistryError::Storage(format!("Failed to acquire write lock: {}", e))
193 })?;
194 watchers.remove(name);
195 }
196
197 Ok(())
198 }
199
200 pub fn check_for_changes(&self) -> Result<Vec<String>> {
202 if !self.config.enabled {
203 return Ok(vec![]);
204 }
205
206 let mut changed_plugins = Vec::new();
207
208 let mut watchers = self
209 .watchers
210 .write()
211 .map_err(|e| RegistryError::Storage(format!("Failed to acquire write lock: {}", e)))?;
212
213 let now = SystemTime::now();
214
215 for (name, watcher) in watchers.iter_mut() {
216 if let Ok(elapsed) = now.duration_since(watcher.last_check) {
218 if elapsed < Duration::from_secs(self.config.check_interval) {
219 continue;
220 }
221 }
222
223 watcher.last_check = now;
224
225 if let Ok(metadata) = std::fs::metadata(&watcher.path) {
227 if let Ok(modified) = metadata.modified() {
228 if modified > watcher.last_modified {
229 if let Ok(elapsed) = now.duration_since(modified) {
232 if elapsed < Duration::from_millis(self.config.debounce_delay) {
233 continue;
235 }
236 }
237
238 watcher.last_modified = modified;
239 changed_plugins.push(name.clone());
240 }
241 }
242 }
243 }
244
245 Ok(changed_plugins)
246 }
247
248 pub fn reload_plugin(&self, name: &str) -> Result<ReloadEvent> {
250 let mut plugins = self
251 .plugins
252 .write()
253 .map_err(|e| RegistryError::Storage(format!("Failed to acquire write lock: {}", e)))?;
254
255 let plugin = plugins.get_mut(name).ok_or_else(|| {
256 RegistryError::PluginNotFound(format!("Plugin not registered: {}", name))
257 })?;
258
259 let old_version = plugin.version.clone();
260 plugin.load_count += 1;
261
262 if let Ok(metadata) = std::fs::metadata(&plugin.path) {
264 if let Ok(modified) = metadata.modified() {
265 plugin.last_modified = modified;
266 }
267 }
268
269 Ok(ReloadEvent {
270 plugin_name: name.to_string(),
271 event_type: ReloadEventType::ReloadCompleted,
272 timestamp: chrono::Utc::now().to_rfc3339(),
273 old_version: Some(old_version),
274 new_version: Some(plugin.version.clone()),
275 })
276 }
277
278 pub fn get_plugin_info(&self, name: &str) -> Result<PluginInfo> {
280 let plugins = self
281 .plugins
282 .read()
283 .map_err(|e| RegistryError::Storage(format!("Failed to acquire read lock: {}", e)))?;
284
285 let plugin = plugins
286 .get(name)
287 .ok_or_else(|| RegistryError::PluginNotFound(name.to_string()))?;
288
289 Ok(PluginInfo {
290 name: plugin.name.clone(),
291 path: plugin.path.clone(),
292 version: plugin.version.clone(),
293 load_count: plugin.load_count,
294 last_modified: plugin.last_modified,
295 })
296 }
297
298 pub fn list_plugins(&self) -> Result<Vec<PluginInfo>> {
300 let plugins = self
301 .plugins
302 .read()
303 .map_err(|e| RegistryError::Storage(format!("Failed to acquire read lock: {}", e)))?;
304
305 Ok(plugins
306 .values()
307 .map(|p| PluginInfo {
308 name: p.name.clone(),
309 path: p.path.clone(),
310 version: p.version.clone(),
311 load_count: p.load_count,
312 last_modified: p.last_modified,
313 })
314 .collect())
315 }
316
317 pub async fn start_watching<F>(&self, mut callback: F) -> Result<()>
319 where
320 F: FnMut(Vec<String>) + Send + 'static,
321 {
322 if !self.config.enabled || !self.config.auto_reload {
323 return Ok(());
324 }
325
326 let check_interval = Duration::from_secs(self.config.check_interval);
327
328 loop {
329 tokio::time::sleep(check_interval).await;
330
331 match self.check_for_changes() {
332 Ok(changed) if !changed.is_empty() => {
333 callback(changed);
334 }
335 Err(e) => {
336 error!("Error checking for changes: {}", e);
337 }
338 _ => {}
339 }
340 }
341 }
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct PluginInfo {
347 pub name: String,
348 pub path: PathBuf,
349 pub version: String,
350 pub load_count: u32,
351 pub last_modified: SystemTime,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct HotReloadStats {
357 pub total_plugins: usize,
358 pub total_reloads: u64,
359 pub failed_reloads: u64,
360 pub average_reload_time_ms: f64,
361 pub last_reload: Option<String>,
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use std::fs::File;
368 use std::io::Write;
369 use tempfile::TempDir;
370
371 #[test]
372 fn test_hot_reload_registration() {
373 let config = HotReloadConfig::default();
374 let manager = HotReloadManager::new(config);
375
376 let temp_dir = TempDir::new().unwrap();
377 let plugin_path = temp_dir.path().join("plugin.so");
378 File::create(&plugin_path).unwrap();
379
380 let result = manager.register_plugin("test-plugin", &plugin_path, "1.0.0");
381 assert!(result.is_ok());
382
383 let info = manager.get_plugin_info("test-plugin");
384 assert!(info.is_ok());
385 let info = info.unwrap();
386 assert_eq!(info.name, "test-plugin");
387 assert_eq!(info.version, "1.0.0");
388 assert_eq!(info.load_count, 1);
389 }
390
391 #[test]
392 fn test_hot_reload_unregister() {
393 let config = HotReloadConfig::default();
394 let manager = HotReloadManager::new(config);
395
396 let temp_dir = TempDir::new().unwrap();
397 let plugin_path = temp_dir.path().join("plugin.so");
398 File::create(&plugin_path).unwrap();
399
400 manager.register_plugin("test-plugin", &plugin_path, "1.0.0").unwrap();
401 manager.unregister_plugin("test-plugin").unwrap();
402
403 let info = manager.get_plugin_info("test-plugin");
404 assert!(info.is_err());
405 }
406
407 #[test]
408 fn test_change_detection() {
409 let config = HotReloadConfig {
410 check_interval: 0, debounce_delay: 0, ..Default::default()
413 };
414 let manager = HotReloadManager::new(config);
415
416 let temp_dir = TempDir::new().unwrap();
417 let plugin_path = temp_dir.path().join("plugin.so");
418 let mut file = File::create(&plugin_path).unwrap();
419
420 manager.register_plugin("test-plugin", &plugin_path, "1.0.0").unwrap();
421
422 std::thread::sleep(Duration::from_millis(100));
424
425 writeln!(file, "modified content").unwrap();
427 file.sync_all().unwrap();
428 drop(file);
429
430 std::thread::sleep(Duration::from_millis(100));
432
433 let _changed = manager.check_for_changes().unwrap();
434 }
437}