pinakes-core: update remaining modules and tests

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9e0ff5ea33a5cf697473423e88f167ce6a6a6964
This commit is contained in:
raf 2026-03-08 00:42:29 +03:00
commit 3d9f8933d2
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
44 changed files with 1207 additions and 578 deletions

View file

@ -97,6 +97,11 @@ impl From<crate::config::PluginsConfig> for PluginManagerConfig {
impl PluginManager {
/// Create a new plugin manager
///
/// # Errors
///
/// Returns an error if the data or cache directories cannot be created, or
/// if the WASM runtime cannot be initialized.
pub fn new(
data_dir: PathBuf,
cache_dir: PathBuf,
@ -123,10 +128,14 @@ impl PluginManager {
}
/// Discover and load all plugins from configured directories
///
/// # Errors
///
/// Returns an error if plugin discovery fails.
pub async fn discover_and_load_all(&self) -> Result<Vec<String>> {
info!("Discovering plugins from {:?}", self.config.plugin_dirs);
let manifests = self.loader.discover_plugins().await?;
let manifests = self.loader.discover_plugins()?;
let mut loaded_plugins = Vec::new();
for manifest in manifests {
@ -145,6 +154,12 @@ impl PluginManager {
}
/// Load a plugin from a manifest file
///
/// # Errors
///
/// Returns an error if the plugin ID is invalid, capability validation
/// fails, the WASM binary cannot be loaded, or the plugin cannot be
/// registered.
async fn load_plugin_from_manifest(
&self,
manifest: &pinakes_plugin_api::PluginManifest,
@ -156,7 +171,7 @@ impl PluginManager {
|| plugin_id.contains('\\')
|| plugin_id.contains("..")
{
return Err(anyhow::anyhow!("Invalid plugin ID: {}", plugin_id));
return Err(anyhow::anyhow!("Invalid plugin ID: {plugin_id}"));
}
// Check if already loaded
@ -202,7 +217,7 @@ impl PluginManager {
// Load WASM binary
let wasm_path = self.loader.resolve_wasm_path(manifest)?;
let wasm_plugin = self.runtime.load_plugin(&wasm_path, context).await?;
let wasm_plugin = self.runtime.load_plugin(&wasm_path, context)?;
// Initialize plugin
let init_succeeded = match wasm_plugin
@ -246,13 +261,20 @@ impl PluginManager {
enabled: init_succeeded,
};
let mut registry = self.registry.write().await;
registry.register(registered)?;
{
let mut registry = self.registry.write().await;
registry.register(registered)?;
}
Ok(plugin_id)
}
/// Install a plugin from a file or URL
///
/// # Errors
///
/// Returns an error if the plugin cannot be downloaded, the manifest cannot
/// be read, or the plugin cannot be loaded.
pub async fn install_plugin(&self, source: &str) -> Result<String> {
info!("Installing plugin from: {}", source);
@ -276,13 +298,18 @@ impl PluginManager {
}
/// Uninstall a plugin
///
/// # Errors
///
/// Returns an error if the plugin ID is invalid, the plugin cannot be shut
/// down, cannot be unregistered, or its data directories cannot be removed.
pub async fn uninstall_plugin(&self, plugin_id: &str) -> Result<()> {
// Validate plugin_id to prevent path traversal
if plugin_id.contains('/')
|| plugin_id.contains('\\')
|| plugin_id.contains("..")
{
return Err(anyhow::anyhow!("Invalid plugin ID: {}", plugin_id));
return Err(anyhow::anyhow!("Invalid plugin ID: {plugin_id}"));
}
info!("Uninstalling plugin: {}", plugin_id);
@ -291,8 +318,10 @@ impl PluginManager {
self.shutdown_plugin(plugin_id).await?;
// Remove from registry
let mut registry = self.registry.write().await;
registry.unregister(plugin_id)?;
{
let mut registry = self.registry.write().await;
registry.unregister(plugin_id)?;
}
// Remove plugin data and cache
let plugin_data_dir = self.data_dir.join(plugin_id);
@ -309,37 +338,55 @@ impl PluginManager {
}
/// Enable a plugin
///
/// # Errors
///
/// Returns an error if the plugin ID is not found in the registry.
pub async fn enable_plugin(&self, plugin_id: &str) -> Result<()> {
let mut registry = self.registry.write().await;
registry.enable(plugin_id)
}
/// Disable a plugin
///
/// # Errors
///
/// Returns an error if the plugin ID is not found in the registry.
pub async fn disable_plugin(&self, plugin_id: &str) -> Result<()> {
let mut registry = self.registry.write().await;
registry.disable(plugin_id)
}
/// Shutdown a specific plugin
///
/// # Errors
///
/// Returns an error if the plugin ID is not found in the registry.
pub async fn shutdown_plugin(&self, plugin_id: &str) -> Result<()> {
debug!("Shutting down plugin: {}", plugin_id);
let registry = self.registry.read().await;
if let Some(plugin) = registry.get(plugin_id) {
plugin.wasm_plugin.call_function("shutdown", &[]).await.ok();
let _ = plugin.wasm_plugin.call_function("shutdown", &[]).await;
Ok(())
} else {
Err(anyhow::anyhow!("Plugin not found: {}", plugin_id))
Err(anyhow::anyhow!("Plugin not found: {plugin_id}"))
}
}
/// Shutdown all plugins
///
/// # Errors
///
/// This function always returns `Ok(())`. Individual plugin shutdown errors
/// are logged but do not cause the overall operation to fail.
pub async fn shutdown_all(&self) -> Result<()> {
info!("Shutting down all plugins");
let registry = self.registry.read().await;
let plugin_ids: Vec<String> =
registry.list_all().iter().map(|p| p.id.clone()).collect();
let plugin_ids: Vec<String> = {
let registry = self.registry.read().await;
registry.list_all().iter().map(|p| p.id.clone()).collect()
};
for plugin_id in plugin_ids {
if let Err(e) = self.shutdown_plugin(&plugin_id).await {
@ -373,6 +420,11 @@ impl PluginManager {
}
/// Reload a plugin (for hot-reload during development)
///
/// # Errors
///
/// Returns an error if hot-reload is disabled, the plugin is not found, it
/// cannot be shut down, or the reloaded plugin cannot be registered.
pub async fn reload_plugin(&self, plugin_id: &str) -> Result<()> {
if !self.config.enable_hot_reload {
return Err(anyhow::anyhow!("Hot-reload is disabled"));
@ -387,15 +439,21 @@ impl PluginManager {
let plugin = registry
.get(plugin_id)
.ok_or_else(|| anyhow::anyhow!("Plugin not found"))?;
if let Some(ref manifest_path) = plugin.manifest_path {
pinakes_plugin_api::PluginManifest::from_file(manifest_path)
.unwrap_or_else(|e| {
warn!("Failed to re-read manifest from disk, using cached: {}", e);
plugin.manifest.clone()
})
} else {
plugin.manifest.clone()
}
let manifest = plugin.manifest_path.as_ref().map_or_else(
|| plugin.manifest.clone(),
|manifest_path| {
pinakes_plugin_api::PluginManifest::from_file(manifest_path)
.unwrap_or_else(|e| {
warn!(
"Failed to re-read manifest from disk, using cached: {}",
e
);
plugin.manifest.clone()
})
},
);
drop(registry);
manifest
};
// Shutdown and unload current version