Skip to main content

Grove/Host/
ExtensionManager.rs

1//! Extension Manager Module
2//!
3//! Handles extension discovery, loading, and management.
4//! Provides query and monitoring capabilities for extensions.
5
6use std::{
7	collections::HashMap,
8	path::{Path, PathBuf},
9	sync::Arc,
10};
11
12use anyhow::{Context, Result};
13use serde::{Deserialize, Serialize};
14use tokio::sync::RwLock;
15
16use crate::{Host::HostConfig, WASM::Runtime::WASMRuntime, dev_log};
17
18/// Extension manager for handling extension lifecycle
19pub struct ExtensionManagerImpl {
20	/// WASM runtime for executing extensions
21	#[allow(dead_code)]
22	wasm_runtime:Arc<WASMRuntime>,
23
24	/// Host configuration
25	config:HostConfig,
26
27	/// Loaded extensions
28	extensions:Arc<RwLock<HashMap<String, ExtensionInfo>>>,
29
30	/// Extension statistics
31	stats:Arc<RwLock<ExtensionStats>>,
32}
33
34/// Extension information
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ExtensionInfo {
37	/// Extension ID (e.g., "publisher.extension-name")
38	pub id:String,
39
40	/// Extension display name
41	pub display_name:String,
42
43	/// Extension description
44	pub description:String,
45
46	/// Extension version
47	pub version:String,
48
49	/// Publisher name
50	pub publisher:String,
51
52	/// Path to extension directory
53	pub path:PathBuf,
54
55	/// Entry point file
56	pub entry_point:PathBuf,
57
58	/// Activation events
59	pub activation_events:Vec<String>,
60
61	/// Type of extension (wasm, native, etc.)
62	pub extension_type:ExtensionType,
63
64	/// Extension state
65	pub state:ExtensionState,
66
67	/// Extension capabilities
68	pub capabilities:Vec<String>,
69
70	/// Dependencies
71	pub dependencies:Vec<String>,
72
73	/// Extension manifest (JSON)
74	pub manifest:serde_json::Value,
75
76	/// Load timestamp
77	pub loaded_at:u64,
78
79	/// Activation timestamp
80	pub activated_at:Option<u64>,
81}
82
83/// Extension type
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
85pub enum ExtensionType {
86	/// WebAssembly extension
87	WASM,
88
89	/// Native Rust extension
90	Native,
91
92	/// JavaScript/TypeScript extension (via Cocoon compatibility)
93	JavaScript,
94
95	/// Unknown type
96	Unknown,
97}
98
99/// Extension state
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101pub enum ExtensionState {
102	/// Extension is loaded but not activated
103	Loaded,
104
105	/// Extension is activated and running
106	Activated,
107
108	/// Extension is deactivated
109	Deactivated,
110
111	/// Extension encountered an error
112	Error,
113}
114
115/// Extension statistics
116#[derive(Debug, Clone, Default, Serialize, Deserialize)]
117pub struct ExtensionStats {
118	/// Total number of extensions loaded
119	pub total_loaded:usize,
120
121	/// Total number of extensions activated
122	pub total_activated:usize,
123
124	/// Total number of extensions deactivated
125	pub total_deactivated:usize,
126
127	/// Total activation time in milliseconds
128	pub total_activation_time_ms:u64,
129
130	/// Number of errors encountered
131	pub errors:u64,
132}
133
134impl ExtensionManagerImpl {
135	/// Create a new extension manager
136	pub fn new(wasm_runtime:Arc<WASMRuntime>, config:HostConfig) -> Self {
137		Self {
138			wasm_runtime,
139
140			config,
141
142			extensions:Arc::new(RwLock::new(HashMap::new())),
143
144			stats:Arc::new(RwLock::new(ExtensionStats::default())),
145		}
146	}
147
148	/// Load an extension from a path
149	pub async fn load_extension(&self, path:&PathBuf) -> Result<String> {
150		dev_log!("extensions", "Loading extension from: {:?}", path);
151
152		// Validate path
153		if !path.exists() {
154			return Err(anyhow::anyhow!("Extension path does not exist: {:?}", path));
155		}
156
157		// Parse manifest
158		let manifest = self.parse_manifest(path)?;
159
160		let extension_id = self.extract_extension_id(&manifest)?;
161
162		// Check if extension is already loaded
163		let extensions = self.extensions.read().await;
164
165		if extensions.contains_key(&extension_id) {
166			dev_log!("extensions", "warn: extension already loaded: {}", extension_id);
167
168			return Ok(extension_id);
169		}
170
171		drop(extensions);
172
173		// Determine extension type
174		let extension_type = self.determine_extension_type(path, &manifest)?;
175
176		// Create extension info
177		let extension_info = ExtensionInfo {
178			id:extension_id.clone(),
179
180			display_name:manifest.get("displayName").and_then(|v| v.as_str()).unwrap_or("").to_string(),
181
182			description:manifest.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string(),
183
184			version:manifest.get("version").and_then(|v| v.as_str()).unwrap_or("0.0.0").to_string(),
185
186			publisher:manifest.get("publisher").and_then(|v| v.as_str()).unwrap_or("").to_string(),
187
188			path:path.clone(),
189
190			entry_point:path.join(manifest.get("main").and_then(|v| v.as_str()).unwrap_or("dist/extension.js")),
191
192			activation_events:self.extract_activation_events(&manifest),
193
194			extension_type,
195
196			state:ExtensionState::Loaded,
197
198			capabilities:self.extract_capabilities(&manifest),
199
200			dependencies:self.extract_dependencies(&manifest),
201
202			manifest,
203
204			loaded_at:std::time::SystemTime::now()
205				.duration_since(std::time::UNIX_EPOCH)
206				.map(|d| d.as_secs())
207				.unwrap_or(0),
208
209			activated_at:None,
210		};
211
212		// Register extension
213		let mut extensions = self.extensions.write().await;
214
215		extensions.insert(extension_id.clone(), extension_info);
216
217		// Update statistics
218		let mut stats = self.stats.write().await;
219
220		stats.total_loaded += 1;
221
222		dev_log!("extensions", "Extension loaded successfully: {}", extension_id);
223
224		Ok(extension_id)
225	}
226
227	/// Unload an extension
228	pub async fn unload_extension(&self, extension_id:&str) -> Result<()> {
229		dev_log!("extensions", "Unloading extension: {}", extension_id);
230
231		let mut extensions = self.extensions.write().await;
232
233		extensions.remove(extension_id);
234
235		dev_log!("extensions", "Extension unloaded: {}", extension_id);
236
237		Ok(())
238	}
239
240	/// Get an extension by ID
241	pub async fn get_extension(&self, extension_id:&str) -> Option<ExtensionInfo> {
242		self.extensions.read().await.get(extension_id).cloned()
243	}
244
245	/// List all loaded extensions
246	pub async fn list_extensions(&self) -> Vec<String> { self.extensions.read().await.keys().cloned().collect() }
247
248	/// List extensions in a specific state
249	pub async fn list_extensions_by_state(&self, state:ExtensionState) -> Vec<ExtensionInfo> {
250		self.extensions
251			.read()
252			.await
253			.values()
254			.filter(|ext| ext.state == state)
255			.cloned()
256			.collect()
257	}
258
259	/// Update extension state
260	pub async fn update_state(&self, extension_id:&str, state:ExtensionState) -> Result<()> {
261		let mut extensions = self.extensions.write().await;
262
263		if let Some(info) = extensions.get_mut(extension_id) {
264			info.state = state;
265
266			if state == ExtensionState::Activated {
267				info.activated_at = Some(
268					std::time::SystemTime::now()
269						.duration_since(std::time::UNIX_EPOCH)
270						.map(|d| d.as_secs())
271						.unwrap_or(0),
272				);
273
274				let mut stats = self.stats.write().await;
275
276				stats.total_activated += 1;
277			} else if state == ExtensionState::Deactivated {
278				let mut stats = self.stats.write().await;
279
280				stats.total_deactivated += 1;
281			}
282
283			Ok(())
284		} else {
285			Err(anyhow::anyhow!("Extension not found: {}", extension_id))
286		}
287	}
288
289	/// Get extension manager statistics
290	pub async fn stats(&self) -> ExtensionStats { self.stats.read().await.clone() }
291
292	/// Discover extensions in configured paths
293	pub async fn discover_extensions(&self) -> Result<Vec<PathBuf>> {
294		dev_log!("extensions", "Discovering extensions in configured paths");
295
296		let mut extensions = Vec::new();
297
298		for discovery_path in &self.config.discovery_paths {
299			match self.discover_in_path(discovery_path).await {
300				Ok(mut found) => extensions.append(&mut found),
301
302				Err(e) => {
303					dev_log!("extensions", "warn: failed to discover extensions in {}: {}", discovery_path, e);
304				},
305			}
306		}
307
308		dev_log!("extensions", "Discovered {} extensions", extensions.len());
309
310		Ok(extensions)
311	}
312
313	/// Discover extensions in a specific path
314	async fn discover_in_path(&self, path:&str) -> Result<Vec<PathBuf>> {
315		let path = PathBuf::from(shellexpand::tilde(path).as_ref());
316
317		if !path.exists() {
318			return Ok(Vec::new());
319		}
320
321		let mut extensions = Vec::new();
322
323		// Read directory entries
324		let mut entries = tokio::fs::read_dir(&path)
325			.await
326			.context(format!("Failed to read directory: {:?}", path))?;
327
328		while let Some(entry) = entries.next_entry().await? {
329			let entry_path = entry.path();
330
331			// Skip if not a directory
332			if !entry_path.is_dir() {
333				continue;
334			}
335
336			// Check for package.json or manifest.json
337			let manifest_path = entry_path.join("package.json");
338
339			let alt_manifest_path = entry_path.join("manifest.json");
340
341			if manifest_path.exists() || alt_manifest_path.exists() {
342				extensions.push(entry_path.clone());
343
344				dev_log!("extensions", "Discovered extension: {:?}", entry_path);
345			}
346		}
347
348		Ok(extensions)
349	}
350
351	/// Parse extension manifest
352	fn parse_manifest(&self, path:&Path) -> Result<serde_json::Value> {
353		let manifest_path = path.join("package.json");
354
355		let alt_manifest_path = path.join("manifest.json");
356
357		let manifest_content = if manifest_path.exists() {
358			tokio::runtime::Runtime::new()
359				.unwrap()
360				.block_on(tokio::fs::read_to_string(&manifest_path))
361				.context("Failed to read package.json")?
362		} else if alt_manifest_path.exists() {
363			tokio::runtime::Runtime::new()
364				.unwrap()
365				.block_on(tokio::fs::read_to_string(&alt_manifest_path))
366				.context("Failed to read manifest.json")?
367		} else {
368			return Err(anyhow::anyhow!("No manifest found in extension path"));
369		};
370
371		let manifest:serde_json::Value = serde_json::from_str(&manifest_content).context("Failed to parse manifest")?;
372
373		Ok(manifest)
374	}
375
376	/// Extract extension ID from manifest
377	fn extract_extension_id(&self, manifest:&serde_json::Value) -> Result<String> {
378		let publisher = manifest
379			.get("publisher")
380			.and_then(|v| v.as_str())
381			.ok_or_else(|| anyhow::anyhow!("Missing publisher in manifest"))?;
382
383		let name = manifest
384			.get("name")
385			.and_then(|v| v.as_str())
386			.ok_or_else(|| anyhow::anyhow!("Missing name in manifest"))?;
387
388		Ok(format!("{}.{}", publisher, name))
389	}
390
391	/// Determine extension type
392	fn determine_extension_type(&self, path:&Path, manifest:&serde_json::Value) -> Result<ExtensionType> {
393		// Check for WASM file
394		let wasm_path = path.join("extension.wasm");
395
396		if wasm_path.exists() {
397			return Ok(ExtensionType::WASM);
398		}
399
400		// Check for Rust project
401		let cargo_path = path.join("Cargo.toml");
402
403		if cargo_path.exists() {
404			return Ok(ExtensionType::Native);
405		}
406
407		// Check for JavaScript/TypeScript
408		let main = manifest.get("main").and_then(|v| v.as_str());
409
410		if let Some(main) = main {
411			let main_path = path.join(main);
412
413			if main_path.exists() && (main.ends_with(".js") || main.ends_with(".ts")) {
414				return Ok(ExtensionType::JavaScript);
415			}
416		}
417
418		Ok(ExtensionType::Unknown)
419	}
420
421	/// Extract activation events from manifest
422	fn extract_activation_events(&self, manifest:&serde_json::Value) -> Vec<String> {
423		manifest
424			.get("activationEvents")
425			.and_then(|v| v.as_array())
426			.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
427			.unwrap_or_default()
428	}
429
430	/// Extract capabilities from manifest
431	fn extract_capabilities(&self, manifest:&serde_json::Value) -> Vec<String> {
432		manifest
433			.get("capabilities")
434			.and_then(|v| v.as_object())
435			.map(|obj| obj.keys().cloned().collect())
436			.unwrap_or_default()
437	}
438
439	/// Extract dependencies from manifest
440	fn extract_dependencies(&self, manifest:&serde_json::Value) -> Vec<String> {
441		manifest
442			.get("extensionDependencies")
443			.and_then(|v| v.as_array())
444			.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
445			.unwrap_or_default()
446	}
447}
448
449#[cfg(test)]
450mod tests {
451
452	use super::*;
453
454	#[test]
455	fn test_extension_type() {
456		assert_eq!(ExtensionType::WASM, ExtensionType::WASM);
457
458		assert_eq!(ExtensionType::Native, ExtensionType::Native);
459
460		assert_eq!(ExtensionType::JavaScript, ExtensionType::JavaScript);
461	}
462
463	#[test]
464	fn test_extension_state() {
465		assert_eq!(ExtensionState::Loaded, ExtensionState::Loaded);
466
467		assert_eq!(ExtensionState::Activated, ExtensionState::Activated);
468
469		assert_eq!(ExtensionState::Deactivated, ExtensionState::Deactivated);
470
471		assert_eq!(ExtensionState::Error, ExtensionState::Error);
472	}
473
474	#[tokio::test]
475	async fn test_extension_manager_creation() {
476		let wasm_runtime = Arc::new(
477			tokio::runtime::Runtime::new()
478				.unwrap()
479				.block_on(crate::WASM::Runtime::WASMRuntime::new(
480					crate::WASM::Runtime::WASMConfig::default(),
481				))
482				.unwrap(),
483		);
484
485		let config = HostConfig::default();
486
487		let manager = ExtensionManagerImpl::new(wasm_runtime, config);
488
489		assert_eq!(manager.list_extensions().await.len(), 0);
490	}
491
492	#[test]
493	fn test_extension_stats_default() {
494		let stats = ExtensionStats::default();
495
496		assert_eq!(stats.total_loaded, 0);
497
498		assert_eq!(stats.total_activated, 0);
499	}
500}