1use gestura_core_foundation::error::AppError;
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
15pub struct PluginMetadata {
16 pub id: String,
17 pub name: String,
18 pub version: String,
19 pub description: String,
20 pub author: String,
21 pub license: String,
22 pub homepage: Option<String>,
23 pub repository: Option<String>,
25 pub keywords: Vec<String>,
26 pub dependencies: Vec<PluginDependency>,
27 pub permissions: Vec<PluginPermission>,
28 pub entry_point: String,
29 pub supported_platforms: Vec<String>,
30 pub min_app_version: String,
31}
32
33#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
35pub struct PluginDependency {
36 pub name: String,
37 pub version: String,
38 pub optional: bool,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
43pub enum PluginPermission {
44 FileSystem(String),
46 Network(String),
48 VoiceAccess,
49 GestureAccess,
50 RingAccess,
51 SystemInfo,
52 Notifications,
53 ClipboardAccess,
54 ProcessSpawn,
55 DatabaseAccess,
56}
57
58#[derive(Debug, Clone, PartialEq, serde::Serialize)]
60pub enum PluginState {
61 Loaded,
62 Running,
63 Stopped,
64 Error(String),
65 Disabled,
66}
67
68#[derive(Debug, Clone)]
70pub struct Plugin {
71 pub metadata: PluginMetadata,
72 pub state: PluginState,
73 pub path: PathBuf,
74 pub config: serde_json::Value,
75 pub last_error: Option<String>,
76 pub load_time: chrono::DateTime<chrono::Utc>,
77 pub last_activity: chrono::DateTime<chrono::Utc>,
78}
79
80pub trait PluginApi {
82 fn initialize(&mut self, config: serde_json::Value) -> Result<(), String>;
84
85 fn start(&mut self) -> Result<(), String>;
87
88 fn stop(&mut self) -> Result<(), String>;
90
91 fn handle_command(
93 &mut self,
94 command: &str,
95 args: serde_json::Value,
96 ) -> Result<serde_json::Value, String>;
97
98 fn get_status(&self) -> serde_json::Value;
100
101 fn handle_event(&mut self, event: &str, data: serde_json::Value) -> Result<(), String>;
103}
104
105pub struct PluginManager {
107 plugins: Arc<RwLock<HashMap<String, Plugin>>>,
108 plugin_directory: PathBuf,
109 enabled_plugins: Arc<RwLock<Vec<String>>>,
110 event_handlers: Arc<RwLock<HashMap<String, Vec<String>>>>, command_handlers: Arc<RwLock<HashMap<String, String>>>, }
113
114impl PluginManager {
115 pub fn new(plugin_directory: PathBuf) -> Self {
117 Self {
118 plugins: Arc::new(RwLock::new(HashMap::new())),
119 plugin_directory,
120 enabled_plugins: Arc::new(RwLock::new(Vec::new())),
121 event_handlers: Arc::new(RwLock::new(HashMap::new())),
122 command_handlers: Arc::new(RwLock::new(HashMap::new())),
123 }
124 }
125
126 pub async fn discover_plugins(&self) -> Result<Vec<PluginMetadata>, AppError> {
128 let mut discovered = Vec::new();
129
130 if !self.plugin_directory.exists() {
131 tokio::fs::create_dir_all(&self.plugin_directory)
132 .await
133 .map_err(AppError::Io)?;
134 return Ok(discovered);
135 }
136
137 let mut entries = tokio::fs::read_dir(&self.plugin_directory)
138 .await
139 .map_err(AppError::Io)?;
140
141 while let Some(entry) = entries.next_entry().await.map_err(AppError::Io)? {
142 let path = entry.path();
143
144 if path.is_dir() {
145 let manifest_path = path.join("plugin.json");
146 if manifest_path.exists() {
147 match self.load_plugin_metadata(&manifest_path).await {
148 Ok(metadata) => discovered.push(metadata),
149 Err(e) => tracing::warn!(
150 "Failed to load plugin metadata from {}: {}",
151 manifest_path.display(),
152 e
153 ),
154 }
155 }
156 }
157 }
158
159 tracing::info!("Discovered {} plugins", discovered.len());
160 Ok(discovered)
161 }
162
163 async fn load_plugin_metadata(&self, manifest_path: &Path) -> Result<PluginMetadata, AppError> {
165 let content = tokio::fs::read_to_string(manifest_path)
166 .await
167 .map_err(AppError::Io)?;
168
169 let metadata: PluginMetadata = serde_json::from_str(&content)
170 .map_err(|e| AppError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
171
172 self.validate_plugin_metadata(&metadata)?;
174
175 Ok(metadata)
176 }
177
178 fn validate_plugin_metadata(&self, metadata: &PluginMetadata) -> Result<(), AppError> {
180 if metadata.id.trim().is_empty() {
181 return Err(AppError::Io(std::io::Error::new(
182 std::io::ErrorKind::InvalidData,
183 "Plugin ID cannot be empty",
184 )));
185 }
186
187 if metadata.name.trim().is_empty() {
188 return Err(AppError::Io(std::io::Error::new(
189 std::io::ErrorKind::InvalidData,
190 "Plugin name cannot be empty",
191 )));
192 }
193
194 if metadata.entry_point.trim().is_empty() {
195 return Err(AppError::Io(std::io::Error::new(
196 std::io::ErrorKind::InvalidData,
197 "Plugin entry point cannot be empty",
198 )));
199 }
200
201 if !metadata.version.contains('.') {
203 return Err(AppError::Io(std::io::Error::new(
204 std::io::ErrorKind::InvalidData,
205 "Invalid version format",
206 )));
207 }
208
209 Ok(())
210 }
211
212 pub async fn load_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
214 let plugin_path = self.plugin_directory.join(plugin_id);
215 let manifest_path = plugin_path.join("plugin.json");
216
217 if !manifest_path.exists() {
218 return Err(AppError::Io(std::io::Error::new(
219 std::io::ErrorKind::NotFound,
220 format!("Plugin manifest not found: {}", plugin_id),
221 )));
222 }
223
224 let metadata = self.load_plugin_metadata(&manifest_path).await?;
225
226 {
228 let plugins = self.plugins.read().await;
229 if plugins.contains_key(plugin_id) {
230 return Err(AppError::Io(std::io::Error::new(
231 std::io::ErrorKind::AlreadyExists,
232 format!("Plugin already loaded: {}", plugin_id),
233 )));
234 }
235 }
236
237 self.validate_plugin_permissions(&metadata.permissions)
239 .await?;
240
241 let plugin = Plugin {
243 metadata: metadata.clone(),
244 state: PluginState::Loaded,
245 path: plugin_path,
246 config: serde_json::Value::Null,
247 last_error: None,
248 load_time: chrono::Utc::now(),
249 last_activity: chrono::Utc::now(),
250 };
251
252 let mut plugins = self.plugins.write().await;
254 plugins.insert(plugin_id.to_string(), plugin);
255
256 tracing::info!("Loaded plugin: {} v{}", metadata.name, metadata.version);
257 Ok(())
258 }
259
260 async fn validate_plugin_permissions(
262 &self,
263 permissions: &[PluginPermission],
264 ) -> Result<(), AppError> {
265 for permission in permissions {
266 match permission {
267 PluginPermission::FileSystem(path)
268 if path.contains("..") || path.starts_with('/') =>
269 {
270 return Err(AppError::Io(std::io::Error::new(
271 std::io::ErrorKind::PermissionDenied,
272 "Invalid file system permission pattern",
273 )));
274 }
275 PluginPermission::Network(host) if host == "*" => {
276 return Err(AppError::Io(std::io::Error::new(
277 std::io::ErrorKind::PermissionDenied,
278 "Wildcard network access not allowed",
279 )));
280 }
281 PluginPermission::ProcessSpawn => {
282 tracing::warn!("Plugin requests process spawn permission");
284 }
285 _ => {}
286 }
287 }
288 Ok(())
289 }
290
291 pub async fn start_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
293 let mut plugins = self.plugins.write().await;
294
295 let plugin = plugins.get_mut(plugin_id).ok_or_else(|| {
296 AppError::Io(std::io::Error::new(
297 std::io::ErrorKind::NotFound,
298 format!("Plugin not found: {}", plugin_id),
299 ))
300 })?;
301
302 if plugin.state == PluginState::Running {
303 return Ok(());
304 }
305
306 plugin.state = PluginState::Running;
308 plugin.last_activity = chrono::Utc::now();
309
310 tracing::info!("Started plugin: {}", plugin_id);
311 Ok(())
312 }
313
314 pub async fn stop_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
316 let mut plugins = self.plugins.write().await;
317
318 let plugin = plugins.get_mut(plugin_id).ok_or_else(|| {
319 AppError::Io(std::io::Error::new(
320 std::io::ErrorKind::NotFound,
321 format!("Plugin not found: {}", plugin_id),
322 ))
323 })?;
324
325 if plugin.state == PluginState::Stopped {
326 return Ok(());
327 }
328
329 plugin.state = PluginState::Stopped;
331 plugin.last_activity = chrono::Utc::now();
332
333 tracing::info!("Stopped plugin: {}", plugin_id);
334 Ok(())
335 }
336
337 pub async fn unload_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
339 self.stop_plugin(plugin_id).await?;
341
342 let mut plugins = self.plugins.write().await;
344 let mut enabled = self.enabled_plugins.write().await;
345 let mut event_handlers = self.event_handlers.write().await;
346 let mut command_handlers = self.command_handlers.write().await;
347
348 plugins.remove(plugin_id);
349 enabled.retain(|id| id != plugin_id);
350
351 for handlers in event_handlers.values_mut() {
353 handlers.retain(|id| id != plugin_id);
354 }
355
356 command_handlers.retain(|_, id| id != plugin_id);
358
359 tracing::info!("Unloaded plugin: {}", plugin_id);
360 Ok(())
361 }
362
363 pub async fn execute_command(
365 &self,
366 command: &str,
367 _args: serde_json::Value,
368 ) -> Result<serde_json::Value, AppError> {
369 let command_handlers = self.command_handlers.read().await;
370
371 if let Some(plugin_id) = command_handlers.get(command) {
372 tracing::info!("Executing command '{}' on plugin '{}'", command, plugin_id);
374
375 Ok(serde_json::json!({
376 "status": "success",
377 "plugin_id": plugin_id,
378 "command": command,
379 "result": "Command executed successfully"
380 }))
381 } else {
382 Err(AppError::Io(std::io::Error::new(
383 std::io::ErrorKind::NotFound,
384 format!("No handler found for command: {}", command),
385 )))
386 }
387 }
388
389 pub async fn broadcast_event(
391 &self,
392 event: &str,
393 _data: serde_json::Value,
394 ) -> Result<(), AppError> {
395 let event_handlers = self.event_handlers.read().await;
396
397 if let Some(handlers) = event_handlers.get(event) {
398 for plugin_id in handlers {
399 tracing::debug!("Broadcasting event '{}' to plugin '{}'", event, plugin_id);
401 }
402 }
403
404 Ok(())
405 }
406
407 pub async fn get_plugins(&self) -> Vec<Plugin> {
409 let plugins = self.plugins.read().await;
410 plugins.values().cloned().collect()
411 }
412
413 pub async fn get_plugin(&self, plugin_id: &str) -> Option<Plugin> {
415 let plugins = self.plugins.read().await;
416 plugins.get(plugin_id).cloned()
417 }
418
419 pub async fn get_stats(&self) -> serde_json::Value {
421 let plugins = self.plugins.read().await;
422 let enabled = self.enabled_plugins.read().await;
423
424 let total_plugins = plugins.len();
425 let running_plugins = plugins
426 .values()
427 .filter(|p| p.state == PluginState::Running)
428 .count();
429 let enabled_plugins = enabled.len();
430
431 serde_json::json!({
432 "total_plugins": total_plugins,
433 "running_plugins": running_plugins,
434 "enabled_plugins": enabled_plugins,
435 "plugin_directory": self.plugin_directory.display().to_string()
436 })
437 }
438}
439
440static PLUGIN_MANAGER: tokio::sync::OnceCell<PluginManager> = tokio::sync::OnceCell::const_new();
442
443pub async fn get_plugin_manager() -> &'static PluginManager {
445 PLUGIN_MANAGER
446 .get_or_init(|| async {
447 let plugin_dir = std::env::current_dir()
448 .unwrap_or_else(|_| PathBuf::from("."))
449 .join("plugins");
450 PluginManager::new(plugin_dir)
451 })
452 .await
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use tempfile::TempDir;
459
460 #[tokio::test]
461 async fn test_plugin_discovery() {
462 let temp_dir = TempDir::new().unwrap();
463 let manager = PluginManager::new(temp_dir.path().to_path_buf());
464
465 let plugin_dir = temp_dir.path().join("test-plugin");
467 tokio::fs::create_dir_all(&plugin_dir).await.unwrap();
468
469 let manifest = serde_json::json!({
470 "id": "test-plugin",
471 "name": "Test Plugin",
472 "version": "1.0.0",
473 "description": "A test plugin",
474 "author": "Test Author",
475 "license": "MIT",
476 "keywords": ["test"],
477 "dependencies": [],
478 "permissions": [],
479 "entry_point": "main.js",
480 "supported_platforms": ["linux", "macos", "windows"],
481 "min_app_version": "1.0.0"
482 });
483
484 tokio::fs::write(plugin_dir.join("plugin.json"), manifest.to_string())
485 .await
486 .unwrap();
487
488 let discovered = manager.discover_plugins().await.unwrap();
489 assert_eq!(discovered.len(), 1);
490 assert_eq!(discovered[0].id, "test-plugin");
491 }
492
493 #[tokio::test]
494 async fn test_plugin_loading() {
495 let temp_dir = TempDir::new().unwrap();
496 let manager = PluginManager::new(temp_dir.path().to_path_buf());
497
498 let plugin_dir = temp_dir.path().join("test-plugin");
500 tokio::fs::create_dir_all(&plugin_dir).await.unwrap();
501
502 let manifest = serde_json::json!({
503 "id": "test-plugin",
504 "name": "Test Plugin",
505 "version": "1.0.0",
506 "description": "A test plugin",
507 "author": "Test Author",
508 "license": "MIT",
509 "keywords": ["test"],
510 "dependencies": [],
511 "permissions": [],
512 "entry_point": "main.js",
513 "supported_platforms": ["linux", "macos", "windows"],
514 "min_app_version": "1.0.0"
515 });
516
517 tokio::fs::write(plugin_dir.join("plugin.json"), manifest.to_string())
518 .await
519 .unwrap();
520
521 manager.load_plugin("test-plugin").await.unwrap();
522
523 let plugin = manager.get_plugin("test-plugin").await;
524 assert!(plugin.is_some());
525 assert_eq!(plugin.unwrap().state, PluginState::Loaded);
526 }
527}