1/*
2 * SPDX-FileCopyrightText: 2025 Copyright (c) Contributors to the Eclipse Foundation
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 */
13
14// The `run_with_ext` async state machine grows with every enabled transport;
15// with `--all-features` its layout computation overflows rustc's default
16// query depth (128) on stable 1.97. Default limit otherwise.
17#![recursion_limit = "256"]
18
19use std::{
20 path::{Path, PathBuf},
21 sync::Arc,
22 time::Duration,
23};
24
25use cda_comm_can::{CanDiagGateway, config::CanConfig};
26use cda_comm_doip::{DoipDiagGateway, config::DoipConfig};
27use cda_comm_uds::{UdsManager, state_coordinator::EcuStateCoordinator};
28use cda_core::EcuManager;
29use cda_database::FileManager;
30use cda_interfaces::{
31 EcuConnectivityHandler, EcuRuntimeState, FunctionalDescriptionConfig, HashMap,
32 HashMapExtensions, TransportType, VariantDetectionReceiver, VariantDetectionSender,
33 communication_control::CommunicationAccess, component_slot::ComponentSlot,
34 config::ConfigSanity, datatypes::FaultConfig, dlt_ctx, health::HealthProvider,
35};
36use cda_plugin_communication_management::plugin::CommunicationPluginBuilder;
37use cda_plugin_security::{
38 DefaultSecurityPlugin, DefaultSecurityPluginData, SecurityPlugin, SecurityPluginLoader,
39};
40use cda_sovd::Locks;
41use cda_tracing::{OtelGuard, TracingSetupError, TracingWorkerGuard};
42use cda_transport_router::DiagnosticTransportRouter;
43use clap::{Parser, Subcommand};
44use tokio::sync::{RwLock, mpsc};
45use tracing_subscriber::layer::SubscriberExt;
46
47use crate::{
48 config::{configfile::Configuration, generate::generate_config_cmd},
49 mdd::{load_databases, resolve_mdd_paths},
50 setup::PreLoadHook,
51 update::{UpdatePluginBuilder, create_default_update_plugin, update_plugin_fn},
52};
53
54pub mod cda_factory;
55pub mod config;
56pub mod error;
57pub mod mdd;
58pub mod setup;
59pub mod update;
60
61pub use error::AppError;
62pub use setup::Setup;
63
64// Valgrind and other profing tools intercept the system allocator, whereas mimalloc
65// manages allocations internally. Keep mimalloc in normal builds but omit it
66// from profiling builds so allocation call stacks remain visible.
67#[cfg(not(feature = "heap-profiling"))]
68#[global_allocator]
69static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
70
71const DOIP_HEALTH_COMPONENT_KEY: &str = "doip";
72
73#[cfg(feature = "health")]
74const MAIN_HEALTH_COMPONENT_KEY: &str = "main";
75
76pub type DatabaseMap<S> = HashMap<String, RwLock<EcuManager<S>>>;
77pub type FileManagerMap = HashMap<String, FileManager>;
78
79#[derive(Subcommand, Debug)]
80pub enum Command {
81 /// Generate a reference TOML configuration file with all fields commented out
82 GenerateConfig {
83 /// Output file path (defaults to opensovd-cda.toml). Use "-" for stdout.
84 #[arg(short, long)]
85 output: Option<PathBuf>,
86 },
87}
88
89#[derive(Parser, Debug)]
90#[command(version, about, long_about = None)]
91pub struct AppArgs {
92 #[arg(short, long, env = "CDA_CONFIG_FILE")]
93 pub config: Option<PathBuf>,
94
95 #[command(subcommand)]
96 pub command: Option<Command>,
97
98 /// Directory with diagnostic databases to load, if none have been loaded into storage before.
99 #[arg(short = 'd', long)]
100 pub seed_databases_dir: Option<String>,
101
102 #[arg(short, long)]
103 pub tester_address: Option<String>,
104
105 #[arg(long)]
106 pub tester_subnet: Option<String>,
107
108 #[arg(long)]
109 pub gateway_port: Option<u16>,
110
111 /// Protocol name used for com-param lookups
112 /// in the diagnostic database (matched case-insensitively).
113 /// Examples: `UDS_Ethernet_DoIP`, `UDS_Ethernet_DoIP_DOBT`
114 #[arg(long)]
115 pub protocol_name: Option<String>,
116
117 #[arg(long)]
118 pub listen_address: Option<String>,
119
120 #[arg(long)]
121 pub listen_port: Option<u16>,
122
123 #[arg(short, long)]
124 pub flash_files_path: Option<String>,
125
126 #[arg(long)]
127 pub file_logging: Option<bool>,
128
129 #[arg(long)]
130 pub log_file_dir: Option<String>,
131
132 #[arg(long)]
133 pub log_file_name: Option<String>,
134
135 #[arg(long)]
136 pub exit_no_database_loaded: Option<bool>,
137
138 #[arg(long)]
139 pub fallback_to_base_variant: Option<bool>,
140
141 /// Set to true, to rewrite mdd files without compression, which
142 /// reduces memory usage due to mmap significantly.
143 // Could use Action::SetFalse here, as the default is false but then we would have
144 // two different ways to set booleans (with and without `true`)
145 #[arg(long)]
146 pub mdd_decompress: Option<bool>,
147}
148
149pub struct VehicleData<S: SecurityPlugin> {
150 pub file_managers: FileManagerMap,
151 pub diagnostic_gateway:
152 ComponentSlot<DiagnosticTransportRouter<DoipDiagGateway<EcuManager<S>>, CanDiagGateway>>,
153 pub locks: Arc<cda_sovd::Locks>,
154 pub(crate) prepared: PreparedVehicleComponents<S>,
155 pub databases: Arc<DatabaseMap<S>>,
156 pub health_providers: Option<HashMap<String, Arc<dyn HealthProvider>>>,
157}
158
159pub struct VehicleComponents<S: SecurityPlugin> {
160 pub uds_manager: UdsManagerType<S>,
161 pub diagnostic_gateway:
162 DiagnosticTransportRouter<DoipDiagGateway<EcuManager<S>>, CanDiagGateway>,
163 pub databases: Arc<DatabaseMap<S>>,
164 pub file_managers: FileManagerMap,
165}
166
167impl AppArgs {
168 #[tracing::instrument(skip(self, config),
169 fields(
170 dlt_context = dlt_ctx!("MAIN"),
171 )
172 )]
173 pub fn update_config(self, config: &mut Configuration) {
174 if let Some(seed_databases_dir) = self.seed_databases_dir {
175 config.database.seed_dir = seed_databases_dir;
176 }
177 if let Some(exit_no_database_loaded) = self.exit_no_database_loaded {
178 config.database.exit_no_database_loaded = exit_no_database_loaded;
179 }
180 if let Some(fallback_to_base_variant) = self.fallback_to_base_variant {
181 config.database.fallback_to_base_variant = fallback_to_base_variant;
182 }
183 if let Some(flash_files_path) = self.flash_files_path {
184 config.flash_files_path = flash_files_path;
185 }
186 if let Some(tester_address) = self.tester_address {
187 config.doip.tester_address = tester_address;
188 }
189 if let Some(tester_subnet) = self.tester_subnet {
190 config.doip.tester_subnet = tester_subnet;
191 }
192 if let Some(gateway_port) = self.gateway_port {
193 config.doip.gateway_port = gateway_port;
194 }
195 if let Some(protocol_name) = self.protocol_name {
196 config.doip.protocol_name = protocol_name;
197 }
198 if let Some(listen_address) = self.listen_address {
199 config.server.address = listen_address;
200 }
201 if let Some(listen_port) = self.listen_port {
202 config.server.port = listen_port;
203 }
204 if let Some(file_logging) = self.file_logging {
205 config.logging.log_file_config.enabled = file_logging;
206 }
207 if let Some(log_file_dir) = self.log_file_dir {
208 config.logging.log_file_config.path = log_file_dir;
209 }
210 if let Some(log_file_name) = self.log_file_name {
211 config.logging.log_file_config.name = log_file_name;
212 }
213 if let Some(mdd_decompress) = self.mdd_decompress {
214 config.flat_buf.mdd_decompress = mdd_decompress;
215 }
216 }
217}
218
219/// Parse CLI arguments and start the CDA with the default startup flow.
220///
221/// # Errors
222/// Returns [`AppError`] if configuration loading, validation, or startup fails.
223pub async fn run_from_cli() -> Result<(), AppError> {
224 // Box is needed because it's a large future with a size of 16392 bytes
225 Box::pin(run(AppArgs::parse())).await
226}
227
228#[tracing::instrument(
229 skip(args, setup),
230 fields(
231 dlt_context = dlt_ctx!("MAIN"),
232 )
233)]
234/// Run the CDA from parsed CLI arguments with a custom [`Setup`].
235///
236/// This is the primary setup-aware entry point. Pass a [`Setup`] created with
237/// [`Setup::new`] and optionally configured with
238/// [`Setup::with_preload`] / [`Setup::with_update_plugin`].
239///
240/// # Errors
241/// Returns [`AppError`] if configuration loading, validation, or startup fails.
242pub async fn run_with_ext<SP, SL, UPB, CPB>(
243 args: AppArgs,
244 setup: Setup<SP, SL, UPB, CPB>,
245) -> Result<(), AppError>
246where
247 SP: SecurityPlugin,
248 SL: SecurityPluginLoader,
249 UPB: UpdatePluginBuilder<SP>,
250 CPB: CommunicationPluginBuilder,
251{
252 if let Some(Command::GenerateConfig { output }) = args.command.as_ref() {
253 // Exiting after generating config is on purpose.
254 return generate_config_cmd(output.as_ref());
255 }
256
257 let config_file = match &args.config {
258 Some(config_file) => {
259 if config_file.exists() {
260 config_file
261 } else {
262 // ignore `config-optional` feature here, because it's specifically for when no config was specified
263 return Err(AppError::ConfigurationError {
264 message: format!(
265 "Specified configuration file {} does not exist",
266 config_file.display()
267 ),
268 source: None,
269 });
270 }
271 }
272 None => Path::new("opensovd-cda.toml"),
273 };
274
275 let (mut config, disk_loaded) = config::load_config_with_fallback(config_file);
276 if !disk_loaded {
277 config::require_config_source()?;
278 }
279
280 // Command line arguments always take precedence over file configuration.
281 args.update_config(&mut config);
282
283 config.validate_sanity().map_err(AppError::from)?;
284
285 run_with_ext_from_config(config, setup).await
286}
287
288/// Start the CDA runtime from a prepared configuration with a custom [`Setup`].
289///
290/// This is the setup-aware version of [`run_with_config`]. Supply a [`Setup`] to
291/// configure a custom update plugin and/or a preload hook:
292///
293/// ```rust,ignore
294/// use opensovd_cda_lib::{Setup, run_with_ext_from_config, update::update_plugin_fn};
295/// use opensovd_cda_lib::config::configfile::Configuration;
296///
297/// let config: Configuration = // ... load or construct ...
298/// # todo!();
299///
300/// run_with_ext_from_config::<MySecurityPlugin, MySecurityLoader, _, _>(
301/// config,
302/// Setup::new().with_update_plugin(update_plugin_fn(|infra| async move {
303/// Ok(MyPlugin::new(infra))
304/// })),
305/// ).await?;
306/// ```
307///
308/// # Errors
309/// Returns [`AppError`] if tracing setup, webserver startup, data loading, or route setup fails.
310pub async fn run_with_ext_from_config<SP, SL, UPB, CPB>(
311 config: Configuration,
312 setup: Setup<SP, SL, UPB, CPB>,
313) -> Result<(), AppError>
314where
315 SP: SecurityPlugin,
316 SL: SecurityPluginLoader,
317 UPB: UpdatePluginBuilder<SP>,
318 CPB: CommunicationPluginBuilder,
319{
320 let webserver_state = init_webserver(
321 &config,
322 setup.pre_load,
323 setup.initialize_tracing,
324 setup.shutdown_signal,
325 )
326 .await?;
327
328 tracing::debug!("Webserver is running. Loading sovd routes...");
329
330 let vehicle_data =
331 match load_vehicle_data::<SP>(&config, webserver_state.health_state.as_ref()).await {
332 Ok(data) => data,
333 Err(AppError::ShutdownRequested) => {
334 tracing::info!("Shutdown requested during database load, exiting cleanly");
335 return Ok(());
336 }
337 Err(e) => return Err(e),
338 };
339
340 if vehicle_data.databases.is_empty() && config.database.exit_no_database_loaded {
341 return Err(AppError::ResourceError(
342 "No database loaded, exiting as configured".to_string(),
343 ));
344 }
345
346 // Retained for the full server lifetime, so its event dispatcher keeps
347 // running until explicit shutdown.
348 let communication_runtime = setup::setup_runtime_routes::<SP, SL, UPB, CPB>(
349 config,
350 vehicle_data,
351 &webserver_state,
352 setup.build_update_plugin,
353 setup.build_communication_plugin,
354 )
355 .await?;
356
357 tracing::info!("CDA fully initialized and ready to serve requests");
358 if let Some(provider) = &webserver_state.main_health_provider {
359 provider.update_status(cda_health::Status::Up).await;
360 }
361
362 // Wait for shutdown signal
363 webserver_state.shutdown_signal.clone().await;
364 tracing::info!("Shutting down...");
365 webserver_state.join().await?;
366 cda_interfaces::Shutdown::shutdown(&*communication_runtime.plugin).await;
367
368 Ok(())
369}
370
371/// Run the CDA from parsed CLI arguments.
372///
373/// Uses the default security plugin and the default runtime update plugin.
374/// To customize startup behavior, use [`run_with_ext`] instead.
375///
376/// # Errors
377/// Returns [`AppError`] if configuration loading, validation, or startup fails.
378pub async fn run(args: AppArgs) -> Result<(), AppError> {
379 Box::pin(run_with_ext::<
380 DefaultSecurityPluginData,
381 DefaultSecurityPlugin,
382 _,
383 _,
384 >(
385 args,
386 Setup::new().with_update_plugin(update_plugin_fn(|infra| async move {
387 create_default_update_plugin::<DefaultSecurityPluginData, DefaultSecurityPlugin>(infra)
388 .await
389 })),
390 ))
391 .await
392}
393
394/// Start the CDA runtime from a prepared configuration.
395///
396/// Uses the default security plugin and the default runtime update plugin.
397/// To customize startup behavior, use [`run_with_ext_from_config`] instead.
398///
399/// # Errors
400/// Returns [`AppError`] if tracing setup, webserver startup, data loading, or route setup fails.
401pub async fn run_with_config(config: Configuration) -> Result<(), AppError> {
402 Box::pin(run_with_ext_from_config::<
403 DefaultSecurityPluginData,
404 DefaultSecurityPlugin,
405 _,
406 _,
407 >(
408 config,
409 Setup::new().with_update_plugin(update_plugin_fn(
410 |infra: setup::CdaRuntime<DefaultSecurityPluginData>| async move {
411 create_default_update_plugin::<DefaultSecurityPluginData, DefaultSecurityPlugin>(
412 infra,
413 )
414 .await
415 },
416 )),
417 ))
418 .await
419}
420
421async fn init_webserver(
422 config: &Configuration,
423 pre_load: Option<PreLoadHook>,
424 initialize_tracing: bool,
425 shutdown_signal: Option<cda_interfaces::ShutdownSignal>,
426) -> Result<ApplicationState, AppError> {
427 let tracing_guards = if initialize_tracing {
428 setup_tracing(config)?
429 } else {
430 TracingGuards {
431 _file: None,
432 _otel: None,
433 }
434 };
435 tracing::info!("Starting CDA - version {}", cda_version());
436
437 // Vendor overrides are registered via `linkme` distributed slices, whose
438 // final contents are only known after linking; this checks that at most
439 // one override is linked in per overridable function before any of them
440 // are used. Every crate that defines vendor-overridable functions must
441 // be listed here.
442 if let Err(errors) = cda_core::validate_vendor_overrides() {
443 return Err(AppError::InitializationFailed(format!(
444 "Vendor override configuration error(s): {}",
445 errors.join("; ")
446 )));
447 }
448
449 let webserver_config = cda_sovd::WebServerConfig {
450 host: config.server.address.clone(),
451 port: config.server.port,
452 };
453
454 let clonable_shutdown_signal = shutdown_signal
455 .unwrap_or_else(|| cda_interfaces::shutdown_signal(crate::shutdown_signal()));
456
457 let (dynamic_router, webserver_task) =
458 cda_sovd::launch_webserver(webserver_config.clone(), clonable_shutdown_signal.clone())
459 .await?;
460
461 let mut webserver_state = ApplicationState {
462 _tracing_guards: tracing_guards,
463 dynamic_router,
464 webserver_task: Some(webserver_task),
465 shutdown_signal: clonable_shutdown_signal,
466 health_state: None,
467 main_health_provider: None,
468 };
469
470 #[cfg(feature = "health")]
471 let (health_state, main_health_provider) = if config.health.enabled {
472 let health_state = cda_health::add_health_routes(
473 &webserver_state.dynamic_router,
474 cda_version().to_owned(),
475 )
476 .await;
477 let main_health_provider = Arc::new(cda_health::StatusHealthProvider::new(
478 cda_health::Status::Starting,
479 ));
480 let registration = health_state
481 .register_provider(
482 MAIN_HEALTH_COMPONENT_KEY,
483 Arc::clone(&main_health_provider) as Arc<dyn cda_health::HealthProvider>,
484 )
485 .await
486 .map_err(|e| AppError::InitializationFailed(e.to_string()));
487 registration?;
488 (Some(health_state), Some(main_health_provider))
489 } else {
490 (None, None)
491 };
492
493 #[cfg(not(feature = "health"))]
494 let (health_state, main_health_provider): (
495 Option<cda_health::HealthState>,
496 Option<Arc<cda_health::StatusHealthProvider>>,
497 ) = (None, None);
498
499 webserver_state.health_state = health_state;
500 webserver_state.main_health_provider = main_health_provider;
501
502 #[cfg(feature = "systemd-notify")]
503 let _sd_notify_task = cda_extra::create_sd_notify_task(
504 webserver_state.health_state.clone(),
505 webserver_state.shutdown_signal.clone(),
506 );
507
508 register_version_endpoints(&webserver_state.dynamic_router).await;
509
510 if let Some(hook) = pre_load {
511 hook(webserver_state.dynamic_router.clone()).await?;
512 }
513
514 Ok(webserver_state)
515}
516
517async fn register_version_endpoints(dynamic_router: &cda_sovd::dynamic_router::DynamicRouter) {
[docs] 518 // [[ dimpl~sovd-api-version-endpoint, Register Version Endpoint ]]
519 let serde_json::Value::Object(version_info) = serde_json::json!({
520 "id": "version",
521 "data": {
522 "name": "Eclipse OpenSOVD Classic Diagnostic Adapter",
523 "api": {
524 "version": "1.1"
525 },
526 "implementation": {
527 "version": cda_version(),
528 "commit": env!("GIT_COMMIT_HASH").to_owned(),
529 "build_date": env!("BUILD_DATE").to_owned(),
530 }
531 }
532 }) else {
533 tracing::error!("Failed to build version information");
534 return;
535 };
536 cda_sovd::add_static_data_endpoint(
537 dynamic_router,
538 version_info.clone(),
539 "/vehicle/v15/apps/sovd2uds/data/version",
540 )
541 .await;
542 cda_sovd::add_static_data_endpoint(dynamic_router, version_info, "/vehicle/v15/data/version")
543 .await;
544}
545
546/// Loads vehicle data including MDD databases and vehicle components.
547///
548/// # Errors
549/// Returns [`AppError`] if MDD path resolution, database loading, or component creation fails.
550pub async fn load_vehicle_data<S: SecurityPlugin>(
551 config: &Configuration,
552 health: Option<&cda_health::HealthState>,
553) -> Result<VehicleData<S>, AppError> {
554 let mdd_paths: Vec<PathBuf> = {
555 let storage_dir = &config.runtime_update_config.storage_dir;
556 let paths = resolve_mdd_paths(storage_dir, &config.database.seed_dir).await;
557 if paths.is_empty() && config.database.exit_no_database_loaded {
558 return Err(AppError::InitializationFailed(
559 "No MDD files found".to_string(),
560 ));
561 }
562 paths
563 };
564
565 let health_providers = if let Some(health_state) = health {
566 let doip = Arc::new(cda_health::StatusHealthProvider::new(
567 cda_health::Status::Starting,
568 ));
569 let database = Arc::new(cda_health::StatusHealthProvider::new(
570 cda_health::Status::Starting,
571 ));
572 health_state
573 .register_provider(
574 DOIP_HEALTH_COMPONENT_KEY,
575 Arc::clone(&doip) as Arc<dyn cda_health::HealthProvider>,
576 )
577 .await
578 .map_err(|e| AppError::InitializationFailed(e.to_string()))?;
579 health_state
580 .register_provider(
581 mdd::DB_HEALTH_COMPONENT_KEY,
582 Arc::clone(&database) as Arc<dyn cda_health::HealthProvider>,
583 )
584 .await
585 .map_err(|e| AppError::InitializationFailed(e.to_string()))?;
586 let mut providers: HashMap<String, Arc<dyn HealthProvider>> = HashMap::default();
587 providers.insert(
588 DOIP_HEALTH_COMPONENT_KEY.to_owned(),
589 doip as Arc<dyn HealthProvider>,
590 );
591 providers.insert(
592 mdd::DB_HEALTH_COMPONENT_KEY.to_owned(),
593 database as Arc<dyn HealthProvider>,
594 );
595 Some(providers)
596 } else {
597 None
598 };
599
600 let prepared =
601 prepare_vehicle_components::<S>(config, &mdd_paths, health_providers.as_ref()).await?;
602
603 // Gateway constructors are passive. The selected plugin is built before
604 // consumers receive narrow views over the communication framework.
605 let gateway = ComponentSlot::new(prepared.diagnostic_gateway.clone());
606 Ok(VehicleData {
607 diagnostic_gateway: gateway,
608 file_managers: prepared.file_managers.clone(),
609 locks: Arc::new(Locks::new(Vec::new())),
610 databases: Arc::clone(&prepared.databases),
611 health_providers,
612 prepared,
613 })
614}
615
616pub type UdsManagerType<S> = UdsManager<
617 DiagnosticTransportRouter<DoipDiagGateway<EcuManager<S>>, CanDiagGateway>,
618 EcuManager<S>,
619>;
620
621/// The transport sections of the configuration, bundled for
622/// [`create_diagnostic_gateway`] so its signature stays within clippy's
623/// argument budget as transports are added.
624pub struct TransportConfigs<'a> {
625 pub doip: &'a DoipConfig,
626 /// `None` disables the CAN transport (no `[can]` section).
627 pub can: Option<&'a CanConfig>,
628}
629
630#[allow(
631 clippy::implicit_hasher,
632 reason = "Type alias does not allow specifying hasher. Hasher is set globally"
633)]
634#[tracing::instrument(skip_all,
635 fields(
636 database_count = databases.len(),
637 dlt_context = dlt_ctx!("MAIN"),
638 )
639)]
640#[allow(
641 clippy::too_many_arguments,
642 reason = "Combining parameters into a struct is not preferred here, to keep constructor call \
643 semantics explicit"
644)]
645pub fn create_uds_manager<S: SecurityPlugin>(
646 gateway: DiagnosticTransportRouter<DoipDiagGateway<EcuManager<S>>, CanDiagGateway>,
647 databases: Arc<HashMap<String, RwLock<EcuManager<S>>>>,
648 variant_detection_receiver: VariantDetectionReceiver,
649 state_coordinator: EcuStateCoordinator,
650 functional_description_config: &FunctionalDescriptionConfig,
651 fault_config: FaultConfig,
652 communication_access: Arc<dyn CommunicationAccess>,
653 communication_retry_after: Duration,
654) -> UdsManagerType<S> {
655 UdsManager::new(
656 gateway,
657 databases,
658 variant_detection_receiver,
659 state_coordinator,
660 functional_description_config,
661 fault_config,
662 communication_access,
663 communication_retry_after,
664 )
665}
666
667/// Collected webserver state produced by [`init_webserver`].
668///
669/// Passed to [`setup::setup_runtime_routes`] and [`run_with_ext_from_config`] so that
670/// the shutdown signal and health provider are accessible after the webserver is started.
671///
672/// Dropping this value aborts the webserver task, so all error paths are covered
673/// automatically without any explicit cleanup calls.
674pub(crate) struct ApplicationState {
675 _tracing_guards: TracingGuards,
676 pub dynamic_router: cda_sovd::dynamic_router::DynamicRouter,
677 webserver_task: Option<tokio::task::JoinHandle<()>>,
678 pub shutdown_signal: cda_interfaces::ShutdownSignal,
679 health_state: Option<cda_health::HealthState>,
680 main_health_provider: Option<Arc<cda_health::StatusHealthProvider>>,
681}
682
683impl Drop for ApplicationState {
684 fn drop(&mut self) {
685 if let Some(task) = self.webserver_task.take() {
686 task.abort();
687 }
688 }
689}
690
691impl ApplicationState {
692 /// Waits for the normally signaled webserver task to finish.
693 async fn join(mut self) -> Result<(), AppError> {
694 if let Some(task) = self.webserver_task.take() {
695 task.await
696 .map_err(|e| AppError::RuntimeError(format!("Webserver task join error: {e}")))?;
697 }
698 Ok(())
699 }
700}
701
702/// Creates vehicle components (databases, `DoIP` gateway, UDS manager) from configuration.
703///
704/// # Errors
705/// Returns [`AppError`] if database loading or diagnostic gateway creation fails.
706#[allow(
707 clippy::implicit_hasher,
708 reason = "Type alias doesn't allow specifying hasher"
709)]
710pub async fn create_vehicle_components<S: SecurityPlugin>(
711 config: &Configuration,
712 mdd_paths: &[PathBuf],
713 health_providers: Option<&HashMap<String, Arc<dyn HealthProvider>>>,
714 communication_access: Arc<dyn CommunicationAccess>,
715) -> Result<VehicleComponents<S>, AppError> {
716 let prepared = prepare_vehicle_components(config, mdd_paths, health_providers).await?;
717 Ok(finish_vehicle_components(
718 prepared,
719 config,
720 communication_access,
721 ))
722}
723
724struct PreparedVehicleComponents<S: SecurityPlugin> {
725 databases: Arc<DatabaseMap<S>>,
726 file_managers: FileManagerMap,
727 diagnostic_gateway: DiagnosticTransportRouter<DoipDiagGateway<EcuManager<S>>, CanDiagGateway>,
728 variant_detection_rx: VariantDetectionReceiver,
729 state_coordinator: EcuStateCoordinator,
730}
731
732async fn prepare_vehicle_components<S: SecurityPlugin>(
733 config: &Configuration,
734 mdd_paths: &[PathBuf],
735 health_providers: Option<&HashMap<String, Arc<dyn HealthProvider>>>,
736) -> Result<PreparedVehicleComponents<S>, AppError> {
737 let db_provider: Option<&Arc<dyn HealthProvider>> =
738 health_providers.and_then(|h| h.get(mdd::DB_HEALTH_COMPONENT_KEY));
739 let doip_provider: Option<&Arc<dyn HealthProvider>> =
740 health_providers.and_then(|h| h.get(DOIP_HEALTH_COMPONENT_KEY));
741
742 let (databases, file_managers) = load_databases::<S>(config, mdd_paths, db_provider).await?;
743
744 let (variant_detection_tx, variant_detection_rx) = mpsc::channel(50);
745 let variant_detection_tx = VariantDetectionSender::new(variant_detection_tx);
746 let variant_detection_rx = VariantDetectionReceiver::new(variant_detection_rx);
747 let databases = Arc::new(databases);
748
749 let runtime_states = build_runtime_states(&databases).await;
750 let state_coordinator = EcuStateCoordinator::new(runtime_states, variant_detection_tx.clone());
751 let connectivity_handler: Arc<dyn EcuConnectivityHandler> = Arc::new(state_coordinator.clone());
752
753 let diagnostic_gateway = create_diagnostic_gateway(
754 Arc::clone(&databases),
755 TransportConfigs {
756 doip: &config.doip,
757 can: config.can.as_ref(),
758 },
759 variant_detection_tx,
760 connectivity_handler,
761 doip_provider,
762 )
763 .await?;
764
765 Ok(PreparedVehicleComponents {
766 databases,
767 file_managers,
768 diagnostic_gateway,
769 variant_detection_rx,
770 state_coordinator,
771 })
772}
773
774async fn build_runtime_states<S: SecurityPlugin>(
775 databases: &DatabaseMap<S>,
776) -> HashMap<String, EcuRuntimeState> {
777 let mut states = HashMap::new();
778 for (ecu_name, ecu_lock) in databases {
779 states.insert(ecu_name.clone(), ecu_lock.read().await.runtime_state());
780 }
781 states
782}
783
784// The UDS manager, and the SOVD routes `setup::setup_runtime_routes` builds
785// from it, are constructed eagerly regardless of `init_mode`, pointed at a
786// gateway that stays network-inert until an authorized
787// `activate()`/`trigger_detection()` binds its DoIP socket (see
788// `init_doip_gateway`).
789pub(crate) fn finish_vehicle_components<S: SecurityPlugin>(
790 prepared: PreparedVehicleComponents<S>,
791 config: &Configuration,
792 communication_access: Arc<dyn CommunicationAccess>,
793) -> VehicleComponents<S> {
794 let uds_manager = create_uds_manager(
795 prepared.diagnostic_gateway.clone(),
796 Arc::clone(&prepared.databases),
797 prepared.variant_detection_rx,
798 prepared.state_coordinator,
799 &config.functional_description,
800 config.faults.clone(),
801 communication_access,
802 Duration::from_secs(config.communication.deferred_retry_after_seconds),
803 );
804 VehicleComponents {
805 uds_manager,
806 diagnostic_gateway: prepared.diagnostic_gateway,
807 databases: prepared.databases,
808 file_managers: prepared.file_managers,
809 }
810}
811
812#[tracing::instrument(
813 skip(databases, transports, variant_detection, connectivity_handler, doip_health_provider),
814 fields(
815 database_count = databases.len(),
816 dlt_context = dlt_ctx!("MAIN"),
817 )
818)]
819/// # Errors
820/// Returns [`AppError`] if the initialization of any configured transport
821/// fails. Transport init failure is always fatal: a CDA that starts without
822/// one of its configured transports cannot be told apart from a healthy one,
823/// and a supervisor restart is what actually recovers transient causes.
824pub async fn create_diagnostic_gateway<S: SecurityPlugin>(
825 databases: Arc<DatabaseMap<S>>,
826 transports: TransportConfigs<'_>,
827 variant_detection: VariantDetectionSender,
828 connectivity_handler: Arc<dyn EcuConnectivityHandler>,
829 doip_health_provider: Option<&Arc<dyn HealthProvider>>,
830) -> Result<DiagnosticTransportRouter<DoipDiagGateway<EcuManager<S>>, CanDiagGateway>, AppError> {
831 let TransportConfigs {
832 doip: doip_config,
833 can: can_config,
834 } = transports;
835 // Build the diagnostic transport router skeleton, then populate the configured
836 // transports. ECUs route over CAN or DoIP per `transport_overrides`,
837 // defaulting to DoIP-preferred with CAN fallback.
838 let transport_overrides: HashMap<String, TransportType> = can_config
839 .map(|c| {
840 c.transport_overrides
841 .iter()
842 .map(|o| (o.ecu_name.to_lowercase(), o.transport))
843 .collect()
844 })
845 .unwrap_or_default();
846
847 let mut gateway =
848 DiagnosticTransportRouter::<DoipDiagGateway<EcuManager<S>>, CanDiagGateway>::new(
849 transport_overrides,
850 );
851
852 // Fail clearly when CAN is configured on a build without CAN support.
853 // (validate_sanity rejects this too; kept as defense in depth for direct
854 // callers of this function.)
855 #[cfg(not(feature = "can"))]
856 if can_config.is_some() {
857 return Err(AppError::ConfigurationError {
858 message: "[can] is configured, but this binary was built without CAN support. Rebuild \
859 with `--features can` or remove the [can] section."
860 .to_owned(),
861 source: None,
862 });
863 }
864
865 if let Some(doip) = init_doip_gateway(
866 &databases,
867 doip_config,
868 variant_detection.clone(),
869 connectivity_handler,
870 doip_health_provider,
871 )
872 .await?
873 {
874 gateway = gateway.with_doip(doip);
875 }
876
877 #[cfg(feature = "can")]
878 if let Some(can_cfg) = can_config {
879 gateway = gateway.with_can(init_can_gateway(&databases, can_cfg, variant_detection).await?);
880 }
881
882 Ok(gateway)
883}
884
885/// Constructs the (passive) `DoIP` gateway, reporting the attempt on the health
886/// provider. Returns `Ok(None)` when `DoIP` is disabled by config, marking the
887/// health provider `Up` immediately so that readiness does not wait forever on
888/// an intentionally disabled transport.
889///
890/// [`DoipDiagGateway::new`] is purely in-memory. Binding the UDP socket,
891/// broadcasting VIR and starting listeners all happen lazily in the gateway's
892/// own `enable()`, reached only through an authorized `activate()` or
893/// `trigger_detection()`. Safe to call at startup in any `init_mode`.
894async fn init_doip_gateway<S: SecurityPlugin>(
895 databases: &Arc<DatabaseMap<S>>,
896 doip_config: &DoipConfig,
897 variant_detection: VariantDetectionSender,
898 connectivity_handler: Arc<dyn EcuConnectivityHandler>,
899 doip_health_provider: Option<&Arc<dyn HealthProvider>>,
900) -> Result<Option<DoipDiagGateway<EcuManager<S>>>, AppError> {
901 if !doip_config.enabled {
902 tracing::info!("DoIP transport disabled by config (doip.enabled = false)");
903 if let Some(provider) = doip_health_provider {
904 provider.set_status(cda_health::Status::Up).await;
905 }
906 return Ok(None);
907 }
908
909 if let Some(provider) = doip_health_provider {
910 provider.set_status(cda_health::Status::Starting).await;
911 }
912 let result = DoipDiagGateway::new(
913 doip_config,
914 Arc::clone(databases),
915 variant_detection,
916 connectivity_handler,
917 )
918 .await;
919 let status = if result.is_ok() {
920 cda_health::Status::Up
921 } else {
922 cda_health::Status::Failed
923 };
924 if let Some(provider) = doip_health_provider {
925 provider.set_status(status).await;
926 }
927 match result {
928 Ok(d) => {
929 tracing::info!("DoIP gateway initialized");
930 Ok(Some(d))
931 }
932 // Fatal; main reports the error on exit.
933 Err(e) => Err(e.into()),
934 }
935}
936
937/// Initializes the CAN transport. Like for `DoIP`, an init failure is fatal.
938#[cfg(feature = "can")]
939async fn init_can_gateway<S: SecurityPlugin>(
940 databases: &Arc<DatabaseMap<S>>,
941 can_cfg: &CanConfig,
942 variant_detection: VariantDetectionSender,
943) -> Result<CanDiagGateway, AppError> {
944 match CanDiagGateway::new(can_cfg, databases, variant_detection).await {
945 Ok(c) => {
946 tracing::info!(interface = %can_cfg.interface, "CAN gateway initialized");
947 Ok(c)
948 }
949 // Fatal; main reports the error on exit.
950 Err(e) => Err(e.into()),
951 }
952}
953
954/// # Panics
955/// Panics if the OS signal handlers cannot be installed.
956pub async fn shutdown_signal() {
957 let ctrl_c = async {
958 tokio::signal::ctrl_c()
959 .await
960 .expect("failed to install Ctrl+C handler");
961 };
962
963 #[cfg(unix)]
964 let terminate = async {
965 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
966 .expect("failed to install signal handler")
967 .recv()
968 .await;
969 };
970
971 #[cfg(not(unix))]
972 let terminate = std::future::pending::<()>();
973
974 tokio::select! {
975 () = ctrl_c => {},
976 () = terminate => {},
977 }
978}
979
980pub struct TracingGuards {
981 _file: Option<TracingWorkerGuard>,
982 _otel: Option<OtelGuard>,
983}
984
985/// # Errors
986/// Returns [`TracingSetupError`] if subscriber or exporter initialization fails.
987pub fn setup_tracing(config: &Configuration) -> Result<TracingGuards, TracingSetupError> {
988 let tracing = cda_tracing::new();
989 let mut layers = vec![];
990 layers.push(cda_tracing::new_term_subscriber(&config.logging));
991 #[cfg(feature = "tokio-tracing")]
992 layers.push(cda_tracing::new_tokio_tracing(
993 &config.logging.tokio_tracing,
994 )?);
995 let otel_guard = if config.logging.otel.enabled {
996 println!(
997 "Starting OpenTelemetry tracing with {}",
998 config.logging.otel.endpoint
999 );
1000 let (guard, metrics_layer, otel_layer) =
1001 cda_tracing::new_otel_subscriber(&config.logging.otel)?;
1002 layers.push(metrics_layer);
1003 layers.push(otel_layer);
1004 Some(guard)
1005 } else {
1006 None
1007 };
1008
1009 let file_guard = if config.logging.log_file_config.enabled {
1010 let (guard, file_layer) =
1011 cda_tracing::new_file_subscriber(&config.logging.log_file_config)?;
1012 layers.push(file_layer);
1013 Some(guard)
1014 } else {
1015 None
1016 };
1017
1018 #[cfg(feature = "dlt-tracing")]
1019 if config.logging.dlt_tracing.enabled {
1020 layers.push(cda_tracing::new_dlt_tracing(&config.logging.dlt_tracing)?);
1021 }
1022
1023 cda_tracing::init_tracing(tracing.with(layers))?;
1024 Ok(TracingGuards {
1025 _file: file_guard,
1026 _otel: otel_guard,
1027 })
1028}
1029
1030/// Returns the CDA version string, which is either
1031/// the value of the `CDA_VERSION` environment variable (if set)
1032/// or the Cargo package version.
1033#[must_use]
1034pub fn cda_version() -> &'static str {
1035 option_env!("CDA_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
1036}
1037
1038#[cfg(test)]
1039mod webserver_lifecycle_tests {
1040 use super::*;
1041
1042 #[tokio::test]
1043 async fn drop_aborts_webserver_task() {
1044 let task = tokio::spawn(std::future::pending::<()>());
1045 let abort_handle = task.abort_handle();
1046
1047 let state = ApplicationState {
1048 _tracing_guards: TracingGuards {
1049 _file: None,
1050 _otel: None,
1051 },
1052 dynamic_router: cda_sovd::dynamic_router::DynamicRouter::new(),
1053 webserver_task: Some(task),
1054 shutdown_signal: cda_interfaces::shutdown_signal(std::future::pending()),
1055 health_state: None,
1056 main_health_provider: None,
1057 };
1058
1059 drop(state);
1060
1061 // abort() is asynchronous; yield to let the cancellation propagate.
1062 tokio::task::yield_now().await;
1063 assert!(abort_handle.is_finished());
1064 }
1065}