feat(plugin-api): add support for plugin dependencies
This commit is contained in:
parent
4bdc3d187b
commit
9012a92287
@ -112,7 +112,7 @@ proc-macro-error2 = "2"
|
|||||||
aes = "0.9"
|
aes = "0.9"
|
||||||
async-compression = "0.4.41"
|
async-compression = "0.4.41"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bitflags = "2.11.0"
|
bitflags = "2.11.1"
|
||||||
cesu8 = "1.1"
|
cesu8 = "1.1"
|
||||||
cfb8 = "0.9"
|
cfb8 = "0.9"
|
||||||
colored = "3.1"
|
colored = "3.1"
|
||||||
@ -131,7 +131,7 @@ hmac = "=0.13.0"
|
|||||||
indexmap = "2.14"
|
indexmap = "2.14"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
libloading = "0.9"
|
libloading = "0.9"
|
||||||
lru = "0.16.3"
|
lru = "0.17.0"
|
||||||
lz4-java-wrc = "0.2.0"
|
lz4-java-wrc = "0.2.0"
|
||||||
md5 = "0.8"
|
md5 = "0.8"
|
||||||
num-bigint = "0.4"
|
num-bigint = "0.4"
|
||||||
|
|||||||
@ -74,9 +74,8 @@ and customizable experience. It prioritizes performance and player enjoyment whi
|
|||||||
- [x] Mobs (W.I.P)
|
- [x] Mobs (W.I.P)
|
||||||
- [x] Animals (W.I.P)
|
- [x] Animals (W.I.P)
|
||||||
- [Entity AI](https://github.com/Pumpkin-MC/Pumpkin/issues/1406)
|
- [Entity AI](https://github.com/Pumpkin-MC/Pumpkin/issues/1406)
|
||||||
- [ ] Boss
|
- [x] Boss
|
||||||
- [ ] Villagers
|
- [ ] Villagers
|
||||||
- [ ] Mobs Inventory
|
|
||||||
- [X] Entity Saving
|
- [X] Entity Saving
|
||||||
- Server
|
- Server
|
||||||
- [Plugins](https://github.com/Pumpkin-MC/Pumpkin/issues/1407)
|
- [Plugins](https://github.com/Pumpkin-MC/Pumpkin/issues/1407)
|
||||||
|
|||||||
@ -87,6 +87,7 @@ pub fn plugin_impl(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
authors: env!("CARGO_PKG_AUTHORS").split(',').map(String::from).collect(),
|
authors: env!("CARGO_PKG_AUTHORS").split(',').map(String::from).collect(),
|
||||||
description: env!("CARGO_PKG_DESCRIPTION").to_string(),
|
description: env!("CARGO_PKG_DESCRIPTION").to_string(),
|
||||||
|
dependencies: Vec::new(),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
//! version: "0.1.0".into(),
|
//! version: "0.1.0".into(),
|
||||||
//! authors: vec!["you".into()],
|
//! authors: vec!["you".into()],
|
||||||
//! description: "An example plugin.".into(),
|
//! description: "An example plugin.".into(),
|
||||||
|
//! dependencies: vec![],
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
@ -78,6 +79,8 @@ pub struct PluginMetadata {
|
|||||||
pub authors: Vec<String>,
|
pub authors: Vec<String>,
|
||||||
/// A short description of what the plugin does.
|
/// A short description of what the plugin does.
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
/// The list of plugin dependencies.
|
||||||
|
pub dependencies: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl wit::exports::pumpkin::plugin::metadata::Guest for Component {
|
impl wit::exports::pumpkin::plugin::metadata::Guest for Component {
|
||||||
@ -89,6 +92,7 @@ impl wit::exports::pumpkin::plugin::metadata::Guest for Component {
|
|||||||
version: metadata.version,
|
version: metadata.version,
|
||||||
authors: metadata.authors,
|
authors: metadata.authors,
|
||||||
description: metadata.description,
|
description: metadata.description,
|
||||||
|
dependencies: metadata.dependencies,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ interface metadata {
|
|||||||
authors: list<string>,
|
authors: list<string>,
|
||||||
/// Short description of the plugin.
|
/// Short description of the plugin.
|
||||||
description: string,
|
description: string,
|
||||||
|
/// Plugin dependencies.
|
||||||
|
dependencies: list<string>,
|
||||||
}
|
}
|
||||||
|
|
||||||
get-metadata: func() -> plugin-metadata;
|
get-metadata: func() -> plugin-metadata;
|
||||||
|
|||||||
@ -22,6 +22,8 @@ pub struct PluginMetadata {
|
|||||||
pub authors: Vec<String>,
|
pub authors: Vec<String>,
|
||||||
/// A description of the plugin.
|
/// A description of the plugin.
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
/// The dependencies of the plugin.
|
||||||
|
pub dependencies: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This type represents a future for the plugin.
|
/// This type represents a future for the plugin.
|
||||||
|
|||||||
@ -64,6 +64,7 @@ pub async fn init_plugin(
|
|||||||
version: metadata.version,
|
version: metadata.version,
|
||||||
authors: metadata.authors,
|
authors: metadata.authors,
|
||||||
description: metadata.description,
|
description: metadata.description,
|
||||||
|
dependencies: metadata.dependencies,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
|
|||||||
@ -201,6 +201,9 @@ pub enum ManagerError {
|
|||||||
|
|
||||||
#[error("Plugin manager not initialized properly")]
|
#[error("Plugin manager not initialized properly")]
|
||||||
ManagerNotInitialized,
|
ManagerNotInitialized,
|
||||||
|
|
||||||
|
#[error("Dependency error: {0}")]
|
||||||
|
DependencyError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PluginManager {
|
impl Default for PluginManager {
|
||||||
@ -291,6 +294,180 @@ impl PluginManager {
|
|||||||
self.loaders.read().await.clone()
|
self.loaders.read().await.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper for topological sort of plugins based on dependencies
|
||||||
|
fn topological_sort(plugins: &[(String, Vec<String>)]) -> Result<Vec<String>, String> {
|
||||||
|
fn visit(
|
||||||
|
name: &str,
|
||||||
|
deps_map: &HashMap<String, &Vec<String>>,
|
||||||
|
plugin_names: &HashSet<String>,
|
||||||
|
visited: &mut HashSet<String>,
|
||||||
|
current_path: &mut HashSet<String>,
|
||||||
|
sorted: &mut Vec<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if current_path.contains(name) {
|
||||||
|
return Err(format!(
|
||||||
|
"Circular dependency detected involving plugin: {name}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !visited.contains(name) {
|
||||||
|
current_path.insert(name.to_string());
|
||||||
|
if let Some(deps) = deps_map.get(name) {
|
||||||
|
for dep in *deps {
|
||||||
|
if !plugin_names.contains(dep) {
|
||||||
|
return Err(format!("Plugin {name} depends on missing plugin: {dep}"));
|
||||||
|
}
|
||||||
|
visit(dep, deps_map, plugin_names, visited, current_path, sorted)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current_path.remove(name);
|
||||||
|
visited.insert(name.to_string());
|
||||||
|
sorted.push(name.to_string());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
let mut sorted = Vec::new();
|
||||||
|
let mut visited = HashSet::new();
|
||||||
|
let mut current_path = HashSet::new();
|
||||||
|
let plugin_names: HashSet<String> = plugins.iter().map(|(n, _)| n.clone()).collect();
|
||||||
|
let deps_map: HashMap<String, &Vec<String>> =
|
||||||
|
plugins.iter().map(|(n, d)| (n.clone(), d)).collect();
|
||||||
|
|
||||||
|
for (name, _) in plugins {
|
||||||
|
visit(
|
||||||
|
name,
|
||||||
|
&deps_map,
|
||||||
|
&plugin_names,
|
||||||
|
&mut visited,
|
||||||
|
&mut current_path,
|
||||||
|
&mut sorted,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(sorted)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn initialization for a single plugin
|
||||||
|
async fn spawn_plugin_initialization(
|
||||||
|
&self,
|
||||||
|
mut instance: Box<dyn Plugin>,
|
||||||
|
metadata: PluginMetadata,
|
||||||
|
loader_data: Box<dyn Any + Send + Sync>,
|
||||||
|
loader: Arc<dyn PluginLoader>,
|
||||||
|
) -> Result<tokio::task::JoinHandle<()>, ManagerError> {
|
||||||
|
// Mark plugin as loading
|
||||||
|
self.plugin_states
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.insert(metadata.name.clone(), PluginState::Loading);
|
||||||
|
|
||||||
|
let self_ref = self
|
||||||
|
.self_ref
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.clone()
|
||||||
|
.ok_or(ManagerError::ServerNotInitialized)?;
|
||||||
|
|
||||||
|
let context = Arc::new(Context::new(
|
||||||
|
metadata.clone(),
|
||||||
|
Arc::clone(
|
||||||
|
&self
|
||||||
|
.server
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.clone()
|
||||||
|
.ok_or(ManagerError::ServerNotInitialized)?,
|
||||||
|
),
|
||||||
|
Arc::clone(&self.handlers),
|
||||||
|
Arc::clone(&self_ref),
|
||||||
|
Arc::clone(&LOGGER_IMPL),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Create the plugin structure first
|
||||||
|
let plugin = LoadedPlugin {
|
||||||
|
metadata: metadata.clone(),
|
||||||
|
instance: None, // Will be set after successful initialization
|
||||||
|
loader: loader.clone(),
|
||||||
|
loader_data: Some(loader_data),
|
||||||
|
is_active: false, // Will be set to true after successful initialization
|
||||||
|
context: context.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let plugin_index = {
|
||||||
|
let mut plugins = self.plugins.write().await;
|
||||||
|
plugins.push(plugin);
|
||||||
|
plugins.len() - 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spawn async task for plugin initialization
|
||||||
|
let self_ref_clone = Arc::clone(&self_ref);
|
||||||
|
let state_notify = Arc::clone(&self.state_notify);
|
||||||
|
let plugin_name = metadata.name.clone();
|
||||||
|
let loader_clone = loader.clone();
|
||||||
|
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
// Initialize the plugin
|
||||||
|
match instance.on_load(context.clone()).await {
|
||||||
|
Ok(()) => {
|
||||||
|
// Update plugin state to loaded
|
||||||
|
{
|
||||||
|
let mut plugins = self_ref_clone.plugins.write().await;
|
||||||
|
if let Some(plugin) = plugins.get_mut(plugin_index) {
|
||||||
|
plugin.instance = Some(instance);
|
||||||
|
plugin.is_active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self_ref_clone
|
||||||
|
.plugin_states
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.insert(plugin_name.clone(), PluginState::Loaded);
|
||||||
|
state_notify.notify_waiters();
|
||||||
|
|
||||||
|
info!("Loaded {} ({})", metadata.name, metadata.version);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Handle initialization failure
|
||||||
|
let error_msg = format!("Initialization failed: {e}");
|
||||||
|
let _ = instance.on_unload(context).await;
|
||||||
|
|
||||||
|
// Get the loader data before removing the plugin
|
||||||
|
let loader_data: Option<Box<dyn Any + Send + Sync>> = {
|
||||||
|
let mut plugins = self_ref_clone.plugins.write().await;
|
||||||
|
if let Some(plugin) = plugins.get_mut(plugin_index) {
|
||||||
|
plugin.loader_data.take()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to unload the plugin data
|
||||||
|
if let Some(data) = loader_data {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loader_clone.unload(data).await.ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut plugins = self_ref_clone.plugins.write().await;
|
||||||
|
if plugin_index < plugins.len() {
|
||||||
|
plugins.remove(plugin_index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self_ref_clone
|
||||||
|
.plugin_states
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.insert(plugin_name.clone(), PluginState::Failed(error_msg.clone()));
|
||||||
|
state_notify.notify_waiters();
|
||||||
|
|
||||||
|
error!("Failed to initialize plugin {plugin_name}: {error_msg}",);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(task)
|
||||||
|
}
|
||||||
|
|
||||||
/// Load all plugins from the plugin directory
|
/// Load all plugins from the plugin directory
|
||||||
pub async fn load_plugins(&self) -> Result<(), ManagerError> {
|
pub async fn load_plugins(&self) -> Result<(), ManagerError> {
|
||||||
const PLUGIN_DIR: &str = "./plugins";
|
const PLUGIN_DIR: &str = "./plugins";
|
||||||
@ -301,7 +478,7 @@ impl PluginManager {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut load_tasks = Vec::new();
|
let mut prepared_plugins = Vec::new();
|
||||||
|
|
||||||
for entry in std::fs::read_dir(path)? {
|
for entry in std::fs::read_dir(path)? {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
@ -319,143 +496,87 @@ impl PluginManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start loading plugin concurrently
|
// Find a loader that can handle this file
|
||||||
match self.start_loading_plugin(&path).await {
|
let loaders = self.loaders.read().await;
|
||||||
Ok(task) => load_tasks.push(task),
|
let mut loader_found = false;
|
||||||
Err(err) => error!("{}", err),
|
for loader in loaders.iter() {
|
||||||
|
if loader.can_load(&path) {
|
||||||
|
match loader.load(&path).await {
|
||||||
|
Ok((instance, metadata, loader_data)) => {
|
||||||
|
prepared_plugins.push((
|
||||||
|
instance,
|
||||||
|
metadata,
|
||||||
|
loader_data,
|
||||||
|
loader.clone(),
|
||||||
|
));
|
||||||
|
loader_found = true;
|
||||||
|
}
|
||||||
|
Err(err) => error!("Failed to load plugin from {:?}: {}", path, err),
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !loader_found {
|
||||||
|
self.unloaded_files.write().await.insert(path.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all plugins to complete loading (but don't block on individual plugin initialization)
|
// Resolve dependencies
|
||||||
join_all(load_tasks).await;
|
let metadata_list: Vec<(String, Vec<String>)> = prepared_plugins
|
||||||
|
.iter()
|
||||||
|
.map(|(_, m, _, _)| (m.name.clone(), m.dependencies.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let sorted_names =
|
||||||
|
Self::topological_sort(&metadata_list).map_err(ManagerError::DependencyError)?;
|
||||||
|
|
||||||
|
// Map names back to prepared plugins
|
||||||
|
#[expect(clippy::type_complexity)]
|
||||||
|
let mut plugins_map: HashMap<
|
||||||
|
String,
|
||||||
|
(
|
||||||
|
Box<dyn Plugin>,
|
||||||
|
PluginMetadata,
|
||||||
|
Box<dyn Any + Send + Sync>,
|
||||||
|
Arc<dyn PluginLoader>,
|
||||||
|
),
|
||||||
|
> = prepared_plugins
|
||||||
|
.into_iter()
|
||||||
|
.map(|(i, m, d, l)| (m.name.clone(), (i, m, d, l)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for name in sorted_names {
|
||||||
|
if let Some((instance, metadata, loader_data, loader)) = plugins_map.remove(&name) {
|
||||||
|
match self
|
||||||
|
.spawn_plugin_initialization(instance, metadata, loader_data, loader)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(task) => {
|
||||||
|
// We must await each initialization to ensure dependencies are ready
|
||||||
|
if let Err(err) = task.await {
|
||||||
|
error!("Plugin initialization task panicked: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => error!("{}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start loading a plugin asynchronously
|
/// Start loading a plugin asynchronously
|
||||||
#[expect(clippy::too_many_lines)]
|
|
||||||
async fn start_loading_plugin(
|
async fn start_loading_plugin(
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> Result<tokio::task::JoinHandle<()>, ManagerError> {
|
) -> Result<tokio::task::JoinHandle<()>, ManagerError> {
|
||||||
for loader in self.loaders.read().await.iter() {
|
for loader in self.loaders.read().await.iter() {
|
||||||
if loader.can_load(path) {
|
if loader.can_load(path) {
|
||||||
let (mut instance, metadata, loader_data) = loader.load(path).await?;
|
let (instance, metadata, loader_data) = loader.load(path).await?;
|
||||||
|
return self
|
||||||
// Mark plugin as loading
|
.spawn_plugin_initialization(instance, metadata, loader_data, loader.clone())
|
||||||
self.plugin_states
|
.await;
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.insert(metadata.name.clone(), PluginState::Loading);
|
|
||||||
|
|
||||||
let self_ref = self
|
|
||||||
.self_ref
|
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
.clone()
|
|
||||||
.ok_or(ManagerError::ServerNotInitialized)?;
|
|
||||||
|
|
||||||
let context = Arc::new(Context::new(
|
|
||||||
metadata.clone(),
|
|
||||||
Arc::clone(
|
|
||||||
&self
|
|
||||||
.server
|
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
.clone()
|
|
||||||
.ok_or(ManagerError::ServerNotInitialized)?,
|
|
||||||
),
|
|
||||||
Arc::clone(&self.handlers),
|
|
||||||
Arc::clone(&self_ref),
|
|
||||||
Arc::clone(&LOGGER_IMPL),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Create the plugin structure first
|
|
||||||
let plugin = LoadedPlugin {
|
|
||||||
metadata: metadata.clone(),
|
|
||||||
instance: None, // Will be set after successful initialization
|
|
||||||
loader: loader.clone(),
|
|
||||||
loader_data: Some(loader_data),
|
|
||||||
is_active: false, // Will be set to true after successful initialization
|
|
||||||
context: context.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let plugin_index = {
|
|
||||||
let mut plugins = self.plugins.write().await;
|
|
||||||
plugins.push(plugin);
|
|
||||||
plugins.len() - 1
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove from unloaded files if it was there
|
|
||||||
self.unloaded_files.write().await.remove(path);
|
|
||||||
|
|
||||||
// Spawn async task for plugin initialization
|
|
||||||
let self_ref_clone = Arc::clone(&self_ref);
|
|
||||||
let state_notify = Arc::clone(&self.state_notify);
|
|
||||||
let plugin_name = metadata.name.clone();
|
|
||||||
let loader_clone = loader.clone();
|
|
||||||
|
|
||||||
let task = tokio::spawn(async move {
|
|
||||||
// Initialize the plugin
|
|
||||||
match instance.on_load(context.clone()).await {
|
|
||||||
Ok(()) => {
|
|
||||||
// Update plugin state to loaded
|
|
||||||
{
|
|
||||||
let mut plugins = self_ref_clone.plugins.write().await;
|
|
||||||
if let Some(plugin) = plugins.get_mut(plugin_index) {
|
|
||||||
plugin.instance = Some(instance);
|
|
||||||
plugin.is_active = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self_ref_clone
|
|
||||||
.plugin_states
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.insert(plugin_name.clone(), PluginState::Loaded);
|
|
||||||
state_notify.notify_waiters();
|
|
||||||
|
|
||||||
info!("Loaded {} ({})", metadata.name, metadata.version);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
// Handle initialization failure
|
|
||||||
let error_msg = format!("Initialization failed: {e}");
|
|
||||||
let _ = instance.on_unload(context).await;
|
|
||||||
|
|
||||||
// Get the loader data before removing the plugin
|
|
||||||
let loader_data: Option<Box<dyn Any + Send + Sync>> = {
|
|
||||||
let mut plugins = self_ref_clone.plugins.write().await;
|
|
||||||
if let Some(plugin) = plugins.get_mut(plugin_index) {
|
|
||||||
plugin.loader_data.take()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to unload the plugin data
|
|
||||||
if let Some(data) = loader_data {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loader_clone.unload(data).await.ok();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut plugins = self_ref_clone.plugins.write().await;
|
|
||||||
if plugin_index < plugins.len() {
|
|
||||||
plugins.remove(plugin_index);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self_ref_clone.plugin_states.write().await.insert(
|
|
||||||
plugin_name.clone(),
|
|
||||||
PluginState::Failed(error_msg.clone()),
|
|
||||||
);
|
|
||||||
state_notify.notify_waiters();
|
|
||||||
|
|
||||||
error!("Failed to initialize plugin {plugin_name}: {error_msg}",);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(task);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -657,3 +778,41 @@ impl PluginManager {
|
|||||||
event
|
event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn topological_sort() {
|
||||||
|
let plugins = vec![
|
||||||
|
("A".to_string(), vec!["B".to_string()]),
|
||||||
|
("B".to_string(), vec!["C".to_string()]),
|
||||||
|
("C".to_string(), vec![]),
|
||||||
|
];
|
||||||
|
let sorted = PluginManager::topological_sort(&plugins).unwrap();
|
||||||
|
assert_eq!(sorted, vec!["C", "B", "A"]);
|
||||||
|
|
||||||
|
let plugins_complex = vec![
|
||||||
|
("A".to_string(), vec!["B".to_string(), "C".to_string()]),
|
||||||
|
("B".to_string(), vec!["D".to_string()]),
|
||||||
|
("C".to_string(), vec!["D".to_string()]),
|
||||||
|
("D".to_string(), vec![]),
|
||||||
|
];
|
||||||
|
let sorted = PluginManager::topological_sort(&plugins_complex).unwrap();
|
||||||
|
// Multiple valid sorts possible, but D must be before B and C, and B, C must be before A.
|
||||||
|
assert_eq!(sorted[0], "D");
|
||||||
|
assert!(sorted[1] == "B" || sorted[1] == "C");
|
||||||
|
assert!(sorted[2] == "B" || sorted[2] == "C");
|
||||||
|
assert_eq!(sorted[3], "A");
|
||||||
|
|
||||||
|
let plugins_circular = vec![
|
||||||
|
("A".to_string(), vec!["B".to_string()]),
|
||||||
|
("B".to_string(), vec!["A".to_string()]),
|
||||||
|
];
|
||||||
|
assert!(PluginManager::topological_sort(&plugins_circular).is_err());
|
||||||
|
|
||||||
|
let plugins_missing = vec![("A".to_string(), vec!["B".to_string()])];
|
||||||
|
assert!(PluginManager::topological_sort(&plugins_missing).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user