[refactor](trx-rs): adopt shared app infra modules

Phase 4: route config loading and plugin discovery through trx-app, remove duplicated local plugin loaders, and align binary dependencies.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-12 21:19:03 +01:00
parent b7fb9adef7
commit 0d8314cab6
7 changed files with 31 additions and 362 deletions
-2
View File
@@ -14,10 +14,8 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
clap = { workspace = true, features = ["derive"] }
dirs = "6"
libloading = "0.8"
bytes = "1"
cpal = "0.15"
opus = "0.3"
+14 -62
View File
@@ -14,6 +14,7 @@ use std::net::IpAddr;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use trx_app::{ConfigError, ConfigFile};
use trx_core::rig::state::RigMode;
@@ -204,44 +205,13 @@ impl Default for AudioConfig {
impl ServerConfig {
/// Load configuration from a specific file path.
pub fn load_from_file(path: &Path) -> Result<Self, ConfigError> {
let contents = std::fs::read_to_string(path)
.map_err(|e| ConfigError::ReadError(path.to_path_buf(), e.to_string()))?;
toml::from_str(&contents)
.map_err(|e| ConfigError::ParseError(path.to_path_buf(), e.to_string()))
<Self as ConfigFile>::load_from_file(path)
}
/// Load configuration from the default search paths.
/// Returns default config if no config file is found.
pub fn load_from_default_paths() -> Result<(Self, Option<PathBuf>), ConfigError> {
let search_paths = Self::default_search_paths();
for path in search_paths {
if path.exists() {
let config = Self::load_from_file(&path)?;
return Ok((config, Some(path)));
}
}
Ok((Self::default(), None))
}
/// Get the default search paths for config files.
pub fn default_search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
// Current directory
paths.push(PathBuf::from("trx-server.toml"));
// XDG config directory
if let Some(config_dir) = dirs::config_dir() {
paths.push(config_dir.join("trx-rs").join("server.toml"));
}
// System-wide config
paths.push(PathBuf::from("/etc/trx-rs/server.toml"));
paths
<Self as ConfigFile>::load_from_default_paths()
}
/// Generate an example configuration as a TOML string.
@@ -274,40 +244,22 @@ impl ServerConfig {
}
}
/// Errors that can occur when loading configuration.
#[derive(Debug)]
pub enum ConfigError {
/// Failed to read the config file
ReadError(PathBuf, String),
/// Failed to parse the config file
ParseError(PathBuf, String),
}
impl ConfigFile for ServerConfig {
fn config_filename() -> &'static str {
"server.toml"
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ReadError(path, err) => {
write!(
f,
"failed to read config file '{}': {}",
path.display(),
err
)
}
Self::ParseError(path, err) => {
write!(
f,
"failed to parse config file '{}': {}",
path.display(),
err
)
}
fn default_search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
paths.push(PathBuf::from("trx-server.toml"));
if let Some(config_dir) = dirs::config_dir() {
paths.push(config_dir.join("trx-rs").join("server.toml"));
}
paths.push(PathBuf::from("/etc/trx-rs/server.toml"));
paths
}
}
impl std::error::Error for ConfigError {}
#[cfg(test)]
mod tests {
use super::*;
-115
View File
@@ -1,115 +0,0 @@
// 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)
}