1pub mod cli;
14pub mod testing;
15pub mod config;
16pub mod console;
17pub mod features;
18pub mod integration;
19pub mod lambda;
20pub mod local;
21pub mod properties;
22#[cfg(feature = "validate")]
23pub mod schema;
24pub mod signal;
25pub mod tools;
26#[cfg(feature = "validate")]
27pub mod validate;
28pub mod yaml_rest;
29
30use std::collections::BTreeMap;
31use std::path::{Path, PathBuf};
32use std::sync::atomic::{AtomicU64, Ordering};
33use std::sync::Arc;
34use std::time::{Duration, Instant};
35
36use cameleer_core::context::CamelEvent;
37use cameleer_core::prelude::*;
38use cameleer_core::spi::EventNotifier;
39
40pub type RouteLoader = Arc<dyn Fn(&CamelContext, &str, &Path) -> Result<()> + Send + Sync>;
42
43#[derive(Clone, Debug, Default)]
45pub struct MainConfig {
46 pub name: Option<String>,
47 pub routes_include_pattern: Vec<String>,
49 pub duration_max_seconds: Option<u64>,
50 pub duration_max_messages: Option<u64>,
51 pub duration_max_idle_seconds: Option<u64>,
52 pub shutdown_timeout: Option<Duration>,
53 pub log_level: Option<String>,
54 pub dev_reload: bool,
56}
57
58pub struct Main {
60 ctx: CamelContext,
61 config: MainConfig,
62 properties: BTreeMap<String, String>,
63 loaders: BTreeMap<String, RouteLoader>,
64 route_files: Vec<PathBuf>,
65 configurers: Vec<Box<dyn FnOnce(&CamelContext) -> Result<()>>>,
66 console: Option<console::DevConsoleServer>,
67 mcp: std::sync::Mutex<Option<Box<dyn std::any::Any + Send>>>,
69 explicit: BTreeMap<String, String>,
71 defaults: BTreeMap<String, String>,
73 profile_docs: Vec<(String, BTreeMap<String, String>)>,
75 config_dirs: Vec<PathBuf>,
77 profile: Option<String>,
78 env: Option<BTreeMap<String, String>>,
79 loaded_files: Vec<(PathBuf, Option<std::time::SystemTime>)>,
81 local: Option<local::LocalStatus>,
82 local_dir: Option<PathBuf>,
83 route_sources: Vec<(String, String)>,
85 file_route_ids: Vec<String>,
87 lambda_api: Option<String>,
89}
90
91struct CompletedCounter {
92 completed: Arc<AtomicU64>,
93 last_activity: Arc<std::sync::Mutex<Instant>>,
94 max_messages: Arc<std::sync::OnceLock<Option<u64>>>,
97 wake: Arc<Wake>,
98}
99
100#[derive(Default)]
102struct Wake {
103 flag: std::sync::Mutex<bool>,
104 cv: std::sync::Condvar,
105}
106
107impl Wake {
108 fn notify(&self) {
109 *self.flag.lock().unwrap() = true;
110 self.cv.notify_all();
111 }
112
113 fn wait(&self, timeout: Duration) {
115 let guard = self.flag.lock().unwrap();
116 let (mut guard, _) = self.cv.wait_timeout_while(guard, timeout, |woken| !*woken).unwrap();
117 *guard = false;
118 }
119}
120
121impl EventNotifier for CompletedCounter {
122 fn notify(&self, event: &CamelEvent) {
123 match event {
124 CamelEvent::ExchangeCompleted { .. } | CamelEvent::ExchangeFailed { .. } => {
125 let n = self.completed.fetch_add(1, Ordering::SeqCst) + 1;
126 *self.last_activity.lock().unwrap() = Instant::now();
127 if self.max_messages.get().copied().flatten() == Some(n) {
128 self.wake.notify();
129 }
130 }
131 CamelEvent::ExchangeCreated { .. } => {
132 *self.last_activity.lock().unwrap() = Instant::now();
133 }
134 _ => {}
135 }
136 }
137}
138
139impl Default for Main {
140 fn default() -> Self {
141 Self::new()
142 }
143}
144
145impl Main {
146 pub fn new() -> Self {
147 Main {
148 ctx: CamelContext::new(),
149 config: MainConfig::default(),
150 properties: BTreeMap::new(),
151 loaders: BTreeMap::new(),
152 route_files: Vec::new(),
153 configurers: Vec::new(),
154 console: None,
155 mcp: std::sync::Mutex::new(None),
156 defaults: BTreeMap::new(),
157 explicit: BTreeMap::new(),
158 profile_docs: Vec::new(),
159 config_dirs: Vec::new(),
160 profile: None,
161 env: None,
162 loaded_files: Vec::new(),
163 local: None,
164 local_dir: None,
165 route_sources: Vec::new(),
166 file_route_ids: Vec::new(),
167 lambda_api: lambda::runtime_api(),
168 }
169 }
170
171 pub fn set_lambda_runtime_api(&mut self, api: Option<String>) {
174 self.lambda_api = api;
175 }
176
177 pub fn is_lambda(&self) -> bool {
179 self.lambda_api.is_some()
180 }
181
182 pub fn enable_local_status(&mut self, dir: impl Into<PathBuf>) {
185 self.local_dir = Some(dir.into());
186 }
187
188 pub fn local_status(&self) -> Option<&local::LocalStatus> {
190 self.local.as_ref()
191 }
192
193 pub fn reload_route_files(&mut self) -> Result<()> {
196 for id in std::mem::take(&mut self.file_route_ids) {
197 let _ = self.ctx.stop_route(&id);
198 let _ = self.ctx.remove_route(&id);
199 }
200 let before: Vec<String> = self.ctx.route_ids();
201 let files: Vec<PathBuf> = self.loaded_files.iter().map(|(f, _)| f.clone()).collect();
202 let mut result = Ok(());
203 self.loaded_files.clear();
204 for f in files {
205 let mtime = std::fs::metadata(&f).and_then(|m| m.modified()).ok();
206 if let Err(e) = self.load_route_file(&f) {
207 cameleer_core::camel_error!("Main", "Failed to reload {}: {}", f.display(), e);
208 result = Err(e);
209 } else {
210 cameleer_core::camel_info!("Main", "Reloaded routes from: {}", f.display());
211 }
212 self.loaded_files.push((f, mtime));
213 }
214 self.file_route_ids = self.ctx.route_ids().into_iter().filter(|id| !before.contains(id)).collect();
215 result
216 }
217
218 fn route_files_changed(&self) -> bool {
220 self.loaded_files.iter().any(|(f, t)| std::fs::metadata(f).and_then(|m| m.modified()).ok() != *t)
221 }
222
223 pub fn with_context(ctx: CamelContext) -> Self {
225 Main { ctx, ..Self::new() }
226 }
227
228 pub fn context(&self) -> &CamelContext {
229 &self.ctx
230 }
231
232 pub fn config_mut(&mut self) -> &mut MainConfig {
233 &mut self.config
234 }
235
236 pub fn add_route_loader(&mut self, extension: &str, loader: impl Fn(&CamelContext, &str, &Path) -> Result<()> + Send + Sync + 'static) {
238 self.loaders.insert(extension.to_ascii_lowercase(), Arc::new(loader));
239 }
240
241 pub fn supported_extensions(&self) -> Vec<String> {
242 self.loaders.keys().cloned().collect()
243 }
244
245 pub fn add_route_file(&mut self, path: impl Into<PathBuf>) {
247 self.route_files.push(path.into());
248 }
249
250 pub fn add_route_source(&mut self, name: impl Into<String>, content: impl Into<String>) {
253 self.route_sources.push((name.into(), content.into()));
254 }
255
256 pub fn load_properties_text(&mut self, text: &str, yaml: bool) -> Result<()> {
258 if yaml {
259 let (base, docs) = config::parse_yaml_config(text)?;
260 self.properties.extend(base);
261 self.profile_docs.extend(docs);
262 } else {
263 self.properties.extend(properties::parse_properties(text));
264 }
265 Ok(())
266 }
267
268 pub fn configure(&mut self, f: impl FnOnce(&CamelContext) -> Result<()> + 'static) {
270 self.configurers.push(Box::new(f));
271 }
272
273 pub fn set_property(&mut self, key: impl Into<String>, value: impl Into<String>) {
276 self.explicit.insert(key.into(), value.into());
277 }
278
279 pub fn set_default_property(&mut self, key: impl Into<String>, value: impl Into<String>) {
282 self.defaults.insert(key.into(), value.into());
283 }
284
285 pub fn properties(&self) -> &BTreeMap<String, String> {
288 &self.properties
289 }
290
291 pub fn set_profile(&mut self, profile: impl Into<String>) {
293 self.profile = Some(profile.into());
294 }
295
296 pub fn set_environment(&mut self, env: BTreeMap<String, String>) {
298 self.env = Some(env);
299 }
300
301 pub fn load_properties_file(&mut self, path: impl AsRef<Path>) -> Result<()> {
304 let path = path.as_ref();
305 let (base, docs) = config::load_file(path)?;
306 self.properties.extend(base);
307 self.profile_docs.extend(docs);
308 let dir = path.parent().map(Path::to_path_buf).unwrap_or_default();
309 if !self.config_dirs.contains(&dir) {
310 self.config_dirs.push(dir);
311 }
312 Ok(())
313 }
314
315 pub fn load_application_config(&mut self, dir: impl AsRef<Path>) -> Result<usize> {
318 let files = config::application_files(dir.as_ref());
319 for f in &files {
320 self.load_properties_file(f)?;
321 }
322 if !self.config_dirs.contains(&dir.as_ref().to_path_buf()) {
323 self.config_dirs.push(dir.as_ref().to_path_buf());
324 }
325 Ok(files.len())
326 }
327
328 fn merge_configuration(&mut self) -> Result<()> {
330 let env: BTreeMap<String, String> = self.env.clone().unwrap_or_else(|| std::env::vars().collect());
331 let mut props = self.properties.clone();
332 let mut probe = props.clone();
333 probe.extend(self.explicit.clone());
334 let profile = self.profile.clone().or_else(|| config::active_profile(&probe, &env));
335 if let Some(p) = &profile {
336 for name in p.split(',').map(str::trim) {
337 for dir in self.config_dirs.clone() {
338 for ext in ["properties", "yaml", "yml"] {
339 let f = dir.join(format!("application-{}.{}", name, ext));
340 if f.is_file() {
341 let (base, docs) = config::load_file(&f)?;
342 props.extend(base);
343 self.profile_docs.extend(docs);
344 }
345 }
346 }
347 }
348 cameleer_core::camel_info!("Main", "The following profiles are active: {}", p);
349 }
350 config::apply_profile(&mut props, &self.profile_docs, profile.as_deref());
351 let schemes = self.ctx.component_names();
352 for (k, v) in &env {
353 if let Some(key) = config::env_to_property(k, &schemes) {
354 props.insert(key, v.clone());
355 }
356 }
357 props.extend(self.explicit.clone());
358 for (k, v) in &self.defaults {
359 if config::relaxed_get(&props, k).is_none() {
360 props.insert(k.clone(), v.clone());
361 }
362 }
363 config::apply_aliases(&mut props);
364 if let Some(p) = profile {
365 props.entry("camel.main.profile".into()).or_insert(p);
366 }
367 resolve_runtime_placeholders(&mut props);
368 self.properties = props;
369 Ok(())
370 }
371
372 fn apply_properties(&mut self) -> Result<()> {
374 self.merge_configuration()?;
375 let get = |k: &str| -> Option<String> { config::relaxed_get(&self.properties, k).cloned() };
376 if let Some(v) = get("camel.main.name") {
377 self.config.name = Some(v);
378 }
379 if let Some(v) = get("camel.main.routes-include-pattern") {
380 self.config.routes_include_pattern.extend(v.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()));
381 }
382 let num = |v: Option<String>, k: &str| -> Result<Option<u64>> {
383 match v {
384 None => Ok(None),
385 Some(s) => s.trim().parse::<i64>().map(|n| if n > 0 { Some(n as u64) } else { None }).map_err(|_| CamelError::illegal_argument(format!("Invalid number for {}: {}", k, s))),
386 }
387 };
388 if let Some(v) = num(get("camel.main.duration-max-seconds"), "duration-max-seconds")? {
389 self.config.duration_max_seconds = Some(v);
390 }
391 if let Some(v) = num(get("camel.main.duration-max-messages"), "duration-max-messages")? {
392 self.config.duration_max_messages = Some(v);
393 }
394 if let Some(v) = num(get("camel.main.duration-max-idle-seconds"), "duration-max-idle-seconds")? {
395 self.config.duration_max_idle_seconds = Some(v);
396 }
397 if let Some(v) = get("camel.main.shutdown-timeout") {
398 let secs: u64 = v.trim().parse().map_err(|_| CamelError::illegal_argument(format!("Invalid shutdown-timeout: {}", v)))?;
399 self.config.shutdown_timeout = Some(Duration::from_secs(secs));
400 }
401 if let Some(v) = get("camel.main.log-level").or_else(|| get("logging.level.root")) {
402 self.config.log_level = Some(v);
403 }
404 if let Some(v) = get("camel.main.dev-reload") {
405 self.config.dev_reload = v.trim().eq_ignore_ascii_case("true");
406 }
407 self.ctx.properties().set_all(self.properties.clone());
409 config::create_beans(&self.ctx, &self.properties)?;
410 Ok(())
411 }
412
413 fn load_kamelet_locations(&self) -> Result<()> {
418 let explicit = config::relaxed_get(&self.properties, "camel.main.kamelet-locations")
419 .or_else(|| config::relaxed_get(&self.properties, "camel.component.kamelet.location"))
420 .cloned();
421 let locations = explicit.clone().unwrap_or_else(|| "classpath:kamelets".into());
422 for loc in locations.split(',').map(str::trim).filter(|l| !l.is_empty()) {
423 if loc.starts_with("github:") || loc.starts_with("http:") || loc.starts_with("https:") {
424 cameleer_core::camel_warn!("Main", "Remote Kamelet location {} is not supported (no network access); skipped", loc);
425 continue;
426 }
427 let dir = loc.strip_prefix("file:").or_else(|| loc.strip_prefix("classpath:")).unwrap_or(loc);
428 let dir = Path::new(dir);
429 if dir.is_dir() {
430 let names = cameleer_yaml::load_kamelets_from_dir(&self.ctx, dir)?;
431 cameleer_core::camel_info!("Main", "Loaded {} Kamelets from {}: {}", names.len(), dir.display(), names.join(", "));
432 } else if explicit.is_some() {
433 cameleer_core::camel_warn!("Main", "Kamelet location {} does not exist", dir.display());
434 }
435 }
436 Ok(())
437 }
438
439 fn prescan_integrations(&mut self) -> Result<()> {
442 for f in self.route_files.clone() {
443 let is_yaml = matches!(f.extension().and_then(|e| e.to_str()), Some("yaml" | "yml"));
444 if !is_yaml {
445 continue;
446 }
447 if let Ok(text) = std::fs::read_to_string(&f) {
448 if let Some(it) = integration::parse_integration(&text)? {
449 for (k, v) in it.properties {
450 self.properties.entry(k).or_insert(v);
451 }
452 }
453 }
454 }
455 Ok(())
456 }
457
458 fn discover_route_files(&self) -> Result<Vec<PathBuf>> {
460 let mut files = self.route_files.clone();
461 for pattern in &self.config.routes_include_pattern {
462 let pattern = pattern.strip_prefix("file:").unwrap_or(pattern);
463 let pattern = pattern.strip_prefix("classpath:").unwrap_or(pattern);
464 for f in glob_files(pattern)? {
465 if !files.contains(&f) {
466 files.push(f);
467 }
468 }
469 }
470 Ok(files)
471 }
472
473 pub fn load_route_file(&self, path: &Path) -> Result<()> {
475 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase();
476 let loader = self
478 .loaders
479 .get(&ext)
480 .ok_or_else(|| CamelError::illegal_argument(format!("No route loader for file {} (supported: {:?})", path.display(), self.supported_extensions())))?;
481 let text = std::fs::read_to_string(path).map_err(|e| CamelError::io(format!("Cannot read route file {}: {}", path.display(), e)))?;
482 loader(&self.ctx, &text, path).map_err(|e| CamelError::new(e.kind.clone(), format!("Error loading routes from {}: {}", path.display(), e.message)).with_cause(e))
483 }
484
485 pub fn start(&mut self) -> Result<()> {
487 cameleer_core::startup::step("main", "configure", || -> Result<()> {
488 self.prescan_integrations()?;
489 self.apply_properties()
490 })?;
491 if let Some(l) = &self.config.log_level {
492 if let Some(level) = cameleer_core::log::Level::parse(l) {
493 cameleer_core::log::set_level(level);
494 }
495 }
496 if let Some(t) = self.config.shutdown_timeout {
497 self.ctx.set_shutdown_timeout(t);
498 }
499 self.apply_management_properties()?;
500 for c in std::mem::take(&mut self.configurers) {
501 c(&self.ctx)?;
502 }
503 self.load_kamelet_locations()?;
504 self.loaded_files.clear();
505 let before: Vec<String> = self.ctx.assign_pending_route_ids();
506 for f in self.discover_route_files()? {
507 cameleer_core::camel_info!("Main", "Loading routes from: {}", f.display());
508 cameleer_core::startup::step("routes-loader", f.display().to_string(), || self.load_route_file(&f))?;
509 let mtime = std::fs::metadata(&f).and_then(|m| m.modified()).ok();
510 self.loaded_files.push((f, mtime));
511 }
512 self.file_route_ids = self.ctx.assign_pending_route_ids().into_iter().filter(|id| !before.contains(id)).collect();
513 for (name, content) in self.route_sources.clone() {
514 let ext = Path::new(&name).extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase();
515 let loader = self.loaders.get(&ext).cloned().ok_or_else(|| CamelError::illegal_argument(format!("No route loader for {}", name)))?;
516 cameleer_core::camel_info!("Main", "Loading routes from: {} (embedded)", name);
517 loader(&self.ctx, &content, Path::new(&name))?;
518 }
519 cameleer_core::startup::step("context", "start", || self.ctx.start())?;
520 if let Some(dir) = &self.local_dir {
521 let name = self.config.name.clone().unwrap_or_else(|| self.ctx.name().to_string());
522 match local::LocalStatus::start(&self.ctx, &name, dir) {
525 Ok(l) => self.local = Some(l),
526 Err(e) => cameleer_core::camel_warn!("Main", "Local status files disabled: {}", e),
527 }
528 }
529 for line in console::startup_summary(&self.ctx) {
530 cameleer_core::camel_info!("Main", "{}", line);
531 }
532 if self.startup_recorder_logging() {
533 cameleer_core::camel_info!("Main", "Startup steps (offset from process start, duration):");
534 for line in cameleer_core::startup::report_lines() {
535 cameleer_core::camel_info!("Main", "{}", line);
536 }
537 }
538 let cfg = console::ConsoleConfig::from_properties(&self.properties)?;
539 if cfg.is_active() && self.console.is_none() {
540 match console::DevConsoleServer::start(&self.ctx, &cfg) {
541 Ok(c) => self.console = Some(c),
542 Err(e) => {
543 let _ = self.ctx.stop();
544 return Err(e);
545 }
546 }
547 }
548 if let Err(e) = self.start_mcp() {
549 let _ = self.ctx.stop();
550 return Err(e);
551 }
552 Ok(())
553 }
554
555 #[cfg(feature = "ai")]
559 fn start_mcp(&self) -> Result<()> {
560 use cameleer_ai::mcp::{http::McpHttpServer, McpConfig, McpServer};
561 if self.mcp.lock().unwrap().is_some() {
562 return Ok(());
563 }
564 for client in cameleer_ai::mcp::client::McpClientConfig::from_properties(&self.properties)? {
567 let clients = cameleer_ai::mcp::client::McpClients::of(&self.ctx);
568 let name = client.name.clone();
569 match clients.add(&self.ctx, client.clone()) {
570 Ok(n) => cameleer_core::camel_info!("mcp-client", "Imported {} tools from MCP server {}", n, name),
571 Err(e) => {
572 cameleer_core::camel_warn!("mcp-client", "MCP server {} is not reachable ({}); retrying every 30s", name, e.message);
573 let weak = self.ctx.downgrade();
574 let _ = std::thread::Builder::new().name(format!("mcp-connect-{}", name)).spawn(move || loop {
575 std::thread::sleep(Duration::from_secs(30));
576 let Some(ctx) = weak.upgrade() else { return };
577 if let Ok(n) = cameleer_ai::mcp::client::McpClients::of(&ctx).add(&ctx, client.clone()) {
578 cameleer_core::camel_info!("mcp-client", "Imported {} tools from MCP server {}", n, name);
579 return;
580 }
581 });
582 }
583 }
584 }
585 let cfg = McpConfig::from_properties(&self.properties)?;
586 if !cfg.enabled {
587 return Ok(());
588 }
589 let server = McpServer::new(&self.ctx, cfg.clone())?;
590 if cfg.transport == "stdio" {
591 cameleer_core::camel_info!("mcp", "MCP server on stdio (protocol {} and {})", cameleer_ai::mcp::MODERN_VERSION, cameleer_ai::mcp::LEGACY_VERSIONS.join(", "));
592 std::thread::Builder::new()
593 .name("mcp-stdio".into())
594 .spawn(move || {
595 let stdin = std::io::stdin();
596 cameleer_ai::mcp::stdio::serve(server, stdin.lock(), std::io::stdout());
597 cameleer_core::camel_info!("mcp", "stdin closed");
598 signal::trigger();
599 })
600 .map_err(|e| CamelError::io(format!("Cannot start the MCP stdio server: {}", e)))?;
601 *self.mcp.lock().unwrap() = Some(Box::new(()));
602 } else {
603 let http = McpHttpServer::start(server)?;
604 *self.mcp.lock().unwrap() = Some(Box::new(http));
605 }
606 Ok(())
607 }
608
609 #[cfg(not(feature = "ai"))]
610 fn start_mcp(&self) -> Result<()> {
611 let on = console::prop(&self.properties, "camel.server.mcp-enabled").map(|v| v.trim().eq_ignore_ascii_case("true")).unwrap_or(false);
612 if on {
613 return Err(CamelError::illegal_argument("camel.server.mcp-enabled=true needs the ai feature: rebuild with --features ai"));
614 }
615 Ok(())
616 }
617
618 fn startup_recorder_logging(&self) -> bool {
620 config::relaxed_get(&self.properties, "camel.main.startup-recorder").map(|v| v.trim().eq_ignore_ascii_case("logging")).unwrap_or(false)
621 }
622
623 fn apply_management_properties(&mut self) -> Result<()> {
629 let flag = |k: &str| console::prop(&self.properties, k).map(|v| v.trim().eq_ignore_ascii_case("true"));
630 if let Some(t) = flag("camel.main.tracing") {
631 self.ctx.set_tracing(t);
632 }
633 if let Some(t) = flag("camel.main.backlog-tracing") {
634 self.ctx.set_backlog_tracing(t);
635 }
636 if let Some(t) = flag("camel.main.message-history") {
637 self.ctx.set_message_history(t);
638 }
639 let level = console::prop(&self.properties, "camel.main.jmxManagementStatisticsLevel")
641 .or_else(|| console::prop(&self.properties, "camel.main.jmx-management-statistics-level"));
642 if let Some(l) = level {
643 self.ctx.set_statistics_level(cameleer_core::management::ManagementStatisticsLevel::parse(&l)?);
644 }
645 if flag("camel.main.route-controller-supervise-enabled").or_else(|| flag("camel.routecontroller.enabled")).unwrap_or(false) {
646 let dur = |k: &str| -> Result<Option<Duration>> {
647 let v = console::prop(&self.properties, &format!("camel.main.route-controller-{}", k))
648 .or_else(|| console::prop(&self.properties, &format!("camel.routecontroller.{}", k)));
649 v.map(|v| cameleer_core::util::parse_duration(v.trim())).transpose()
650 };
651 let mut c = cameleer_core::supervising::SupervisingRouteControllerConfig::default();
652 if let Some(d) = dur("initial-delay")? {
653 c.initial_delay = d;
654 }
655 if let Some(d) = dur("back-off-delay")? {
656 c.back_off_delay = d;
657 }
658 let attempts = console::prop(&self.properties, "camel.main.route-controller-back-off-max-attempts")
659 .or_else(|| console::prop(&self.properties, "camel.routecontroller.back-off-max-attempts"));
660 if let Some(a) = attempts {
661 c.back_off_max_attempts = a.trim().parse().map_err(|_| CamelError::illegal_argument(format!("Invalid back-off-max-attempts: {}", a)))?;
662 }
663 self.ctx.enable_supervising_route_controller(c);
664 }
665 Ok(())
666 }
667
668 pub fn console_port(&self) -> Option<u16> {
670 self.console.as_ref().map(|c| c.port())
671 }
672
673 fn stop_console(&self) {
674 if let Some(c) = &self.console {
675 c.stop();
676 }
677 self.mcp.lock().unwrap().take();
678 #[cfg(feature = "ai")]
679 cameleer_ai::mcp::client::McpClients::of(&self.ctx).close_all(&self.ctx);
680 }
681
682 pub fn run(&mut self) -> Result<()> {
684 let completed = Arc::new(AtomicU64::new(0));
685 let last_activity = Arc::new(std::sync::Mutex::new(Instant::now()));
686 let wake = Arc::new(Wake::default());
687 let max_messages = Arc::new(std::sync::OnceLock::new());
689 self.ctx.add_event_notifier(CompletedCounter {
690 completed: completed.clone(),
691 last_activity: last_activity.clone(),
692 max_messages: max_messages.clone(),
693 wake: wake.clone(),
694 });
695 signal::install();
696 if let Some(api) = self.lambda_api.clone() {
697 return self.run_lambda(&api);
698 }
699 self.start()?;
700 let _ = max_messages.set(self.config.duration_max_messages);
701 let started = Instant::now();
702 let reason;
703 let mut last_check = Instant::now();
704 loop {
705 if signal::received() {
706 reason = "signal received".to_string();
707 break;
708 }
709 if self.local.as_ref().map(|l| l.stop_requested()).unwrap_or(false) {
710 reason = "stop requested".to_string();
711 break;
712 }
713 let reload_requested = self.local.as_ref().map(|l| l.take_reload_request()).unwrap_or(false);
714 if reload_requested || (self.config.dev_reload && last_check.elapsed() >= Duration::from_millis(300)) {
715 last_check = Instant::now();
716 if reload_requested || self.route_files_changed() {
717 let _ = self.reload_route_files();
718 }
719 }
720 if let Some(s) = self.config.duration_max_seconds {
721 if started.elapsed() >= Duration::from_secs(s) {
722 reason = format!("duration max seconds ({}) reached", s);
723 break;
724 }
725 }
726 if let Some(m) = self.config.duration_max_messages {
727 if completed.load(Ordering::SeqCst) >= m {
728 reason = format!("duration max messages ({}) reached", m);
729 break;
730 }
731 }
732 if let Some(s) = self.config.duration_max_idle_seconds {
733 if last_activity.lock().unwrap().elapsed() >= Duration::from_secs(s) {
734 reason = format!("duration max idle seconds ({}) reached", s);
735 break;
736 }
737 }
738 wake.wait(Duration::from_millis(20));
740 }
741 cameleer_core::camel_info!("Main", "Stopping CamelContext: {}", reason);
742 self.stop_console();
743 if let Some(l) = self.local.take() {
744 l.stop();
745 }
746 self.ctx.stop()
747 }
748
749 fn run_lambda(&mut self, api: &str) -> Result<()> {
753 let started = Instant::now();
754 cameleer_http::registry::registry().set_in_process_only(true);
756 if let Err(e) = self.start() {
757 let runtime = lambda::LambdaRuntime::new(api, lambda::DEFAULT_HANDLER);
758 if let Err(re) = runtime.init_error(&e) {
759 cameleer_core::camel_warn!("Lambda", "Cannot report the init error: {}", re);
760 }
761 return Err(e);
762 }
763 let property = config::relaxed_get(&self.properties, "camel.main.lambda-handler").cloned();
764 let handler = lambda::handler_endpoint(property.as_deref(), std::env::var("_HANDLER").ok().as_deref());
765 let runtime = lambda::LambdaRuntime::new(api, handler);
766 cameleer_core::camel_info!(
767 "Lambda",
768 "Serving AWS Lambda invocations from {} (init {}; HTTP events to platform-http routes, others to {})",
769 api,
770 cameleer_core::startup::format_duration(started.elapsed()),
771 runtime.handler()
772 );
773 let result = runtime.run(&self.ctx, self.config.duration_max_messages, &signal::received);
774 let reason = if signal::received() { "signal received" } else { "duration max messages reached" };
775 cameleer_core::camel_info!("Main", "Stopping CamelContext: {}", reason);
776 self.stop_console();
777 if let Some(l) = self.local.take() {
778 l.stop();
779 }
780 let stopped = self.ctx.stop();
781 result?;
782 stopped
783 }
784
785 pub fn stop(&self) -> Result<()> {
786 self.stop_console();
787 if let Some(l) = &self.local {
788 l.stop();
789 }
790 self.ctx.stop()
791 }
792}
793
794fn resolve_runtime_placeholders(props: &mut BTreeMap<String, String>) {
798 let resolver = cameleer_core::context::PropertiesComponent::default();
799 resolver.set_all(props.clone());
800 for (k, v) in props.iter_mut() {
801 if v.contains("{{") && (k.starts_with("camel.main.") || k.starts_with("camel.server.")) {
802 if let Ok(r) = resolver.resolve(v) {
803 *v = r;
804 }
805 }
806 }
807}
808
809pub fn glob_files(pattern: &str) -> Result<Vec<PathBuf>> {
811 if !pattern.contains('*') && !pattern.contains('?') {
812 let p = PathBuf::from(pattern);
813 return Ok(if p.is_file() { vec![p] } else if p.is_dir() { list_dir(&p, false)? } else { vec![] });
814 }
815 let normalized = pattern.replace('\\', "/");
817 let parts: Vec<&str> = normalized.split('/').collect();
818 let idx = parts.iter().position(|p| p.contains('*') || p.contains('?')).unwrap();
819 let base = if idx == 0 { ".".to_string() } else { parts[..idx].join("/") };
820 let base = if base.is_empty() { "/".to_string() } else { base };
821 let rest = parts[idx..].join("/");
822 let recursive = rest.contains("**");
823 let mut out = Vec::new();
824 let base_path = PathBuf::from(&base);
825 if !base_path.is_dir() {
826 return Ok(out);
827 }
828 for f in list_dir(&base_path, recursive)? {
829 let rel = f.strip_prefix(&base_path).unwrap_or(&f).to_string_lossy().replace('\\', "/");
830 if ant_match(&rest, &rel) {
831 out.push(f);
832 }
833 }
834 out.sort();
835 Ok(out)
836}
837
838fn list_dir(dir: &Path, recursive: bool) -> Result<Vec<PathBuf>> {
839 let mut out = Vec::new();
840 for entry in std::fs::read_dir(dir)? {
841 let p = entry?.path();
842 if p.is_dir() {
843 if recursive {
844 out.extend(list_dir(&p, true)?);
845 }
846 } else {
847 out.push(p);
848 }
849 }
850 out.sort();
851 Ok(out)
852}
853
854pub fn ant_match(pattern: &str, path: &str) -> bool {
856 let p: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
857 let s: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
858 fn rec(p: &[&str], s: &[&str]) -> bool {
859 match p.first() {
860 None => s.is_empty(),
861 Some(&"**") => (0..=s.len()).any(|i| rec(&p[1..], &s[i..])),
862 Some(seg) => !s.is_empty() && cameleer_core::util::wildcard_match(s[0], seg) && rec(&p[1..], &s[1..]),
863 }
864 }
865 rec(&p, &s)
866}
867
868pub fn new_main() -> Main {
870 cameleer_core::startup::step("main", "register components", new_main_inner)
871}
872
873fn new_main_inner() -> Main {
874 let mut main = Main::new();
875 cameleer_core::component::set_unavailable_hint(features::unavailable_hint);
877 #[cfg(feature = "xml")]
878 cameleer_xml::register(main.context()); cameleer_http::register(main.context());
880 cameleer_yaml::register_kamelet_component(main.context());
881 #[cfg(feature = "ssh")]
882 cameleer_ssh::register(main.context()); #[cfg(feature = "protocols")]
885 cameleer_protocols::register(main.context());
886 #[cfg(feature = "kafka-mqtt")]
887 cameleer_kafka_mqtt::register(main.context()); #[cfg(feature = "sql")]
890 {
891 cameleer_sql::register(main.context());
892 main.configure(cameleer_sql::configure_datasources_from_properties);
893 }
894 #[cfg(feature = "ai")]
895 cameleer_ai::register(main.context()); #[cfg(feature = "amqp")]
897 cameleer_amqp::register(main.context());
898 #[cfg(feature = "amqp10")]
899 cameleer_amqp10::register(main.context()); #[cfg(feature = "grpc")]
901 cameleer_grpc::register(main.context()); #[cfg(feature = "cloud")]
903 cameleer_cloud::register(main.context()); #[cfg(feature = "datastores")]
905 cameleer_datastores::register(main.context()); #[cfg(feature = "script")]
907 cameleer_script::register(main.context()); #[cfg(feature = "saas")]
909 cameleer_saas::register(main.context()); #[cfg(feature = "soap")]
911 cameleer_soap::register(main.context()); #[cfg(feature = "dataformats")]
913 cameleer_dataformats::register(main.context()); yaml_rest::register();
916 for ext in ["yaml", "yml"] {
917 main.add_route_loader(ext, |ctx, text, _path| match integration::parse_integration(text)? {
918 Some(it) => {
921 for (k, v) in &it.properties {
922 if ctx.properties().get(k).is_none() {
923 ctx.properties().set(k.clone(), v.clone());
924 }
925 }
926 integration::load_integration(ctx, &it)
927 }
928 None => cameleer_yaml::add_routes_from_yaml(ctx, text),
929 });
930 }
931 main.add_route_loader("xml", |ctx, text, _path| cameleer_yaml::add_routes_from_xml(ctx, text));
932 main
933}
934
935#[cfg(test)]
936mod tests {
937 use super::*;
938
939 #[test]
940 fn ant() {
941 assert!(ant_match("*.yaml", "a.yaml"));
942 assert!(!ant_match("*.yaml", "x/a.yaml"));
943 assert!(ant_match("**/*.yaml", "x/y/a.yaml"));
944 assert!(ant_match("**/*.yaml", "a.yaml"));
945 }
946
947 #[test]
948 #[cfg(feature = "full")]
949 fn new_main_binds_datasources_from_properties() {
950 let mut main = new_main();
951 main.set_property("camel.datasource.url", "jdbc:rustsql:mem:camelMainDsTest");
952 main.configure(|ctx| {
953 ctx.add_routes(|b| {
954 b.from("direct:q").to("sql:select 6 * 7 as answer?dataSource=#dataSource&outputType=SelectOne");
955 })
956 });
957 main.start().unwrap();
958 let v = main.context().create_producer_template().request_body("direct:q", 0).unwrap();
959 assert_eq!(v, cameleer_core::value::Value::Int(42));
960 main.stop().unwrap();
961 }
962
963 #[test]
964 #[cfg(feature = "full")]
965 fn new_main_registers_xml_and_saas() {
966 let main = new_main();
967 let ctx = main.context();
968 for scheme in ["xslt", "xslt-saxon", "validator", "xj", "slack", "telegram", "github", "twilio", "jira", "openai", "webhook", "graphql", "rss", "atom"] {
969 assert!(ctx.has_component(scheme), "{}", scheme);
970 }
971 let e = ctx.resolve_language("xpath").unwrap().create_expression("count(/a/b[1]/following::*)", &Default::default()).unwrap();
973 let ex = cameleer_core::message::Exchange::with_body(Some(ctx.clone()), "<a><b/><b/><c/></a>");
974 assert_eq!(e.evaluate(&ex).unwrap(), cameleer_core::value::Value::Float(2.0));
975 }
976
977 #[test]
978 #[cfg(feature = "full")]
979 fn new_main_registers_dataformats() {
980 let main = new_main();
981 for name in ["avro", "bindyCsv", "yaml", "tarFile", "pgp", "crypto", "cbor", "iCal", "mimeMultipart"] {
982 assert!(cameleer_core::dataformat::lookup_data_format_factory(main.context().registry(), name).is_some(), "{}", name);
983 }
984 let df = cameleer_core::dataformat::DataFormatDefinition::new("cbor").resolve(main.context()).unwrap();
985 let mut ex = cameleer_core::message::Exchange::with_body(Some(main.context().clone()), cameleer_core::value::Value::Int(500));
986 df.marshal(&mut ex).unwrap();
987 assert_eq!(ex.message.body, cameleer_core::value::Value::Bytes(vec![0x19, 0x01, 0xf4]));
988 }
989
990 #[test]
991 fn reaching_max_messages_stops_without_waiting_for_the_next_poll() {
992 let mut main = Main::new();
994 main.set_property("camel.main.duration-max-messages", "1");
995 main.configure(|ctx| {
996 ctx.add_routes(|b| {
997 b.from("timer:once?repeatCount=1&delay=0").set_body(constant("x")).to("mock:out");
998 })
999 });
1000 let t0 = Instant::now();
1001 main.run().unwrap();
1002 let wake = Wake::default();
1003 wake.notify();
1005 let w0 = Instant::now();
1006 wake.wait(Duration::from_secs(5));
1007 assert!(w0.elapsed() < Duration::from_secs(1));
1008 assert!(t0.elapsed() < Duration::from_secs(5), "{:?}", t0.elapsed());
1009 assert_eq!(main.context().status(), ServiceStatus::Stopped);
1010 }
1011
1012 #[test]
1013 fn main_runs_with_max_messages() {
1014 let mut main = Main::new();
1015 main.set_property("camel.main.duration-max-messages", "2");
1016 main.set_property("greeting", "hi");
1017 main.configure(|ctx| {
1018 ctx.add_routes(|b| {
1019 b.from("direct:in").set_body(constant("x")).to("mock:out");
1020 })
1021 });
1022 let ctx = main.context().clone();
1023 let h = std::thread::spawn(move || {
1024 std::thread::sleep(Duration::from_millis(200));
1025 let t = ctx.create_producer_template();
1026 t.send_body("direct:in", 1).unwrap();
1027 t.send_body("direct:in", 2).unwrap();
1028 });
1029 main.run().unwrap();
1030 h.join().unwrap();
1031 assert_eq!(main.context().resolve_placeholders("{{greeting}}").unwrap(), "hi");
1032 assert_eq!(main.context().status(), ServiceStatus::Stopped);
1033 }
1034}