registry: add backend/frontend registries and plugin loader
This commit is contained in:
@@ -18,4 +18,3 @@ tokio = { workspace = true, features = ["full"] }
|
||||
tokio-serial = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tracing = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
|
||||
+72
-42
@@ -2,39 +2,15 @@
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
use clap::ValueEnum;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use trx_core::rig::RigCat;
|
||||
use trx_core::DynResult;
|
||||
|
||||
#[cfg(feature = "ft817")]
|
||||
use trx_backend_ft817::Ft817;
|
||||
|
||||
/// Supported rig backends selectable at runtime.
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum RigKind {
|
||||
#[cfg(feature = "ft817")]
|
||||
#[value(alias = "ft-817")]
|
||||
Ft817,
|
||||
}
|
||||
|
||||
impl RigKind {
|
||||
pub fn all() -> &'static [RigKind] {
|
||||
&[
|
||||
#[cfg(feature = "ft817")]
|
||||
RigKind::Ft817,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RigKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#[cfg(feature = "ft817")]
|
||||
RigKind::Ft817 => write!(f, "ft817"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection details for instantiating a rig backend.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RigAccess {
|
||||
@@ -42,21 +18,75 @@ pub enum RigAccess {
|
||||
Tcp { addr: String },
|
||||
}
|
||||
|
||||
/// Instantiate a rig backend based on the selected kind and access method.
|
||||
pub fn build_rig(kind: RigKind, access: RigAccess) -> DynResult<Box<dyn RigCat>> {
|
||||
match (kind, access) {
|
||||
// Yaesu FT-817
|
||||
#[cfg(feature = "ft817")]
|
||||
(RigKind::Ft817, RigAccess::Serial { path, baud }) => {
|
||||
Ok(Box::new(Ft817::new(&path, baud)?))
|
||||
}
|
||||
#[cfg(feature = "ft817")]
|
||||
(RigKind::Ft817, RigAccess::Tcp { .. }) => {
|
||||
Err("FT-817 only supports serial CAT access".into())
|
||||
}
|
||||
type BackendFactory = fn(RigAccess) -> DynResult<Box<dyn RigCat>>;
|
||||
|
||||
// Fallback for unsupported combinations
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err("Selected rig is not enabled/available".into()),
|
||||
struct BackendRegistry {
|
||||
factories: HashMap<String, BackendFactory>,
|
||||
}
|
||||
|
||||
impl BackendRegistry {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
factories: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn registry() -> &'static Mutex<BackendRegistry> {
|
||||
static REGISTRY: OnceLock<Mutex<BackendRegistry>> = OnceLock::new();
|
||||
REGISTRY.get_or_init(|| Mutex::new(BackendRegistry::new()))
|
||||
}
|
||||
|
||||
fn normalize_name(name: &str) -> String {
|
||||
name.to_ascii_lowercase()
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Register a backend factory under a stable name (e.g. "ft817").
|
||||
pub fn register_backend(name: &str, factory: BackendFactory) {
|
||||
let key = normalize_name(name);
|
||||
let mut reg = registry().lock().expect("backend registry mutex poisoned");
|
||||
reg.factories.insert(key, factory);
|
||||
}
|
||||
|
||||
/// Register all built-in backends enabled by features.
|
||||
pub fn register_builtin_backends() {
|
||||
#[cfg(feature = "ft817")]
|
||||
register_backend("ft817", ft817_factory);
|
||||
}
|
||||
|
||||
/// Check whether a backend name is registered.
|
||||
pub fn is_backend_registered(name: &str) -> bool {
|
||||
let key = normalize_name(name);
|
||||
let reg = registry().lock().expect("backend registry mutex poisoned");
|
||||
reg.factories.contains_key(&key)
|
||||
}
|
||||
|
||||
/// List registered backend names.
|
||||
pub fn registered_backends() -> Vec<String> {
|
||||
let reg = registry().lock().expect("backend registry mutex poisoned");
|
||||
let mut names: Vec<String> = reg.factories.keys().cloned().collect();
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
/// Instantiate a rig backend based on the selected name and access method.
|
||||
pub fn build_rig(name: &str, access: RigAccess) -> DynResult<Box<dyn RigCat>> {
|
||||
let key = normalize_name(name);
|
||||
let reg = registry().lock().expect("backend registry mutex poisoned");
|
||||
let factory = reg
|
||||
.factories
|
||||
.get(&key)
|
||||
.ok_or_else(|| format!("Unknown rig backend: {}", name))?;
|
||||
factory(access)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ft817")]
|
||||
fn ft817_factory(access: RigAccess) -> DynResult<Box<dyn RigCat>> {
|
||||
match access {
|
||||
RigAccess::Serial { path, baud } => Ok(Box::new(Ft817::new(&path, baud)?)),
|
||||
RigAccess::Tcp { .. } => Err("FT-817 only supports serial CAT access".into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use libloading::{Library, Symbol};
|
||||
use tracing::{info, warn};
|
||||
|
||||
const PLUGIN_ENV: &str = "TRX_PLUGIN_DIRS";
|
||||
const PLUGIN_ENTRYPOINT: &str = "trx_register";
|
||||
|
||||
#[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_plugins() -> Vec<Library> {
|
||||
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, &mut libraries) {
|
||||
warn!("Plugin scan failed for {:?}: {}", path, err);
|
||||
}
|
||||
}
|
||||
|
||||
libraries
|
||||
}
|
||||
|
||||
fn load_plugins_from_dir(path: &Path, libraries: &mut Vec<Library>) -> 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;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
match Library::new(&path) {
|
||||
Ok(lib) => {
|
||||
if let Err(err) = register_library(&lib, &path) {
|
||||
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) -> Result<(), String> {
|
||||
let entry: Symbol<unsafe extern "C" fn()> = lib
|
||||
.get(PLUGIN_ENTRYPOINT.as_bytes())
|
||||
.map_err(|e| format!("missing entrypoint {}: {}", PLUGIN_ENTRYPOINT, e))?;
|
||||
entry();
|
||||
info!("Registered plugin {:?}", path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn plugin_search_paths() -> Vec<PathBuf> {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -2,10 +2,14 @@
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use tokio::sync::{mpsc, watch};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use trx_core::{RigRequest, RigState};
|
||||
use trx_core::{DynResult, RigRequest, RigState};
|
||||
|
||||
/// Trait implemented by concrete frontends to expose a runner entrypoint.
|
||||
pub trait FrontendSpawner {
|
||||
@@ -13,5 +17,76 @@ pub trait FrontendSpawner {
|
||||
state_rx: watch::Receiver<RigState>,
|
||||
rig_tx: mpsc::Sender<RigRequest>,
|
||||
callsign: Option<String>,
|
||||
listen_addr: SocketAddr,
|
||||
) -> JoinHandle<()>;
|
||||
}
|
||||
|
||||
type FrontendSpawnFn = fn(
|
||||
watch::Receiver<RigState>,
|
||||
mpsc::Sender<RigRequest>,
|
||||
Option<String>,
|
||||
SocketAddr,
|
||||
) -> JoinHandle<()>;
|
||||
|
||||
struct FrontendRegistry {
|
||||
spawners: HashMap<String, FrontendSpawnFn>,
|
||||
}
|
||||
|
||||
impl FrontendRegistry {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
spawners: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn registry() -> &'static Mutex<FrontendRegistry> {
|
||||
static REGISTRY: OnceLock<Mutex<FrontendRegistry>> = OnceLock::new();
|
||||
REGISTRY.get_or_init(|| Mutex::new(FrontendRegistry::new()))
|
||||
}
|
||||
|
||||
fn normalize_name(name: &str) -> String {
|
||||
name.to_ascii_lowercase()
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Register a frontend spawner under a stable name (e.g. "http").
|
||||
pub fn register_frontend(name: &str, spawner: FrontendSpawnFn) {
|
||||
let key = normalize_name(name);
|
||||
let mut reg = registry().lock().expect("frontend registry mutex poisoned");
|
||||
reg.spawners.insert(key, spawner);
|
||||
}
|
||||
|
||||
/// Check whether a frontend name is registered.
|
||||
pub fn is_frontend_registered(name: &str) -> bool {
|
||||
let key = normalize_name(name);
|
||||
let reg = registry().lock().expect("frontend registry mutex poisoned");
|
||||
reg.spawners.contains_key(&key)
|
||||
}
|
||||
|
||||
/// List registered frontend names.
|
||||
pub fn registered_frontends() -> Vec<String> {
|
||||
let reg = registry().lock().expect("frontend registry mutex poisoned");
|
||||
let mut names: Vec<String> = reg.spawners.keys().cloned().collect();
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
/// Spawn a registered frontend by name.
|
||||
pub fn spawn_frontend(
|
||||
name: &str,
|
||||
state_rx: watch::Receiver<RigState>,
|
||||
rig_tx: mpsc::Sender<RigRequest>,
|
||||
callsign: Option<String>,
|
||||
listen_addr: SocketAddr,
|
||||
) -> DynResult<JoinHandle<()>> {
|
||||
let key = normalize_name(name);
|
||||
let reg = registry().lock().expect("frontend registry mutex poisoned");
|
||||
let spawner = reg
|
||||
.spawners
|
||||
.get(&key)
|
||||
.ok_or_else(|| format!("Unknown frontend: {}", name))?;
|
||||
Ok(spawner(state_rx, rig_tx, callsign, listen_addr))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user