Skip to main content

memlink_runtime/
discovery.rs

1//! Module discovery and auto-loading.
2//!
3//! Watches directories for module files and auto-loads them.
4
5use std::path::PathBuf;
6use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
7use std::time::Duration;
8
9use dashmap::DashMap;
10
11/// Module file information.
12#[derive(Debug, Clone)]
13pub struct ModuleFile {
14    /// Path to the module file.
15    pub path: PathBuf,
16    /// Module name (derived from filename).
17    pub name: String,
18    /// File size in bytes.
19    pub size: u64,
20    /// Last modified timestamp.
21    pub modified: std::time::SystemTime,
22}
23
24/// Discovery configuration.
25#[derive(Debug, Clone)]
26pub struct DiscoveryConfig {
27    /// Directories to watch.
28    pub watch_dirs: Vec<PathBuf>,
29    /// File extensions to look for.
30    pub extensions: Vec<String>,
31    /// Polling interval for file system.
32    pub poll_interval: Duration,
33    /// Whether to auto-load discovered modules.
34    pub auto_load: bool,
35    /// Whether to reload on file changes.
36    pub auto_reload: bool,
37}
38
39impl Default for DiscoveryConfig {
40    fn default() -> Self {
41        Self {
42            watch_dirs: vec![PathBuf::from("./modules")],
43            extensions: vec![
44                ".so".to_string(),    // Linux
45                ".dll".to_string(),   // Windows
46                ".dylib".to_string(), // macOS
47            ],
48            poll_interval: Duration::from_secs(5),
49            auto_load: true,
50            auto_reload: false,
51        }
52    }
53}
54
55/// Discovered module status.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum ModuleStatus {
58    /// Module discovered but not loaded.
59    Discovered,
60    /// Module is loading.
61    Loading,
62    /// Module is loaded and active.
63    Loaded,
64    /// Module failed to load.
65    Failed,
66    /// Module file was removed.
67    Removed,
68}
69
70/// Discovered module information.
71#[derive(Debug, Clone)]
72pub struct DiscoveredModule {
73    /// Module file information.
74    pub file: ModuleFile,
75    /// Current status.
76    pub status: ModuleStatus,
77    /// Load attempts.
78    pub attempts: u32,
79    /// Last error message.
80    pub last_error: Option<String>,
81    /// Discovery timestamp.
82    pub discovered_at: std::time::SystemTime,
83}
84
85/// Module discovery service.
86#[derive(Debug)]
87pub struct ModuleDiscovery {
88    /// Configuration.
89    config: DiscoveryConfig,
90    /// Discovered modules.
91    modules: DashMap<String, DiscoveredModule>,
92    /// Discovery running flag.
93    running: AtomicBool,
94    /// Total discoveries.
95    total_discoveries: AtomicU64,
96}
97
98impl ModuleDiscovery {
99    /// Creates a new discovery service.
100    pub fn new(config: DiscoveryConfig) -> Self {
101        Self {
102            config,
103            modules: DashMap::new(),
104            running: AtomicBool::new(false),
105            total_discoveries: AtomicU64::new(0),
106        }
107    }
108
109    /// Creates a discovery service with default configuration.
110    pub fn with_defaults() -> Self {
111        Self::new(DiscoveryConfig::default())
112    }
113
114    /// Scans configured directories for modules.
115    pub fn scan(&self) -> Vec<ModuleFile> {
116        let mut found = Vec::new();
117
118        for dir in &self.config.watch_dirs {
119            if !dir.exists() {
120                continue;
121            }
122
123            if let Ok(entries) = std::fs::read_dir(dir) {
124                for entry in entries.flatten() {
125                    let path = entry.path();
126
127                    // Check extension
128                    let ext = path.extension()
129                        .and_then(|e| e.to_str())
130                        .unwrap_or("");
131
132                    if !self.config.extensions.iter().any(|e| e == &format!(".{}", ext)) {
133                        continue;
134                    }
135
136                    // Get file info
137                    if let Ok(metadata) = path.metadata() {
138                        let name = path.file_stem()
139                            .and_then(|s| s.to_str())
140                            .unwrap_or("unknown")
141                            .to_string();
142
143                        found.push(ModuleFile {
144                            path,
145                            name,
146                            size: metadata.len(),
147                            modified: metadata.modified().unwrap_or(std::time::UNIX_EPOCH),
148                        });
149                    }
150                }
151            }
152        }
153
154        found
155    }
156
157    /// Registers a discovered module.
158    pub fn register(&self, file: ModuleFile) {
159        let module = DiscoveredModule {
160            file,
161            status: ModuleStatus::Discovered,
162            attempts: 0,
163            last_error: None,
164            discovered_at: std::time::SystemTime::now(),
165        };
166
167        self.modules.insert(module.file.name.clone(), module);
168        self.total_discoveries.fetch_add(1, Ordering::Relaxed);
169    }
170
171    /// Updates module status.
172    pub fn update_status(&self, name: &str, status: ModuleStatus) {
173        if let Some(mut module) = self.modules.get_mut(name) {
174            module.status = status;
175        }
176    }
177
178    /// Records a load failure.
179    pub fn record_failure(&self, name: &str, error: String) {
180        if let Some(mut module) = self.modules.get_mut(name) {
181            module.attempts += 1;
182            module.last_error = Some(error);
183            module.status = ModuleStatus::Failed;
184        }
185    }
186
187    /// Returns discovered modules.
188    pub fn discovered_modules(&self) -> Vec<DiscoveredModule> {
189        self.modules.iter().map(|e| e.value().clone()).collect()
190    }
191
192    /// Returns modules by status.
193    pub fn modules_by_status(&self, status: ModuleStatus) -> Vec<DiscoveredModule> {
194        self.modules
195            .iter()
196            .filter(|e| e.value().status == status)
197            .map(|e| e.value().clone())
198            .collect()
199    }
200
201    /// Returns whether discovery is running.
202    pub fn is_running(&self) -> bool {
203        self.running.load(Ordering::Relaxed)
204    }
205
206    /// Returns total discoveries.
207    pub fn total_discoveries(&self) -> u64 {
208        self.total_discoveries.load(Ordering::Relaxed)
209    }
210}
211
212impl Default for ModuleDiscovery {
213    fn default() -> Self {
214        Self::with_defaults()
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_discovery_config() {
224        let config = DiscoveryConfig::default();
225        assert!(!config.watch_dirs.is_empty());
226        assert_eq!(config.extensions.len(), 3);
227    }
228
229    #[test]
230    fn test_discovery_creation() {
231        let discovery = ModuleDiscovery::with_defaults();
232        assert!(!discovery.is_running());
233        assert_eq!(discovery.total_discoveries(), 0);
234    }
235}