1use crate::error::{PluginError, PluginResult};
19use crate::version_resolver::SemVer;
20use std::collections::HashMap;
21use std::time::Instant;
22
23pub fn compute_hash(data: &[u8]) -> u64 {
30 const FNV_OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
31 const FNV_PRIME: u64 = 1_099_511_628_211;
32
33 let mut hash = FNV_OFFSET_BASIS;
34 for &byte in data {
35 hash ^= u64::from(byte);
36 hash = hash.wrapping_mul(FNV_PRIME);
37 }
38 hash
39}
40
41pub fn compute_hash_file(path: &std::path::Path) -> std::io::Result<u64> {
45 let data = std::fs::read(path)?;
46 Ok(compute_hash(&data))
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum ReloadPolicy {
54 OnChange,
56 OnSignal,
58 Scheduled { interval_ms: u64 },
60 Disabled,
62}
63
64#[derive(Debug, Clone)]
68pub struct PluginVersion {
69 pub id: String,
71 pub version: SemVer,
73 pub hash: u64,
75 pub loaded_at: Instant,
77}
78
79impl PluginVersion {
80 pub fn new(id: impl Into<String>, version: SemVer, hash: u64) -> Self {
82 Self {
83 id: id.into(),
84 version,
85 hash,
86 loaded_at: Instant::now(),
87 }
88 }
89}
90
91#[derive(Debug, Clone)]
95pub struct WatchEntry {
96 pub plugin_id: String,
98 pub path: String,
100 pub last_hash: u64,
102}
103
104impl WatchEntry {
105 pub fn new(plugin_id: impl Into<String>, path: impl Into<String>, initial_hash: u64) -> Self {
107 Self {
108 plugin_id: plugin_id.into(),
109 path: path.into(),
110 last_hash: initial_hash,
111 }
112 }
113}
114
115pub trait PluginLifecycle {
122 fn on_load(&mut self);
124
125 fn on_unload(&mut self);
127
128 fn on_reload(&mut self, old_version: &PluginVersion);
133}
134
135pub struct HotReloadManager {
146 pub loaded_plugins: HashMap<String, PluginVersion>,
148 pub policy: ReloadPolicy,
150 pub watchers: Vec<WatchEntry>,
152 last_check: Instant,
154}
155
156impl HotReloadManager {
157 pub fn new(policy: ReloadPolicy) -> Self {
159 Self {
160 loaded_plugins: HashMap::new(),
161 policy,
162 watchers: Vec::new(),
163 last_check: Instant::now(),
164 }
165 }
166
167 pub fn register_loaded(&mut self, version: PluginVersion) {
169 self.loaded_plugins.insert(version.id.clone(), version);
170 }
171
172 pub fn watch(
177 &mut self,
178 plugin_id: impl Into<String>,
179 path: impl Into<String>,
180 initial_content: &[u8],
181 ) {
182 let plugin_id = plugin_id.into();
183 let path = path.into();
184 let hash = compute_hash(initial_content);
185 self.watchers.push(WatchEntry::new(plugin_id, path, hash));
186 }
187
188 pub fn check_for_changes(&self, current_content: &HashMap<String, Vec<u8>>) -> Vec<String> {
195 self.watchers
196 .iter()
197 .filter_map(|w| {
198 let content = current_content.get(&w.plugin_id)?;
199 let new_hash = compute_hash(content);
200 if new_hash != w.last_hash {
201 Some(w.plugin_id.clone())
202 } else {
203 None
204 }
205 })
206 .collect()
207 }
208
209 pub fn update_hash(&mut self, plugin_id: &str, new_content: &[u8]) {
211 let new_hash = compute_hash(new_content);
212 for w in &mut self.watchers {
213 if w.plugin_id == plugin_id {
214 w.last_hash = new_hash;
215 return;
216 }
217 }
218 }
219
220 pub fn reload_plugin(
229 &mut self,
230 plugin_id: &str,
231 new_version: PluginVersion,
232 ) -> PluginResult<()> {
233 if !self.loaded_plugins.contains_key(plugin_id) {
234 return Err(PluginError::NotFound(plugin_id.to_string()));
235 }
236 self.loaded_plugins
237 .insert(plugin_id.to_string(), new_version);
238 Ok(())
239 }
240
241 pub fn unload_plugin(&mut self, plugin_id: &str) -> PluginResult<PluginVersion> {
247 self.loaded_plugins
248 .remove(plugin_id)
249 .ok_or_else(|| PluginError::NotFound(plugin_id.to_string()))
250 }
251
252 pub fn is_scheduled_reload_due(&mut self) -> bool {
257 if let ReloadPolicy::Scheduled { interval_ms } = &self.policy {
258 let elapsed = self.last_check.elapsed().as_millis() as u64;
259 if elapsed >= *interval_ms {
260 self.last_check = Instant::now();
261 return true;
262 }
263 }
264 false
265 }
266
267 pub fn auto_reload_enabled(&self) -> bool {
269 !matches!(self.policy, ReloadPolicy::Disabled)
270 }
271}
272
273pub struct GracefulReload {
282 pub drain_timeout_ms: u64,
284}
285
286impl GracefulReload {
287 pub fn new(drain_timeout_ms: u64) -> Self {
289 Self { drain_timeout_ms }
290 }
291
292 pub fn drain_and_reload(
303 &self,
304 plugin_id: &str,
305 manager: &mut HotReloadManager,
306 new_version: PluginVersion,
307 ) -> PluginResult<()> {
308 let _start = Instant::now();
315 manager.reload_plugin(plugin_id, new_version)
316 }
317}
318
319#[cfg(test)]
322mod tests {
323 use super::*;
324
325 fn make_version(id: &str, major: u32, minor: u32, patch: u32) -> PluginVersion {
326 PluginVersion::new(id, SemVer::new(major, minor, patch), 0)
327 }
328
329 #[test]
331 fn test_hash_deterministic() {
332 let h1 = compute_hash(b"hello");
333 let h2 = compute_hash(b"hello");
334 assert_eq!(h1, h2);
335 }
336
337 #[test]
339 fn test_hash_distinct() {
340 assert_ne!(compute_hash(b"foo"), compute_hash(b"bar"));
341 }
342
343 #[test]
345 fn test_hash_empty() {
346 let h = compute_hash(&[]);
347 assert_eq!(h, 14_695_981_039_346_656_037);
348 }
349
350 #[test]
352 fn test_manager_new_empty() {
353 let m = HotReloadManager::new(ReloadPolicy::OnChange);
354 assert!(m.loaded_plugins.is_empty());
355 assert!(m.watchers.is_empty());
356 }
357
358 #[test]
360 fn test_register_loaded() {
361 let mut m = HotReloadManager::new(ReloadPolicy::Disabled);
362 m.register_loaded(make_version("codec-a", 1, 0, 0));
363 assert!(m.loaded_plugins.contains_key("codec-a"));
364 }
365
366 #[test]
368 fn test_watch_hash() {
369 let mut m = HotReloadManager::new(ReloadPolicy::OnChange);
370 let content = b"binary data";
371 m.watch("plug", "/lib/plug.so", content);
372 assert_eq!(m.watchers.len(), 1);
373 assert_eq!(m.watchers[0].last_hash, compute_hash(content));
374 }
375
376 #[test]
378 fn test_no_changes() {
379 let mut m = HotReloadManager::new(ReloadPolicy::OnChange);
380 let content = b"same content";
381 m.watch("p", "/lib/p.so", content);
382
383 let mut current = HashMap::new();
384 current.insert("p".to_string(), content.to_vec());
385
386 let changed = m.check_for_changes(¤t);
387 assert!(changed.is_empty());
388 }
389
390 #[test]
392 fn test_change_detected() {
393 let mut m = HotReloadManager::new(ReloadPolicy::OnChange);
394 m.watch("p", "/lib/p.so", b"v1");
395
396 let mut current = HashMap::new();
397 current.insert("p".to_string(), b"v2".to_vec());
398
399 let changed = m.check_for_changes(¤t);
400 assert_eq!(changed, vec!["p".to_string()]);
401 }
402
403 #[test]
405 fn test_update_hash_clears_change() {
406 let mut m = HotReloadManager::new(ReloadPolicy::OnChange);
407 m.watch("p", "/lib/p.so", b"v1");
408 m.update_hash("p", b"v2");
409
410 let mut current = HashMap::new();
411 current.insert("p".to_string(), b"v2".to_vec());
412
413 let changed = m.check_for_changes(¤t);
414 assert!(changed.is_empty());
415 }
416
417 #[test]
419 fn test_reload_plugin() {
420 let mut m = HotReloadManager::new(ReloadPolicy::OnChange);
421 m.register_loaded(make_version("p", 1, 0, 0));
422
423 let new_v = make_version("p", 1, 1, 0);
424 m.reload_plugin("p", new_v).expect("reload");
425
426 assert_eq!(m.loaded_plugins["p"].version, SemVer::new(1, 1, 0));
427 }
428
429 #[test]
431 fn test_reload_unknown() {
432 let mut m = HotReloadManager::new(ReloadPolicy::OnChange);
433 let err = m.reload_plugin("ghost", make_version("ghost", 1, 0, 0));
434 assert!(matches!(err, Err(PluginError::NotFound(_))));
435 }
436
437 #[test]
439 fn test_unload_plugin() {
440 let mut m = HotReloadManager::new(ReloadPolicy::Disabled);
441 m.register_loaded(make_version("p", 1, 0, 0));
442 m.unload_plugin("p").expect("unload");
443 assert!(!m.loaded_plugins.contains_key("p"));
444 }
445
446 #[test]
448 fn test_unload_unknown() {
449 let mut m = HotReloadManager::new(ReloadPolicy::Disabled);
450 assert!(matches!(
451 m.unload_plugin("ghost"),
452 Err(PluginError::NotFound(_))
453 ));
454 }
455
456 #[test]
458 fn test_auto_reload_disabled() {
459 let m = HotReloadManager::new(ReloadPolicy::Disabled);
460 assert!(!m.auto_reload_enabled());
461 }
462
463 #[test]
465 fn test_auto_reload_on_change() {
466 let m = HotReloadManager::new(ReloadPolicy::OnChange);
467 assert!(m.auto_reload_enabled());
468 }
469
470 #[test]
472 fn test_scheduled_not_due() {
473 let mut m = HotReloadManager::new(ReloadPolicy::Scheduled {
474 interval_ms: 1_000_000,
475 });
476 assert!(!m.is_scheduled_reload_due());
477 }
478
479 #[test]
481 fn test_graceful_reload_ok() {
482 let mut m = HotReloadManager::new(ReloadPolicy::OnChange);
483 m.register_loaded(make_version("p", 1, 0, 0));
484
485 let gr = GracefulReload::new(100);
486 let new_v = make_version("p", 1, 2, 0);
487 gr.drain_and_reload("p", &mut m, new_v)
488 .expect("graceful reload");
489
490 assert_eq!(m.loaded_plugins["p"].version, SemVer::new(1, 2, 0));
491 }
492
493 #[test]
495 fn test_graceful_reload_not_found() {
496 let mut m = HotReloadManager::new(ReloadPolicy::OnChange);
497 let gr = GracefulReload::new(0);
498 let err = gr.drain_and_reload("ghost", &mut m, make_version("ghost", 1, 0, 0));
499 assert!(matches!(err, Err(PluginError::NotFound(_))));
500 }
501
502 #[test]
504 fn test_watch_entry_path() {
505 let w = WatchEntry::new("plug", "/some/path.so", 0xABCD);
506 assert_eq!(w.plugin_id, "plug");
507 assert_eq!(w.path, "/some/path.so");
508 assert_eq!(w.last_hash, 0xABCD);
509 }
510
511 #[test]
513 fn test_multiple_watchers_selective() {
514 let mut m = HotReloadManager::new(ReloadPolicy::OnChange);
515 m.watch("a", "/lib/a.so", b"v1a");
516 m.watch("b", "/lib/b.so", b"v1b");
517
518 let mut current = HashMap::new();
519 current.insert("a".to_string(), b"v2a".to_vec()); current.insert("b".to_string(), b"v1b".to_vec()); let changed = m.check_for_changes(¤t);
523 assert_eq!(changed.len(), 1);
524 assert_eq!(changed[0], "a");
525 }
526}