#[cfg(feature = "wasm")]
use camel_bean::BeanProcessor;
#[cfg(feature = "wasm")]
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
fn load_config_or_default(
config_path: &str,
) -> Result<camel_config::config::CamelConfig, camel_api::CamelError> {
match std::path::Path::new(config_path).try_exists() {
Ok(false) => {
config::Config::builder()
.build()
.and_then(|c| c.try_deserialize())
.map_err(|e| {
camel_api::CamelError::Config(format!("Failed to build default config: {e}"))
})
}
Err(e) => Err(camel_api::CamelError::Config(format!(
"failed to check config path {config_path}: {e}"
))),
Ok(true) => {
camel_config::config::CamelConfig::from_file_with_env(config_path).map_err(|e| {
camel_api::CamelError::Config(format!("failed to load {config_path}: {e}"))
})
}
}
}
fn canonical_project_root(config_path: &std::path::Path) -> std::path::PathBuf {
config_path
.parent()
.map(|p| {
if p.as_os_str().is_empty() {
std::path::Path::new(".")
} else {
p
}
})
.unwrap_or(std::path::Path::new("."))
.canonicalize()
.unwrap_or_else(|e| {
eprintln!("Error: cannot resolve project root: {e}");
std::process::exit(1);
})
}
pub async fn run(
routes_override: Option<String>,
config_path: String,
cli_watch: Option<bool>,
otel: bool,
otel_endpoint: Option<String>,
service_name: Option<String>,
health_port: Option<u16>,
) -> Result<(), camel_api::CamelError> {
let mut camel_config: camel_config::config::CamelConfig = load_config_or_default(&config_path)?;
let otel_enabled = otel || otel_endpoint.is_some() || service_name.is_some();
if otel_enabled {
let otel_cfg =
camel_config
.observability
.otel
.get_or_insert(camel_config::OtelCamelConfig {
enabled: true,
endpoint: "http://localhost:4317".to_string(),
service_name: "rust-camel".to_string(),
..Default::default()
});
otel_cfg.enabled = true;
if let Some(ep) = otel_endpoint {
otel_cfg.endpoint = ep;
}
if let Some(name) = service_name {
otel_cfg.service_name = name;
}
}
if let Some(port) = health_port {
let health_cfg = camel_config
.observability
.health
.get_or_insert(camel_config::config::HealthCamelConfig::default());
health_cfg.enabled = true;
health_cfg.port = port;
}
let beans_registry = {
let bean_reg = std::sync::Arc::new(std::sync::Mutex::new(camel_bean::BeanRegistry::new()));
if camel_config.beans.is_empty() {
None
} else {
Some(bean_reg)
}
};
let mut ctx = camel_config::config::CamelConfig::configure_context_with_beans(
&camel_config,
beans_registry.clone(),
)
.await
.unwrap_or_else(|e| {
crate::commands::errors::report_cli_failure_and_exit("run", &e);
});
tracing::warn!(
"camel run trusts the current working directory and will execute route \
scripts, WASM modules, and beans resolved from it; only run from a \
trusted directory"
);
match camel_function::FunctionRuntimeService::with_default_container_provider(
camel_function::FunctionConfig::default(),
) {
Ok(svc) => ctx = ctx.with_lifecycle(svc),
Err(e) => tracing::warn!("Function runtime disabled: {e}"),
}
#[cfg(feature = "wasm")]
if let Some(ref bean_reg) = beans_registry {
let component_registry = ctx.registry_arc();
let plugins_dir_raw = camel_config
.components
.raw
.get("wasm")
.and_then(|v| v.get("plugins_dir"))
.and_then(|v| v.as_str())
.unwrap_or("plugins");
let camel_root = canonical_project_root(std::path::Path::new(&config_path));
crate::commands::plugin::validate_plugins_dir(&camel_root, plugins_dir_raw).unwrap_or_else(
|e| {
eprintln!("Error: invalid plugins_dir: {e}");
std::process::exit(1);
},
);
let plugins_dir = camel_root.join(plugins_dir_raw);
for (bean_name, bean_cfg) in &camel_config.beans {
tracing::info!(bean = %bean_name, plugin = %bean_cfg.plugin, "registering WASM bean");
if !bean_cfg
.plugin
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
eprintln!(
"Invalid bean plugin name '{}': must be alphanumeric with - or _",
bean_cfg.plugin
);
std::process::exit(1);
}
let wasm_path = plugins_dir.join(format!("{}.wasm", bean_cfg.plugin));
let canonical_plugins = plugins_dir.canonicalize().unwrap_or_else(|_| {
eprintln!("Plugins directory not found: {}", plugins_dir.display());
std::process::exit(1);
});
let canonical_path = wasm_path.canonicalize().unwrap_or_else(|_| {
eprintln!("WASM bean plugin not found: {}", wasm_path.display());
std::process::exit(1);
});
if !canonical_path.starts_with(&canonical_plugins) {
eprintln!(
"Bean plugin path escapes plugins directory: {}",
bean_cfg.plugin
);
std::process::exit(1);
}
let wasm_config =
camel_component_wasm::config::WasmConfig::from_limits(&bean_cfg.limits);
let wasm_bean = camel_component_wasm::bean::WasmBean::new(
&wasm_path,
wasm_config,
Arc::new(camel_core::RegistryComponentContext::new(
component_registry.clone(),
Some(ctx.metrics()),
camel_component_api::ComponentContext::component_metrics_enabled(&ctx),
)),
bean_cfg.config.clone(),
)
.await
.unwrap_or_else(|e| {
eprintln!("Failed to load WASM bean '{}': {}", bean_name, e);
std::process::exit(1);
});
tracing::info!(
bean = %bean_name,
plugin = %bean_cfg.plugin,
methods = ?wasm_bean.methods(),
"WASM bean loaded"
);
bean_reg
.lock()
.expect("beans registry lock") .register(bean_name, wasm_bean)
.unwrap_or_else(|e| {
eprintln!("Bean registration failed for '{}': {}", bean_name, e);
std::process::exit(1);
});
}
}
let config_routes = Some(camel_config.routes.clone());
let patterns: Vec<String> = resolve_route_patterns(&routes_override, &config_routes);
tracing::info!("camel-cli: loading routes from patterns: {:?}", patterns);
#[cfg(feature = "security")]
let security_compile_context =
camel_bundles::security_boot::build_security_compile_context_from_config(
&camel_config,
ctx.registry_arc(),
)
.await?;
#[cfg(not(feature = "security"))]
camel_bundles::security_boot::ensure_security_supported(&camel_config)?;
#[cfg(not(feature = "security"))]
let security_compile_context = camel_dsl::SecurityCompileContext::default();
camel_bundles::security_boot::install_bind_exposure_acks(&mut ctx, &camel_config).await;
let project_root = canonical_project_root(std::path::Path::new(&config_path));
let boot_handle = camel_bundles::boot(&mut ctx, &camel_config, &project_root).await?;
match camel_dsl::discover_routes_with_threshold_and_security(
&patterns,
camel_config.stream_caching.threshold,
security_compile_context.clone(),
) {
Ok(defs) => {
if defs.is_empty() {
tracing::warn!(
"route discovery matched zero route files for patterns {:?}; \
starting with no routes",
patterns
);
}
#[cfg(feature = "exec")]
{
let exec_used = camel_core::startup_validation::route_definitions_reference_scheme(
&defs, "exec",
);
let exec_configured = camel_config.components.raw.contains_key("exec");
if exec_used || exec_configured {
camel_bundles::register_bundle::<camel_component_exec::ExecBundle>(
&mut ctx,
&camel_config,
)?;
}
}
camel_bundles::security_boot::install_sql_startup_checks(&mut ctx, &defs);
let defs = crate::commands::bench_instrument::maybe_instrument_routes(defs);
for def in defs {
let id = def.route_id().to_string();
if let Err(e) = ctx.add_route_definition(def).await {
tracing::error!("Failed to add route '{}': {}", id, e);
}
}
}
Err(e) => {
match &e {
camel_dsl::DiscoveryError::MaterializationFailures { failures } => {
tracing::error!("Failed to discover routes: template materialization failed:");
for failure in failures {
match &failure.route_id {
Some(route_id) => {
tracing::error!(
" {} (template '{}', route '{}'): {}",
failure.path,
failure.template_ref,
route_id,
failure.error
);
}
None => {
tracing::error!(
" {} (template '{}'): {}",
failure.path,
failure.template_ref,
failure.error
);
}
}
}
}
_ => {
tracing::error!("Failed to discover routes: {}", e);
}
}
crate::commands::errors::report_cli_failure_and_exit(
"run",
&camel_api::CamelError::RouteError(e.to_string()),
);
}
}
if let Err(e) = ctx.start().await {
tracing::error!("Failed to start CamelContext: {}", e);
crate::commands::errors::report_cli_failure_and_exit("run", &e);
}
tracing::info!("camel-cli: context started");
#[cfg(feature = "jemalloc")]
crate::allocator_metrics::spawn_allocator_sampler(ctx.metrics());
let watch_enabled = cli_watch.unwrap_or(camel_config.watch);
let watcher_shutdown = CancellationToken::new();
if watch_enabled {
let ctrl = ctx.runtime_execution_handle();
let watch_routes_override = routes_override.clone();
let watch_config_routes = config_routes.clone();
let watch_patterns = patterns.clone();
let watch_security_compile_context = security_compile_context.clone();
let drain_timeout = std::time::Duration::from_millis(camel_config.drain_timeout_ms);
let debounce = std::time::Duration::from_millis(camel_config.watch_debounce_ms);
let watcher_token = watcher_shutdown.clone();
tokio::spawn(async move {
let watch_dirs = camel_core::reload_watcher::resolve_watch_dirs(&watch_patterns);
let result = camel_core::reload_watcher::watch_and_reload(
watch_dirs,
ctrl,
move || {
let patterns =
resolve_route_patterns(&watch_routes_override, &watch_config_routes);
camel_dsl::discover_routes_with_threshold_and_security(
&patterns,
camel_config.stream_caching.threshold,
watch_security_compile_context.clone(),
)
.map_err(|e| camel_api::CamelError::RouteError(e.to_string()))
},
Some(watcher_token),
drain_timeout,
debounce,
)
.await;
if let Err(e) = result {
tracing::error!("File watcher failed: {}", e);
}
});
tracing::info!(
"camel-cli: hot-reload watching {:?}. Press Ctrl+C to stop.",
patterns
);
} else {
tracing::info!("camel-cli: running (hot-reload disabled). Press Ctrl+C to stop.");
}
tokio::select! {
_ = tokio::signal::ctrl_c() => tracing::info!("Received Ctrl+C"),
_ = async {
#[cfg(unix)]
{
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("Failed to install SIGTERM handler") .recv()
.await
}
#[cfg(not(unix))]
{
std::future::pending::<()>().await
}
} => tracing::info!("Received SIGTERM"),
}
let force_exit = tokio::spawn(async {
tokio::signal::ctrl_c().await.ok();
tracing::warn!("Second Ctrl+C — forcing exit");
std::process::exit(1);
});
tracing::info!("camel-cli: shutting down...");
watcher_shutdown.cancel();
let _ = boot_handle.shutdown(&mut ctx).await;
force_exit.abort();
tracing::info!("camel-cli: stopped");
Ok(())
}
fn default_patterns() -> Vec<String> {
vec!["routes/*.yaml".to_string()]
}
fn resolve_route_patterns_with(
defaults: &[String],
routes_override: &Option<String>,
config_routes: &Option<Vec<String>>,
) -> Vec<String> {
if let Some(ov) = routes_override {
vec![ov.clone()]
} else if let Some(routes) = config_routes {
if routes.is_empty() {
defaults.to_vec()
} else {
routes.clone()
}
} else {
defaults.to_vec()
}
}
fn resolve_route_patterns(
routes_override: &Option<String>,
config_routes: &Option<Vec<String>>,
) -> Vec<String> {
resolve_route_patterns_with(&default_patterns(), routes_override, config_routes)
}
#[cfg(test)]
pub(crate) static WASM_ACKS_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
#[path = "run_tests.rs"]
mod tests;