use std::collections::BTreeMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use arc_swap::ArcSwapOption;
use nichlink_run_method::{FlowContract, FrameworkId, PluginMode, VerifiedPluginArtifact};
use crate::{HostError, PluginInstance, WasmBackend};
#[path = "lazy_wasm/slot_state.rs"]
mod slot_state;
use slot_state::SlotState;
pub use nichlink_run_method::PluginChannel as ValidationChannel;
#[derive(Clone, Copy, Debug)]
pub struct WasmPluginSlot {
pub name: &'static str,
pub framework: FrameworkId,
pub mode: PluginMode,
pub contract: FlowContract,
pub channels: &'static [ValidationChannel],
}
impl WasmPluginSlot {
pub const fn new(
name: &'static str,
framework: FrameworkId,
mode: PluginMode,
contract: FlowContract,
channels: &'static [ValidationChannel],
) -> Self {
Self {
name,
framework,
mode,
contract,
channels,
}
}
}
pub struct WasmPluginTable {
backend: WasmBackend,
slots: BTreeMap<&'static str, SlotState>,
}
impl WasmPluginTable {
pub fn new(slots: &'static [WasmPluginSlot]) -> Result<Self, HostError> {
Self::with_backend(slots, WasmBackend::default())
}
pub fn with_backend(
slots: &'static [WasmPluginSlot],
backend: WasmBackend,
) -> Result<Self, HostError> {
let mut states = BTreeMap::new();
for definition in slots {
if definition.name.trim().is_empty() {
return Err(HostError::Slot("plugin slot name is empty".to_owned()));
}
if definition.channels.is_empty() {
return Err(HostError::Slot(format!(
"plugin slot `{}` has no validation channel",
definition.name
)));
}
if states
.insert(
definition.name,
SlotState {
definition: *definition,
active: ArcSwapOption::empty(),
pending: Mutex::new(None),
has_pending: AtomicBool::new(false),
next_generation: AtomicU64::new(0),
activation_error: Mutex::new(None),
},
)
.is_some()
{
return Err(HostError::Slot(format!(
"duplicate plugin slot `{}`",
definition.name
)));
}
}
Ok(Self {
backend,
slots: states,
})
}
pub fn install(
&self,
slot: &str,
channel: ValidationChannel,
artifact: VerifiedPluginArtifact,
) -> Result<u64, HostError> {
self.slot(slot)?.install(channel, artifact)
}
pub fn call(&self, slot: &str, operation: &str, input: &[u8]) -> Result<Vec<u8>, HostError> {
let state = self.slot(slot)?;
state.activate(self.backend)?;
let active = state
.active
.load_full()
.ok_or_else(|| HostError::Slot(format!("plugin slot `{slot}` is not installed")))?;
active.instance.call(operation, input)
}
pub fn is_loaded(&self, slot: &str) -> Result<bool, HostError> {
let state = self.slot(slot)?;
Ok(!state.has_pending.load(Ordering::Acquire) && state.active.load().is_some())
}
pub fn generation(&self, slot: &str) -> Result<Option<u64>, HostError> {
Ok(self
.slot(slot)?
.active
.load_full()
.map(|active| active.generation))
}
pub fn activation_error(&self, slot: &str) -> Result<Option<String>, HostError> {
self.slot(slot)?
.activation_error
.lock()
.map(|error| error.clone())
.map_err(|_| HostError::State("plugin slot lock was poisoned".to_owned()))
}
fn slot(&self, name: &str) -> Result<&SlotState, HostError> {
self.slots
.get(name)
.ok_or_else(|| HostError::Slot(format!("unknown plugin slot `{name}`")))
}
}
fn validate_artifact(
slot: WasmPluginSlot,
channel: ValidationChannel,
artifact: &VerifiedPluginArtifact,
) -> Result<(), HostError> {
nichlink_run_method::validate_artifact(
slot.name,
slot.framework,
slot.mode,
slot.contract,
slot.channels,
channel,
artifact,
)
.map_err(|error| match error {
nichlink_run_method::SlotValidationError::Policy(message) => HostError::Policy(message),
nichlink_run_method::SlotValidationError::Contract(message) => HostError::Contract(message),
})
}