feat(owlry-core): move backend modules from owlry
Move the following modules from crates/owlry/src/ to crates/owlry-core/src/: - config/ (configuration loading and types) - data/ (frecency store) - filter.rs (provider filtering and prefix parsing) - notify.rs (desktop notifications) - paths.rs (XDG path handling) - plugins/ (plugin system: native loader, manifest, registry, runtime loader, Lua API) - providers/ (provider trait, manager, application, command, native_provider, lua_provider) Notable changes from the original: - providers/mod.rs: ProviderManager constructor changed from with_native_plugins() to new(core_providers, native_providers) to decouple from DmenuProvider (which stays in owlry as a UI concern) - plugins/mod.rs: commands module removed (stays in owlry as CLI concern) - Added thiserror and tempfile dependencies to owlry-core Cargo.toml
This commit is contained in:
391
crates/owlry-core/src/plugins/native_loader.rs
Normal file
391
crates/owlry-core/src/plugins/native_loader.rs
Normal file
@@ -0,0 +1,391 @@
|
||||
//! Native Plugin Loader
|
||||
//!
|
||||
//! Loads pre-compiled Rust plugins (.so files) from `/usr/lib/owlry/plugins/`.
|
||||
//! These plugins use the ABI-stable interface defined in `owlry-plugin-api`.
|
||||
//!
|
||||
//! Note: This module is infrastructure for the plugin architecture. Full integration
|
||||
//! with ProviderManager is pending Phase 5 (AUR Packaging) when native plugins
|
||||
//! will actually be deployed.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Once};
|
||||
|
||||
use libloading::Library;
|
||||
use log::{debug, error, info, warn};
|
||||
use owlry_plugin_api::{
|
||||
HostAPI, NotifyUrgency, PluginInfo, PluginVTable, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
RStr, API_VERSION,
|
||||
};
|
||||
|
||||
use crate::notify;
|
||||
|
||||
// ============================================================================
|
||||
// Host API Implementation
|
||||
// ============================================================================
|
||||
|
||||
/// Host notification handler
|
||||
extern "C" fn host_notify(summary: RStr<'_>, body: RStr<'_>, icon: RStr<'_>, urgency: NotifyUrgency) {
|
||||
let icon_str = icon.as_str();
|
||||
let icon_opt = if icon_str.is_empty() { None } else { Some(icon_str) };
|
||||
|
||||
let notify_urgency = match urgency {
|
||||
NotifyUrgency::Low => notify::NotifyUrgency::Low,
|
||||
NotifyUrgency::Normal => notify::NotifyUrgency::Normal,
|
||||
NotifyUrgency::Critical => notify::NotifyUrgency::Critical,
|
||||
};
|
||||
|
||||
notify::notify_with_options(summary.as_str(), body.as_str(), icon_opt, notify_urgency);
|
||||
}
|
||||
|
||||
/// Host log info handler
|
||||
extern "C" fn host_log_info(message: RStr<'_>) {
|
||||
info!("[plugin] {}", message.as_str());
|
||||
}
|
||||
|
||||
/// Host log warning handler
|
||||
extern "C" fn host_log_warn(message: RStr<'_>) {
|
||||
warn!("[plugin] {}", message.as_str());
|
||||
}
|
||||
|
||||
/// Host log error handler
|
||||
extern "C" fn host_log_error(message: RStr<'_>) {
|
||||
error!("[plugin] {}", message.as_str());
|
||||
}
|
||||
|
||||
/// Static host API instance
|
||||
static HOST_API: HostAPI = HostAPI {
|
||||
notify: host_notify,
|
||||
log_info: host_log_info,
|
||||
log_warn: host_log_warn,
|
||||
log_error: host_log_error,
|
||||
};
|
||||
|
||||
/// Initialize the host API (called once before loading plugins)
|
||||
static HOST_API_INIT: Once = Once::new();
|
||||
|
||||
fn ensure_host_api_initialized() {
|
||||
HOST_API_INIT.call_once(|| {
|
||||
// SAFETY: We only call this once, before any plugins are loaded
|
||||
unsafe {
|
||||
owlry_plugin_api::init_host_api(&HOST_API);
|
||||
}
|
||||
debug!("Host API initialized for plugins");
|
||||
});
|
||||
}
|
||||
|
||||
use super::error::{PluginError, PluginResult};
|
||||
|
||||
/// Default directory for system-installed native plugins
|
||||
pub const SYSTEM_PLUGINS_DIR: &str = "/usr/lib/owlry/plugins";
|
||||
|
||||
/// A loaded native plugin with its library handle and vtable
|
||||
pub struct NativePlugin {
|
||||
/// Plugin metadata
|
||||
pub info: PluginInfo,
|
||||
/// List of providers this plugin offers
|
||||
pub providers: Vec<ProviderInfo>,
|
||||
/// The vtable for calling plugin functions
|
||||
vtable: &'static PluginVTable,
|
||||
/// The loaded library (must be kept alive)
|
||||
_library: Library,
|
||||
}
|
||||
|
||||
impl NativePlugin {
|
||||
/// Get the plugin ID
|
||||
pub fn id(&self) -> &str {
|
||||
self.info.id.as_str()
|
||||
}
|
||||
|
||||
/// Get the plugin name
|
||||
pub fn name(&self) -> &str {
|
||||
self.info.name.as_str()
|
||||
}
|
||||
|
||||
/// Initialize a provider by ID
|
||||
pub fn init_provider(&self, provider_id: &str) -> ProviderHandle {
|
||||
(self.vtable.provider_init)(provider_id.into())
|
||||
}
|
||||
|
||||
/// Refresh a static provider
|
||||
pub fn refresh_provider(&self, handle: ProviderHandle) -> Vec<owlry_plugin_api::PluginItem> {
|
||||
(self.vtable.provider_refresh)(handle).into_iter().collect()
|
||||
}
|
||||
|
||||
/// Query a dynamic provider
|
||||
pub fn query_provider(
|
||||
&self,
|
||||
handle: ProviderHandle,
|
||||
query: &str,
|
||||
) -> Vec<owlry_plugin_api::PluginItem> {
|
||||
(self.vtable.provider_query)(handle, query.into()).into_iter().collect()
|
||||
}
|
||||
|
||||
/// Drop a provider handle
|
||||
pub fn drop_provider(&self, handle: ProviderHandle) {
|
||||
(self.vtable.provider_drop)(handle)
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: NativePlugin is safe to send between threads because:
|
||||
// - `info` and `providers` are plain data (RString, RVec from abi_stable are Send+Sync)
|
||||
// - `vtable` is a &'static reference to immutable function pointers
|
||||
// - `_library` (libloading::Library) is Send+Sync
|
||||
unsafe impl Send for NativePlugin {}
|
||||
unsafe impl Sync for NativePlugin {}
|
||||
|
||||
/// Manages native plugin discovery and loading
|
||||
pub struct NativePluginLoader {
|
||||
/// Directory to scan for plugins
|
||||
plugins_dir: PathBuf,
|
||||
/// Loaded plugins by ID (Arc for shared ownership with providers)
|
||||
plugins: HashMap<String, Arc<NativePlugin>>,
|
||||
/// Plugin IDs that are disabled
|
||||
disabled: Vec<String>,
|
||||
}
|
||||
|
||||
impl NativePluginLoader {
|
||||
/// Create a new loader with the default system plugins directory
|
||||
pub fn new() -> Self {
|
||||
Self::with_dir(PathBuf::from(SYSTEM_PLUGINS_DIR))
|
||||
}
|
||||
|
||||
/// Create a new loader with a custom plugins directory
|
||||
pub fn with_dir(plugins_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
plugins_dir,
|
||||
plugins: HashMap::new(),
|
||||
disabled: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the list of disabled plugin IDs
|
||||
pub fn set_disabled(&mut self, disabled: Vec<String>) {
|
||||
self.disabled = disabled;
|
||||
}
|
||||
|
||||
/// Check if the plugins directory exists
|
||||
pub fn plugins_dir_exists(&self) -> bool {
|
||||
self.plugins_dir.exists()
|
||||
}
|
||||
|
||||
/// Discover and load all native plugins
|
||||
pub fn discover(&mut self) -> PluginResult<usize> {
|
||||
// Initialize host API before loading any plugins
|
||||
ensure_host_api_initialized();
|
||||
|
||||
if !self.plugins_dir.exists() {
|
||||
debug!(
|
||||
"Native plugins directory does not exist: {}",
|
||||
self.plugins_dir.display()
|
||||
);
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
info!(
|
||||
"Discovering native plugins in {}",
|
||||
self.plugins_dir.display()
|
||||
);
|
||||
|
||||
let entries = std::fs::read_dir(&self.plugins_dir).map_err(|e| {
|
||||
PluginError::LoadError(format!(
|
||||
"Failed to read plugins directory {}: {}",
|
||||
self.plugins_dir.display(),
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut loaded_count = 0;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
// Only process .so files
|
||||
if path.extension() != Some(OsStr::new("so")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match self.load_plugin(&path) {
|
||||
Ok(plugin) => {
|
||||
let id = plugin.id().to_string();
|
||||
|
||||
// Check if disabled
|
||||
if self.disabled.contains(&id) {
|
||||
info!("Native plugin '{}' is disabled, skipping", id);
|
||||
continue;
|
||||
}
|
||||
|
||||
info!(
|
||||
"Loaded native plugin '{}' v{} with {} providers",
|
||||
plugin.name(),
|
||||
plugin.info.version.as_str(),
|
||||
plugin.providers.len()
|
||||
);
|
||||
|
||||
self.plugins.insert(id, Arc::new(plugin));
|
||||
loaded_count += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to load plugin {:?}: {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Loaded {} native plugins", loaded_count);
|
||||
Ok(loaded_count)
|
||||
}
|
||||
|
||||
/// Load a single plugin from a .so file
|
||||
fn load_plugin(&self, path: &Path) -> PluginResult<NativePlugin> {
|
||||
debug!("Loading native plugin from {:?}", path);
|
||||
|
||||
// Load the library
|
||||
// SAFETY: We trust plugins in /usr/lib/owlry/plugins/ as they were
|
||||
// installed by the package manager
|
||||
let library = unsafe { Library::new(path) }.map_err(|e| {
|
||||
PluginError::LoadError(format!("Failed to load library {:?}: {}", path, e))
|
||||
})?;
|
||||
|
||||
// Get the vtable function
|
||||
let vtable: &'static PluginVTable = unsafe {
|
||||
let func: libloading::Symbol<extern "C" fn() -> &'static PluginVTable> =
|
||||
library.get(b"owlry_plugin_vtable").map_err(|e| {
|
||||
PluginError::LoadError(format!(
|
||||
"Plugin {:?} missing owlry_plugin_vtable symbol: {}",
|
||||
path, e
|
||||
))
|
||||
})?;
|
||||
func()
|
||||
};
|
||||
|
||||
// Get plugin info
|
||||
let info = (vtable.info)();
|
||||
|
||||
// Check API version compatibility
|
||||
if info.api_version != API_VERSION {
|
||||
return Err(PluginError::LoadError(format!(
|
||||
"Plugin '{}' has API version {} but owlry requires version {}",
|
||||
info.id.as_str(),
|
||||
info.api_version,
|
||||
API_VERSION
|
||||
)));
|
||||
}
|
||||
|
||||
// Get provider list
|
||||
let providers: Vec<ProviderInfo> = (vtable.providers)().into_iter().collect();
|
||||
|
||||
Ok(NativePlugin {
|
||||
info,
|
||||
providers,
|
||||
vtable,
|
||||
_library: library,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a loaded plugin by ID
|
||||
pub fn get(&self, id: &str) -> Option<Arc<NativePlugin>> {
|
||||
self.plugins.get(id).cloned()
|
||||
}
|
||||
|
||||
/// Get all loaded plugins as Arc references
|
||||
pub fn plugins(&self) -> impl Iterator<Item = Arc<NativePlugin>> + '_ {
|
||||
self.plugins.values().cloned()
|
||||
}
|
||||
|
||||
/// Get all loaded plugins as a Vec (for passing to create_providers)
|
||||
pub fn into_plugins(self) -> Vec<Arc<NativePlugin>> {
|
||||
self.plugins.into_values().collect()
|
||||
}
|
||||
|
||||
/// Get the number of loaded plugins
|
||||
pub fn plugin_count(&self) -> usize {
|
||||
self.plugins.len()
|
||||
}
|
||||
|
||||
/// Create providers from all loaded native plugins
|
||||
///
|
||||
/// Returns a vec of (plugin_id, provider_info, handle) tuples that can be
|
||||
/// used to create NativeProvider instances.
|
||||
pub fn create_provider_handles(&self) -> Vec<(String, ProviderInfo, ProviderHandle)> {
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for plugin in self.plugins.values() {
|
||||
for provider_info in &plugin.providers {
|
||||
let handle = plugin.init_provider(provider_info.id.as_str());
|
||||
handles.push((plugin.id().to_string(), provider_info.clone(), handle));
|
||||
}
|
||||
}
|
||||
|
||||
handles
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NativePluginLoader {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Active provider instance from a native plugin
|
||||
pub struct NativeProviderInstance {
|
||||
/// Plugin ID this provider belongs to
|
||||
pub plugin_id: String,
|
||||
/// Provider metadata
|
||||
pub info: ProviderInfo,
|
||||
/// Handle to the provider state
|
||||
pub handle: ProviderHandle,
|
||||
/// Cached items for static providers
|
||||
pub cached_items: Vec<owlry_plugin_api::PluginItem>,
|
||||
}
|
||||
|
||||
impl NativeProviderInstance {
|
||||
/// Create a new provider instance
|
||||
pub fn new(plugin_id: String, info: ProviderInfo, handle: ProviderHandle) -> Self {
|
||||
Self {
|
||||
plugin_id,
|
||||
info,
|
||||
handle,
|
||||
cached_items: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is a static provider
|
||||
pub fn is_static(&self) -> bool {
|
||||
self.info.provider_type == ProviderKind::Static
|
||||
}
|
||||
|
||||
/// Check if this is a dynamic provider
|
||||
pub fn is_dynamic(&self) -> bool {
|
||||
self.info.provider_type == ProviderKind::Dynamic
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_loader_nonexistent_dir() {
|
||||
let mut loader = NativePluginLoader::with_dir(PathBuf::from("/nonexistent/path"));
|
||||
let count = loader.discover().unwrap();
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loader_empty_dir() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let mut loader = NativePluginLoader::with_dir(temp.path().to_path_buf());
|
||||
let count = loader.discover().unwrap();
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_plugins() {
|
||||
let mut loader = NativePluginLoader::new();
|
||||
loader.set_disabled(vec!["test-plugin".to_string()]);
|
||||
assert!(loader.disabled.contains(&"test-plugin".to_string()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user