1use camel_api::datasource::DatasourceCatalog;
12use camel_bean::BeanProcessor;
13use camel_core::datasource::RuntimeDatasourceCatalog;
14use std::sync::Arc;
15use std::time::Duration;
16use tokio_util::sync::CancellationToken;
17
18struct BridgeCleanup {
19 xslt: Arc<camel_xslt::XsltBridgeRuntime>,
20 xj: Arc<camel_xj::XjBridgeRuntime>,
21 validator: Option<Arc<camel_component_validator::xsd_bridge::XsdBridgeBackend>>,
22}
23
24#[async_trait::async_trait]
25impl camel_api::lifecycle::Lifecycle for BridgeCleanup {
26 fn name(&self) -> &str {
27 "bridge-cleanup"
28 }
29
30 async fn start(&mut self) -> Result<(), camel_api::CamelError> {
31 Ok(())
32 }
33
34 async fn stop(&mut self) -> Result<(), camel_api::CamelError> {
35 self.xslt.shutdown().await;
36 self.xj.shutdown().await;
37 if let Some(validator) = &self.validator {
38 validator.shutdown().await;
39 }
40 Ok(())
41 }
42}
43
44pub async fn run(
45 routes_override: Option<String>,
46 config_path: String,
47 cli_watch: Option<bool>,
48 otel: bool,
49 otel_endpoint: Option<String>,
50 service_name: Option<String>,
51 health_port: Option<u16>,
52) -> Result<(), camel_api::CamelError> {
53 let mut camel_config: camel_config::config::CamelConfig =
55 camel_config::config::CamelConfig::from_file(&config_path).unwrap_or_else(|_| {
56 config::Config::builder()
58 .build()
59 .and_then(|c| c.try_deserialize())
60 .unwrap_or_else(|e| {
61 eprintln!("Failed to build default config: {e}");
62 std::process::exit(1);
63 })
64 });
65
66 let otel_enabled = otel || otel_endpoint.is_some() || service_name.is_some();
68 if otel_enabled {
69 let otel_cfg =
70 camel_config
71 .observability
72 .otel
73 .get_or_insert(camel_config::OtelCamelConfig {
74 enabled: true,
75 endpoint: "http://localhost:4317".to_string(),
76 service_name: "rust-camel".to_string(),
77 ..Default::default()
78 });
79 otel_cfg.enabled = true;
80 if let Some(ep) = otel_endpoint {
81 otel_cfg.endpoint = ep;
82 }
83 if let Some(name) = service_name {
84 otel_cfg.service_name = name;
85 }
86 }
87
88 if let Some(port) = health_port {
89 let health_cfg = camel_config
90 .observability
91 .health
92 .get_or_insert(camel_config::config::HealthCamelConfig::default());
93 health_cfg.enabled = true;
94 health_cfg.port = port;
95 }
96
97 let beans_registry = {
99 let bean_reg = std::sync::Arc::new(std::sync::Mutex::new(camel_bean::BeanRegistry::new()));
100 if camel_config.beans.is_empty() {
101 None
102 } else {
103 Some(bean_reg)
104 }
105 };
106
107 let mut ctx = camel_config::config::CamelConfig::configure_context_with_beans(
108 &camel_config,
109 beans_registry.clone(),
110 )
111 .await
112 .unwrap_or_else(|e| {
113 eprintln!("Failed to configure CamelContext: {e}");
114 std::process::exit(1);
115 });
116
117 tracing::warn!(
120 "camel run trusts the current working directory and will execute route \
121 scripts, WASM modules, and beans resolved from it; only run from a \
122 trusted directory"
123 );
124
125 match camel_function::FunctionRuntimeService::with_default_container_provider(
126 camel_function::FunctionConfig::default(),
127 ) {
128 Ok(svc) => ctx = ctx.with_lifecycle(svc),
129 Err(e) => tracing::warn!("Function runtime disabled: {e}"),
130 }
131
132 let datasource_catalog: Arc<dyn DatasourceCatalog> = {
134 let catalog = RuntimeDatasourceCatalog::new(camel_config.datasources.clone())
135 .with_health_registry(ctx.health_registry());
136 Arc::new(catalog)
137 };
138
139 #[cfg(feature = "wasm")]
141 if let Some(ref bean_reg) = beans_registry {
142 let component_registry = ctx.registry_arc();
143 let plugins_dir_raw = camel_config
144 .components
145 .raw
146 .get("wasm")
147 .and_then(|v| v.get("plugins_dir"))
148 .and_then(|v| v.as_str())
149 .unwrap_or("plugins");
150 let config_dir = std::path::Path::new(&config_path)
151 .parent()
152 .map(|p| {
153 if p.as_os_str().is_empty() {
154 std::path::Path::new(".")
155 } else {
156 p
157 }
158 })
159 .unwrap_or(std::path::Path::new("."));
160 let camel_root = config_dir.canonicalize().unwrap_or_else(|e| {
161 eprintln!("Error: cannot resolve project root: {e}");
162 std::process::exit(1);
163 });
164 crate::commands::plugin::validate_plugins_dir(&camel_root, plugins_dir_raw).unwrap_or_else(
165 |e| {
166 eprintln!("Error: invalid plugins_dir: {e}");
167 std::process::exit(1);
168 },
169 );
170 let plugins_dir = camel_root.join(plugins_dir_raw);
171 for (bean_name, bean_cfg) in &camel_config.beans {
172 tracing::info!(bean = %bean_name, plugin = %bean_cfg.plugin, "registering WASM bean");
173
174 if !bean_cfg
175 .plugin
176 .chars()
177 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
178 {
179 eprintln!(
180 "Invalid bean plugin name '{}': must be alphanumeric with - or _",
181 bean_cfg.plugin
182 );
183 std::process::exit(1);
184 }
185
186 let wasm_path = plugins_dir.join(format!("{}.wasm", bean_cfg.plugin));
187 let canonical_plugins = plugins_dir.canonicalize().unwrap_or_else(|_| {
188 eprintln!("Plugins directory not found: {}", plugins_dir.display());
189 std::process::exit(1);
190 });
191 let canonical_path = wasm_path.canonicalize().unwrap_or_else(|_| {
192 eprintln!("WASM bean plugin not found: {}", wasm_path.display());
193 std::process::exit(1);
194 });
195 if !canonical_path.starts_with(&canonical_plugins) {
196 eprintln!(
197 "Bean plugin path escapes plugins directory: {}",
198 bean_cfg.plugin
199 );
200 std::process::exit(1);
201 }
202 let wasm_config =
203 camel_component_wasm::config::WasmConfig::from_limits(&bean_cfg.limits);
204 let wasm_bean = camel_component_wasm::bean::WasmBean::new(
205 &wasm_path,
206 wasm_config,
207 component_registry.clone(),
208 bean_cfg.config.clone(),
209 )
210 .await
211 .unwrap_or_else(|e| {
212 eprintln!("Failed to load WASM bean '{}': {}", bean_name, e);
213 std::process::exit(1);
214 });
215 tracing::info!(
216 bean = %bean_name,
217 plugin = %bean_cfg.plugin,
218 methods = ?wasm_bean.methods(),
219 "WASM bean loaded"
220 );
221 bean_reg
222 .lock()
223 .expect("beans registry lock") .register(bean_name, wasm_bean)
225 .unwrap_or_else(|e| {
226 eprintln!("Bean registration failed for '{}': {}", bean_name, e);
227 std::process::exit(1);
228 });
229 }
230 }
231
232 let patterns: Vec<String> = if let Some(p) = routes_override {
234 vec![p]
235 } else if !camel_config.routes.is_empty() {
236 camel_config.routes.clone()
237 } else {
238 vec!["routes/*.yaml".to_string()]
239 };
240
241 tracing::info!("camel-cli: loading routes from patterns: {:?}", patterns);
242
243 let security_compile_context =
244 crate::build_security_compile_context_from_config(&camel_config, ctx.registry_arc())
245 .await?;
246
247 macro_rules! register_bundle {
251 ($ctx:expr, $cfg:expr, $Bundle:ty) => {
252 let raw = $cfg
253 .components
254 .raw
255 .get(<$Bundle as camel_component_api::ComponentBundle>::config_key())
256 .cloned()
257 .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
258 match <$Bundle as camel_component_api::ComponentBundle>::from_toml(raw) {
259 Ok(bundle) => <$Bundle as camel_component_api::ComponentBundle>::register_all(
260 bundle, &mut $ctx,
261 ),
262 Err(e) => {
263 return Err(camel_api::CamelError::Config(format!(
264 "Failed to load {} config: {}",
265 <$Bundle as camel_component_api::ComponentBundle>::config_key(),
266 e
267 )));
268 }
269 }
270 };
271 }
272
273 ctx.register_component(camel_component_timer::TimerComponent::new());
275 ctx.register_component(camel_component_cron::CronComponent::new());
276 ctx.register_component(camel_component_log::LogComponent::new());
277 ctx.register_component(camel_component_direct::DirectComponent::new());
278 ctx.register_component(camel_component_seda::SedaComponent::new());
279 ctx.register_component(camel_component_mock::MockComponent::new());
280 ctx.register_component(camel_component_controlbus::ControlBusComponent::new());
281 let validator_component = camel_component_validator::ValidatorComponent::new();
282 let validator_backend = validator_component.xsd_bridge_backend();
283 ctx.register_component(validator_component);
284
285 let xslt_component = camel_xslt::XsltComponent::default();
286 let xslt_runtime = xslt_component.bridge_runtime();
287 ctx.register_component(xslt_component);
288
289 let xj_component = camel_xj::XjComponent::default();
290 let xj_runtime = xj_component.bridge_runtime();
291 ctx.register_component(xj_component);
292
293 ctx = ctx.with_lifecycle(BridgeCleanup {
294 xslt: xslt_runtime,
295 xj: xj_runtime,
296 validator: validator_backend,
297 });
298
299 register_bundle!(ctx, camel_config, camel_component_http::HttpBundle);
301 #[cfg(feature = "http-static")]
302 register_bundle!(ctx, camel_config, camel_component_http::HttpStaticBundle);
303 register_bundle!(ctx, camel_config, camel_component_ws::WsBundle);
304 register_bundle!(ctx, camel_config, camel_component_file::FileBundle);
305 register_bundle!(
306 ctx,
307 camel_config,
308 camel_component_container::ContainerBundle
309 );
310
311 let jms_pool = {
313 let raw = camel_config
314 .components
315 .raw
316 .get("jms")
317 .cloned()
318 .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
319 match <camel_component_jms::JmsBundle as camel_component_api::ComponentBundle>::from_toml(
320 raw,
321 ) {
322 Ok(bundle) => {
323 let pool = bundle.pool();
324 <camel_component_jms::JmsBundle as camel_component_api::ComponentBundle>::register_all(bundle, &mut ctx);
325 pool
326 }
327 Err(e) => {
328 return Err(camel_api::CamelError::Config(format!(
329 "Failed to load jms config: {e}"
330 )));
331 }
332 }
333 };
334
335 let cxf_pool = {
336 let raw = camel_config
337 .components
338 .raw
339 .get("cxf")
340 .cloned()
341 .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
342 match <camel_component_cxf::CxfBundle as camel_component_api::ComponentBundle>::from_toml(
343 raw,
344 ) {
345 Ok(bundle) => {
346 let pool = bundle.pool();
347 <camel_component_cxf::CxfBundle as camel_component_api::ComponentBundle>::register_all(bundle, &mut ctx);
348 pool
349 }
350 Err(e) => {
351 return Err(camel_api::CamelError::Config(format!(
352 "Failed to load cxf config: {e}"
353 )));
354 }
355 }
356 };
357
358 #[cfg(feature = "kafka")]
359 register_bundle!(ctx, camel_config, camel_component_kafka::KafkaBundle);
360 #[cfg(feature = "mqtt")]
361 register_bundle!(ctx, camel_config, camel_component_mqtt::MqttBundle);
362 register_bundle!(ctx, camel_config, camel_master::MasterBundle);
363 register_bundle!(
364 ctx,
365 camel_config,
366 camel_component_opensearch::OpenSearchBundle
367 );
368 register_bundle!(ctx, camel_config, camel_component_redis::RedisBundle);
369 {
370 let sql_raw = camel_config
371 .components
372 .raw
373 .get(<camel_component_sql::SqlBundle as camel_component_api::ComponentBundle>::config_key())
374 .cloned()
375 .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
376 match <camel_component_sql::SqlBundle as camel_component_api::ComponentBundle>::from_toml(
377 sql_raw,
378 ) {
379 Ok(bundle) => {
380 let bundle = bundle.with_catalog(Arc::clone(&datasource_catalog));
381 <camel_component_sql::SqlBundle as camel_component_api::ComponentBundle>::register_all(bundle, &mut ctx);
382 }
383 Err(e) => {
384 tracing::error!("failed to initialize SQL bundle: {}", e);
386 }
387 }
388 }
389 #[cfg(feature = "surrealdb")]
390 {
391 let surrealdb_raw = camel_config
392 .components
393 .raw
394 .get(<camel_component_surrealdb::SurrealDbBundle as camel_component_api::ComponentBundle>::config_key())
395 .cloned()
396 .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
397 match <camel_component_surrealdb::SurrealDbBundle as camel_component_api::ComponentBundle>::from_toml(
398 surrealdb_raw,
399 ) {
400 Ok(bundle) => {
401 let bundle = bundle.with_catalog(Arc::clone(&datasource_catalog));
402 <camel_component_surrealdb::SurrealDbBundle as camel_component_api::ComponentBundle>::register_all(
403 bundle, &mut ctx,
404 );
405 }
406 Err(e) => {
407 tracing::error!("failed to initialize SurrealDB bundle: {}", e);
409 }
410 }
411 }
412 #[cfg(feature = "grpc")]
413 register_bundle!(ctx, camel_config, camel_component_grpc::GrpcBundle);
414
415 #[cfg(feature = "exec")]
416 register_bundle!(ctx, camel_config, camel_component_exec::ExecBundle);
417
418 #[cfg(feature = "llm")]
419 register_bundle!(ctx, camel_config, camel_component_llm::LlmBundle);
420
421 #[cfg(feature = "wasm")]
422 {
423 let base_dir = std::path::Path::new(&config_path)
424 .parent()
425 .unwrap_or(std::path::Path::new("."))
426 .to_path_buf();
427 let wasm_bundle = camel_component_wasm::WasmBundle::new(ctx.registry_arc(), base_dir);
428 <camel_component_wasm::WasmBundle as camel_component_api::ComponentBundle>::register_all(
429 wasm_bundle,
430 &mut ctx,
431 );
432 }
433
434 match camel_dsl::discover_routes_with_threshold_and_security(
442 &patterns,
443 camel_config.stream_caching.threshold,
444 security_compile_context.clone(),
445 ) {
446 Ok(defs) => {
447 for check in
452 camel_core::startup_validation::scan_route_definitions_for_sql_checks(&defs)
453 {
454 ctx.add_startup_check(check);
455 }
456 for def in defs {
457 let id = def.route_id().to_string();
458 if let Err(e) = ctx.add_route_definition(def).await {
459 tracing::error!("Failed to add route '{}': {}", id, e);
461 }
462 }
463 }
464 Err(e) => {
465 tracing::error!("Failed to discover routes: {}", e);
467 std::process::exit(1);
468 }
469 }
470
471 if let Err(e) = ctx.start().await {
473 tracing::error!("Failed to start CamelContext: {}", e);
475 std::process::exit(1);
476 }
477
478 tracing::info!("camel-cli: context started");
479
480 let watch_enabled = cli_watch.unwrap_or(camel_config.watch);
483
484 let watcher_shutdown = CancellationToken::new();
486 if watch_enabled {
487 let ctrl = ctx.runtime_execution_handle();
488 let watch_patterns = patterns.clone();
489 let watch_security_compile_context = security_compile_context.clone();
490 let drain_timeout = std::time::Duration::from_millis(camel_config.drain_timeout_ms);
491 let debounce = std::time::Duration::from_millis(camel_config.watch_debounce_ms);
492 let watcher_token = watcher_shutdown.clone();
493 tokio::spawn(async move {
494 let watch_dirs = camel_core::reload_watcher::resolve_watch_dirs(&watch_patterns);
495 let result = camel_core::reload_watcher::watch_and_reload(
496 watch_dirs,
497 ctrl,
498 move || {
499 camel_dsl::discover_routes_with_threshold_and_security(
500 &watch_patterns,
501 camel_config.stream_caching.threshold,
502 watch_security_compile_context.clone(),
503 )
504 .map_err(|e| camel_api::CamelError::RouteError(e.to_string()))
505 },
506 Some(watcher_token),
507 drain_timeout,
508 debounce,
509 )
510 .await;
511 if let Err(e) = result {
512 tracing::error!("File watcher failed: {}", e);
514 }
515 });
516 tracing::info!(
517 "camel-cli: hot-reload watching {:?}. Press Ctrl+C to stop.",
518 patterns
519 );
520 } else {
521 tracing::info!("camel-cli: running (hot-reload disabled). Press Ctrl+C to stop.");
522 }
523
524 tokio::select! {
525 _ = tokio::signal::ctrl_c() => tracing::info!("Received Ctrl+C"),
526 _ = async {
527 #[cfg(unix)]
528 {
529 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
530 .expect("Failed to install SIGTERM handler") .recv()
532 .await
533 }
534 #[cfg(not(unix))]
535 {
536 std::future::pending::<()>().await
537 }
538 } => tracing::info!("Received SIGTERM"),
539 }
540
541 let force_exit = tokio::spawn(async {
543 tokio::signal::ctrl_c().await.ok();
544 tracing::warn!("Second Ctrl+C — forcing exit");
545 std::process::exit(1);
546 });
547
548 tracing::info!("camel-cli: shutting down...");
549 watcher_shutdown.cancel();
550
551 jms_pool.begin_shutdown();
553 cxf_pool.begin_shutdown();
554
555 ctx.stop().await.unwrap_or_else(|e| {
557 tracing::error!("Error during shutdown: {}", e);
559 });
560
561 const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30);
563
564 match tokio::time::timeout(SHUTDOWN_TIMEOUT, jms_pool.shutdown()).await {
565 Ok(Ok(())) => {}
566 Ok(Err(e)) => {
567 tracing::error!("JMS pool shutdown failed: {}", e);
569 }
570 Err(_) => tracing::warn!("JMS pool shutdown timed out after 30s"),
571 }
572
573 match tokio::time::timeout(SHUTDOWN_TIMEOUT, cxf_pool.shutdown()).await {
574 Ok(Ok(())) => {}
575 Ok(Err(e)) => {
576 tracing::error!("CXF pool shutdown failed: {}", e);
578 }
579 Err(_) => tracing::warn!("CXF pool shutdown timed out after 30s"),
580 }
581
582 force_exit.abort();
583
584 tracing::info!("camel-cli: stopped");
585 Ok(())
586}
587
588#[cfg(test)]
593mod tests {
594 #[test]
596 fn startup_warning_emitted() {
597 let source = include_str!("run.rs");
598 let a = "camel run trusts the current working directory";
601 let b = " and will execute route";
602 let msg = format!("{a}{b}");
603 let count = source.matches(&msg).count();
604 assert_eq!(
605 count, 1,
606 "expected exactly one tracing::warn! with the trust-model message in run.rs; found {count}"
607 );
608 }
609
610 #[test]
612 fn clap_help_documents_trust_model() {
613 let source = include_str!("../main.rs");
614 let has_trust_doc = source
615 .contains("Trust model: `camel run` executes route scripts, WASM modules, and beans")
616 || source
617 .contains("Trust model: camel run executes route scripts, WASM modules, and beans");
618 assert!(
619 has_trust_doc,
620 "expected trust model documentation in the Run subcommand help in main.rs"
621 );
622 }
623}