diff --git a/CLAUDE.md b/CLAUDE.md index 445bc93..ac160ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,10 +88,6 @@ The rig controller (`src/trx-core/src/rig/controller/`) is the central state man Signal decoders run as background tasks in `trx-server`, consuming decoded audio. `trx-ftx` provides the FT8/FT4/FT2 decoder in pure Rust. Decoded frames can be forwarded to PSKReporter and APRS-IS (IGate) uplinks, or logged via `trx-decode-log`. -### Plugin system - -Both `trx-server` and `trx-client` can load shared-library plugins exporting a `trx_register` symbol. Search paths: `./plugins`, `~/.config/trx-rs/plugins`, `TRX_PLUGIN_DIRS` env var. - ## Commit Format ``` diff --git a/Cargo.lock b/Cargo.lock index 7b46f51..b4db6be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2546,7 +2546,6 @@ name = "trx-app" version = "0.1.0" dependencies = [ "dirs", - "libloading", "serde", "thiserror 2.0.17", "toml", diff --git a/README.md b/README.md index 28817d6..a1de24e 100644 --- a/README.md +++ b/README.md @@ -161,16 +161,6 @@ Audio is transported as Opus between server, client, and browser. - `trx-client` relays audio to the HTTP frontend - Browsers connect over `/audio` -## Plugins - -Both binaries can discover shared-library plugins through: - -- `./plugins` -- `~/.config/trx-rs/plugins` -- `TRX_PLUGIN_DIRS` - -See [`examples/trx-plugin-example/README.md`](examples/trx-plugin-example/README.md). - ## Documentation - [User Manual](docs/User-Manual.md): configuration, features, and usage diff --git a/examples/trx-plugin-example/Cargo.toml b/examples/trx-plugin-example/Cargo.toml deleted file mode 100644 index 4727803..0000000 --- a/examples/trx-plugin-example/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Stan Grams -# -# SPDX-License-Identifier: BSD-2-Clause - -[package] -name = "trx-plugin-example" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -trx-backend = { path = "../../src/trx-server/trx-backend" } -trx-core = { path = "../../src/trx-core" } -trx-frontend = { path = "../../src/trx-client/trx-frontend" } -tokio = { workspace = true, features = ["full"] } -tracing = { workspace = true } diff --git a/examples/trx-plugin-example/README.md b/examples/trx-plugin-example/README.md deleted file mode 100644 index 4d4ccc5..0000000 --- a/examples/trx-plugin-example/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# trx-plugin-example - -This is a minimal shared-library plugin that registers a backend and frontend. -The backend is a stub that returns an error; the frontend is a no-op spawner. - -Build: - -```bash -cargo build -p trx-plugin-example --release -``` - -Install (example): - -```bash -mkdir -p plugins -cp target/release/libtrx_plugin_example.* plugins/ -``` - -Run `trx-server` or `trx-client` with `TRX_PLUGIN_DIRS=./plugins` to discover the plugin. diff --git a/examples/trx-plugin-example/src/lib.rs b/examples/trx-plugin-example/src/lib.rs deleted file mode 100644 index a1943c0..0000000 --- a/examples/trx-plugin-example/src/lib.rs +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Stan Grams -// -// SPDX-License-Identifier: BSD-2-Clause - -use std::net::SocketAddr; - -use tokio::sync::{mpsc, watch}; -use tokio::task::JoinHandle; -use tracing::info; - -use trx_backend::{RegistrationContext, RigAccess}; -use trx_core::{DynResult, RigRequest, RigState}; -use trx_frontend::{FrontendRuntimeContext, FrontendSpawner, FrontendRegistrationContext}; - -const BACKEND_NAME: &str = "example"; -const FRONTEND_NAME: &str = "example-frontend"; - -/// Entry point called by trx-server when the plugin is loaded. -#[no_mangle] -pub extern "C" fn trx_register_backend(context: *mut std::ffi::c_void) { - let context = unsafe { &mut *(context as *mut RegistrationContext) }; - context.register_backend(BACKEND_NAME, example_backend_factory); -} - -/// Entry point called by trx-client when the plugin is loaded. -#[no_mangle] -pub extern "C" fn trx_register_frontend(context: *mut std::ffi::c_void) { - let context = unsafe { &mut *(context as *mut FrontendRegistrationContext) }; - context.register_frontend(FRONTEND_NAME, ExampleFrontend::spawn_frontend); -} - -fn example_backend_factory(_access: RigAccess) -> DynResult> { - Err("example plugin backend not implemented".into()) -} - -struct ExampleFrontend; - -impl FrontendSpawner for ExampleFrontend { - fn spawn_frontend( - _state_rx: watch::Receiver, - _rig_tx: mpsc::Sender, - _callsign: Option, - listen_addr: SocketAddr, - _context: std::sync::Arc, - ) -> JoinHandle<()> { - tokio::spawn(async move { - info!("example frontend loaded at {} (no-op)", listen_addr); - }) - } -} diff --git a/src/trx-app/Cargo.toml b/src/trx-app/Cargo.toml index f63f490..2d40ca2 100644 --- a/src/trx-app/Cargo.toml +++ b/src/trx-app/Cargo.toml @@ -14,5 +14,4 @@ toml = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } dirs = "6" -libloading = "0.8" thiserror = "2" diff --git a/src/trx-app/src/lib.rs b/src/trx-app/src/lib.rs index 9bebc04..bcc94af 100644 --- a/src/trx-app/src/lib.rs +++ b/src/trx-app/src/lib.rs @@ -4,10 +4,8 @@ pub mod config; pub mod logging; -pub mod plugins; pub mod util; pub use config::{ConfigError, ConfigFile}; pub use logging::init_logging; -pub use plugins::{load_backend_plugins, load_frontend_plugins}; pub use util::normalize_name; diff --git a/src/trx-app/src/plugins.rs b/src/trx-app/src/plugins.rs deleted file mode 100644 index d9e49a8..0000000 --- a/src/trx-app/src/plugins.rs +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Stan Grams -// -// SPDX-License-Identifier: BSD-2-Clause - -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; -use std::ptr::NonNull; - -use libloading::{Library, Symbol}; -use tracing::{info, warn}; - -/// Environment variable to disable plugin loading entirely. -const PLUGIN_DISABLE_ENV: &str = "TRX_PLUGINS_DISABLED"; - -const PLUGIN_ENV: &str = "TRX_PLUGIN_DIRS"; -const BACKEND_ENTRYPOINT: &str = "trx_register_backend"; -const FRONTEND_ENTRYPOINT: &str = "trx_register_frontend"; - -#[cfg(windows)] -const PATH_SEPARATOR: char = ';'; -#[cfg(not(windows))] -const PATH_SEPARATOR: char = ':'; - -#[cfg(windows)] -const PLUGIN_EXTENSIONS: &[&str] = &["dll"]; -#[cfg(target_os = "macos")] -const PLUGIN_EXTENSIONS: &[&str] = &["dylib"]; -#[cfg(all(unix, not(target_os = "macos")))] -const PLUGIN_EXTENSIONS: &[&str] = &["so"]; - -pub fn load_backend_plugins(context: NonNull) -> Vec { - load_plugins_for_entrypoint(BACKEND_ENTRYPOINT, context) -} - -pub fn load_frontend_plugins(context: NonNull) -> Vec { - load_plugins_for_entrypoint(FRONTEND_ENTRYPOINT, context) -} - -fn load_plugins_for_entrypoint( - entrypoint: &str, - context: NonNull, -) -> Vec { - // Allow disabling plugin loading entirely via environment variable. - if std::env::var(PLUGIN_DISABLE_ENV).is_ok() { - info!("Plugin loading disabled via {}", PLUGIN_DISABLE_ENV); - return Vec::new(); - } - - let mut libraries = Vec::new(); - let search_paths = plugin_search_paths(); - - if search_paths.is_empty() { - return libraries; - } - - info!("Plugin search paths: {:?}", search_paths); - - for path in search_paths { - if let Err(err) = load_plugins_from_dir(&path, entrypoint, context, &mut libraries) { - warn!("Plugin scan failed for {:?}: {}", path, err); - } - } - - libraries -} - -fn load_plugins_from_dir( - path: &Path, - entrypoint: &str, - context: NonNull, - libraries: &mut Vec, -) -> std::io::Result<()> { - if !path.exists() { - return Ok(()); - } - - for entry in std::fs::read_dir(path)? { - let entry = entry?; - let path = entry.path(); - if !path.is_file() { - continue; - } - if !is_plugin_file(&path) { - continue; - } - - // Validate file permissions before loading. - if !validate_plugin_file(&path) { - continue; - } - - unsafe { - match Library::new(&path) { - Ok(lib) => { - if let Err(err) = register_library(&lib, &path, entrypoint, context) { - warn!("Plugin {:?} failed to register: {}", path, err); - continue; - } - info!("Loaded plugin {:?}", path); - libraries.push(lib); - } - Err(err) => { - warn!("Failed to load plugin {:?}: {}", path, err); - } - } - } - } - - Ok(()) -} - -unsafe fn register_library( - lib: &Library, - path: &Path, - entrypoint: &str, - context: NonNull, -) -> Result<(), String> { - let entry: Symbol = lib - .get(entrypoint.as_bytes()) - .map_err(|e| format!("missing entrypoint {}: {}", entrypoint, e))?; - entry(context.as_ptr()); - info!("Registered plugin {:?}", path); - Ok(()) -} - -fn plugin_search_paths() -> Vec { - let mut paths = Vec::new(); - - if let Ok(env_paths) = std::env::var(PLUGIN_ENV) { - for raw in env_paths.split(PATH_SEPARATOR) { - if raw.trim().is_empty() { - continue; - } - paths.push(PathBuf::from(raw)); - } - } - - paths.push(PathBuf::from("plugins")); - - if let Some(config_dir) = dirs::config_dir() { - paths.push(config_dir.join("trx-rs").join("plugins")); - } - - paths -} - -/// Validate plugin file before loading: check ownership and permissions. -/// -/// On Unix, reject files that are world-writable (mode & 0o002) to prevent -/// loading tampered libraries. On non-Unix platforms, always accept. -fn validate_plugin_file(path: &Path) -> bool { - #[cfg(unix)] - { - use std::os::unix::fs::MetadataExt; - match std::fs::metadata(path) { - Ok(meta) => { - let mode = meta.mode(); - if mode & 0o002 != 0 { - warn!( - "Skipping world-writable plugin {:?} (mode {:o})", - path, mode - ); - return false; - } - true - } - Err(e) => { - warn!("Cannot stat plugin {:?}: {}", path, e); - false - } - } - } - #[cfg(not(unix))] - { - let _ = path; - true - } -} - -fn is_plugin_file(path: &Path) -> bool { - path.extension() - .and_then(OsStr::to_str) - .map(|ext| { - PLUGIN_EXTENSIONS - .iter() - .any(|e| ext.eq_ignore_ascii_case(e)) - }) - .unwrap_or(false) -} diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index 923921a..6c22f90 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -10,7 +10,6 @@ mod remote_client; use std::collections::HashMap; use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; -use std::ptr::NonNull; use std::time::Duration; use bytes::Bytes; @@ -20,7 +19,7 @@ use tokio::sync::{broadcast, mpsc, watch}; use tokio::task::JoinHandle; use tracing::{error, info}; -use trx_app::{init_logging, load_frontend_plugins, normalize_name}; +use trx_app::{init_logging, normalize_name}; use trx_core::audio::AudioStreamInfo; use trx_core::decode::DecodedMessage; @@ -147,9 +146,6 @@ async fn async_init() -> DynResult { init_logging(cfg.general.log_level.as_deref()); - let frontend_ctx_ptr = NonNull::from(&mut frontend_reg_ctx).cast(); - let _plugin_libs = load_frontend_plugins(frontend_ctx_ptr); - if let Some(ref path) = config_path { info!("Loaded configuration from {}", path.display()); } diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index 0e373b4..88cf286 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -14,7 +14,6 @@ use std::collections::HashMap; use std::collections::HashSet; use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; -use std::ptr::NonNull; use std::sync::Arc; use std::time::Duration; @@ -27,7 +26,7 @@ use tracing::{error, info, warn}; use trx_core::audio::AudioStreamInfo; -use trx_app::{init_logging, load_backend_plugins, normalize_name}; +use trx_app::{init_logging, normalize_name}; use trx_backend::{register_builtin_backends_on, RegistrationContext, RigAccess}; use trx_core::rig::controller::{AdaptivePolling, ExponentialBackoff}; use trx_core::rig::request::RigRequest; @@ -854,9 +853,6 @@ async fn main() -> DynResult<()> { init_logging(cfg.general.log_level.as_deref()); - let bootstrap_ctx_ptr = NonNull::from(&mut bootstrap_ctx).cast(); - let _plugin_libs = load_backend_plugins(bootstrap_ctx_ptr); - if let Some(ref path) = config_path { info!("Loaded configuration from {}", path.display()); }