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};
9
10pub struct HotReloadManager {
12 plugins: Arc<RwLock<HashMap<String, LoadedPlugin>>>,
14
15 watchers: Arc<RwLock<HashMap<String, FileWatcher>>>,
17
18 config: HotReloadConfig,
20}
21
22#[derive(Debug, Clone)]
24struct LoadedPlugin {
25 name: String,
27
28 path: PathBuf,
30
31 last_modified: SystemTime,
33
34 load_count: u32,
36
37 version: String,
39}
40
41#[derive(Debug)]
43struct FileWatcher {
44 path: PathBuf,
45 last_check: SystemTime,
46 last_modified: SystemTime,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct HotReloadConfig {
52 pub enabled: bool,
54
55 pub check_interval: u64,
57
58 pub debounce_delay: u64,
60
61 pub auto_reload: bool,
63
64 pub watch_recursive: bool,
66
67 pub watch_patterns: Vec<String>,
69
70 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#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ReloadEvent {
96 pub plugin_name: String,
98
99 pub event_type: ReloadEventType,
101
102 pub timestamp: String,
104
105 pub old_version: Option<String>,
107
108 pub new_version: Option<String>,
110}
111
112#[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 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 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 {
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 {
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 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 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 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 if let Ok(metadata) = std::fs::metadata(&watcher.path) {
226 if let Ok(modified) = metadata.modified() {
227 if modified > watcher.last_modified {
228 if let Ok(elapsed) = now.duration_since(modified) {
231 if elapsed < Duration::from_millis(self.config.debounce_delay) {
232 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 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 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 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 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 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#[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#[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, debounce_delay: 0, ..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 std::thread::sleep(Duration::from_millis(100));
423
424 writeln!(file, "modified content").unwrap();
426 file.sync_all().unwrap();
427 drop(file);
428
429 std::thread::sleep(Duration::from_millis(100));
431
432 let _changed = manager.check_for_changes().unwrap();
433 }
436}