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