565 lines
19 KiB
Rust
565 lines
19 KiB
Rust
use {
|
|
agave_geyser_plugin_interface::geyser_plugin_interface::GeyserPlugin,
|
|
jsonrpc_core::{ErrorCode, Result as JsonRpcResult},
|
|
jsonrpc_server_utils::tokio::sync::oneshot::Sender as OneShotSender,
|
|
libloading::Library,
|
|
log::*,
|
|
std::{
|
|
ops::{Deref, DerefMut},
|
|
path::Path,
|
|
},
|
|
};
|
|
|
|
#[derive(Debug)]
|
|
pub struct LoadedGeyserPlugin {
|
|
name: String,
|
|
plugin: Box<dyn GeyserPlugin>,
|
|
}
|
|
|
|
impl LoadedGeyserPlugin {
|
|
pub fn new(plugin: Box<dyn GeyserPlugin>, name: Option<String>) -> Self {
|
|
Self {
|
|
name: name.unwrap_or_else(|| plugin.name().to_owned()),
|
|
plugin,
|
|
}
|
|
}
|
|
|
|
pub fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
}
|
|
|
|
impl Deref for LoadedGeyserPlugin {
|
|
type Target = Box<dyn GeyserPlugin>;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.plugin
|
|
}
|
|
}
|
|
|
|
impl DerefMut for LoadedGeyserPlugin {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.plugin
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Debug)]
|
|
pub struct GeyserPluginManager {
|
|
pub plugins: Vec<LoadedGeyserPlugin>,
|
|
libs: Vec<Library>,
|
|
}
|
|
|
|
impl GeyserPluginManager {
|
|
pub fn new() -> Self {
|
|
GeyserPluginManager {
|
|
plugins: Vec::default(),
|
|
libs: Vec::default(),
|
|
}
|
|
}
|
|
|
|
/// Unload all plugins and loaded plugin libraries, making sure to fire
|
|
/// their `on_plugin_unload()` methods so they can do any necessary cleanup.
|
|
pub fn unload(&mut self) {
|
|
for mut plugin in self.plugins.drain(..) {
|
|
info!("Unloading plugin for {:?}", plugin.name());
|
|
plugin.on_unload();
|
|
}
|
|
|
|
for lib in self.libs.drain(..) {
|
|
drop(lib);
|
|
}
|
|
}
|
|
|
|
/// Check if there is any plugin interested in account data
|
|
pub fn account_data_notifications_enabled(&self) -> bool {
|
|
for plugin in &self.plugins {
|
|
if plugin.account_data_notifications_enabled() {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Check if there is any plugin interested in transaction data
|
|
pub fn transaction_notifications_enabled(&self) -> bool {
|
|
for plugin in &self.plugins {
|
|
if plugin.transaction_notifications_enabled() {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Check if there is any plugin interested in entry data
|
|
pub fn entry_notifications_enabled(&self) -> bool {
|
|
for plugin in &self.plugins {
|
|
if plugin.entry_notifications_enabled() {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Admin RPC request handler
|
|
pub(crate) fn list_plugins(&self) -> JsonRpcResult<Vec<String>> {
|
|
Ok(self.plugins.iter().map(|p| p.name().to_owned()).collect())
|
|
}
|
|
|
|
/// Admin RPC request handler
|
|
/// # Safety
|
|
///
|
|
/// This function loads the dynamically linked library specified in the path. The library
|
|
/// must do necessary initializations.
|
|
///
|
|
/// The string returned is the name of the plugin loaded, which can only be accessed once
|
|
/// the plugin has been loaded and calling the name method.
|
|
pub(crate) fn load_plugin(
|
|
&mut self,
|
|
geyser_plugin_config_file: impl AsRef<Path>,
|
|
) -> JsonRpcResult<String> {
|
|
// First load plugin
|
|
let (mut new_plugin, new_lib, new_config_file) =
|
|
load_plugin_from_config(geyser_plugin_config_file.as_ref()).map_err(|e| {
|
|
jsonrpc_core::Error {
|
|
code: ErrorCode::InvalidRequest,
|
|
message: format!("Failed to load plugin: {e}"),
|
|
data: None,
|
|
}
|
|
})?;
|
|
|
|
// Then see if a plugin with this name already exists. If so, abort
|
|
if self
|
|
.plugins
|
|
.iter()
|
|
.any(|plugin| plugin.name().eq(new_plugin.name()))
|
|
{
|
|
return Err(jsonrpc_core::Error {
|
|
code: ErrorCode::InvalidRequest,
|
|
message: format!(
|
|
"There already exists a plugin named {} loaded. Did not load requested plugin",
|
|
new_plugin.name()
|
|
),
|
|
data: None,
|
|
});
|
|
}
|
|
|
|
setup_logger_for_plugin(&*new_plugin.plugin)?;
|
|
|
|
// Call on_load and push plugin
|
|
new_plugin
|
|
.on_load(new_config_file, false)
|
|
.map_err(|on_load_err| jsonrpc_core::Error {
|
|
code: ErrorCode::InvalidRequest,
|
|
message: format!(
|
|
"on_load method of plugin {} failed: {on_load_err}",
|
|
new_plugin.name()
|
|
),
|
|
data: None,
|
|
})?;
|
|
let name = new_plugin.name().to_string();
|
|
self.plugins.push(new_plugin);
|
|
self.libs.push(new_lib);
|
|
|
|
Ok(name)
|
|
}
|
|
|
|
pub(crate) fn unload_plugin(&mut self, name: &str) -> JsonRpcResult<()> {
|
|
// Check if any plugin names match this one
|
|
let Some(idx) = self
|
|
.plugins
|
|
.iter()
|
|
.position(|plugin| plugin.name().eq(name))
|
|
else {
|
|
// If we don't find one return an error
|
|
return Err(jsonrpc_core::error::Error {
|
|
code: ErrorCode::InvalidRequest,
|
|
message: String::from("The plugin you requested to unload is not loaded"),
|
|
data: None,
|
|
});
|
|
};
|
|
|
|
// Unload and drop plugin and lib
|
|
self._drop_plugin(idx);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Checks for a plugin with a given `name`.
|
|
/// If it exists, first unload it.
|
|
/// Then, attempt to load a new plugin
|
|
pub(crate) fn reload_plugin(&mut self, name: &str, config_file: &str) -> JsonRpcResult<()> {
|
|
// Check if any plugin names match this one
|
|
let Some(idx) = self
|
|
.plugins
|
|
.iter()
|
|
.position(|plugin| plugin.name().eq(name))
|
|
else {
|
|
// If we don't find one return an error
|
|
return Err(jsonrpc_core::error::Error {
|
|
code: ErrorCode::InvalidRequest,
|
|
message: String::from("The plugin you requested to reload is not loaded"),
|
|
data: None,
|
|
});
|
|
};
|
|
|
|
// Unload and drop current plugin first in case plugin requires exclusive access to resource,
|
|
// such as a particular port or database.
|
|
self._drop_plugin(idx);
|
|
|
|
// Try to load plugin, library
|
|
// SAFETY: It is up to the validator to ensure this is a valid plugin library.
|
|
let (mut new_plugin, new_lib, new_parsed_config_file) =
|
|
load_plugin_from_config(config_file.as_ref()).map_err(|err| jsonrpc_core::Error {
|
|
code: ErrorCode::InvalidRequest,
|
|
message: err.to_string(),
|
|
data: None,
|
|
})?;
|
|
|
|
// Then see if a plugin with this name already exists. If so, abort
|
|
if self
|
|
.plugins
|
|
.iter()
|
|
.any(|plugin| plugin.name().eq(new_plugin.name()))
|
|
{
|
|
return Err(jsonrpc_core::Error {
|
|
code: ErrorCode::InvalidRequest,
|
|
message: format!(
|
|
"There already exists a plugin named {} loaded, while reloading {name}. Did not load requested plugin",
|
|
new_plugin.name()
|
|
),
|
|
data: None,
|
|
});
|
|
}
|
|
|
|
setup_logger_for_plugin(&*new_plugin.plugin)?;
|
|
|
|
// Attempt to on_load with new plugin
|
|
match new_plugin.on_load(new_parsed_config_file, true) {
|
|
// On success, push plugin and library
|
|
Ok(()) => {
|
|
self.plugins.push(new_plugin);
|
|
self.libs.push(new_lib);
|
|
}
|
|
|
|
// On failure, return error
|
|
Err(err) => {
|
|
return Err(jsonrpc_core::error::Error {
|
|
code: ErrorCode::InvalidRequest,
|
|
message: format!(
|
|
"Failed to start new plugin (previous plugin was dropped!): {err}"
|
|
),
|
|
data: None,
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn _drop_plugin(&mut self, idx: usize) {
|
|
let current_lib = self.libs.remove(idx);
|
|
let mut current_plugin = self.plugins.remove(idx);
|
|
let name = current_plugin.name().to_string();
|
|
current_plugin.on_unload();
|
|
// The plugin must be dropped before the library to avoid a crash.
|
|
drop(current_plugin);
|
|
drop(current_lib);
|
|
info!("Unloaded plugin {name} at idx {idx}");
|
|
}
|
|
}
|
|
|
|
// Initialize logging for the plugin
|
|
fn setup_logger_for_plugin(new_plugin: &dyn GeyserPlugin) -> Result<(), jsonrpc_core::Error> {
|
|
new_plugin
|
|
.setup_logger(log::logger(), log::max_level())
|
|
.map_err(|setup_logger_err| jsonrpc_core::Error {
|
|
code: ErrorCode::InvalidRequest,
|
|
message: format!(
|
|
"setup_logger method of plugin {} failed: {setup_logger_err}",
|
|
new_plugin.name()
|
|
),
|
|
data: None,
|
|
})
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum GeyserPluginManagerRequest {
|
|
ReloadPlugin {
|
|
name: String,
|
|
config_file: String,
|
|
response_sender: OneShotSender<JsonRpcResult<()>>,
|
|
},
|
|
UnloadPlugin {
|
|
name: String,
|
|
response_sender: OneShotSender<JsonRpcResult<()>>,
|
|
},
|
|
LoadPlugin {
|
|
config_file: String,
|
|
response_sender: OneShotSender<JsonRpcResult<String>>,
|
|
},
|
|
ListPlugins {
|
|
response_sender: OneShotSender<JsonRpcResult<Vec<String>>>,
|
|
},
|
|
}
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum GeyserPluginManagerError {
|
|
#[error("Cannot open the plugin config file")]
|
|
CannotOpenConfigFile(String),
|
|
|
|
#[error("Cannot read the plugin config file")]
|
|
CannotReadConfigFile(String),
|
|
|
|
#[error("The config file is not in a valid Json format")]
|
|
InvalidConfigFileFormat(String),
|
|
|
|
#[error("Plugin library path is not specified in the config file")]
|
|
LibPathNotSet,
|
|
|
|
#[error("Invalid plugin path")]
|
|
InvalidPluginPath,
|
|
|
|
#[error("Cannot load plugin shared library (error: {0})")]
|
|
PluginLoadError(String),
|
|
|
|
#[error("The geyser plugin {0} is already loaded shared library")]
|
|
PluginAlreadyLoaded(String),
|
|
|
|
#[error("The GeyserPlugin on_load method failed (error: {0})")]
|
|
PluginStartError(String),
|
|
}
|
|
|
|
/// # Safety
|
|
///
|
|
/// This function loads the dynamically linked library specified in the path. The library
|
|
/// must do necessary initializations.
|
|
///
|
|
/// This returns the geyser plugin, the dynamic library, and the parsed config file as a &str.
|
|
/// (The geyser plugin interface requires a &str for the on_load method).
|
|
#[cfg(not(test))]
|
|
pub(crate) fn load_plugin_from_config(
|
|
geyser_plugin_config_file: &Path,
|
|
) -> Result<(LoadedGeyserPlugin, Library, &str), GeyserPluginManagerError> {
|
|
use std::{fs::File, io::Read, path::PathBuf};
|
|
type PluginConstructor = unsafe fn() -> *mut dyn GeyserPlugin;
|
|
use libloading::Symbol;
|
|
|
|
let mut file = match File::open(geyser_plugin_config_file) {
|
|
Ok(file) => file,
|
|
Err(err) => {
|
|
return Err(GeyserPluginManagerError::CannotOpenConfigFile(format!(
|
|
"Failed to open the plugin config file {geyser_plugin_config_file:?}, error: {err:?}"
|
|
)));
|
|
}
|
|
};
|
|
|
|
let mut contents = String::new();
|
|
if let Err(err) = file.read_to_string(&mut contents) {
|
|
return Err(GeyserPluginManagerError::CannotReadConfigFile(format!(
|
|
"Failed to read the plugin config file {geyser_plugin_config_file:?}, error: {err:?}"
|
|
)));
|
|
}
|
|
|
|
let result: serde_json::Value = match json5::from_str(&contents) {
|
|
Ok(value) => value,
|
|
Err(err) => {
|
|
return Err(GeyserPluginManagerError::InvalidConfigFileFormat(format!(
|
|
"The config file {geyser_plugin_config_file:?} is not in a valid Json5 format, error: {err:?}"
|
|
)));
|
|
}
|
|
};
|
|
|
|
let libpath = result["libpath"]
|
|
.as_str()
|
|
.ok_or(GeyserPluginManagerError::LibPathNotSet)?;
|
|
let mut libpath = PathBuf::from(libpath);
|
|
if libpath.is_relative() {
|
|
let config_dir = geyser_plugin_config_file.parent().ok_or_else(|| {
|
|
GeyserPluginManagerError::CannotOpenConfigFile(format!(
|
|
"Failed to resolve parent of {geyser_plugin_config_file:?}",
|
|
))
|
|
})?;
|
|
libpath = config_dir.join(libpath);
|
|
}
|
|
|
|
let plugin_name = result["name"].as_str().map(|s| s.to_owned());
|
|
|
|
let config_file = geyser_plugin_config_file
|
|
.as_os_str()
|
|
.to_str()
|
|
.ok_or(GeyserPluginManagerError::InvalidPluginPath)?;
|
|
|
|
let (plugin, lib) = unsafe {
|
|
let lib = Library::new(libpath)
|
|
.map_err(|e| GeyserPluginManagerError::PluginLoadError(e.to_string()))?;
|
|
let constructor: Symbol<PluginConstructor> = lib
|
|
.get(b"_create_plugin")
|
|
.map_err(|e| GeyserPluginManagerError::PluginLoadError(e.to_string()))?;
|
|
let plugin_raw = constructor();
|
|
(Box::from_raw(plugin_raw), lib)
|
|
};
|
|
Ok((
|
|
LoadedGeyserPlugin::new(plugin, plugin_name),
|
|
lib,
|
|
config_file,
|
|
))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
const TESTPLUGIN_CONFIG: &str = "TESTPLUGIN_CONFIG";
|
|
#[cfg(test)]
|
|
const TESTPLUGIN2_CONFIG: &str = "TESTPLUGIN2_CONFIG";
|
|
|
|
// This is mocked for tests to avoid having to do IO with a dynamically linked library
|
|
// across different architectures at test time
|
|
//
|
|
/// This returns mocked values for the geyser plugin, the dynamic library, and the parsed config file as a &str.
|
|
/// (The geyser plugin interface requires a &str for the on_load method).
|
|
#[cfg(test)]
|
|
pub(crate) fn load_plugin_from_config(
|
|
geyser_plugin_config_file: &Path,
|
|
) -> Result<(LoadedGeyserPlugin, Library, &str), GeyserPluginManagerError> {
|
|
if geyser_plugin_config_file.ends_with(TESTPLUGIN_CONFIG) {
|
|
Ok(tests::dummy_plugin_and_library(
|
|
tests::TestPlugin,
|
|
TESTPLUGIN_CONFIG,
|
|
))
|
|
} else if geyser_plugin_config_file.ends_with(TESTPLUGIN2_CONFIG) {
|
|
Ok(tests::dummy_plugin_and_library(
|
|
tests::TestPlugin2,
|
|
TESTPLUGIN2_CONFIG,
|
|
))
|
|
} else {
|
|
Err(GeyserPluginManagerError::CannotOpenConfigFile(
|
|
geyser_plugin_config_file.to_str().unwrap().to_string(),
|
|
))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use {
|
|
crate::geyser_plugin_manager::{
|
|
GeyserPluginManager, LoadedGeyserPlugin, TESTPLUGIN2_CONFIG, TESTPLUGIN_CONFIG,
|
|
},
|
|
agave_geyser_plugin_interface::geyser_plugin_interface::GeyserPlugin,
|
|
libloading::Library,
|
|
std::sync::{Arc, RwLock},
|
|
};
|
|
|
|
pub(super) fn dummy_plugin_and_library<P: GeyserPlugin>(
|
|
plugin: P,
|
|
config_path: &'static str,
|
|
) -> (LoadedGeyserPlugin, Library, &'static str) {
|
|
(
|
|
LoadedGeyserPlugin::new(Box::new(plugin), None),
|
|
Library::from(libloading::os::unix::Library::this()),
|
|
config_path,
|
|
)
|
|
}
|
|
|
|
const DUMMY_NAME: &str = "dummy";
|
|
pub(super) const DUMMY_CONFIG: &str = "dummy_config";
|
|
const ANOTHER_DUMMY_NAME: &str = "another_dummy";
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub(super) struct TestPlugin;
|
|
|
|
impl GeyserPlugin for TestPlugin {
|
|
fn name(&self) -> &'static str {
|
|
DUMMY_NAME
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub(super) struct TestPlugin2;
|
|
|
|
impl GeyserPlugin for TestPlugin2 {
|
|
fn name(&self) -> &'static str {
|
|
ANOTHER_DUMMY_NAME
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_geyser_reload() {
|
|
// Initialize empty manager
|
|
let plugin_manager = Arc::new(RwLock::new(GeyserPluginManager::new()));
|
|
|
|
// No plugins are loaded, this should fail
|
|
let mut plugin_manager_lock = plugin_manager.write().unwrap();
|
|
let reload_result = plugin_manager_lock.reload_plugin(DUMMY_NAME, DUMMY_CONFIG);
|
|
assert_eq!(
|
|
reload_result.unwrap_err().message,
|
|
"The plugin you requested to reload is not loaded"
|
|
);
|
|
|
|
// Mock having loaded plugin (TestPlugin)
|
|
let (mut plugin, lib, config) = dummy_plugin_and_library(TestPlugin, DUMMY_CONFIG);
|
|
plugin.on_load(config, false).unwrap();
|
|
plugin_manager_lock.plugins.push(plugin);
|
|
plugin_manager_lock.libs.push(lib);
|
|
// plugin_manager_lock.libs.push(lib);
|
|
assert_eq!(plugin_manager_lock.plugins[0].name(), DUMMY_NAME);
|
|
plugin_manager_lock.plugins[0].name();
|
|
|
|
// Try wrong name (same error)
|
|
const WRONG_NAME: &str = "wrong_name";
|
|
let reload_result = plugin_manager_lock.reload_plugin(WRONG_NAME, DUMMY_CONFIG);
|
|
assert_eq!(
|
|
reload_result.unwrap_err().message,
|
|
"The plugin you requested to reload is not loaded"
|
|
);
|
|
|
|
// Now try a (dummy) reload, replacing TestPlugin with TestPlugin2
|
|
let reload_result = plugin_manager_lock.reload_plugin(DUMMY_NAME, TESTPLUGIN2_CONFIG);
|
|
assert!(reload_result.is_ok());
|
|
|
|
// The plugin is now replaced with ANOTHER_DUMMY_NAME
|
|
let plugins = plugin_manager_lock.list_plugins().unwrap();
|
|
assert!(plugins.iter().any(|name| name.eq(ANOTHER_DUMMY_NAME)));
|
|
// DUMMY_NAME should no longer be present.
|
|
assert!(!plugins.iter().any(|name| name.eq(DUMMY_NAME)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_plugin_list() {
|
|
// Initialize empty manager
|
|
let plugin_manager = Arc::new(RwLock::new(GeyserPluginManager::new()));
|
|
let mut plugin_manager_lock = plugin_manager.write().unwrap();
|
|
|
|
// Load two plugins
|
|
// First
|
|
let (mut plugin, lib, config) = dummy_plugin_and_library(TestPlugin, TESTPLUGIN_CONFIG);
|
|
plugin.on_load(config, false).unwrap();
|
|
plugin_manager_lock.plugins.push(plugin);
|
|
plugin_manager_lock.libs.push(lib);
|
|
// Second
|
|
let (mut plugin, lib, config) = dummy_plugin_and_library(TestPlugin2, TESTPLUGIN2_CONFIG);
|
|
plugin.on_load(config, false).unwrap();
|
|
plugin_manager_lock.plugins.push(plugin);
|
|
plugin_manager_lock.libs.push(lib);
|
|
|
|
// Check that both plugins are returned in the list
|
|
let plugins = plugin_manager_lock.list_plugins().unwrap();
|
|
assert!(plugins.iter().any(|name| name.eq(DUMMY_NAME)));
|
|
assert!(plugins.iter().any(|name| name.eq(ANOTHER_DUMMY_NAME)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_plugin_load_unload() {
|
|
// Initialize empty manager
|
|
let plugin_manager = Arc::new(RwLock::new(GeyserPluginManager::new()));
|
|
let mut plugin_manager_lock = plugin_manager.write().unwrap();
|
|
|
|
// Load rpc call
|
|
let load_result = plugin_manager_lock.load_plugin(TESTPLUGIN_CONFIG);
|
|
assert!(load_result.is_ok());
|
|
assert_eq!(plugin_manager_lock.plugins.len(), 1);
|
|
|
|
// Unload rpc call
|
|
let unload_result = plugin_manager_lock.unload_plugin(DUMMY_NAME);
|
|
assert!(unload_result.is_ok());
|
|
assert_eq!(plugin_manager_lock.plugins.len(), 0);
|
|
}
|
|
}
|