Skip to content

Commit

Permalink
feat(core): Add plugin hot-reloading support
Browse files Browse the repository at this point in the history
  • Loading branch information
DanEdens committed Jan 15, 2025
1 parent 7fdbd87 commit 46d0cbd
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 11 deletions.
21 changes: 11 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,25 @@
name = "eventghost"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <your.email@example.com>"]
description = "A Rust implementation of EventGhost"

[dependencies]
chrono = "0.4"
uuid = { version = "1.0", features = ["v4"] }
thiserror = "1.0"
parking_lot = "0.12"
tokio = { version = "1.0", features = ["full"] }
uuid = { version = "1.0", features = ["v4", "serde"] }
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
serde_json = "1.0"
thiserror = "1.0"
log = "0.4"
env_logger = "0.10"
windows = { version = "0.52", features = ["Win32_Foundation", "Win32_UI_WindowsAndMessaging"] }
async-trait = "0.1"
futures = "0.3"
windows = { version = "0.52", features = ["Win32_Foundation", "Win32_UI_WindowsAndMessaging", "Win32_System_Pipes", "Win32_Security"] }
egui = "0.24"
eframe = "0.24"
winapi = { version = "0.3", features = ["winuser", "windef"] }
dirs = "5.0"
libloading = "0.8"
notify = "6.1"
tempfile = "3.8"

[dev-dependencies]
tokio-test = "0.4"
mockall = "0.12"
tokio-test = "0.4"
13 changes: 12 additions & 1 deletion src/core/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
//! Core functionality for EventGhost
//!
//! This module provides the core systems including:
//! - Event system
//! - Plugin system with hot-reloading
//! - IPC via named pipes
//! - GUI abstractions
//! - Error handling
pub mod error;
pub mod event;
pub mod plugin;
pub mod gui;
pub mod init;
pub mod named_pipe;
pub mod plugin_loader;
pub mod utils;

pub use error::Error;
pub use event::{Event, EventType, EventHandler};
pub use plugin::{Plugin, PluginInfo, PropertySource};
pub use gui::{Window, WindowConfig};
pub use named_pipe::{NamedPipeServer, NamedPipeClient, PipeError};
pub use named_pipe::{NamedPipeServer, NamedPipeClient, PipeError};
pub use plugin_loader::{PluginLoader, PluginLoadError};
238 changes: 238 additions & 0 deletions src/core/plugin_loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
use libloading::{Library, Symbol};
use notify::{Watcher, RecursiveMode, Event as NotifyEvent};
use crate::core::Error;
use crate::core::plugin::{Plugin, PluginInfo};
use uuid::Uuid;

/// Error type for plugin loading operations
#[derive(Debug)]
pub enum PluginLoadError {
/// Failed to load library
LibraryLoad(String),
/// Failed to find plugin entry point
EntryPoint(String),
/// Failed to create plugin instance
Creation(String),
/// Failed to watch plugin file
Watch(String),
}

impl std::fmt::Display for PluginLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PluginLoadError::LibraryLoad(msg) => write!(f, "Failed to load plugin library: {}", msg),
PluginLoadError::EntryPoint(msg) => write!(f, "Failed to find plugin entry point: {}", msg),
PluginLoadError::Creation(msg) => write!(f, "Failed to create plugin instance: {}", msg),
PluginLoadError::Watch(msg) => write!(f, "Failed to watch plugin file: {}", msg),
}
}
}

impl std::error::Error for PluginLoadError {}

impl From<PluginLoadError> for Error {
fn from(err: PluginLoadError) -> Self {
Error::Plugin(err.to_string())
}
}

type PluginCreateFn = unsafe fn() -> *mut dyn Plugin;

/// A loaded plugin instance
pub struct LoadedPlugin {
/// The plugin instance
pub plugin: Box<dyn Plugin>,
/// The library handle
library: Arc<Library>,
/// Path to the plugin file
path: PathBuf,
/// Plugin ID
id: Uuid,
}

impl LoadedPlugin {
/// Get the plugin's ID
pub fn id(&self) -> Uuid {
self.id
}

/// Get the plugin's path
pub fn path(&self) -> &Path {
&self.path
}
}

/// Manages plugin loading and reloading
pub struct PluginLoader {
/// Currently loaded plugins
plugins: Arc<RwLock<Vec<LoadedPlugin>>>,
/// File system watcher for plugin changes
watcher: notify::RecommendedWatcher,
/// Plugin directory path
plugin_dir: PathBuf,
}

