use std::collections::HashSet;
use std::ffi::{c_char, CString};
use std::sync::OnceLock;
use libloading::Library;
use parking_lot::Mutex;
use serde_json::json;
use thiserror::Error;
use crate::config_manager::get_agent_config;
use crate::types::{KdcCallbacks, KdcPluginSetCbFn, PluginConfig};
#[derive(Error, Debug)]
pub enum PluginError {
#[error("failed to load plugin '{name}': {reason}")]
LoadFailed { name: String, reason: String },
#[error("plugin '{name}' missing symbol '{symbol}': {reason}")]
SymbolMissing {
name: String,
symbol: String,
reason: String,
},
#[error("plugin '{name}' lifecycle error, code: {code}")]
LifecycleError { name: String, code: u32 },
}
#[derive(Clone, Copy, Default)]
pub struct PluginState {
pub initialized: bool,
pub started: bool,
}
pub struct PluginManager {
libraries: Vec<Library>,
plugin_configs: Vec<PluginConfig>,
plugin_states: Vec<PluginState>,
callbacks: Option<KdcCallbacks>,
}
static PLUGIN_MANAGER: OnceLock<Mutex<PluginManager>> = OnceLock::new();
pub fn get_manager() -> &'static Mutex<PluginManager> {
PLUGIN_MANAGER.get_or_init(|| {
Mutex::new(PluginManager {
libraries: Vec::new(),
plugin_configs: Vec::new(),
plugin_states: Vec::new(),
callbacks: None,
})
})
}
pub fn load_plugins() -> Result<(), PluginError> {
let config = get_agent_config().ok_or(PluginError::LoadFailed {
name: "internal".to_string(),
reason: "ConfigCenter not initialized".to_string(),
})?;
let mut manager = get_manager().lock();
let mut seen_names: HashSet<String> = HashSet::new();
for plugin in &config.plugins {
let name = plugin.name.as_deref();
let path = plugin.path.as_deref();
if name.is_none() && path.is_none() {
log::warn!("skipping plugin entry with no name and no path");
continue;
}
if name.is_none() {
log::warn!(
"skipping plugin at path '{}': missing 'name' field",
path.unwrap_or("(unknown)")
);
continue;
}
let name_str = name.unwrap();
if path.is_none() {
log::warn!("skipping plugin '{}': missing 'path' field", name_str);
continue;
}
let path_str = path.unwrap();
if seen_names.contains(name_str) {
log::warn!(
"skipping plugin '{}': duplicate name, already loaded",
name_str
);
continue;
}
seen_names.insert(name_str.to_string());
match unsafe { Library::new(path_str) } {
Ok(lib) => {
log::info!("loaded plugin: {}", name_str);
manager.plugin_configs.push(plugin.clone());
manager.libraries.push(lib);
manager.plugin_states.push(PluginState::default());
}
Err(e) => {
log::error!("failed to load plugin '{}': {}", name_str, e);
}
}
}
if manager.libraries.is_empty() {
log::warn!("no plugins loaded, plugin list is empty or all entries were skipped");
}
Ok(())
}
fn for_each_plugin<F>(reverse: bool, mut action: F) -> Result<(), PluginError>
where
F: FnMut(&Library, &str, &PluginConfig) -> Option<PluginError>,
{
let manager = get_manager().lock();
let n = manager.libraries.len();
for idx in 0..n {
let i = if reverse { n - 1 - idx } else { idx };
let library = &manager.libraries[i];
let name = manager
.plugin_configs
.get(i)
.map(|p| p.name.as_deref().unwrap_or("(unnamed)"))
.unwrap_or("unknown");
let config = match manager.plugin_configs.get(i) {
Some(c) => c,
None => continue,
};
if let Some(err) = action(library, name, config) {
return Err(err);
}
}
Ok(())
}
enum StateCheck {
Proceed,
Skip(&'static str),
SilentSkip,
}
fn for_each_with_state(
symbol: &[u8],
reverse: bool,
ctx: *const c_char,
void_return: bool,
check_state: fn(&PluginState) -> StateCheck,
on_success: fn(usize),
) -> (Vec<String>, Vec<String>) {
let n = { get_manager().lock().libraries.len() };
let mut succeeded: Vec<String> = Vec::new();
let mut failed: Vec<String> = Vec::new();
let sym_display = std::str::from_utf8(symbol)
.unwrap_or("(invalid)")
.trim_end_matches('\0');
for idx in 0..n {
let i = if reverse { n - 1 - idx } else { idx };
let name = {
let manager = get_manager().lock();
let name = manager
.plugin_configs
.get(i)
.map(|p| p.name.as_deref().unwrap_or("(unnamed)").to_string())
.unwrap_or_else(|| format!("plugin[{}]", i));
match check_state(&manager.plugin_states[i]) {
StateCheck::Proceed => name,
StateCheck::Skip(reason) => {
log::warn!("plugin '{}' {}", name, reason);
continue;
}
StateCheck::SilentSkip => continue,
}
};
if void_return {
let func: unsafe extern "C" fn(*const c_char) = {
let manager = get_manager().lock();
match unsafe {
manager.libraries[i].get::<unsafe extern "C" fn(*const c_char)>(symbol)
} {
Ok(f) => *f,
Err(e) => {
log::error!("plugin '{}' missing symbol '{}': {}", name, sym_display, e);
failed.push(name);
continue;
}
}
};
unsafe { func(ctx) };
on_success(i);
succeeded.push(name);
} else {
let func: unsafe extern "C" fn(*const c_char) -> u32 = {
let manager = get_manager().lock();
match unsafe {
manager.libraries[i].get::<unsafe extern "C" fn(*const c_char) -> u32>(symbol)
} {
Ok(f) => *f,
Err(e) => {
log::error!("plugin '{}' missing symbol '{}': {}", name, sym_display, e);
failed.push(name);
continue;
}
}
};
let ret = unsafe { func(ctx) };
if ret == 0 {
on_success(i);
succeeded.push(name);
} else {
log::error!("plugin '{}' {} failed, code: {}", name, sym_display, ret);
failed.push(name);
}
}
}
(succeeded, failed)
}
pub fn set_callbacks_for_all_plugins(callbacks: KdcCallbacks) -> Result<(), PluginError> {
let mut manager = get_manager().lock();
manager.callbacks = Some(callbacks);
drop(manager);
for_each_plugin(false, |library, name, _config| {
unsafe {
match library.get::<KdcPluginSetCbFn>(b"KdcPluginSetCb\0") {
Ok(set_cb) => {
set_cb(callbacks);
}
Err(e) => {
log::error!("plugin '{}' missing symbol 'KdcPluginSetCb': {}", name, e);
}
}
}
None
})
}
pub fn init_all_plugins() -> Result<(), PluginError> {
let manager = get_manager().lock();
let n = manager.libraries.len();
drop(manager);
let mut succeeded: Vec<String> = Vec::new();
let mut failed: Vec<String> = Vec::new();
for i in 0..n {
let (name, context_json) = {
let manager = get_manager().lock();
let config = match manager.plugin_configs.get(i) {
Some(c) => c.clone(),
None => {
let name = format!("plugin[{}]", i);
log::error!("{} config missing", name);
failed.push(name);
continue;
}
};
let name = config.name.as_deref().unwrap_or("(unnamed)").to_string();
let plugin_param = config.param.clone();
let mut context_map = match plugin_param {
serde_json::Value::Object(map) => map,
_ => serde_json::Map::new(),
};
let data_path = get_agent_config()
.map(|c| c.data_path.clone())
.unwrap_or_default();
context_map.insert("dataPath".to_string(), json!(data_path));
let context_json =
match CString::new(serde_json::Value::Object(context_map).to_string()) {
Ok(s) => s,
Err(_) => {
log::error!("plugin '{}' context JSON contains null byte", name);
failed.push(name);
continue;
}
};
(name, context_json)
};
let func: unsafe extern "C" fn(*const c_char) -> u32 = {
let manager = get_manager().lock();
match unsafe {
manager.libraries[i]
.get::<unsafe extern "C" fn(*const c_char) -> u32>(b"KdcPluginInit\0")
} {
Ok(f) => *f,
Err(e) => {
log::error!("plugin '{}' missing symbol 'KdcPluginInit': {}", name, e);
failed.push(name);
continue;
}
}
};
let ret = unsafe { func(context_json.as_ptr()) };
if ret == 0 {
get_manager().lock().plugin_states[i].initialized = true;
succeeded.push(name);
} else {
log::error!("plugin '{}' init failed, code: {}", name, ret);
failed.push(name);
}
}
log::info!(
"plugin init complete: succeeded={:?}, failed={:?}",
succeeded,
failed
);
Ok(())
}
pub fn start_all_plugins() -> Result<(), PluginError> {
let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
let (succeeded, failed) = for_each_with_state(
b"KdcPluginStart\0",
false,
empty_ctx,
false,
|state| {
if !state.initialized {
StateCheck::Skip("not initialized, skipping start")
} else {
StateCheck::Proceed
}
},
|i| {
get_manager().lock().plugin_states[i].started = true;
},
);
log::info!(
"plugin start complete: succeeded={:?}, failed={:?}",
succeeded,
failed
);
Ok(())
}
pub fn stop_all_plugins() -> Result<(), PluginError> {
let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
for_each_with_state(
b"KdcPluginStop\0",
true,
empty_ctx,
false,
|state| {
if !state.started {
StateCheck::Skip("not started, skipping stop")
} else {
StateCheck::Proceed
}
},
|i| {
get_manager().lock().plugin_states[i].started = false;
},
);
Ok(())
}
pub fn uninit_all_plugins() -> Result<(), PluginError> {
let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
for_each_with_state(
b"KdcPluginUninit\0",
true,
empty_ctx,
true,
|state| {
if state.started {
return StateCheck::Skip("still started, skipping uninit");
}
if !state.initialized {
return StateCheck::SilentSkip;
}
StateCheck::Proceed
},
|i| {
get_manager().lock().plugin_states[i].initialized = false;
},
);
Ok(())
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
extern "C" fn noop_log(
_level: crate::types::KdcLogLevel,
_message: *const std::os::raw::c_char,
_file: *const std::os::raw::c_char,
_line: i32,
) {
}
extern "C" fn noop_register(
_msg_type: u32,
_cb: crate::types::KdcRequestHandlerCbFunc,
_freecb: crate::types::KdcFreeResponsePtr,
) -> u32 {
0
}
extern "C" fn noop_unregister(_msg_type: u32) -> u32 {
0
}
extern "C" fn noop_db_execute(_sql: *const std::os::raw::c_char) -> u32 {
0
}
extern "C" fn noop_db_query(
_sql: *const std::os::raw::c_char,
_out_json: *mut *mut std::os::raw::c_char,
) -> u32 {
0
}
extern "C" fn noop_db_free(_s: *mut std::os::raw::c_char) {}
extern "C" fn noop_db_sync() -> u32 {
0
}
fn noop_callbacks() -> KdcCallbacks {
KdcCallbacks {
kdclog: noop_log,
kdc_register_request_handle: noop_register,
kdc_unregister_request_handle: noop_unregister,
kdc_db_execute: noop_db_execute,
kdc_db_query: noop_db_query,
kdc_db_free: noop_db_free,
kdc_db_sync: noop_db_sync,
}
}
#[test]
fn test_plugin_error_load_failed_message() {
let err = PluginError::LoadFailed {
name: "test_plugin".to_string(),
reason: "file not found".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("test_plugin"), "should contain plugin name");
assert!(msg.contains("file not found"), "should contain reason");
}
#[test]
fn test_plugin_error_symbol_missing_message() {
let err = PluginError::SymbolMissing {
name: "my_plugin".to_string(),
symbol: "KdcPluginInit".to_string(),
reason: "undefined symbol".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("my_plugin"), "should contain plugin name");
assert!(msg.contains("KdcPluginInit"), "should contain symbol name");
assert!(msg.contains("undefined symbol"), "should contain reason");
}
#[test]
fn test_plugin_error_lifecycle_message() {
let err = PluginError::LifecycleError {
name: "worker_plugin".to_string(),
code: 42,
};
let msg = format!("{}", err);
assert!(msg.contains("worker_plugin"), "should contain plugin name");
assert!(msg.contains("42"), "should contain error code");
}
#[test]
fn test_plugin_error_debug() {
let err = PluginError::LoadFailed {
name: "p".to_string(),
reason: "r".to_string(),
};
assert!(format!("{:?}", err).contains("LoadFailed"));
let err2 = PluginError::SymbolMissing {
name: "p".to_string(),
symbol: "s".to_string(),
reason: "r".to_string(),
};
assert!(format!("{:?}", err2).contains("SymbolMissing"));
let err3 = PluginError::LifecycleError {
name: "p".to_string(),
code: 1,
};
assert!(format!("{:?}", err3).contains("LifecycleError"));
}
#[test]
fn test_plugin_state_default() {
let state = PluginState::default();
assert!(!state.initialized, "default initialized should be false");
assert!(!state.started, "default started should be false");
}
#[test]
fn test_plugin_manager_singleton() {
let ptr1 = get_manager();
let ptr2 = get_manager();
assert!(
std::ptr::eq(ptr1, ptr2),
"get_manager should return the same singleton instance"
);
}
#[test]
fn test_set_callbacks_for_all_plugins_no_lock_error() {
let callbacks = noop_callbacks();
let result = set_callbacks_for_all_plugins(callbacks);
assert!(
result.is_ok(),
"set_callbacks should succeed: {:?}",
result.err()
);
let manager = get_manager().lock();
assert!(manager.callbacks.is_some());
}
#[test]
fn test_init_all_plugins_ok() {
let result = init_all_plugins();
assert!(
result.is_ok(),
"init_all_plugins should return Ok: {:?}",
result.err()
);
}
#[test]
fn test_load_plugins_no_config() {
if get_agent_config().is_some() {
eprintln!("skipping: config already initialized");
return;
}
let result = load_plugins();
assert!(result.is_err(), "should fail without config");
if let Err(PluginError::LoadFailed { name, reason }) = &result {
assert_eq!(name, "internal");
assert!(reason.contains("ConfigCenter not initialized"));
} else {
panic!("expected LoadFailed error, got: {:?}", result);
}
}
#[test]
fn test_load_plugins_with_existing_config() {
if get_agent_config().is_none() {
eprintln!("skipping: config not initialized");
return;
}
let result = load_plugins();
assert!(
result.is_ok(),
"load_plugins should handle gracefully: {:?}",
result.err()
);
}
#[test]
fn test_plugin_config_missing_fields() {
let json_str = r#"{"param":{"k":1}}"#;
let cfg: PluginConfig = serde_json::from_str(json_str).unwrap();
assert!(cfg.name.is_none());
assert!(cfg.path.is_none());
let json_with_path = r#"{"path":"/x.so","param":{}}"#;
let cfg2: PluginConfig = serde_json::from_str(json_with_path).unwrap();
assert!(cfg2.name.is_none());
assert_eq!(cfg2.path.as_deref(), Some("/x.so"));
}
#[test]
fn test_plugin_config_option_fields() {
let cfg_no_name = PluginConfig {
name: None,
path: Some("/tmp/test.so".to_string()),
param: json!({}),
};
assert!(cfg_no_name.name.is_none());
assert_eq!(cfg_no_name.path.as_deref(), Some("/tmp/test.so"));
let cfg_no_path = PluginConfig {
name: Some("test_plugin".to_string()),
path: None,
param: json!({}),
};
assert_eq!(cfg_no_path.name.as_deref(), Some("test_plugin"));
assert!(cfg_no_path.path.is_none());
let cfg_both = PluginConfig {
name: None,
path: None,
param: json!({}),
};
assert!(cfg_both.name.is_none());
assert!(cfg_both.path.is_none());
}
fn compile_mock_plugin() -> Option<std::path::PathBuf> {
let dir = std::env::temp_dir().join("kdc_plugin_test");
let _ = std::fs::create_dir_all(&dir);
let so_path = dir.join("libmock_plugin.so");
if so_path.exists() {
return Some(so_path);
}
let c_code = r#"
#include <stddef.h>
#include <stdint.h>
typedef struct {
void (*kdclog)(int, const char*, const char*, int);
unsigned int (*reg)(unsigned int, void*, void*);
unsigned int (*unreg)(unsigned int);
unsigned int (*db_execute)(const char*);
unsigned int (*db_query)(const char*, char**);
void (*db_free)(char*);
unsigned int (*db_sync)(void);
} KdcCallbacks;
static KdcCallbacks g_callbacks = {0};
uint32_t KdcPluginInit(const char* ctx) { return 0; }
uint32_t KdcPluginStart(const char* ctx) { return 0; }
uint32_t KdcPluginStop(const char* ctx) { return 0; }
void KdcPluginUninit(const char* ctx) {}
uint32_t KdcPluginSetCb(KdcCallbacks cb) { g_callbacks = cb; return 0; }
"#;
let c_path = dir.join("mock_plugin.c");
std::fs::write(&c_path, c_code).unwrap();
let output = std::process::Command::new("gcc")
.args(["-shared", "-fPIC", "-o"])
.arg(&so_path)
.arg(&c_path)
.output();
match output {
Ok(out) if out.status.success() => Some(so_path),
_ => None,
}
}
#[test]
fn test_z_load_plugins_with_mock_so() {
let so_path = match compile_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
let plugin_cfg = PluginConfig {
name: Some("mock_plugin".to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!({"testKey": "testVal"}),
};
{
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState::default());
}
let manager = get_manager().lock();
assert!(
!manager.libraries.is_empty(),
"libraries should not be empty after loading mock"
);
assert!(
manager
.plugin_configs
.iter()
.any(|p| p.name.as_deref() == Some("mock_plugin")),
"should contain mock_plugin config"
);
}
fn compile_bad_mock_plugin() -> Option<std::path::PathBuf> {
let dir = std::env::temp_dir().join("kdc_plugin_test_bad");
let _ = std::fs::create_dir_all(&dir);
let so_path = dir.join("libbad_mock_plugin.so");
if so_path.exists() {
return Some(so_path);
}
let c_code = r#"
#include <stddef.h>
#include <stdint.h>
typedef struct {
void (*kdclog)(int, const char*, const char*, int);
unsigned int (*reg)(unsigned int, void*, void*);
unsigned int (*unreg)(unsigned int);
unsigned int (*db_execute)(const char*);
unsigned int (*db_query)(const char*, char**);
void (*db_free)(char*);
unsigned int (*db_sync)(void);
} KdcCallbacks;
uint32_t KdcPluginInit(const char* ctx) { return 1; }
uint32_t KdcPluginStart(const char* ctx) { return 1; }
uint32_t KdcPluginStop(const char* ctx) { return 1; }
void KdcPluginUninit(const char* ctx) {}
uint32_t KdcPluginSetCb(KdcCallbacks cb) { return 0; }
"#;
let c_path = dir.join("bad_mock_plugin.c");
std::fs::write(&c_path, c_code).unwrap();
let output = std::process::Command::new("gcc")
.args(["-shared", "-fPIC", "-o"])
.arg(&so_path)
.arg(&c_path)
.output();
match output {
Ok(out) if out.status.success() => Some(so_path),
_ => None,
}
}
#[test]
fn test_zz_bad_mock_init_error() {
let so_path = match compile_bad_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
let plugin_cfg = PluginConfig {
name: Some("bad_mock".to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!({}),
};
{
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState::default());
}
let result = init_all_plugins();
assert!(
result.is_ok(),
"init_all_plugins always returns Ok: {:?}",
result.err()
);
}
#[test]
fn test_zz_for_each_plugin_with_loaded() {
let manager = get_manager().lock();
if manager.libraries.is_empty() {
eprintln!("skipping: no libraries loaded");
return;
}
drop(manager);
let mut count = 0usize;
let result = for_each_plugin(false, |_lib, _name, _config| {
count += 1;
None
});
assert!(result.is_ok(), "for_each_plugin should succeed");
assert!(count > 0, "should have iterated over at least one plugin");
}
#[test]
fn test_zz_for_each_plugin_reverse() {
let manager = get_manager().lock();
if manager.libraries.is_empty() {
eprintln!("skipping: no libraries loaded");
return;
}
drop(manager);
let mut count = 0usize;
let result = for_each_plugin(true, |_lib, _name, _config| {
count += 1;
None
});
assert!(result.is_ok(), "for_each_plugin reverse should succeed");
assert!(count > 0, "should have iterated over at least one plugin");
}
#[test]
fn test_zz_for_each_plugin_error() {
let manager = get_manager().lock();
if manager.libraries.is_empty() {
eprintln!("skipping: no libraries loaded");
return;
}
drop(manager);
let result = for_each_plugin(false, |_lib, name, _config| {
Some(PluginError::LifecycleError {
name: name.to_string(),
code: 99,
})
});
assert!(result.is_err(), "should propagate error from closure");
if let Err(PluginError::LifecycleError { code, .. }) = result {
assert_eq!(code, 99);
}
}
#[test]
fn test_for_each_with_state_missing_symbol() {
let so_path = match compile_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
let plugin_cfg = PluginConfig {
name: Some("missing_sym_test".to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!({}),
};
let idx = {
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState::default());
manager.libraries.len() - 1
};
let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
let (_succeeded, failed) = for_each_with_state(
b"NonExistentSymbol\0",
false,
empty_ctx,
false,
|_state| StateCheck::Proceed,
|_| {},
);
assert!(failed.len() >= 1);
let _ = idx;
}
#[test]
fn test_for_each_with_state_all_skip() {
let so_path = match compile_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
let plugin_cfg = PluginConfig {
name: Some("skip_test".to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!({}),
};
{
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState::default());
}
let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
let (succeeded, failed) = for_each_with_state(
b"KdcPluginInit\0",
false,
empty_ctx,
false,
|_state| StateCheck::Skip("forcefully skipping for test"),
|_| {},
);
assert!(succeeded.is_empty());
assert!(failed.is_empty());
}
#[test]
fn test_for_each_with_state_all_silent_skip() {
let so_path = match compile_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
let plugin_cfg = PluginConfig {
name: Some("silent_skip_test".to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!({}),
};
{
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState::default());
}
let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
let (succeeded, failed) = for_each_with_state(
b"KdcPluginInit\0",
false,
empty_ctx,
false,
|_state| StateCheck::SilentSkip,
|_| {},
);
assert!(succeeded.is_empty());
assert!(failed.is_empty());
}
#[test]
fn test_for_each_with_state_proceed_success() {
let so_path = match compile_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
let plugin_cfg = PluginConfig {
name: Some("proceed_test".to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!({}),
};
{
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState {
initialized: true,
started: false,
});
}
let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
let (succeeded, _failed) = for_each_with_state(
b"KdcPluginInit\0",
false,
empty_ctx,
false,
|_state| StateCheck::Proceed,
|_| {},
);
assert!(
succeeded.iter().any(|n| n == "proceed_test"),
"proceed_test should succeed"
);
}
#[test]
fn test_for_each_with_state_proceed_error() {
let so_path = match compile_bad_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
let plugin_cfg = PluginConfig {
name: Some("error_test".to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!({}),
};
{
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState {
initialized: true,
started: true,
});
}
let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
let (_succeeded, failed) = for_each_with_state(
b"KdcPluginInit\0",
false,
empty_ctx,
false,
|_state| StateCheck::Proceed,
|_| {},
);
assert!(
failed.iter().any(|n| n == "error_test"),
"error_test should fail"
);
}
#[test]
fn test_init_all_plugins_with_non_object_param() {
let so_path = match compile_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
let plugin_cfg = PluginConfig {
name: Some("non_obj_param_test".to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!("not_an_object"),
};
{
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState::default());
}
let result = init_all_plugins();
assert!(result.is_ok(), "init_all_plugins should return Ok: {:?}", result.err());
}
#[test]
fn test_full_lifecycle_with_mock_plugin() {
let so_path = match compile_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
let plugin_cfg = PluginConfig {
name: Some("lifecycle_test".to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!({"key": "value"}),
};
{
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState::default());
}
let callbacks = noop_callbacks();
let _ = set_callbacks_for_all_plugins(callbacks);
let result = init_all_plugins();
assert!(result.is_ok());
{
let manager = get_manager().lock();
let last_idx = manager.plugin_states.len() - 1;
assert!(manager.plugin_states[last_idx].initialized);
}
let result = start_all_plugins();
assert!(result.is_ok());
{
let manager = get_manager().lock();
let last_idx = manager.plugin_states.len() - 1;
assert!(manager.plugin_states[last_idx].started);
}
let result = stop_all_plugins();
assert!(result.is_ok());
{
let manager = get_manager().lock();
let last_idx = manager.plugin_states.len() - 1;
assert!(!manager.plugin_states[last_idx].started);
}
let result = uninit_all_plugins();
assert!(result.is_ok());
{
let manager = get_manager().lock();
let last_idx = manager.plugin_states.len() - 1;
assert!(!manager.plugin_states[last_idx].initialized);
}
}
#[test]
fn test_uninit_skips_started_plugins() {
let so_path = match compile_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
let plugin_cfg = PluginConfig {
name: Some("started_uninit_test".to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!({}),
};
{
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState {
initialized: true,
started: true,
});
}
let result = uninit_all_plugins();
assert!(result.is_ok());
{
let manager = get_manager().lock();
let last_idx = manager.plugin_states.len() - 1;
assert!(manager.plugin_states[last_idx].initialized);
assert!(manager.plugin_states[last_idx].started);
}
}
#[test]
fn test_start_skips_uninitialized() {
let so_path = match compile_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
let plugin_cfg = PluginConfig {
name: Some("uninit_start_test".to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!({}),
};
{
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState {
initialized: false,
started: false,
});
}
let result = start_all_plugins();
assert!(result.is_ok());
{
let manager = get_manager().lock();
let last_idx = manager.plugin_states.len() - 1;
assert!(!manager.plugin_states[last_idx].started);
}
}
#[test]
fn test_stop_skips_not_started() {
let so_path = match compile_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
let plugin_cfg = PluginConfig {
name: Some("not_started_stop_test".to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!({}),
};
{
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState {
initialized: true,
started: false,
});
}
let result = stop_all_plugins();
assert!(result.is_ok());
}
#[test]
fn test_for_each_with_state_reverse_order() {
let so_path = match compile_mock_plugin() {
Some(p) => p,
None => {
eprintln!("skipping: gcc not available");
return;
}
};
for suffix in &["rev_a", "rev_b"] {
let plugin_cfg = PluginConfig {
name: Some(suffix.to_string()),
path: Some(so_path.to_str().unwrap().to_string()),
param: serde_json::json!({}),
};
let mut manager = get_manager().lock();
let lib = unsafe { Library::new(&so_path).unwrap() };
manager.libraries.push(lib);
manager.plugin_configs.push(plugin_cfg);
manager.plugin_states.push(PluginState {
initialized: true,
started: false,
});
}
let empty_ctx: *const c_char = b"\0".as_ptr() as *const c_char;
let (succeeded, _failed) = for_each_with_state(
b"KdcPluginInit\0",
true,
empty_ctx,
false,
|_state| StateCheck::Proceed,
|i| {
get_manager().lock().plugin_states[i].initialized = true;
},
);
assert!(
succeeded.iter().any(|n| n == "rev_a"),
"rev_a should succeed"
);
assert!(
succeeded.iter().any(|n| n == "rev_b"),
"rev_b should succeed"
);
}
#[test]
fn test_z_full_lifecycle_via_config() {
let config = get_agent_config();
if config.is_none() {
eprintln!("skipping: config not initialized");
return;
}
let config = config.unwrap();
if config.plugins.is_empty() {
eprintln!("skipping: no plugins in config");
return;
}
let callbacks = noop_callbacks();
let _ = set_callbacks_for_all_plugins(callbacks);
let result = init_all_plugins();
assert!(result.is_ok(), "init should succeed: {:?}", result.err());
let result = start_all_plugins();
assert!(result.is_ok(), "start should succeed: {:?}", result.err());
let result = stop_all_plugins();
assert!(result.is_ok(), "stop should succeed: {:?}", result.err());
let result = uninit_all_plugins();
assert!(result.is_ok(), "uninit should succeed: {:?}", result.err());
}
#[test]
fn test_z_load_plugins_all_fail_empty_libraries() {
let config = get_agent_config();
if config.is_none() {
return;
}
let cfg = config.unwrap();
let saved_files: Vec<(String, Vec<u8>)> = cfg
.plugins
.iter()
.filter_map(|p| p.path.as_ref())
.filter_map(|p| std::fs::read(p).ok().map(|d| (p.clone(), d)))
.collect();
for (path, _) in &saved_files {
let _ = std::fs::remove_file(path);
}
{
let mut mgr = get_manager().lock();
mgr.libraries.clear();
mgr.plugin_configs.clear();
mgr.plugin_states.clear();
}
let result = load_plugins();
assert!(result.is_ok(), "load_plugins should succeed: {:?}", result.err());
{
let mgr = get_manager().lock();
assert!(mgr.libraries.is_empty(), "libraries should be empty when all plugin files removed, got {} libs", mgr.libraries.len());
}
for (path, data) in &saved_files {
if let Some(parent) = std::path::Path::new(path).parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(path, data);
}
let _ = load_plugins();
}
}