memlink_runtime/
discovery.rs1use std::path::PathBuf;
6use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
7use std::time::Duration;
8
9use dashmap::DashMap;
10
11#[derive(Debug, Clone)]
13pub struct ModuleFile {
14 pub path: PathBuf,
16 pub name: String,
18 pub size: u64,
20 pub modified: std::time::SystemTime,
22}
23
24#[derive(Debug, Clone)]
26pub struct DiscoveryConfig {
27 pub watch_dirs: Vec<PathBuf>,
29 pub extensions: Vec<String>,
31 pub poll_interval: Duration,
33 pub auto_load: bool,
35 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(), ".dll".to_string(), ".dylib".to_string(), ],
48 poll_interval: Duration::from_secs(5),
49 auto_load: true,
50 auto_reload: false,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum ModuleStatus {
58 Discovered,
60 Loading,
62 Loaded,
64 Failed,
66 Removed,
68}
69
70#[derive(Debug, Clone)]
72pub struct DiscoveredModule {
73 pub file: ModuleFile,
75 pub status: ModuleStatus,
77 pub attempts: u32,
79 pub last_error: Option<String>,
81 pub discovered_at: std::time::SystemTime,
83}
84
85#[derive(Debug)]
87pub struct ModuleDiscovery {
88 config: DiscoveryConfig,
90 modules: DashMap<String, DiscoveredModule>,
92 running: AtomicBool,
94 total_discoveries: AtomicU64,
96}
97
98impl ModuleDiscovery {
99 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 pub fn with_defaults() -> Self {
111 Self::new(DiscoveryConfig::default())
112 }
113
114 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 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 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 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 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 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 pub fn discovered_modules(&self) -> Vec<DiscoveredModule> {
189 self.modules.iter().map(|e| e.value().clone()).collect()
190 }
191
192 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 pub fn is_running(&self) -> bool {
203 self.running.load(Ordering::Relaxed)
204 }
205
206 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}