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('/') {
270 return Err(AppError::Io(std::io::Error::new(
271 std::io::ErrorKind::PermissionDenied,
272 "Invalid file system permission pattern",
273 )));
274 }
275 }
276 PluginPermission::Network(host) => {
277 if host == "*" {
279 return Err(AppError::Io(std::io::Error::new(
280 std::io::ErrorKind::PermissionDenied,
281 "Wildcard network access not allowed",
282 )));
283 }
284 }
285 PluginPermission::ProcessSpawn => {
286 tracing::warn!("Plugin requests process spawn permission");
288 }
289 _ => {}
290 }
291 }
292 Ok(())
293 }
294
295 pub async fn start_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
297 let mut plugins = self.plugins.write().await;
298
299 let plugin = plugins.get_mut(plugin_id).ok_or_else(|| {
300 AppError::Io(std::io::Error::new(
301 std::io::ErrorKind::NotFound,
302 format!("Plugin not found: {}", plugin_id),
303 ))
304 })?;
305
306 if plugin.state == PluginState::Running {
307 return Ok(());
308 }
309
310 plugin.state = PluginState::Running;
312 plugin.last_activity = chrono::Utc::now();
313
314 tracing::info!("Started plugin: {}", plugin_id);
315 Ok(())
316 }
317
318 pub async fn stop_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
320 let mut plugins = self.plugins.write().await;
321
322 let plugin = plugins.get_mut(plugin_id).ok_or_else(|| {
323 AppError::Io(std::io::Error::new(
324 std::io::ErrorKind::NotFound,
325 format!("Plugin not found: {}", plugin_id),
326 ))
327 })?;
328
329 if plugin.state == PluginState::Stopped {
330 return Ok(());
331 }
332
333 plugin.state = PluginState::Stopped;
335 plugin.last_activity = chrono::Utc::now();
336
337 tracing::info!("Stopped plugin: {}", plugin_id);
338 Ok(())
339 }
340
341 pub async fn unload_plugin(&self, plugin_id: &str) -> Result<(), AppError> {
343 self.stop_plugin(plugin_id).await?;
345
346 let mut plugins = self.plugins.write().await;
348 let mut enabled = self.enabled_plugins.write().await;
349 let mut event_handlers = self.event_handlers.write().await;
350 let mut command_handlers = self.command_handlers.write().await;
351
352 plugins.remove(plugin_id);
353 enabled.retain(|id| id != plugin_id);
354
355 for handlers in event_handlers.values_mut() {
357 handlers.retain(|id| id != plugin_id);
358 }
359
360 command_handlers.retain(|_, id| id != plugin_id);
362
363 tracing::info!("Unloaded plugin: {}", plugin_id);
364 Ok(())
365 }
366
367 pub async fn execute_command(
369 &self,
370 command: &str,
371 _args: serde_json::Value,
372 ) -> Result<serde_json::Value, AppError> {
373 let command_handlers = self.command_handlers.read().await;
374
375 if let Some(plugin_id) = command_handlers.get(command) {
376 tracing::info!("Executing command '{}' on plugin '{}'", command, plugin_id);
378
379 Ok(serde_json::json!({
380 "status": "success",
381 "plugin_id": plugin_id,
382 "command": command,
383 "result": "Command executed successfully"
384 }))
385 } else {
386 Err(AppError::Io(std::io::Error::new(
387 std::io::ErrorKind::NotFound,
388 format!("No handler found for command: {}", command),
389 )))
390 }
391 }
392
393 pub async fn broadcast_event(
395 &self,
396 event: &str,
397 _data: serde_json::Value,
398 ) -> Result<(), AppError> {
399 let event_handlers = self.event_handlers.read().await;
400
401 if let Some(handlers) = event_handlers.get(event) {
402 for plugin_id in handlers {
403 tracing::debug!("Broadcasting event '{}' to plugin '{}'", event, plugin_id);
405 }
406 }
407
408 Ok(())
409 }
410
411 pub async fn get_plugins(&self) -> Vec<Plugin> {
413 let plugins = self.plugins.read().await;
414 plugins.values().cloned().collect()
415 }
416
417 pub async fn get_plugin(&self, plugin_id: &str) -> Option<Plugin> {
419 let plugins = self.plugins.read().await;
420 plugins.get(plugin_id).cloned()
421 }
422
423 pub async fn get_stats(&self) -> serde_json::Value {
425 let plugins = self.plugins.read().await;
426 let enabled = self.enabled_plugins.read().await;
427
428 let total_plugins = plugins.len();
429 let running_plugins = plugins
430 .values()
431 .filter(|p| p.state == PluginState::Running)
432 .count();
433 let enabled_plugins = enabled.len();
434
435 serde_json::json!({
436 "total_plugins": total_plugins,
437 "running_plugins": running_plugins,
438 "enabled_plugins": enabled_plugins,
439 "plugin_directory": self.plugin_directory.display().to_string()
440 })
441 }
442}
443
444static PLUGIN_MANAGER: tokio::sync::OnceCell<PluginManager> = tokio::sync::OnceCell::const_new();
446
447pub async fn get_plugin_manager() -> &'static PluginManager {
449 PLUGIN_MANAGER
450 .get_or_init(|| async {
451 let plugin_dir = std::env::current_dir()
452 .unwrap_or_else(|_| PathBuf::from("."))
453 .join("plugins");
454 PluginManager::new(plugin_dir)
455 })
456 .await
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use tempfile::TempDir;
463
464 #[tokio::test]
465 async fn test_plugin_discovery() {
466 let temp_dir = TempDir::new().unwrap();
467 let manager = PluginManager::new(temp_dir.path().to_path_buf());
468
469 let plugin_dir = temp_dir.path().join("test-plugin");
471 tokio::fs::create_dir_all(&plugin_dir).await.unwrap();
472
473 let manifest = serde_json::json!({
474 "id": "test-plugin",
475 "name": "Test Plugin",
476 "version": "1.0.0",
477 "description": "A test plugin",
478 "author": "Test Author",
479 "license": "MIT",
480 "keywords": ["test"],
481 "dependencies": [],
482 "permissions": [],
483 "entry_point": "main.js",
484 "supported_platforms": ["linux", "macos", "windows"],
485 "min_app_version": "1.0.0"
486 });
487
488 tokio::fs::write(plugin_dir.join("plugin.json"), manifest.to_string())
489 .await
490 .unwrap();
491
492 let discovered = manager.discover_plugins().await.unwrap();
493 assert_eq!(discovered.len(), 1);
494 assert_eq!(discovered[0].id, "test-plugin");
495 }
496
497 #[tokio::test]
498 async fn test_plugin_loading() {
499 let temp_dir = TempDir::new().unwrap();
500 let manager = PluginManager::new(temp_dir.path().to_path_buf());
501
502 let plugin_dir = temp_dir.path().join("test-plugin");
504 tokio::fs::create_dir_all(&plugin_dir).await.unwrap();
505
506 let manifest = serde_json::json!({
507 "id": "test-plugin",
508 "name": "Test Plugin",
509 "version": "1.0.0",
510 "description": "A test plugin",
511 "author": "Test Author",
512 "license": "MIT",
513 "keywords": ["test"],
514 "dependencies": [],
515 "permissions": [],
516 "entry_point": "main.js",
517 "supported_platforms": ["linux", "macos", "windows"],
518 "min_app_version": "1.0.0"
519 });
520
521 tokio::fs::write(plugin_dir.join("plugin.json"), manifest.to_string())
522 .await
523 .unwrap();
524
525 manager.load_plugin("test-plugin").await.unwrap();
526
527 let plugin = manager.get_plugin("test-plugin").await;
528 assert!(plugin.is_some());
529 assert_eq!(plugin.unwrap().state, PluginState::Loaded);
530 }
531}