sen_plugin_host/
discovery.rs1use crate::{LoadedPlugin, LoaderError, PluginLoader};
6use std::path::{Path, PathBuf};
7use thiserror::Error;
8
9#[derive(Debug, Error)]
11pub enum DiscoveryError {
12 #[error("Directory not found: {0}")]
13 DirectoryNotFound(PathBuf),
14
15 #[error("Failed to read directory: {0}")]
16 ReadDirectory(#[source] std::io::Error),
17
18 #[error("Failed to load plugin {path}: {source}")]
19 LoadPlugin {
20 path: PathBuf,
21 #[source]
22 source: LoaderError,
23 },
24}
25
26pub struct DiscoveryResult {
28 pub plugins: Vec<LoadedPlugin>,
30
31 pub failures: Vec<(PathBuf, DiscoveryError)>,
33}
34
35impl DiscoveryResult {
36 pub fn is_success(&self) -> bool {
38 self.failures.is_empty()
39 }
40
41 pub fn total_found(&self) -> usize {
43 self.plugins.len() + self.failures.len()
44 }
45}
46
47pub struct PluginScanner {
49 loader: PluginLoader,
50}
51
52impl PluginScanner {
53 pub fn new() -> Result<Self, LoaderError> {
55 Ok(Self {
56 loader: PluginLoader::new()?,
57 })
58 }
59
60 pub fn with_loader(loader: PluginLoader) -> Self {
62 Self { loader }
63 }
64
65 pub fn scan_directory(&self, dir: impl AsRef<Path>) -> Result<DiscoveryResult, DiscoveryError> {
67 let dir = dir.as_ref();
68
69 if !dir.exists() {
70 return Err(DiscoveryError::DirectoryNotFound(dir.to_path_buf()));
71 }
72
73 if !dir.is_dir() {
74 return Err(DiscoveryError::DirectoryNotFound(dir.to_path_buf()));
75 }
76
77 let entries = std::fs::read_dir(dir).map_err(DiscoveryError::ReadDirectory)?;
78
79 let mut plugins = Vec::new();
80 let mut failures = Vec::new();
81
82 for entry in entries {
83 let entry = match entry {
84 Ok(e) => e,
85 Err(e) => {
86 failures.push((dir.to_path_buf(), DiscoveryError::ReadDirectory(e)));
87 continue;
88 }
89 };
90
91 let path = entry.path();
92
93 if path.extension().map(|e| e == "wasm").unwrap_or(false) {
95 match self.load_plugin(&path) {
96 Ok(plugin) => plugins.push(plugin),
97 Err(e) => failures.push((path, e)),
98 }
99 }
100 }
101
102 Ok(DiscoveryResult { plugins, failures })
103 }
104
105 pub fn scan_directories(
107 &self,
108 dirs: impl IntoIterator<Item = impl AsRef<Path>>,
109 ) -> DiscoveryResult {
110 let mut all_plugins = Vec::new();
111 let mut all_failures = Vec::new();
112
113 for dir in dirs {
114 match self.scan_directory(dir) {
115 Ok(result) => {
116 all_plugins.extend(result.plugins);
117 all_failures.extend(result.failures);
118 }
119 Err(e) => {
120 if let DiscoveryError::DirectoryNotFound(path) = &e {
122 all_failures.push((path.clone(), e));
123 }
124 }
125 }
126 }
127
128 DiscoveryResult {
129 plugins: all_plugins,
130 failures: all_failures,
131 }
132 }
133
134 fn load_plugin(&self, path: &Path) -> Result<LoadedPlugin, DiscoveryError> {
136 let wasm_bytes = std::fs::read(path).map_err(|e| DiscoveryError::LoadPlugin {
137 path: path.to_path_buf(),
138 source: LoaderError::MemoryAccess(format!("Failed to read file: {}", e)),
139 })?;
140
141 self.loader
142 .load(&wasm_bytes)
143 .map_err(|e| DiscoveryError::LoadPlugin {
144 path: path.to_path_buf(),
145 source: e,
146 })
147 }
148}
149
150pub fn default_plugin_dirs(app_name: &str) -> Vec<PathBuf> {
152 let mut dirs = Vec::new();
153
154 if let Some(data_dir) = dirs::data_local_dir() {
156 dirs.push(data_dir.join(app_name).join("plugins"));
157 }
158
159 dirs.push(PathBuf::from("plugins"));
161
162 dirs
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use std::fs;
169 use tempfile::TempDir;
170
171 #[test]
172 fn test_scan_empty_directory() {
173 let temp = TempDir::new().unwrap();
174 let scanner = PluginScanner::new().unwrap();
175
176 let result = scanner.scan_directory(temp.path()).unwrap();
177 assert!(result.plugins.is_empty());
178 assert!(result.failures.is_empty());
179 assert!(result.is_success());
180 }
181
182 #[test]
183 fn test_scan_nonexistent_directory() {
184 let scanner = PluginScanner::new().unwrap();
185 let result = scanner.scan_directory("/nonexistent/path/to/plugins");
186
187 assert!(result.is_err());
188 match result {
189 Err(DiscoveryError::DirectoryNotFound(_)) => {}
190 _ => panic!("Expected DirectoryNotFound error"),
191 }
192 }
193
194 #[test]
195 fn test_scan_with_wasm_file() {
196 let temp = TempDir::new().unwrap();
197
198 let wasm_bytes = include_bytes!(
200 "../../examples/hello-plugin/target/wasm32-unknown-unknown/release/hello_plugin.wasm"
201 );
202 let plugin_path = temp.path().join("hello.wasm");
203 fs::write(&plugin_path, wasm_bytes).unwrap();
204
205 let scanner = PluginScanner::new().unwrap();
206 let result = scanner.scan_directory(temp.path()).unwrap();
207
208 assert_eq!(result.plugins.len(), 1);
209 assert!(result.failures.is_empty());
210 assert_eq!(result.plugins[0].manifest.command.name, "hello");
211 }
212
213 #[test]
214 fn test_scan_ignores_non_wasm_files() {
215 let temp = TempDir::new().unwrap();
216
217 fs::write(temp.path().join("readme.txt"), "Hello").unwrap();
219 fs::write(temp.path().join("config.json"), "{}").unwrap();
220
221 let scanner = PluginScanner::new().unwrap();
222 let result = scanner.scan_directory(temp.path()).unwrap();
223
224 assert!(result.plugins.is_empty());
225 assert!(result.failures.is_empty());
226 }
227
228 #[test]
229 fn test_default_plugin_dirs() {
230 let dirs = default_plugin_dirs("myapp");
231 assert!(!dirs.is_empty());
232 assert!(dirs.iter().any(|d| d.ends_with("plugins")));
233 }
234}