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 match camel_function::FunctionRuntimeService::with_default_container_provider(
118 camel_function::FunctionConfig::default(),
119 ) {
120 Ok(svc) => ctx = ctx.with_lifecycle(svc),
121 Err(e) => tracing::warn!("Function runtime disabled: {e}"),
122 }
123
124 let datasource_catalog: Arc<dyn DatasourceCatalog> = {
126 let catalog = RuntimeDatasourceCatalog::new(camel_config.datasources.clone())
127 .with_health_registry(ctx.health_registry());
128 Arc::new(catalog)
129 };
130
131 #[cfg(feature = "wasm")]
133 if let Some(ref bean_reg) = beans_registry {
134 let component_registry = ctx.registry_arc();
135 let plugins_dir_raw = camel_config
136 .components
137 .raw
138 .get("wasm")
139 .and_then(|v| v.get("plugins_dir"))
140 .and_then(|v| v.as_str())
141 .unwrap_or("plugins");
142 let config_dir = std::path::Path::new(&config_path)
143 .parent()
144 .map(|p| {
145 if p.as_os_str().is_empty() {
146 std::path::Path::new(".")
147 } else {
148 p
149 }
150 })
151 .unwrap_or(std::path::Path::new("."));
152 let camel_root = config_dir.canonicalize().unwrap_or_else(|e| {
153 eprintln!("Error: cannot resolve project root: {e}");
154 std::process::exit(1);
155 });
156 crate::commands::plugin::validate_plugins_dir(&camel_root, plugins_dir_raw).unwrap_or_else(
157 |e| {
158 eprintln!("Error: invalid plugins_dir: {e}");
159 std::process::exit(1);
160 },
161 );
162 let plugins_dir = camel_root.join(plugins_dir_raw);
163 for (bean_name, bean_cfg) in &camel_config.beans {
164 tracing::info!(bean = %bean_name, plugin = %bean_cfg.plugin, "registering WASM bean");
165
166 if !bean_cfg
167 .plugin
168 .chars()
169 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
170 {
171 eprintln!(
172 "Invalid bean plugin name '{}': must be alphanumeric with - or _",
173 bean_cfg.plugin
174 );
175 std::process::exit(1);
176 }
177
178 let wasm_path = plugins_dir.join(format!("{}.wasm", bean_cfg.plugin));
179 let canonical_plugins = plugins_dir.canonicalize().unwrap_or_else(|_| {
180 eprintln!("Plugins directory not found: {}", plugins_dir.display());
181 std::process::exit(1);
182 });
183 let canonical_path = wasm_path.canonicalize().unwrap_or_else(|_| {
184 eprintln!("WASM bean plugin not found: {}", wasm_path.display());
185 std::process::exit(1);
186 });
187 if !canonical_path.starts_with(&canonical_plugins) {
188 eprintln!(
189 "Bean plugin path escapes plugins directory: {}",
190 bean_cfg.plugin
191 );
192 std::process::exit(1);
193 }
194 let wasm_config =
195 camel_component_wasm::config::WasmConfig::from_limits(&bean_cfg.limits);
196 let wasm_bean = camel_component_wasm::bean::WasmBean::new(
197 &wasm_path,
198 wasm_config,
199 component_registry.clone(),
200 bean_cfg.config.clone(),
201 )
202 .await
203 .unwrap_or_else(|e| {
204 eprintln!("Failed to load WASM bean '{}': {}", bean_name, e);
205 std::process::exit(1);
206 });
207 tracing::info!(
208 bean = %bean_name,
209 plugin = %bean_cfg.plugin,
210 methods = ?wasm_bean.methods(),
211 "WASM bean loaded"
212 );
213 bean_reg
214 .lock()
215 .expect("beans registry lock") .register(bean_name, wasm_bean)
217 .unwrap_or_else(|e| {
218 eprintln!("Bean registration failed for '{}': {}", bean_name, e);
219 std::process::exit(1);
220 });
221 }
222 }
223
224 let patterns: Vec<String> = if let Some(p) = routes_override {
226 vec![p]
227 } else if !camel_config.routes.is_empty() {
228 camel_config.routes.clone()
229 } else {
230 vec!["routes/*.yaml".to_string()]
231 };
232
233 tracing::info!("camel-cli: loading routes from patterns: {:?}", patterns);
234
235 let security_compile_context =
236 crate::build_security_compile_context_from_config(&camel_config, ctx.registry_arc())
237 .await?;
238
239 macro_rules! register_bundle {
243 ($ctx:expr, $cfg:expr, $Bundle:ty) => {
244 let raw = $cfg
245 .components
246 .raw
247 .get(<$Bundle as camel_component_api::ComponentBundle>::config_key())
248 .cloned()
249 .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
250 match <$Bundle as camel_component_api::ComponentBundle>::from_toml(raw) {
251 Ok(bundle) => <$Bundle as camel_component_api::ComponentBundle>::register_all(
252 bundle, &mut $ctx,
253 ),
254 Err(e) => {
255 return Err(camel_api::CamelError::Config(format!(
256 "Failed to load {} config: {}",
257 <$Bundle as camel_component_api::ComponentBundle>::config_key(),
258 e
259 )));
260 }
261 }
262 };
263 }
264
265 ctx.register_component(camel_component_timer::TimerComponent::new());
267 ctx.register_component(camel_component_log::LogComponent::new());
268 ctx.register_component(camel_component_direct::DirectComponent::new());
269 ctx.register_component(camel_component_seda::SedaComponent::new());
270 ctx.register_component(camel_component_mock::MockComponent::new());
271 ctx.register_component(camel_component_controlbus::ControlBusComponent::new());
272 let validator_component = camel_component_validator::ValidatorComponent::new();
273 let validator_backend = validator_component.xsd_bridge_backend();
274 ctx.register_component(validator_component);
275
276 let xslt_component = camel_xslt::XsltComponent::default();
277 let xslt_runtime = xslt_component.bridge_runtime();
278 ctx.register_component(xslt_component);
279
280 let xj_component = camel_xj::XjComponent::default();
281 let xj_runtime = xj_component.bridge_runtime();
282 ctx.register_component(xj_component);
283
284 ctx = ctx.with_lifecycle(BridgeCleanup {
285 xslt: xslt_runtime,
286 xj: xj_runtime,
287 validator: validator_backend,
288 });
289
290 register_bundle!(ctx, camel_config, camel_component_http::HttpBundle);
292 #[cfg(feature = "http-static")]
293 register_bundle!(ctx, camel_config, camel_component_http::HttpStaticBundle);
294 register_bundle!(ctx, camel_config, camel_component_ws::WsBundle);
295 register_bundle!(ctx, camel_config, camel_component_file::FileBundle);
296 register_bundle!(
297 ctx,
298 camel_config,
299 camel_component_container::ContainerBundle
300 );
301
302 let jms_pool = {
304 let raw = camel_config
305 .components
306 .raw
307 .get("jms")
308 .cloned()
309 .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
310 match <camel_component_jms::JmsBundle as camel_component_api::ComponentBundle>::from_toml(
311 raw,
312 ) {
313 Ok(bundle) => {
314 let pool = bundle.pool();
315 <camel_component_jms::JmsBundle as camel_component_api::ComponentBundle>::register_all(bundle, &mut ctx);
316 pool
317 }
318 Err(e) => {
319 return Err(camel_api::CamelError::Config(format!(
320 "Failed to load jms config: {e}"
321 )));
322 }
323 }
324 };
325
326 let cxf_pool = {
327 let raw = camel_config
328 .components
329 .raw
330 .get("cxf")
331 .cloned()
332 .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
333 match <camel_component_cxf::CxfBundle as camel_component_api::ComponentBundle>::from_toml(
334 raw,
335 ) {
336 Ok(bundle) => {
337 let pool = bundle.pool();
338 <camel_component_cxf::CxfBundle as camel_component_api::ComponentBundle>::register_all(bundle, &mut ctx);
339 pool
340 }
341 Err(e) => {
342 return Err(camel_api::CamelError::Config(format!(
343 "Failed to load cxf config: {e}"
344 )));
345 }
346 }
347 };
348
349 #[cfg(feature = "kafka")]
350 register_bundle!(ctx, camel_config, camel_component_kafka::KafkaBundle);
351 #[cfg(feature = "mqtt")]
352 register_bundle!(ctx, camel_config, camel_component_mqtt::MqttBundle);
353 register_bundle!(ctx, camel_config, camel_master::MasterBundle);
354 register_bundle!(
355 ctx,
356 camel_config,
357 camel_component_opensearch::OpenSearchBundle
358 );
359 register_bundle!(ctx, camel_config, camel_component_redis::RedisBundle);
360 {
361 let sql_raw = camel_config
362 .components
363 .raw
364 .get(<camel_component_sql::SqlBundle as camel_component_api::ComponentBundle>::config_key())
365 .cloned()
366 .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
367 match <camel_component_sql::SqlBundle as camel_component_api::ComponentBundle>::from_toml(
368 sql_raw,
369 ) {
370 Ok(bundle) => {
371 let bundle = bundle.with_catalog(Arc::clone(&datasource_catalog));
372 <camel_component_sql::SqlBundle as camel_component_api::ComponentBundle>::register_all(bundle, &mut ctx);
373 }
374 Err(e) => {
375 tracing::error!("failed to initialize SQL bundle: {}", e);
377 }
378 }
379 }
380 #[cfg(feature = "surrealdb")]
381 {
382 let surrealdb_raw = camel_config
383 .components
384 .raw
385 .get(<camel_component_surrealdb::SurrealDbBundle as camel_component_api::ComponentBundle>::config_key())
386 .cloned()
387 .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
388 match <camel_component_surrealdb::SurrealDbBundle as camel_component_api::ComponentBundle>::from_toml(
389 surrealdb_raw,
390 ) {
391 Ok(bundle) => {
392 let bundle = bundle.with_catalog(Arc::clone(&datasource_catalog));
393 <camel_component_surrealdb::SurrealDbBundle as camel_component_api::ComponentBundle>::register_all(
394 bundle, &mut ctx,
395 );
396 }
397 Err(e) => {
398 tracing::error!("failed to initialize SurrealDB bundle: {}", e);
400 }
401 }
402 }
403 #[cfg(feature = "grpc")]
404 register_bundle!(ctx, camel_config, camel_component_grpc::GrpcBundle);
405
406 #[cfg(feature = "llm")]
407 register_bundle!(ctx, camel_config, camel_component_llm::LlmBundle);
408
409 #[cfg(feature = "wasm")]
410 {
411 let base_dir = std::path::Path::new(&config_path)
412 .parent()
413 .unwrap_or(std::path::Path::new("."))
414 .to_path_buf();
415 let wasm_bundle = camel_component_wasm::WasmBundle::new(ctx.registry_arc(), base_dir);
416 <camel_component_wasm::WasmBundle as camel_component_api::ComponentBundle>::register_all(
417 wasm_bundle,
418 &mut ctx,
419 );
420 }
421
422 match camel_dsl::discover_routes_with_threshold_and_security(
430 &patterns,
431 camel_config.stream_caching.threshold,
432 security_compile_context.clone(),
433 ) {
434 Ok(defs) => {
435 for def in defs {
436 let id = def.route_id().to_string();
437 if let Err(e) = ctx.add_route_definition(def).await {
438 tracing::error!("Failed to add route '{}': {}", id, e);
440 }
441 }
442 }
443 Err(e) => {
444 tracing::error!("Failed to discover routes: {}", e);
446 std::process::exit(1);
447 }
448 }
449
450 if let Err(e) = ctx.start().await {
452 tracing::error!("Failed to start CamelContext: {}", e);
454 std::process::exit(1);
455 }
456
457 tracing::info!("camel-cli: context started");
458
459 let watch_enabled = cli_watch.unwrap_or(camel_config.watch);
462
463 let watcher_shutdown = CancellationToken::new();
465 if watch_enabled {
466 let ctrl = ctx.runtime_execution_handle();
467 let watch_patterns = patterns.clone();
468 let watch_security_compile_context = security_compile_context.clone();
469 let drain_timeout = std::time::Duration::from_millis(camel_config.drain_timeout_ms);
470 let debounce = std::time::Duration::from_millis(camel_config.watch_debounce_ms);
471 let watcher_token = watcher_shutdown.clone();
472 tokio::spawn(async move {
473 let watch_dirs = camel_core::reload_watcher::resolve_watch_dirs(&watch_patterns);
474 let result = camel_core::reload_watcher::watch_and_reload(
475 watch_dirs,
476 ctrl,
477 move || {
478 camel_dsl::discover_routes_with_threshold_and_security(
479 &watch_patterns,
480 camel_config.stream_caching.threshold,
481 watch_security_compile_context.clone(),
482 )
483 .map_err(|e| camel_api::CamelError::RouteError(e.to_string()))
484 },
485 Some(watcher_token),
486 drain_timeout,
487 debounce,
488 )
489 .await;
490 if let Err(e) = result {
491 tracing::error!("File watcher failed: {}", e);
493 }
494 });
495 tracing::info!(
496 "camel-cli: hot-reload watching {:?}. Press Ctrl+C to stop.",
497 patterns
498 );
499 } else {
500 tracing::info!("camel-cli: running (hot-reload disabled). Press Ctrl+C to stop.");
501 }
502
503 tokio::select! {
504 _ = tokio::signal::ctrl_c() => tracing::info!("Received Ctrl+C"),
505 _ = async {
506 #[cfg(unix)]
507 {
508 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
509 .expect("Failed to install SIGTERM handler") .recv()
511 .await
512 }
513 #[cfg(not(unix))]
514 {
515 std::future::pending::<()>().await
516 }
517 } => tracing::info!("Received SIGTERM"),
518 }
519
520 let force_exit = tokio::spawn(async {
522 tokio::signal::ctrl_c().await.ok();
523 tracing::warn!("Second Ctrl+C — forcing exit");
524 std::process::exit(1);
525 });
526
527 tracing::info!("camel-cli: shutting down...");
528 watcher_shutdown.cancel();
529
530 jms_pool.begin_shutdown();
532 cxf_pool.begin_shutdown();
533
534 ctx.stop().await.unwrap_or_else(|e| {
536 tracing::error!("Error during shutdown: {}", e);
538 });
539
540 const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30);
542
543 match tokio::time::timeout(SHUTDOWN_TIMEOUT, jms_pool.shutdown()).await {
544 Ok(Ok(())) => {}
545 Ok(Err(e)) => {
546 tracing::error!("JMS pool shutdown failed: {}", e);
548 }
549 Err(_) => tracing::warn!("JMS pool shutdown timed out after 30s"),
550 }
551
552 match tokio::time::timeout(SHUTDOWN_TIMEOUT, cxf_pool.shutdown()).await {
553 Ok(Ok(())) => {}
554 Ok(Err(e)) => {
555 tracing::error!("CXF pool shutdown failed: {}", e);
557 }
558 Err(_) => tracing::warn!("CXF pool shutdown timed out after 30s"),
559 }
560
561 force_exit.abort();
562
563 tracing::info!("camel-cli: stopped");
564 Ok(())
565}