Expand description
§Genja Plugin Manager
A flexible and easy-to-use plugin management system for Rust applications.
This crate provides dynamic loading, registration, and management of plugins at runtime. It supports individual plugins and grouped plugins, making it suitable for various application architectures where extensibility is required.
§Overview
The plugin manager enables building modular applications where functionality can be added through plugins without recompilation. It handles:
- Dynamic loading of plugins from shared libraries (.so, .dll, .dylib)
- Plugin lifecycle management (registration, deregistration)
- Type-safe plugin registry access
- Metadata-driven plugin configuration
- Support for multiple plugin types (Connection, Inventory, AsyncInventory, Runner, Processor, Transform)
§Architecture
┌─────────────────────────────────────────────────────────────────┐
│ PluginManager │
│ - Loads plugins from shared libraries │
│ - Maintains plugin registry │
│ - Provides type-safe access to plugins │
└────────────────┬────────────────────────────────────────────────┘
│
│ manages
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Plugins Enum │
│ - Connection(Box<dyn PluginConnection>) │
│ - Inventory(Box<dyn PluginInventory>) │
│ - AsyncInventory(Box<dyn AsyncPluginInventory>) │
│ - Processor(Box<dyn PluginProcessor>) │
│ - Runner(Box<dyn PluginRunner>) │
│ - TransformFunction(Box<dyn PluginTransformFunction>) │
└─────────────────────────────────────────────────────────────────┘§Quick Start
§Creating a Plugin
use async_trait::async_trait;
use genja_core::inventory::Hosts;
use genja_core::settings::RunnerConfig;
use genja_core::task::{TaskDefinition, TaskResults};
use genja_plugin_manager::plugin_types::{Plugin, PluginRunner, Plugins};
#[derive(Debug)]
struct MyPlugin;
impl Plugin for MyPlugin {
fn name(&self) -> String {
"my_plugin".to_string()
}
}
#[async_trait]
impl PluginRunner for MyPlugin {
async fn run_task(
&self,
_task: &TaskDefinition,
_hosts: &Hosts,
_connection_resolver: Option<std::sync::Arc<dyn genja_core::task::TaskConnectionResolver>>,
_runner_config: &RunnerConfig,
_max_depth: usize,
) -> Result<TaskResults, genja_core::GenjaError> {
// Task execution logic
Ok(TaskResults::new("my_plugin"))
}
// `run_tasks(...)` defaults to executing root task trees in order by
// delegating to `run_task(...)`. Override it only for custom batch behavior.
}
// Export plugin factory function
#[unsafe(no_mangle)]
pub fn create_plugins() -> Vec<Plugins> {
vec![Plugins::Runner(Box::new(MyPlugin))]
}§Using the Plugin Manager
use genja_plugin_manager::PluginManager;
// Load plugins from a runtime plugin directory.
let plugin_manager = PluginManager::new()
.load_plugins_from_directory("plugins")?;
// Access plugins by type
if let Some(runner) = plugin_manager.get_runner_plugin("my_plugin") {
// Use the runner plugin
}
// List all plugins
let all_plugins = plugin_manager.get_all_plugin_names_and_groups();
for (name, group) in all_plugins {
println!("Plugin: {} ({})", name, group);
}§Build Script Helper
End-user applications that declare plugin artifacts in
[package.metadata.plugins] can copy them into the profile-specific runtime
plugin directory from build.rs:
fn main() {
genja_plugin_manager::build_support::copy_plugins_from_manifest().unwrap();
}§Plugin Configuration
Plugin artifacts are declared in the Cargo.toml file of the end-user project
using package metadata. Those entries are consumed by
build_support::copy_plugins_from_manifest during the application’s build,
which copies the referenced shared libraries into target/{PROFILE}/plugins.
Paths are resolved relative to the consuming application’s Cargo.toml.
That means the correct plugin path depends on whether the application is a
standalone crate or a workspace member.
Standalone application example:
[package.metadata.plugins]
my_plugin = "target/{PROFILE}/libmy_plugin.so"Workspace member application example:
[package.metadata.plugins]
my_plugin = "../target/{PROFILE}/libmy_plugin.so"Individual and grouped plugin entries are also supported:
# Individual plugins
[package.metadata.plugins]
my_plugin = "target/{PROFILE}/libmy_plugin.so"
# Grouped plugins
[package.metadata.plugins.network]
ssh = "target/{PROFILE}/libssh.so"
telnet = "target/{PROFILE}/libtelnet.so"
# Grouped by plugin type (recommended)
[package.metadata.plugins.inventory]
inventory_a = "target/{PROFILE}/libinventory.so"
[package.metadata.plugins.connection]
ssh = "target/{PROFILE}/libssh.so"
netconf = "target/{PROFILE}/libnetconf.so"
[package.metadata.plugins.runner]
threaded = "target/{PROFILE}/libthreaded.so"
[package.metadata.plugins.processor]
audit = "target/{PROFILE}/libaudit_processor.so"
[package.metadata.plugins.transform]
normalize = "target/{PROFILE}/libnormalize.so"§Plugin Types
§Connection Plugins
Manage device connections with lifecycle hooks:
use async_trait::async_trait;
use genja_plugin_manager::plugin_types::{Plugin, PluginConnection};
use genja_core::inventory::{ConnectionKey, ResolvedConnectionParams};
#[derive(Debug)]
struct SshPlugin {
key: ConnectionKey,
connected: bool,
}
impl Plugin for SshPlugin {
fn name(&self) -> String { "ssh".to_string() }
}
#[async_trait]
impl PluginConnection for SshPlugin {
fn create(&self, key: &ConnectionKey) -> Box<dyn PluginConnection> {
Box::new(SshPlugin {
key: key.clone(),
connected: false,
})
}
async fn open(&mut self, params: &ResolvedConnectionParams) -> Result<(), String> {
// Establish connection
let _ = params;
self.connected = true;
Ok(())
}
fn close(&mut self) -> ConnectionKey {
self.connected = false;
self.key.clone()
}
fn is_alive(&self) -> bool {
self.connected
}
}§Inventory Plugins
Load inventory data from various sources:
use genja_plugin_manager::plugin_types::{Plugin, PluginInventory};
use genja_plugin_manager::PluginManager;
use genja_core::{Settings, InventoryLoadError};
use genja_core::inventory::Inventory;
#[derive(Debug)]
struct DatabaseInventoryPlugin;
impl Plugin for DatabaseInventoryPlugin {
fn name(&self) -> String { "database_inventory".to_string() }
}
impl PluginInventory for DatabaseInventoryPlugin {
fn load(
&self,
settings: &Settings,
plugins: &PluginManager,
) -> Result<Inventory, InventoryLoadError> {
// Load from database
unimplemented!()
}
}Async inventory plugins are also supported for remote inventory sources:
use async_trait::async_trait;
use genja_plugin_manager::plugin_types::{AsyncPluginInventory, Plugin};
use genja_plugin_manager::PluginManager;
use genja_core::{InventoryLoadError, Settings};
use genja_core::inventory::Inventory;
#[derive(Debug)]
struct RemoteInventoryPlugin;
impl Plugin for RemoteInventoryPlugin {
fn name(&self) -> String { "remote_inventory".to_string() }
}
#[async_trait]
impl AsyncPluginInventory for RemoteInventoryPlugin {
async fn load_async(
&self,
settings: &Settings,
plugins: &PluginManager,
) -> Result<Inventory, InventoryLoadError> {
let _ = (settings, plugins);
Ok(Inventory::builder().build())
}
}§Runner Plugins
Execute tasks against hosts:
use async_trait::async_trait;
use genja_plugin_manager::plugin_types::{Plugin, PluginRunner};
use genja_core::inventory::Hosts;
use genja_core::settings::RunnerConfig;
use genja_core::task::{TaskDefinition, TaskResults};
#[derive(Debug)]
struct ExampleSequentialRunner;
impl Plugin for ExampleSequentialRunner {
// This is a custom plugin example. The built-in Genja runner name is `serial`.
fn name(&self) -> String { "example_sequential".to_string() }
}
#[async_trait]
impl PluginRunner for ExampleSequentialRunner {
async fn run_task(
&self,
task: &TaskDefinition,
hosts: &Hosts,
connection_resolver: Option<std::sync::Arc<dyn genja_core::task::TaskConnectionResolver>>,
runner_config: &RunnerConfig,
max_depth: usize,
) -> Result<TaskResults, genja_core::GenjaError> {
// Execute task on each host sequentially
let _ = (task, hosts, connection_resolver, runner_config, max_depth);
Ok(TaskResults::new("example_sequential"))
}
// `run_tasks(...)` defaults to executing root task trees in order by
// delegating to `run_task(...)`. Override it only for custom batch behavior.
}§Processor Plugins
Process task lifecycle results selected by task processor names:
use genja_core::task::{TaskProcessor, TaskProcessorContext, TaskResults};
use genja_plugin_manager::plugin_types::{Plugin, PluginProcessor};
use std::sync::Arc;
#[derive(Debug)]
struct AuditProcessorPlugin;
impl Plugin for AuditProcessorPlugin {
fn name(&self) -> String { "audit".to_string() }
}
impl PluginProcessor for AuditProcessorPlugin {
fn processor(&self) -> Arc<dyn TaskProcessor> {
Arc::new(AuditProcessor)
}
}
struct AuditProcessor;
impl TaskProcessor for AuditProcessor {
fn on_task_finish(
&self,
context: &TaskProcessorContext,
results: &mut TaskResults,
) -> Result<(), genja_core::GenjaError> {
let _ = (context, results);
Ok(())
}
}§Transform Function Plugins
Provide inventory transformation functions:
use genja_plugin_manager::plugin_types::{Plugin, PluginTransformFunction};
use genja_core::inventory::{TransformFunction, Host, BaseBuilderHost};
#[derive(Debug)]
struct NormalizeHostnamePlugin;
impl Plugin for NormalizeHostnamePlugin {
fn name(&self) -> String { "normalize_hostname".to_string() }
}
impl PluginTransformFunction for NormalizeHostnamePlugin {
fn transform_function(&self) -> TransformFunction {
TransformFunction::new(|host: &Host, _options| {
if let Some(hostname) = host.hostname() {
host.to_builder().hostname(hostname.to_lowercase()).build()
} else {
host.clone()
}
})
}
}§Building Plugins
§Plugin Project Setup
- Add dependency in
Cargo.toml:
[dependencies]
genja-plugin-manager = "0.1.0"
genja-core = "0.1.0"- Configure library type:
[lib]
name = "my_plugin"
crate-type = ["lib", "cdylib"]- Build the plugin:
cargo build --releaseThe compiled library will be in target/release/ with platform-specific naming:
- Linux:
libmy_plugin.so - macOS:
libmy_plugin.dylib - Windows:
my_plugin.dll
When configuring the end-user project, prefer grouping by plugin type in
package.metadata.plugins (see example below).
§Project Structure Differences
§Core Library (Plugin Consumer)
[package]
name = "genja"
version = "0.1.0"
[dependencies]
genja-plugin-manager = "0.1.0"§Plugin Project (Connection/Runner/Inventory/Processor/Transform)
[package]
name = "my_plugin"
version = "0.1.0"
[dependencies]
genja-plugin-manager = "0.1.0"
genja-core = "0.1.0"
[lib]
name = "my_plugin"
crate-type = ["lib", "cdylib"]§End-User Project
[package]
name = "genja-app"
version = "0.1.0"
[dependencies]
genja = "0.1.0"
genja-plugin-manager = "0.1.0"
[build-dependencies]
genja-plugin-manager = "0.1.0"
# Standalone application paths.
# Grouped by plugin type (recommended)
[package.metadata.plugins.connection]
ssh = "target/{PROFILE}/libssh.so"
[package.metadata.plugins.inventory]
inventory_a = "target/{PROFILE}/libinventory.so"
[package.metadata.plugins.runner]
threaded = "target/{PROFILE}/libthreaded.so"
[package.metadata.plugins.processor]
audit = "target/{PROFILE}/libaudit_processor.so"
[package.metadata.plugins.transform]
normalize = "target/{PROFILE}/libnormalize.so"If the application is a workspace member and the workspace target directory
is one level up, use ../target/{PROFILE}/... instead.
Example build.rs for the end-user project:
fn main() {
genja_plugin_manager::build_support::copy_plugins_from_manifest().unwrap();
}Modules§
- build_
support - Build-time support for copying plugin libraries into the Cargo output directory.
- connection_
factory - Connection factory implementation for plugin-based connections.
- plugin_
types - Plugin type system and trait definitions for the plugin manager.
Structs§
- Metadata
- Plugin
Manager - Central registry and loader for dynamic plugins.