Files
MusicFS/musicfs/crates/musicfs-plugins/src/wasm.rs
T
Alexander bc9fa36646 Add Week 10 Plugin System and Week 11 Control API
Week 10 - Plugin System (FR-19):
- Plugin traits: Plugin, OriginPlugin, MetadataPlugin, FormatPlugin
- NativePluginHost with libloading for dynamic loading
- WasmPluginHost (feature-gated) with wasmtime runtime
- PluginManager coordinating both hosts with version checks
- OriginInstance::watch() with WatchHandle, WatchEvent for live updates
- FormatPlugin::synthesize_header() for metadata overlay

Week 11 - Control API & Production (FR-17, FR-18, NFR-6, NFR-10):
- gRPC server with full MusicFS service (status, cache, origins, events)
- Proto extended: MountState enum, TierStats, full StatusResponse/CacheStats
- WebhookHandler with HMAC-SHA256 signing and exponential retry
- Metrics with latency histograms (p50/p95/p99) and origin health gauges
- CLI with mount, status, cache, search, origin, events, shutdown commands
- E2E player compatibility tests (mpv, VLC, file manager)
- systemd service, PKGBUILD, RPM spec for packaging

Plans added for Weeks 10-14 covering P1 features.
All 154 tests passing.
2026-05-13 10:34:01 +02:00

221 lines
5.5 KiB
Rust

use crate::error::{PluginError, Result};
use crate::traits::PluginId;
#[cfg(feature = "wasm")]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "wasm")]
static NEXT_WASM_PLUGIN_ID: AtomicU64 = AtomicU64::new(1_000_000);
#[cfg(feature = "wasm")]
fn next_wasm_plugin_id() -> PluginId {
PluginId::new(NEXT_WASM_PLUGIN_ID.fetch_add(1, Ordering::SeqCst))
}
#[derive(Debug, Clone)]
pub struct ResourceLimits {
pub max_memory_mb: u32,
pub max_cpu_time_ms: u32,
pub allow_network: bool,
pub allow_filesystem: bool,
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_memory_mb: 64,
max_cpu_time_ms: 5000,
allow_network: false,
allow_filesystem: false,
}
}
}
#[cfg(feature = "wasm")]
mod wasm_impl {
use super::*;
use std::collections::HashMap;
use tracing::info;
use wasmtime::{Config, Engine, Linker, Module, Store};
pub struct PluginState {
limits: ResourceLimits,
}
pub struct WasmPlugin {
id: PluginId,
name: String,
_module: Module,
}
impl WasmPlugin {
pub fn id(&self) -> PluginId {
self.id
}
pub fn name(&self) -> &str {
&self.name
}
}
pub struct WasmPluginHost {
engine: Engine,
linker: Linker<PluginState>,
plugins: HashMap<PluginId, WasmPlugin>,
limits: ResourceLimits,
}
impl WasmPluginHost {
pub fn new() -> Result<Self> {
let mut config = Config::new();
config.consume_fuel(true);
config.epoch_interruption(true);
let engine = Engine::new(&config)
.map_err(|e| PluginError::Wasm(format!("Failed to create WASM engine: {}", e)))?;
let linker = Linker::new(&engine);
Ok(Self {
engine,
linker,
plugins: HashMap::new(),
limits: ResourceLimits::default(),
})
}
pub fn set_limits(&mut self, limits: ResourceLimits) {
self.limits = limits;
}
pub fn load(&mut self, wasm_bytes: &[u8]) -> Result<PluginId> {
info!("Loading WASM plugin ({} bytes)", wasm_bytes.len());
let module = Module::new(&self.engine, wasm_bytes)
.map_err(|e| PluginError::Wasm(format!("Failed to compile WASM module: {}", e)))?;
let id = next_wasm_plugin_id();
let name = module.name().unwrap_or("unnamed").to_string();
let plugin = WasmPlugin {
id,
name,
_module: module,
};
self.plugins.insert(id, plugin);
Ok(id)
}
pub fn unload(&mut self, id: PluginId) -> Result<()> {
self.plugins
.remove(&id)
.ok_or_else(|| PluginError::NotFound(format!("WASM plugin {:?}", id)))?;
Ok(())
}
pub fn get(&self, id: PluginId) -> Option<&WasmPlugin> {
self.plugins.get(&id)
}
pub fn list(&self) -> Vec<(PluginId, &str)> {
self.plugins.iter().map(|(id, p)| (*id, p.name())).collect()
}
fn create_store(&self) -> Store<PluginState> {
let state = PluginState {
limits: self.limits.clone(),
};
let mut store = Store::new(&self.engine, state);
let fuel = (self.limits.max_cpu_time_ms as u64) * 1_000_000;
store.set_fuel(fuel).ok();
store
}
}
impl Default for WasmPluginHost {
fn default() -> Self {
Self::new().expect("Failed to create WASM host")
}
}
}
#[cfg(not(feature = "wasm"))]
mod wasm_stub {
use super::*;
pub struct WasmPluginHost {
limits: ResourceLimits,
}
impl WasmPluginHost {
pub fn new() -> Result<Self> {
Ok(Self {
limits: ResourceLimits::default(),
})
}
pub fn set_limits(&mut self, limits: ResourceLimits) {
self.limits = limits;
}
pub fn load(&mut self, _wasm_bytes: &[u8]) -> Result<PluginId> {
Err(PluginError::Wasm(
"WASM support not enabled. Compile with --features wasm".to_string(),
))
}
pub fn unload(&mut self, _id: PluginId) -> Result<()> {
Err(PluginError::Wasm("WASM support not enabled".to_string()))
}
pub fn list(&self) -> Vec<(PluginId, &str)> {
Vec::new()
}
}
impl Default for WasmPluginHost {
fn default() -> Self {
Self::new().expect("Failed to create WASM stub host")
}
}
}
#[cfg(feature = "wasm")]
pub use wasm_impl::*;
#[cfg(not(feature = "wasm"))]
pub use wasm_stub::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_limits_default() {
let limits = ResourceLimits::default();
assert_eq!(limits.max_memory_mb, 64);
assert_eq!(limits.max_cpu_time_ms, 5000);
assert!(!limits.allow_network);
assert!(!limits.allow_filesystem);
}
#[test]
fn test_wasm_host_creation() {
let host = WasmPluginHost::new();
assert!(host.is_ok());
}
#[test]
#[cfg(not(feature = "wasm"))]
fn test_wasm_disabled_load_fails() {
let mut host = WasmPluginHost::new().unwrap();
let result = host.load(&[0x00, 0x61, 0x73, 0x6d]);
assert!(result.is_err());
}
}