impl PluginLoader {
/// Create a new plugin loader
pub fn new(plugin_dir: PathBuf) -> Result<Self, PluginLoadError> {
let plugins = Arc::new(RwLock::new(Vec::new()));
let plugins_clone = plugins.clone();

// Create file system watcher
let mut watcher = notify::recommended_watcher(move |res: Result<NotifyEvent, _>| {
if let Ok(event) = res {
if let notify::EventKind::Modify(_) = event.kind {
for path in event.paths {
if let Some(ext) = path.extension() {
if ext == "dll" {
// Reload plugin
let plugins = plugins_clone.clone();
tokio::spawn(async move {
if let Err(e) = Self::reload_plugin(&path, plugins).await {
eprintln!("Failed to reload plugin: {}", e);
}
});
}
}
}
}
}
}).map_err(|e| PluginLoadError::Watch(e.to_string()))?;

// Start watching plugin directory
watcher.watch(&plugin_dir, RecursiveMode::NonRecursive)
.map_err(|e| PluginLoadError::Watch(e.to_string()))?;

Ok(Self {
plugins,
watcher,
plugin_dir,
})
}

/// Load a plugin from a file
pub async fn load_plugin(&self, path: &Path) -> Result<(), PluginLoadError> {
// Load the dynamic library
let library = Arc::new(unsafe {
Library::new(path)
.map_err(|e| PluginLoadError::LibraryLoad(e.to_string()))?
});

// Get the plugin creation function
let create_fn: Symbol<PluginCreateFn> = unsafe {
library.get(b"create_plugin")
.map_err(|e| PluginLoadError::EntryPoint(e.to_string()))?
};

// Create the plugin instance
let plugin_ptr = unsafe {
create_fn()
};

if plugin_ptr.is_null() {
return Err(PluginLoadError::Creation("Plugin creation returned null".into()));
}

let plugin = unsafe {
Box::from_raw(plugin_ptr)
};

let loaded_plugin = LoadedPlugin {
plugin,
library,
path: path.to_owned(),
id: Uuid::new_v4(),
};

// Add to loaded plugins
self.plugins.write().await.push(loaded_plugin);

Ok(())
}

/// Reload a plugin
async fn reload_plugin(path: &Path, plugins: Arc<RwLock<Vec<LoadedPlugin>>>) -> Result<(), PluginLoadError> {
let mut plugins = plugins.write().await;

// Find the plugin to reload
if let Some(index) = plugins.iter().position(|p| p.path == path) {
// Remove old plugin
plugins.remove(index);

// Load new version
// Note: We create a new PluginLoader just for loading to avoid self-reference
let temp_loader = Self::new(path.parent().unwrap().to_owned())?;
temp_loader.load_plugin(path).await?;

// Transfer the loaded plugin
if let Some(loaded) = temp_loader.plugins.write().await.pop() {
plugins.push(loaded);
}
}

Ok(())
}

/// Get all loaded plugins
pub async fn get_plugins(&self) -> Vec<PluginInfo> {
self.plugins.read().await
.iter()
.map(|p| p.plugin.get_info())
.collect()
}

/// Get a specific plugin by ID
pub async fn get_plugin(&self, id: Uuid) -> Option<Box<dyn Plugin>> {
self.plugins.read().await
.iter()
.find(|p| p.id == id)
.map(|p| Box::new(p.plugin.clone()))
}

/// Unload a plugin
pub async fn unload_plugin(&self, id: Uuid) -> Result<(), PluginLoadError> {
let mut plugins = self.plugins.write().await;
if let Some(index) = plugins.iter().position(|p| p.id == id) {
plugins.remove(index);
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use std::fs;

#[tokio::test]
async fn test_plugin_loading() {
// Create temporary plugin directory
let dir = tempdir().unwrap();
let loader = PluginLoader::new(dir.path().to_owned()).unwrap();

// Copy test plugin to temp directory
let plugin_path = dir.path().join("test_plugin.dll");
fs::copy("path/to/test_plugin.dll", &plugin_path).unwrap();

// Load plugin
loader.load_plugin(&plugin_path).await.unwrap();

// Verify plugin is loaded
let plugins = loader.get_plugins().await;
assert_eq!(plugins.len(), 1);

// Modify plugin to trigger reload
fs::write(&plugin_path, b"modified").unwrap();

// Wait for reload
tokio::time::sleep(std::time::Duration::from_secs(1)).await;

// Verify plugin was reloaded
let plugins = loader.get_plugins().await;
assert_eq!(plugins.len(), 1);
}
}

0 comments on commit 46d0cbd

Please sign in to comment.