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};
9
10/// Hot reload manager
11pub struct HotReloadManager {
12    /// Loaded plugins
13    plugins: Arc<RwLock<HashMap<String, LoadedPlugin>>>,
14
15    /// File watchers
16    watchers: Arc<RwLock<HashMap<String, FileWatcher>>>,
17
18    /// Configuration
19    config: HotReloadConfig,
20}
21
22/// Loaded plugin information
23#[derive(Debug, Clone)]
24struct LoadedPlugin {
25    /// Plugin name
26    name: String,
27
28    /// Plugin path
29    path: PathBuf,
30
31    /// Last modification time
32    last_modified: SystemTime,
33
34    /// Load count (for debugging)
35    load_count: u32,
36
37    /// Current version
38    version: String,
39}
40
41/// File watcher for detecting changes
42#[derive(Debug)]
43struct FileWatcher {
44    path: PathBuf,
45    last_check: SystemTime,
46    last_modified: SystemTime,
47}
48
49/// Hot reload configuration
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct HotReloadConfig {
52    /// Enable hot reloading
53    pub enabled: bool,
54
55    /// Check interval in seconds
56    pub check_interval: u64,
57
58    /// Debounce delay in milliseconds
59    pub debounce_delay: u64,
60
61    /// Auto-reload on file change
62    pub auto_reload: bool,
63
64    /// Watch subdirectories
65    pub watch_recursive: bool,
66
67    /// File patterns to watch (e.g., "*.so", "*.wasm")
68    pub watch_patterns: Vec<String>,
69
70    /// Exclude patterns
71    pub exclude_patterns: Vec<String>,
72}
73
74impl Default for HotReloadConfig {
75    fn default() -> Self {
76        Self {
77            enabled: true,
78            check_interval: 2,
79            debounce_delay: 500,
80            auto_reload: true,
81            watch_recursive: false,
82            watch_patterns: vec![
83                "*.so".to_string(),
84                "*.dylib".to_string(),
85                "*.dll".to_string(),
86                "*.wasm".to_string(),
87            ],
88            exclude_patterns: vec!["*.tmp".to_string(), "*.swp".to_string(), "*~".to_string()],
89        }
90    }
91}
92
93/// Reload event
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ReloadEvent {
96    /// Plugin name
97    pub plugin_name: String,
98
99    /// Event type
100    pub event_type: ReloadEventType,
101
102    /// Timestamp
103    pub timestamp: String,
104
105    /// Old version
106    pub old_version: Option<String>,
107
108    /// New version
109    pub new_version: Option<String>,
110}
111
112/// Reload event type
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case")]
115pub enum ReloadEventType {
116    FileChanged,
117    ReloadStarted,
118    ReloadCompleted,
119    ReloadFailed { error: String },
120    PluginUnloaded,
121}
122
123impl HotReloadManager {
124    /// Create a new hot reload manager
125    pub fn new(config: HotReloadConfig) -> Self {
126        Self {
127            plugins: Arc::new(RwLock::new(HashMap::new())),
128            watchers: Arc::new(RwLock::new(HashMap::new())),
129            config,
130        }
131    }
132
133    /// Register a plugin for hot reloading
134    pub fn register_plugin(&self, name: &str, path: &Path, version: &str) -> Result<()> {
135        if !self.config.enabled {
136            return Ok(());
137        }
138
139        let last_modified = std::fs::metadata(path)
140            .and_then(|m| m.modified())
141            .map_err(|e| RegistryError::Storage(format!("Failed to get file metadata: {}", e)))?;
142
143        // Register plugin
144        {
145            let mut plugins = self.plugins.write().map_err(|e| {
146                RegistryError::Storage(format!("Failed to acquire write lock: {}", e))
147            })?;
148
149            plugins.insert(
150                name.to_string(),
151                LoadedPlugin {
152                    name: name.to_string(),
153                    path: path.to_path_buf(),
154                    last_modified,
155                    load_count: 1,
156                    version: version.to_string(),
157                },
158            );
159        }
160
161        // Register file watcher
162        {
163            let mut watchers = self.watchers.write().map_err(|e| {
164                RegistryError::Storage(format!("Failed to acquire write lock: {}", e))
165            })?;
166
167            watchers.insert(
168                name.to_string(),
169                FileWatcher {
170                    path: path.to_path_buf(),
171                    last_check: SystemTime::now(),
172                    last_modified,
173                },
174            );
175        }
176
177        Ok(())
178    }
179
180    /// Unregister a plugin
181    pub fn unregister_plugin(&self, name: &str) -> Result<()> {
182        {
183            let mut plugins = self.plugins.write().map_err(|e| {
184                RegistryError::Storage(format!("Failed to acquire write lock: {}", e))
185            })?;
186            plugins.remove(name);
187        }
188
189        {
190            let mut watchers = self.watchers.write().map_err(|e| {
191                RegistryError::Storage(format!("Failed to acquire write lock: {}", e))
192            })?;
193            watchers.remove(name);
194        }
195
196        Ok(())
197    }
198
199    /// Check for file changes
200    pub fn check_for_changes(&self) -> Result<Vec<String>> {
201        if !self.config.enabled {
202            return Ok(vec![]);
203        }
204
205        let mut changed_plugins = Vec::new();
206
207        let mut watchers = self
208            .watchers
209            .write()
210            .map_err(|e| RegistryError::Storage(format!("Failed to acquire write lock: {}", e)))?;
211
212        let now = SystemTime::now();
213
214        for (name, watcher) in watchers.iter_mut() {
215            // Check if enough time has passed since last check
216            if let Ok(elapsed) = now.duration_since(watcher.last_check) {
217                if elapsed < Duration::from_secs(self.config.check_interval) {
218                    continue;
219                }
220            }
221
222            watcher.last_check = now;
223
224            // Check file modification time
225            if let Ok(metadata) = std::fs::metadata(&watcher.path) {
226                if let Ok(modified) = metadata.modified() {
227                    if modified > watcher.last_modified {
228                        // File has been modified
229                        // Apply debounce delay
230                        if let Ok(elapsed) = now.duration_since(modified) {
231                            if elapsed < Duration::from_millis(self.config.debounce_delay) {
232                                // Still within debounce period
233                                continue;
234                            }
235                        }
236
237                        watcher.last_modified = modified;
238                        changed_plugins.push(name.clone());
239                    }
240                }
241            }
242        }
243
244        Ok(changed_plugins)
245    }
246
247    /// Reload a plugin
248    pub fn reload_plugin(&self, name: &str) -> Result<ReloadEvent> {
249        let mut plugins = self
250            .plugins
251            .write()
252            .map_err(|e| RegistryError::Storage(format!("Failed to acquire write lock: {}", e)))?;
253
254        let plugin = plugins.get_mut(name).ok_or_else(|| {
255            RegistryError::PluginNotFound(format!("Plugin not registered: {}", name))
256        })?;
257
258        let old_version = plugin.version.clone();
259        plugin.load_count += 1;
260
261        // Update last modified time
262        if let Ok(metadata) = std::fs::metadata(&plugin.path) {
263            if let Ok(modified) = metadata.modified() {
264                plugin.last_modified = modified;
265            }
266        }
267
268        Ok(ReloadEvent {
269            plugin_name: name.to_string(),
270            event_type: ReloadEventType::ReloadCompleted,
271            timestamp: chrono::Utc::now().to_rfc3339(),
272            old_version: Some(old_version),
273            new_version: Some(plugin.version.clone()),
274        })
275    }
276
277    /// Get plugin info
278    pub fn get_plugin_info(&self, name: &str) -> Result<PluginInfo> {
279        let plugins = self
280            .plugins
281            .read()
282            .map_err(|e| RegistryError::Storage(format!("Failed to acquire read lock: {}", e)))?;
283
284        let plugin = plugins
285            .get(name)
286            .ok_or_else(|| RegistryError::PluginNotFound(name.to_string()))?;
287
288        Ok(PluginInfo {
289            name: plugin.name.clone(),
290            path: plugin.path.clone(),
291            version: plugin.version.clone(),
292            load_count: plugin.load_count,
293            last_modified: plugin.last_modified,
294        })
295    }
296
297    /// List all registered plugins
298    pub fn list_plugins(&self) -> Result<Vec<PluginInfo>> {
299        let plugins = self
300            .plugins
301            .read()
302            .map_err(|e| RegistryError::Storage(format!("Failed to acquire read lock: {}", e)))?;
303
304        Ok(plugins
305            .values()
306            .map(|p| PluginInfo {
307                name: p.name.clone(),
308                path: p.path.clone(),
309                version: p.version.clone(),
310                load_count: p.load_count,
311                last_modified: p.last_modified,
312            })
313            .collect())
314    }
315
316    /// Start watching for changes (background task)
317    pub async fn start_watching<F>(&self, mut callback: F) -> Result<()>
318    where
319        F: FnMut(Vec<String>) + Send + 'static,
320    {
321        if !self.config.enabled || !self.config.auto_reload {
322            return Ok(());
323        }
324
325        let check_interval = Duration::from_secs(self.config.check_interval);
326
327        loop {
328            tokio::time::sleep(check_interval).await;
329
330            match self.check_for_changes() {
331                Ok(changed) if !changed.is_empty() => {
332                    callback(changed);
333                }
334                Err(e) => {
335                    eprintln!("Error checking for changes: {}", e);
336                }
337                _ => {}
338            }
339        }
340    }
341}
342
343/// Plugin information
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct PluginInfo {
346    pub name: String,
347    pub path: PathBuf,
348    pub version: String,
349    pub load_count: u32,
350    pub last_modified: SystemTime,
351}
352
353/// Hot reload statistics
354#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct HotReloadStats {
356    pub total_plugins: usize,
357    pub total_reloads: u64,
358    pub failed_reloads: u64,
359    pub average_reload_time_ms: f64,
360    pub last_reload: Option<String>,
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use std::fs::File;
367    use std::io::Write;
368    use tempfile::TempDir;
369
370    #[test]
371    fn test_hot_reload_registration() {
372        let config = HotReloadConfig::default();
373        let manager = HotReloadManager::new(config);
374
375        let temp_dir = TempDir::new().unwrap();
376        let plugin_path = temp_dir.path().join("plugin.so");
377        File::create(&plugin_path).unwrap();
378
379        let result = manager.register_plugin("test-plugin", &plugin_path, "1.0.0");
380        assert!(result.is_ok());
381
382        let info = manager.get_plugin_info("test-plugin");
383        assert!(info.is_ok());
384        let info = info.unwrap();
385        assert_eq!(info.name, "test-plugin");
386        assert_eq!(info.version, "1.0.0");
387        assert_eq!(info.load_count, 1);
388    }
389
390    #[test]
391    fn test_hot_reload_unregister() {
392        let config = HotReloadConfig::default();
393        let manager = HotReloadManager::new(config);
394
395        let temp_dir = TempDir::new().unwrap();
396        let plugin_path = temp_dir.path().join("plugin.so");
397        File::create(&plugin_path).unwrap();
398
399        manager.register_plugin("test-plugin", &plugin_path, "1.0.0").unwrap();
400        manager.unregister_plugin("test-plugin").unwrap();
401
402        let info = manager.get_plugin_info("test-plugin");
403        assert!(info.is_err());
404    }
405
406    #[test]
407    fn test_change_detection() {
408        let config = HotReloadConfig {
409            check_interval: 0, // Check immediately
410            debounce_delay: 0, // No debounce
411            ..Default::default()
412        };
413        let manager = HotReloadManager::new(config);
414
415        let temp_dir = TempDir::new().unwrap();
416        let plugin_path = temp_dir.path().join("plugin.so");
417        let mut file = File::create(&plugin_path).unwrap();
418
419        manager.register_plugin("test-plugin", &plugin_path, "1.0.0").unwrap();
420
421        // Wait a bit
422        std::thread::sleep(Duration::from_millis(100));
423
424        // Modify file
425        writeln!(file, "modified content").unwrap();
426        file.sync_all().unwrap();
427        drop(file);
428
429        // Wait a bit more to ensure modification time updates
430        std::thread::sleep(Duration::from_millis(100));
431
432        let _changed = manager.check_for_changes().unwrap();
433        // Note: This test may be flaky due to filesystem timing
434        // In a real implementation, we'd use a proper file watcher library
435    }
436}