Skip to main content

mockforge_plugin_registry/
hot_reload.rs

1//! Hot reloading support for plugins
2
3use 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
11/// Hot reload manager
12pub struct HotReloadManager {
13    /// Loaded plugins
14    plugins: Arc<RwLock<HashMap<String, LoadedPlugin>>>,
15
16    /// File watchers
17    watchers: Arc<RwLock<HashMap<String, FileWatcher>>>,
18
19    /// Configuration
20    config: HotReloadConfig,
21}
22
23/// Loaded plugin information
24#[derive(Debug, Clone)]
25struct LoadedPlugin {
26    /// Plugin name
27    name: String,
28
29    /// Plugin path
30    path: PathBuf,
31
32    /// Last modification time
33    last_modified: SystemTime,
34
35    /// Load count (for debugging)
36    load_count: u32,
37
38    /// Current version
39    version: String,
40}
41
42/// File watcher for detecting changes
43#[derive(Debug)]
44struct FileWatcher {
45    path: PathBuf,
46    last_check: SystemTime,
47    last_modified: SystemTime,
48}
49
50/// Hot reload configuration
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct HotReloadConfig {
53    /// Enable hot reloading
54    pub enabled: bool,
55
56    /// Check interval in seconds
57    pub check_interval: u64,
58
59    /// Debounce delay in milliseconds
60    pub debounce_delay: u64,
61
62    /// Auto-reload on file change
63    pub auto_reload: bool,
64
65    /// Watch subdirectories
66    pub watch_recursive: bool,
67
68    /// File patterns to watch (e.g., "*.so", "*.wasm")
69    pub watch_patterns: Vec<String>,
70
71    /// Exclude patterns
72    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/// Reload event
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ReloadEvent {
97    /// Plugin name
98    pub plugin_name: String,
99
100    /// Event type
101    pub event_type: ReloadEventType,
102
103    /// Timestamp
104    pub timestamp: String,
105
106    /// Old version
107    pub old_version: Option<String>,
108
109    /// New version
110    pub new_version: Option<String>,
111}
112
113/// Reload event type
114#[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    /// Create a new hot reload manager
126    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    /// Register a plugin for hot reloading
135    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        // Register plugin
145        {
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        // Register file watcher
163        {
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    /// Unregister a plugin
182    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    /// Check for file changes
201    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            // Check if enough time has passed since last check
217            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            // Check file modification time
226            if let Ok(metadata) = std::fs::metadata(&watcher.path) {
227                if let Ok(modified) = metadata.modified() {
228                    if modified > watcher.last_modified {
229                        // File has been modified
230                        // Apply debounce delay
231                        if let Ok(elapsed) = now.duration_since(modified) {
232                            if elapsed < Duration::from_millis(self.config.debounce_delay) {
233                                // Still within debounce period
234                                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    /// Reload a plugin
249    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        // Update last modified time
263        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    /// Get plugin info
279    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    /// List all registered plugins
299    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    /// Start watching for changes (background task)
318    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/// Plugin information
345#[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/// Hot reload statistics
355#[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, // Check immediately
411            debounce_delay: 0, // No debounce
412            ..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        // Wait a bit
423        std::thread::sleep(Duration::from_millis(100));
424
425        // Modify file
426        writeln!(file, "modified content").unwrap();
427        file.sync_all().unwrap();
428        drop(file);
429
430        // Wait a bit more to ensure modification time updates
431        std::thread::sleep(Duration::from_millis(100));
432
433        let _changed = manager.check_for_changes().unwrap();
434        // Note: This test may be flaky due to filesystem timing
435        // In a real implementation, we'd use a proper file watcher library
436    }
437}