use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, RwLock};
use tokio::sync::mpsc::UnboundedSender;
use wasmtime::Engine;
use crate::commands::{CommandRegistry, command};
use crate::operation::Operation;
use crate::schema::SchemaRegistry;
use super::activation::ActivationState;
use super::config::{ExtensionConfig, ExtensionStatus};
use super::loader::{load_cyix, CyixSchema};
use super::manifest::ExtensionManifest;
use super::runtime::ExtensionInstance;
struct LoadedExtension {
manifest: ExtensionManifest,
instance: ExtensionInstance,
state: ActivationState,
schemas: Vec<CyixSchema>,
schemas_registered: bool,
log_matchers: Vec<crate::log_matcher::CompiledMatcher>,
}
impl LoadedExtension {
fn is_active(&self) -> bool {
self.state.is_active()
}
}
pub(crate) struct ExtensionManager {
extensions: Vec<LoadedExtension>,
#[allow(dead_code)]
_engine: Engine,
schema_registry: Arc<RwLock<SchemaRegistry>>,
matcher_registry: std::sync::Arc<crate::log_matcher::MatcherRegistry>,
}
impl ExtensionManager {
pub(crate) fn empty() -> Self {
Self {
extensions: Vec::new(),
_engine: Engine::default(),
schema_registry: SchemaRegistry::shared(),
matcher_registry: std::sync::Arc::new(crate::log_matcher::MatcherRegistry::new()),
}
}
pub(crate) fn load_all(
ext_dir: &Path,
op_tx: UnboundedSender<Vec<Operation>>,
project_languages: Vec<String>,
config: &ExtensionConfig,
) -> (Self, Vec<(String, String)>) {
let engine = Engine::default();
let mut extensions = Vec::new();
let mut extension_defaults = Vec::new();
let schema_registry = SchemaRegistry::shared();
let entries = match std::fs::read_dir(ext_dir) {
Ok(e) => e,
Err(e) => {
log::debug!("extensions dir {:?} not readable: {}", ext_dir, e);
return (
Self {
extensions,
_engine: engine,
schema_registry: schema_registry.clone(),
matcher_registry: std::sync::Arc::new(crate::log_matcher::MatcherRegistry::new()),
},
extension_defaults,
);
}
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("cyix") {
continue;
}
match load_cyix(&path) {
Ok(pkg) => {
let ext_status = config.get(&pkg.manifest.name);
if ext_status == ExtensionStatus::Disabled {
log::debug!("Extension {:?} is disabled, skipping load", pkg.manifest.name);
continue;
}
let ext_config_yaml = pkg.default_config.clone();
let ext_config = parse_config(ext_config_yaml.as_deref());
match ExtensionInstance::new(&engine, &pkg.wasm_bytes, op_tx.clone(), ext_config, project_languages.clone()) {
Ok(instance) => {
let state = match ext_status {
ExtensionStatus::Enabled => ActivationState::Global,
ExtensionStatus::Auto => ActivationState::Auto,
ExtensionStatus::Disabled => continue,
};
log::info!(
"loaded extension {:?} v{} ({:?})",
pkg.manifest.name,
pkg.manifest.version,
state
);
if let Some(yaml) = ext_config_yaml {
extension_defaults.push((pkg.manifest.name.clone(), yaml));
}
extensions.push(LoadedExtension {
manifest: pkg.manifest,
instance,
state,
schemas: pkg.schemas,
schemas_registered: false,
log_matchers: pkg.log_matchers,
});
if extensions.last().map(|e| e.state.is_active()).unwrap_or(false) {
let idx = extensions.len() - 1;
let mut reg = schema_registry.write().unwrap();
for s in &extensions[idx].schemas {
let id = s.id.clone();
let name = s.name.clone().unwrap_or_else(|| id.clone());
reg.register_from_extension(id, name, s.patterns.clone(), s.schema_json.clone());
}
extensions[idx].schemas_registered = true;
}
}
Err(e) => {
log::error!("failed to instantiate {:?}: {}", path, e);
}
}
}
Err(e) => {
log::error!("failed to load {:?}: {}", path, e);
}
};
}
let manager = Self {
extensions,
_engine: engine,
schema_registry,
matcher_registry: std::sync::Arc::new(crate::log_matcher::MatcherRegistry::new()),
};
let combined: Vec<crate::log_matcher::CompiledMatcher> = manager
.extensions
.iter()
.filter(|e| e.is_active() || e.state == ActivationState::Auto)
.flat_map(|e| e.log_matchers.iter().cloned())
.collect();
if let Err(e) = manager.matcher_registry.replace_all(combined) {
log::warn!("matcher_registry: failed to populate at load_all: {}", e);
}
(manager, extension_defaults)
}
pub(crate) fn detect_project(&mut self) {
let mut to_unregister: Vec<crate::log_matcher::MatcherId> = Vec::new();
for ext in &mut self.extensions {
if ext.state != ActivationState::Auto {
continue;
}
let relevant = ext.instance.detect_project();
log::debug!("detect_project for {:?}: {}", ext.manifest.name, relevant);
if !relevant {
ext.state = ActivationState::Disabled;
for m in &ext.log_matchers {
to_unregister.push(m.id.clone());
}
} else {
ext.state = ActivationState::Project;
if !ext.schemas_registered {
let mut reg = self.schema_registry.write().unwrap();
for s in &ext.schemas {
let id = s.id.clone();
let name = s.name.clone().unwrap_or_else(|| id.clone());
reg.register_from_extension(id, name, s.patterns.clone(), s.schema_json.clone());
}
ext.schemas_registered = true;
}
}
}
if !to_unregister.is_empty() {
let removed = self.matcher_registry.unregister_by_ids(&to_unregister);
log::debug!("matcher_registry: unregistered {} matchers after detect_project", removed);
}
}
pub(crate) fn dispatch_event(
&mut self,
event_name: &str,
payload: &HashMap<String, String>,
) -> Vec<Operation> {
let payload_yaml = map_to_yaml(payload);
for ext in &mut self.extensions {
if !ext.is_active() {
continue;
}
if !ext.manifest.events.iter().any(|e| e == event_name) {
continue;
}
if let Err(e) = ext.instance.call_export("oo_event", &payload_yaml) {
log::warn!(
"error dispatching event {:?} to {:?}: {}",
event_name,
ext.manifest.name,
e
);
}
}
vec![]
}
#[allow(dead_code)]
pub(crate) fn call_command(&mut self, ext_name: &str, fn_name: &str) {
let export = format!("__oo_cmd_{}", fn_name);
for ext in &mut self.extensions {
if ext.manifest.name != ext_name || !ext.is_active() {
continue;
}
if let Err(e) = ext.instance.call_export(&export, "") {
log::warn!("error calling command {:?}: {}", export, e);
}
}
}
pub(crate) fn register_commands(&self, registry: &mut CommandRegistry) {
for ext in &self.extensions {
if !ext.is_active() {
continue;
}
for cmd in &ext.manifest.commands {
let ext_name = ext.manifest.name.clone();
let fn_name = cmd.function.clone();
let id_str = cmd.id.clone();
let title = cmd.title.clone();
let description = cmd.description.clone();
let context = cmd.activation_context.clone();
let Ok(id) = id_str.parse::<crate::commands::CommandId>() else {
log::warn!(
"extension {:?} has invalid command id {:?}",
ext_name,
id_str
);
continue;
};
let mut builder = command(id.clone())
.title(title.clone())
.description(description.clone());
if !context.is_empty() {
builder = builder.context(context.clone());
}
let op_id = id.clone();
let handler_ext = ext_name.clone();
let handler_fn = fn_name.clone();
let _ = (handler_ext, handler_fn);
registry.register(builder.handler(move |_ctx, _| {
vec![Operation::RunCommand {
id: op_id.clone(),
args: Default::default(),
}]
}));
}
}
}
pub(crate) fn matcher_registry_arc(&self) -> std::sync::Arc<crate::log_matcher::MatcherRegistry> {
std::sync::Arc::clone(&self.matcher_registry)
}
#[cfg(test)]
pub(crate) fn states(&self) -> Vec<(String, String, ActivationState)> {
self.extensions
.iter()
.map(|e| {
(
e.manifest.name.clone(),
e.manifest.version.clone(),
e.state.clone(),
)
})
.collect()
}
pub(crate) fn set_state(&mut self, name: &str, state: ActivationState) {
let mut changed_idx: Option<(usize, bool)> = None;
for (i, ext) in self.extensions.iter_mut().enumerate() {
if ext.manifest.name == name {
let was_active = ext.state.is_active();
ext.state = state;
let now_active = ext.state.is_active();
if !was_active && now_active && !ext.schemas_registered {
let mut reg = self.schema_registry.write().unwrap();
for s in &ext.schemas {
let id = s.id.clone();
let name = s.name.clone().unwrap_or_else(|| id.clone());
reg.register_from_extension(id, name, s.patterns.clone(), s.schema_json.clone());
}
ext.schemas_registered = true;
}
if was_active != now_active {
changed_idx = Some((i, was_active));
}
break;
}
}
if let Some((idx, was_active)) = changed_idx {
let ext = &self.extensions[idx];
if !was_active && ext.state.is_active() {
if let Err(e) = self.matcher_registry.register(ext.log_matchers.clone()) {
log::warn!("matcher_registry: failed to register matchers for {}: {}", name, e);
}
} else if was_active && !ext.state.is_active() {
let ids: Vec<crate::log_matcher::MatcherId> = ext
.log_matchers
.iter()
.map(|m| m.id.clone())
.collect();
let removed = self.matcher_registry.unregister_by_ids(&ids);
log::debug!("matcher_registry: unregistered {} matchers for {}", removed, name);
}
}
}
}
fn parse_config(yaml: Option<&str>) -> HashMap<String, String> {
let Some(text) = yaml else {
return HashMap::new();
};
serde_saphyr::from_str(text).unwrap_or_default()
}
fn map_to_yaml(map: &HashMap<String, String>) -> String {
serde_saphyr::to_string(map).unwrap_or_default()
}