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
14use std::{future::Future, path::PathBuf, sync::Arc};
15
16use cda_comm_doip::{DoipDiagGateway, config::DoipConfig};
17use cda_comm_uds::UdsManager;
18use cda_core::EcuManager;
19use cda_database::FileManager;
20use cda_interfaces::{
21 DiagServiceError, DoipGatewaySetupError, FunctionalDescriptionConfig, HashMap, UdsQuery,
22 UdsVariant,
23 config::{ConfigSanity, ConfigSanityError},
24 datatypes::{ComParams, FaultConfig},
25 dlt_ctx,
26};
27use cda_plugin_security::{
28 DefaultSecurityPlugin, DefaultSecurityPluginData, SecurityPlugin, SecurityPluginLoader,
29};
30use cda_sovd::Locks;
31use cda_tracing::{OtelGuard, TracingSetupError, TracingWorkerGuard};
32use clap::{Parser, Subcommand};
33use figment::{
34 Figment,
35 providers::{Format, Serialized, Toml},
36};
37use futures::future::FutureExt;
38use tokio::sync::{Mutex, RwLock, mpsc};
39use tracing_subscriber::layer::SubscriberExt;
40
41use crate::{
42 config::configfile::Configuration,
43 mdd::{load_databases, resolve_mdd_paths},
44 update::{RuntimeUpdateContext, security::UpdateSecurityHandler},
45};
46
47pub mod config;
48pub mod mdd;
49pub mod update;
50
51#[global_allocator]
52static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
53
54const DOIP_HEALTH_COMPONENT_KEY: &str = "doip";
55
56#[cfg(feature = "health")]
57const MAIN_HEALTH_COMPONENT_KEY: &str = "main";
58
59pub type DatabaseMap<S> = HashMap<String, RwLock<EcuManager<S>>>;
60pub type FileManagerMap = HashMap<String, FileManager>;
61
62#[derive(Subcommand, Debug)]
63pub enum Command {
64 /// Generate a reference TOML configuration file with all fields commented out
65 GenerateConfig {
66 /// Output file path (defaults to opensovd-cda.toml). Use "-" for stdout.
67 #[arg(short, long)]
68 output: Option<PathBuf>,
69 },
70}
71
72#[derive(Parser, Debug)]
73#[command(version, about, long_about = None)]
74pub struct AppArgs {
75 #[arg(short, long, env = "CDA_CONFIG_FILE")]
76 pub config: Option<String>,
77
78 #[command(subcommand)]
79 pub command: Option<Command>,
80
81 #[arg(short, long)]
82 pub databases_path: Option<String>,
83
84 #[arg(short, long)]
85 pub tester_address: Option<String>,
86
87 #[arg(long)]
88 pub tester_subnet: Option<String>,
89
90 #[arg(long)]
91 pub gateway_port: Option<u16>,
92
93 /// Protocol name used for com-param lookups
94 /// in the diagnostic database (matched case-insensitively).
95 /// Examples: `UDS_Ethernet_DoIP`, `UDS_Ethernet_DoIP_DOBT`
96 #[arg(long)]
97 pub protocol_name: Option<String>,
98
99 #[arg(long)]
100 pub listen_address: Option<String>,
101
102 #[arg(long)]
103 pub listen_port: Option<u16>,
104
105 #[arg(short, long)]
106 pub flash_files_path: Option<String>,
107
108 #[arg(long)]
109 pub file_logging: Option<bool>,
110
111 #[arg(long)]
112 pub log_file_dir: Option<String>,
113
114 #[arg(long)]
115 pub log_file_name: Option<String>,
116
117 #[arg(long)]
118 pub exit_no_database_loaded: Option<bool>,
119
120 #[arg(long)]
121 pub fallback_to_base_variant: Option<bool>,
122
123 /// Set to true, to rewrite mdd files without compression, which
124 /// reduces memory usage due to mmap significantly.
125 // Could use Action::SetFalse here, as the default is false but then we would have
126 // two different ways to set booleans (with and without `true`)
127 #[arg(long)]
128 pub mdd_decompress: Option<bool>,
129}
130
131pub struct VehicleData<S: SecurityPlugin> {
132 pub file_managers: FileManagerMap,
133 pub uds_manager: UdsManagerType<S>,
134 pub diagnostic_gateway: DoipDiagGateway<EcuManager<S>>,
135 pub locks: Arc<cda_sovd::Locks>,
136 pub update_guard: cda_sovd::UpdateGuardState,
137 pub databases: Arc<DatabaseMap<S>>,
138 pub variant_detection_handle: tokio::task::JoinHandle<()>,
139 pub health_providers: Option<HealthProviders>,
140}
141
142pub struct VehicleComponents<S: SecurityPlugin> {
143 pub uds_manager: UdsManagerType<S>,
144 pub diagnostic_gateway: DoipDiagGateway<EcuManager<S>>,
145 pub databases: Arc<DatabaseMap<S>>,
146 pub file_managers: FileManagerMap,
147 pub variant_detection_handle: tokio::task::JoinHandle<()>,
148}
149
150#[derive(thiserror::Error, Debug)]
151pub enum AppError {
152 #[error("Initialization failed `{0}`")]
153 InitializationFailed(String),
154 #[error("Resource error: `{0}`")]
155 ResourceError(String),
156 #[error("Connection error `{0}`")]
157 ConnectionError(String),
158 #[error("Configuration error `{0}`")]
159 ConfigurationError(String),
160 #[error("Data error `{0}`")]
161 DataError(String),
162 #[error("Error during execution `{0}`")]
163 RuntimeError(String),
164 #[error("Not found: `{0}`")]
165 NotFound(String),
166 #[error("Server error: `{0}`")]
167 ServerError(String),
168 #[error("Shutdown requested")]
169 ShutdownRequested,
170}
171
172impl From<DiagServiceError> for AppError {
173 fn from(value: DiagServiceError) -> Self {
174 match value {
175 DiagServiceError::RequestNotSupported(_)
176 | DiagServiceError::BadPayload(_)
177 | DiagServiceError::ConnectionClosed(_)
178 | DiagServiceError::UnexpectedResponse(_)
179 | DiagServiceError::EcuOffline(_)
180 | DiagServiceError::NoResponse(_)
181 | DiagServiceError::SendFailed(_)
182 | DiagServiceError::InvalidAddress(_)
183 | DiagServiceError::InvalidRequest(_)
184 | DiagServiceError::Timeout => Self::ConnectionError(value.to_string()),
185
186 DiagServiceError::ParameterConversionError(_)
187 | DiagServiceError::UnknownOperation
188 | DiagServiceError::UdsLookupError(_)
189 | DiagServiceError::VariantDetectionError(_)
190 | DiagServiceError::AccessDenied(_)
191 | DiagServiceError::InvalidState(_)
192 | DiagServiceError::Nack(_) => Self::RuntimeError(value.to_string()),
193
194 DiagServiceError::InvalidConfiguration(_) | DiagServiceError::InvalidSecurityPlugin => {
195 Self::ConfigurationError(value.to_string())
196 }
197
198 DiagServiceError::ResourceError(_) => Self::ResourceError(value.to_string()),
199
200 DiagServiceError::NotFound(_) => Self::NotFound(value.to_string()),
201
202 DiagServiceError::DataError(_)
203 | DiagServiceError::InvalidDatabase(_)
204 | DiagServiceError::AmbiguousParameters { .. }
205 | DiagServiceError::InvalidParameter { .. }
206 | DiagServiceError::NotEnoughData { .. } => Self::DataError(value.to_string()),
207 }
208 }
209}
210
211impl From<DoipGatewaySetupError> for AppError {
212 fn from(value: DoipGatewaySetupError) -> Self {
213 match value {
214 DoipGatewaySetupError::InvalidAddress(_) => Self::ConnectionError(value.to_string()),
215 DoipGatewaySetupError::SocketCreationFailed(_)
216 | DoipGatewaySetupError::PortBindFailed(_) => {
217 Self::InitializationFailed(value.to_string())
218 }
219 DoipGatewaySetupError::InvalidConfiguration(_) => {
220 Self::ConfigurationError(value.to_string())
221 }
222 DoipGatewaySetupError::ResourceError(_) => Self::ResourceError(value.to_string()),
223 DoipGatewaySetupError::ServerError(_) => Self::ServerError(value.to_string()),
224 }
225 }
226}
227
228impl From<TracingSetupError> for AppError {
229 fn from(value: TracingSetupError) -> Self {
230 match value {
231 TracingSetupError::ResourceCreationFailed(_) => Self::ResourceError(value.to_string()),
232 TracingSetupError::SubscriberInitializationFailed(_) => {
233 Self::InitializationFailed(value.to_string())
234 }
235 }
236 }
237}
238
239impl From<ConfigSanityError> for AppError {
240 fn from(value: ConfigSanityError) -> Self {
241 AppError::ConfigurationError(value.to_string())
242 }
243}
244
245impl AppArgs {
246 #[tracing::instrument(skip(self, config),
247 fields(
248 dlt_context = dlt_ctx!("MAIN"),
249 )
250 )]
251 pub fn update_config(self, config: &mut Configuration) {
252 if let Some(databases_path) = self.databases_path {
253 config.database.path = databases_path;
254 }
255 if let Some(exit_no_database_loaded) = self.exit_no_database_loaded {
256 config.database.exit_no_database_loaded = exit_no_database_loaded;
257 }
258 if let Some(fallback_to_base_variant) = self.fallback_to_base_variant {
259 config.database.fallback_to_base_variant = fallback_to_base_variant;
260 }
261 if let Some(flash_files_path) = self.flash_files_path {
262 config.flash_files_path = flash_files_path;
263 }
264 if let Some(tester_address) = self.tester_address {
265 config.doip.tester_address = tester_address;
266 }
267 if let Some(tester_subnet) = self.tester_subnet {
268 config.doip.tester_subnet = tester_subnet;
269 }
270 if let Some(gateway_port) = self.gateway_port {
271 config.doip.gateway_port = gateway_port;
272 }
273 if let Some(protocol_name) = self.protocol_name {
274 config.doip.protocol_name = protocol_name;
275 }
276 if let Some(listen_address) = self.listen_address {
277 config.server.address = listen_address;
278 }
279 if let Some(listen_port) = self.listen_port {
280 config.server.port = listen_port;
281 }
282 if let Some(file_logging) = self.file_logging {
283 config.logging.log_file_config.enabled = file_logging;
284 }
285 if let Some(log_file_dir) = self.log_file_dir {
286 config.logging.log_file_config.path = log_file_dir;
287 }
288 if let Some(log_file_name) = self.log_file_name {
289 config.logging.log_file_config.name = log_file_name;
290 }
291 if let Some(mdd_decompress) = self.mdd_decompress {
292 config.flat_buf.mdd_decompress = mdd_decompress;
293 }
294 }
295}
296
297/// Generate a reference CDA configuration and write it to the requested output.
298///
299/// # Errors
300/// Returns [`AppError`] if generating the reference configuration or writing it fails.
301pub fn generate_config_cmd(output: Option<&PathBuf>) -> Result<(), AppError> {
302 let content = config::generate::generate_reference_config()
303 .map_err(|e| AppError::RuntimeError(format!("Failed to generate config: {e}")))?;
304
305 match output.map(|p| p.as_os_str()) {
306 Some(p) if p == "-" => {
307 use std::io::Write;
308 std::io::stdout()
309 .write_all(content.as_bytes())
310 .map_err(|e| AppError::RuntimeError(format!("Failed to write stdout: {e}")))?;
311 }
312 Some(path) => {
313 std::fs::write(path, &content)
314 .map_err(|e| AppError::RuntimeError(format!("Failed to write config: {e}")))?;
315 }
316 None => {
317 std::fs::write("opensovd-cda.toml", &content)
318 .map_err(|e| AppError::RuntimeError(format!("Failed to write config: {e}")))?;
319 }
320 }
321 Ok(())
322}
323
324/// Parse CLI arguments and start the CDA with the default startup flow.
325///
326/// # Errors
327/// Returns [`AppError`] if configuration loading, validation, or startup fails.
328pub async fn run_from_cli() -> Result<(), AppError> {
329 // Box is needed because it's a large future with a size of 16392 bytes
330 Box::pin(run(AppArgs::parse())).await
331}
332
333#[tracing::instrument(
334 skip(args, extra_health_providers, pre_load_hook),
335 fields(
336 dlt_context = dlt_ctx!("MAIN"),
337 )
338)]
339/// Run the CDA from parsed CLI arguments, with optional extra health providers and a
340/// pre-vehicle-load hook. See [`run_with_config_ext`] for parameter documentation.
341///
342/// # Errors
343/// Returns [`AppError`] if configuration loading, validation, or startup fails.
344pub async fn run_with_ext<SP, SL, H, Fut>(
345 args: AppArgs,
346 extra_health_providers: Vec<(&'static str, Arc<dyn cda_health::HealthProvider>)>,
347 pre_load_hook: H,
348) -> Result<(), AppError>
349where
350 SP: SecurityPlugin,
351 SL: SecurityPluginLoader,
352 H: FnOnce(cda_sovd::dynamic_router::DynamicRouter) -> Fut + Send,
353 Fut: Future<Output = Result<(), AppError>> + Send,
354{
355 if let Some(Command::GenerateConfig { output }) = args.command.as_ref() {
356 // Exiting after generating config is on purpose.
357 return generate_config_cmd(output.as_ref());
358 }
359
360 let (mut config, disk_loaded) = config::load_config_with_fallback(args.config.as_deref());
361
362 if disk_loaded && config.runtime_update_config.init_storage_from_config_file {
363 let config_file = config::resolve_config_file_path(args.config.as_deref());
364 config::seed_storage_from_config_file(
365 &config.runtime_update_config.storage_dir,
366 &config_file,
367 )
368 .await;
369 }
370
371 if let Some(storage_config) =
372 config::load_config_with_storage_override(&config.runtime_update_config.storage_dir).await?
373 {
374 config = storage_config;
375 } else if !disk_loaded {
376 config::require_config_source()?;
377 }
378
379 // Command line arguments always take precedence over stored configuration
380 args.update_config(&mut config);
381
382 config.validate_sanity().map_err(AppError::from)?;
383
384 run_with_config_ext::<SP, SL, _, _>(config, extra_health_providers, pre_load_hook).await
385}
386
387/// Run the CDA from parsed CLI arguments.
388///
389/// # Errors
390/// Returns [`AppError`] if configuration loading, validation, or startup fails.
391pub async fn run(args: AppArgs) -> Result<(), AppError> {
392 Box::pin(run_with_ext::<
393 DefaultSecurityPluginData,
394 DefaultSecurityPlugin,
395 _,
396 _,
397 >(args, vec![], |_| async { Ok(()) }))
398 .await
399}
400
401/// Start the CDA runtime from a prepared configuration, with optional extra health providers
402/// and a pre-vehicle-load hook.
403///
404/// - `extra_health_providers`: additional `(key, provider)` pairs registered into the health
405/// state alongside the built-in `main` provider. Ignored when the `health` feature is
406/// disabled or `config.health.enabled` is `false`.
407/// - `pre_load_hook`: called after the webserver, health state, and sd-notify are set up but
408/// **before** vehicle data is loaded. Use it to register extra routes or endpoints that
409/// should be available during (and benefit from parallelism with) the database load.
410/// Must return `Ok(())` to continue startup; an `Err` aborts immediately.
411/// - `SP` / `SL`: security plugin data and loader types. Use [`DefaultSecurityPluginData`] and
412/// [`DefaultSecurityPlugin`] for the default behaviour.
413///
414/// # Errors
415/// Returns [`AppError`] if tracing setup, webserver startup, hook execution, data loading, or
416/// route setup fails.
417pub async fn run_with_config_ext<SP, SL, H, Fut>(
418 config: Configuration,
419 extra_health_providers: Vec<(&'static str, Arc<dyn cda_health::HealthProvider>)>,
420 pre_load_hook: H,
421) -> Result<(), AppError>
422where
423 SP: SecurityPlugin,
424 SL: SecurityPluginLoader,
425 H: FnOnce(cda_sovd::dynamic_router::DynamicRouter) -> Fut + Send,
426 Fut: Future<Output = Result<(), AppError>> + Send,
427{
428 let _tracing_guards = setup_tracing(&config)?;
429 tracing::info!("Starting CDA - version {}", cda_version());
430
431 let webserver_config = cda_sovd::WebServerConfig {
432 host: config.server.address.clone(),
433 port: config.server.port,
434 };
435
436 let clonable_shutdown_signal = shutdown_signal().shared();
437
438 let (dynamic_router, webserver_task) =
439 cda_sovd::launch_webserver(webserver_config.clone(), clonable_shutdown_signal.clone())
440 .await?;
441
442 #[cfg(feature = "health")]
443 let (health_state, main_health_provider) = if config.health.enabled {
444 let health_state =
445 cda_health::add_health_routes(&dynamic_router, cda_version().to_owned()).await;
446 let main_health_provider = Arc::new(cda_health::StatusHealthProvider::new(
447 cda_health::Status::Starting,
448 ));
449
450 health_state
451 .register_provider(
452 MAIN_HEALTH_COMPONENT_KEY,
453 Arc::clone(&main_health_provider) as Arc<dyn cda_health::HealthProvider>,
454 )
455 .await
456 .map_err(|e| AppError::InitializationFailed(e.to_string()))?;
457 for (key, provider) in extra_health_providers {
458 health_state
459 .register_provider(key, provider)
460 .await
461 .map_err(|e| AppError::InitializationFailed(e.to_string()))?;
462 }
463 (Some(health_state), Some(main_health_provider))
464 } else {
465 (None, None)
466 };
467
468 #[cfg(not(feature = "health"))]
469 let (health_state, main_health_provider): (
470 Option<cda_health::HealthState>,
471 Option<Arc<cda_health::StatusHealthProvider>>,
472 ) = {
473 // Prevents compiler warning for unused variable when health feature is disabled
474 let _ = extra_health_providers;
475 (None, None)
476 };
477
478 #[cfg(feature = "systemd-notify")]
479 let _sd_notify_task =
480 cda_extra::create_sd_notify_task(health_state.clone(), clonable_shutdown_signal.clone());
481
482 register_version_endpoints(&dynamic_router).await;
483 pre_load_hook(dynamic_router.clone()).await?;
484
485 setup_vehicle_and_routes::<SP, SL>(
486 config,
487 &dynamic_router,
488 &webserver_config,
489 health_state.as_ref(),
490 clonable_shutdown_signal.clone(),
491 )
492 .await?;
493
494 tracing::info!("CDA fully initialized and ready to serve requests");
495 if let Some(provider) = main_health_provider {
496 provider.update_status(cda_health::Status::Up).await;
497 }
498
499 // Wait for shutdown signal
500 clonable_shutdown_signal.await;
501 tracing::info!("Shutting down...");
502 webserver_task
503 .await
504 .map_err(|e| AppError::RuntimeError(format!("Webserver task join error: {e}")))?;
505
506 Ok(())
507}
508
509/// Start the CDA runtime from a prepared configuration.
510///
511/// # Errors
512/// Returns [`AppError`] if tracing setup, webserver startup, data loading, or route setup fails.
513pub async fn run_with_config(config: Configuration) -> Result<(), AppError> {
514 run_with_config_ext::<DefaultSecurityPluginData, DefaultSecurityPlugin, _, _>(
515 config,
516 vec![],
517 |_| async { Ok(()) },
518 )
519 .await
520}
521
522/// Loads vehicle data, registers all SOVD routes, runtime-update routes, `OpenAPI` routes,
523/// and installs the update guard. Extracted from `run_with_config` to keep it under the line limit.
524///
525/// The type parameters `SP` and `SL` select the security plugin data and loader implementations.
526/// Use [`DefaultSecurityPluginData`] and [`DefaultSecurityPlugin`] for the default behaviour.
527///
528/// # Errors
529/// Returns [`AppError`] if vehicle data loading, route registration, or update plugin setup fails.
530pub async fn setup_vehicle_and_routes<SP: SecurityPlugin, SL: SecurityPluginLoader>(
531 config: Configuration,
532 dynamic_router: &cda_sovd::dynamic_router::DynamicRouter,
533 webserver_config: &cda_sovd::WebServerConfig,
534 health_state: Option<&cda_health::HealthState>,
535 clonable_shutdown_signal: futures::future::Shared<
536 impl std::future::Future<Output = ()> + Send + 'static,
537 >,
538) -> Result<(), AppError> {
539 tracing::debug!("Webserver is running. Loading sovd routes...");
540
541 let vehicle_data =
542 match load_vehicle_data::<_, SP>(&config, clonable_shutdown_signal.clone(), health_state)
543 .await
544 {
545 Ok(data) => data,
546 Err(AppError::ShutdownRequested) => {
547 tracing::info!("Shutdown requested during database load, exiting cleanly");
548 return Ok(());
549 }
550 Err(e) => return Err(e),
551 };
552
553 if vehicle_data.databases.is_empty() && config.database.exit_no_database_loaded {
554 return Err(AppError::ResourceError(
555 "No database loaded, exiting as configured".to_string(),
556 ));
557 }
558
559 let flash_files_path = config.flash_files_path.clone();
560 let components_config = config.components.clone();
561 let runtime_update_config = config.runtime_update_config.clone();
562
563 let (ecu_execution_registry, vehicle_route_handle) = cda_sovd::add_vehicle_routes::<_, _, SL>(
564 dynamic_router,
565 cda_sovd::VehicleConfig {
566 flash_files_path: config.flash_files_path.clone(),
567 functional_group_config: config.functional_description.clone(),
568 components_config: config.components.clone(),
569 },
570 cda_sovd::VehicleResources {
571 ecu_uds: vehicle_data.uds_manager.clone(),
572 file_manager: vehicle_data.file_managers,
573 locks: Arc::clone(&vehicle_data.locks),
574 update_in_progress: vehicle_data.update_guard.busy_handle(),
575 },
576 )
577 .await?;
578
579 let lock_provider: Arc<cda_sovd::SovdLockStateProvider> = Arc::new(
580 cda_sovd::SovdLockStateProvider::new(Arc::clone(&vehicle_data.locks)),
581 );
582
583 let flash_transfer_guard = vehicle_data.uds_manager.flash_transfer_guard();
584 let runtime_update_plugin =
585 update::init_default_runtime_update_plugin::<SP, _, SL>(Box::new(RuntimeUpdateContext {
586 dynamic_router: dynamic_router.clone(),
587 vehicle_route_handle,
588 config,
589 flash_files_path,
590 components_config,
591 lock_provider: Arc::clone(&lock_provider),
592 update_guard: vehicle_data.update_guard.clone(),
593 shutdown_signal: clonable_shutdown_signal,
594 runtime_update_config: runtime_update_config.clone(),
595 ecu_execution_registry: ecu_execution_registry.clone(),
596 uds_manager: vehicle_data.uds_manager,
597 doip_gateway: vehicle_data.diagnostic_gateway,
598 health: vehicle_data.health_providers,
599 variant_detection_handle: Some(vehicle_data.variant_detection_handle),
600 security_handler: Arc::new(UpdateSecurityHandler::new(
601 Arc::clone(&lock_provider),
602 vec![
603 Box::new(flash_transfer_guard),
604 Box::new(ecu_execution_registry),
605 ],
606 )),
607 }))
608 .await?;
609 update::add_runtime_update_routes::<SL, _>(
610 dynamic_router,
611 runtime_update_plugin,
612 lock_provider,
613 &vehicle_data.update_guard,
614 runtime_update_config.upload_body_limit_bytes,
615 runtime_update_config.retry_after_seconds,
616 )
617 .await;
618
619 cda_sovd::add_openapi_routes(dynamic_router, &vehicle_data.update_guard, webserver_config)
620 .await;
621
622 // SAFETY: Must be applied AFTER all routes are registered (layer only covers existing routes).
623 cda_sovd::install_update_guard(dynamic_router, vehicle_data.update_guard.clone()).await;
624
625 Ok(())
626}
627
628async fn register_version_endpoints(dynamic_router: &cda_sovd::dynamic_router::DynamicRouter) {
[docs] 629 // [[ dimpl~sovd-api-version-endpoint, Register Version Endpoint ]]
630 let serde_json::Value::Object(version_info) = serde_json::json!({
631 "id": "version",
632 "data": {
633 "name": "Eclipse OpenSOVD Classic Diagnostic Adapter",
634 "api": {
635 "version": "1.1"
636 },
637 "implementation": {
638 "version": cda_version(),
639 "commit": env!("GIT_COMMIT_HASH").to_owned(),
640 "build_date": env!("BUILD_DATE").to_owned(),
641 }
642 }
643 }) else {
644 tracing::error!("Failed to build version information");
645 return;
646 };
647 cda_sovd::add_static_data_endpoint(
648 dynamic_router,
649 version_info.clone(),
650 "/vehicle/v15/apps/sovd2uds/data/version",
651 )
652 .await;
653 cda_sovd::add_static_data_endpoint(dynamic_router, version_info, "/vehicle/v15/data/version")
654 .await;
655}
656
657/// Loads vehicle data including MDD databases and vehicle components.
658///
659/// # Errors
660/// Returns [`AppError`] if MDD path resolution, database loading, or component creation fails.
661pub async fn load_vehicle_data<
662 F: Future<Output = ()> + Clone + Send + 'static,
663 S: SecurityPlugin,
664>(
665 config: &Configuration,
666 clonable_shutdown_signal: F,
667 health: Option<&cda_health::HealthState>,
668) -> Result<VehicleData<S>, AppError> {
669 let mdd_paths: Vec<PathBuf> = {
670 let storage_dir = &config.runtime_update_config.storage_dir;
671 let paths = resolve_mdd_paths(storage_dir, &config.database.path).await;
672 if paths.is_empty() {
673 return Err(AppError::InitializationFailed(
674 "No MDD files found".to_string(),
675 ));
676 }
677 paths
678 };
679
680 let health_providers = if let Some(health_state) = health {
681 let doip = Arc::new(cda_health::StatusHealthProvider::new(
682 cda_health::Status::Starting,
683 ));
684 let database = Arc::new(cda_health::StatusHealthProvider::new(
685 cda_health::Status::Starting,
686 ));
687 health_state
688 .register_provider(
689 DOIP_HEALTH_COMPONENT_KEY,
690 Arc::clone(&doip) as Arc<dyn cda_health::HealthProvider>,
691 )
692 .await
693 .map_err(|e| AppError::InitializationFailed(e.to_string()))?;
694 health_state
695 .register_provider(
696 mdd::DB_HEALTH_COMPONENT_KEY,
697 Arc::clone(&database) as Arc<dyn cda_health::HealthProvider>,
698 )
699 .await
700 .map_err(|e| AppError::InitializationFailed(e.to_string()))?;
701 Some(HealthProviders { doip, database })
702 } else {
703 None
704 };
705
706 let update_guard = cda_sovd::UpdateGuardState::new();
707 let doip_socket = cda_comm_doip::create_socket(
708 &config.doip.tester_address,
709 config.doip.gateway_port,
710 config.doip.protocol_version,
711 )
712 .map_err(|e| AppError::InitializationFailed(format!("Failed to create DoIP socket: {e}")))?;
713 let components = create_vehicle_components::<F, S>(
714 config,
715 &mdd_paths,
716 clonable_shutdown_signal,
717 health_providers.as_ref(),
718 update_guard.busy_handle(),
719 Arc::new(Mutex::new(doip_socket)),
720 )
721 .await?;
722
723 let ecu_names = components.uds_manager.get_physical_ecus().await;
724 Ok(VehicleData {
725 uds_manager: components.uds_manager,
726 diagnostic_gateway: components.diagnostic_gateway,
727 file_managers: components.file_managers,
728 locks: Arc::new(Locks::new(ecu_names)),
729 update_guard,
730 databases: components.databases,
731 variant_detection_handle: components.variant_detection_handle,
732 health_providers,
733 })
734}
735
736pub type UdsManagerType<S> = UdsManager<DoipDiagGateway<EcuManager<S>>, EcuManager<S>>;
737
738#[allow(
739 clippy::implicit_hasher,
740 reason = "Type alias does not allow specifying hasher. Hasher is set globally"
741)]
742#[tracing::instrument(skip_all,
743 fields(
744 database_count = databases.len(),
745 dlt_context = dlt_ctx!("MAIN"),
746 )
747)]
748pub fn create_uds_manager<S: SecurityPlugin>(
749 gateway: DoipDiagGateway<EcuManager<S>>,
750 databases: Arc<HashMap<String, RwLock<EcuManager<S>>>>,
751 variant_detection_receiver: mpsc::Receiver<Vec<String>>,
752 functional_description_config: &FunctionalDescriptionConfig,
753 fault_config: FaultConfig,
754 update_in_progress: Arc<std::sync::atomic::AtomicBool>,
755) -> UdsManagerType<S> {
756 UdsManager::new(
757 gateway,
758 databases,
759 variant_detection_receiver,
760 functional_description_config,
761 fault_config,
762 update_in_progress,
763 )
764}
765
766pub struct HealthProviders {
767 pub doip: Arc<cda_health::StatusHealthProvider>,
768 pub database: Arc<cda_health::StatusHealthProvider>,
769}
770
771/// Creates vehicle components (databases, `DoIP` gateway, UDS manager) from configuration.
772///
773/// # Errors
774/// Returns [`AppError`] if database loading or diagnostic gateway creation fails.
775pub async fn create_vehicle_components<
776 F: Future<Output = ()> + Clone + Send + 'static,
777 S: SecurityPlugin,
778>(
779 config: &Configuration,
780 mdd_paths: &[PathBuf],
781 shutdown_signal: F,
782 health_providers: Option<&HealthProviders>,
783 update_in_progress: Arc<std::sync::atomic::AtomicBool>,
784 doip_socket: Arc<tokio::sync::Mutex<cda_comm_doip::socket::DoIPUdpSocket>>,
785) -> Result<VehicleComponents<S>, AppError> {
786 let db_provider = health_providers.map(|h| &h.database);
787 let doip_provider = health_providers.map(|h| &h.doip);
788
789 let (databases, file_managers) = load_databases::<S>(config, mdd_paths, db_provider).await?;
790
791 let (variant_detection_tx, variant_detection_rx) = mpsc::channel(50);
792 let databases = Arc::new(databases);
793 let diagnostic_gateway = create_diagnostic_gateway(
794 Arc::clone(&databases),
795 &config.doip,
796 variant_detection_tx,
797 shutdown_signal,
798 doip_provider,
799 doip_socket,
800 )
801 .await?;
802
803 let uds_manager = create_uds_manager(
804 diagnostic_gateway.clone(),
805 Arc::clone(&databases),
806 variant_detection_rx,
807 &config.functional_description,
808 config.faults.clone(),
809 update_in_progress,
810 );
811
812 let vd = uds_manager.clone();
813 let variant_detection_handle = cda_interfaces::spawn_named!("variant-detection", async move {
814 vd.start_variant_detection().await;
815 });
816
817 Ok(VehicleComponents {
818 uds_manager,
819 diagnostic_gateway,
820 databases,
821 file_managers,
822 variant_detection_handle,
823 })
824}
825
826#[tracing::instrument(
827 skip(databases, variant_detection, shutdown_signal, doip_health_provider, doip_socket),
828 fields(
829 database_count = databases.len(),
830 dlt_context = dlt_ctx!("MAIN"),
831 )
832)]
833/// # Errors
834/// Returns [`DoipGatewaySetupError`] if `DoIP` gateway initialization fails.
835pub async fn create_diagnostic_gateway<S: SecurityPlugin>(
836 databases: Arc<DatabaseMap<S>>,
837 doip_config: &DoipConfig,
838 variant_detection: mpsc::Sender<Vec<String>>,
839 shutdown_signal: impl Future<Output = ()> + Send + 'static,
840 doip_health_provider: Option<&Arc<cda_health::StatusHealthProvider>>,
841 doip_socket: Arc<Mutex<cda_comm_doip::socket::DoIPUdpSocket>>,
842) -> Result<DoipDiagGateway<EcuManager<S>>, DoipGatewaySetupError> {
843 if let Some(provider) = doip_health_provider {
844 provider.update_status(cda_health::Status::Starting).await;
845 }
846
847 let result = DoipDiagGateway::new(
848 doip_config,
849 databases,
850 variant_detection,
851 shutdown_signal,
852 doip_socket,
853 )
854 .await;
855 let status = if result.is_ok() {
856 cda_health::Status::Up
857 } else {
858 cda_health::Status::Failed
859 };
860 if let Some(provider) = doip_health_provider {
861 provider.update_status(status).await;
862 }
863 result
864}
865
866/// # Panics
867/// Panics if the OS signal handlers cannot be installed.
868pub async fn shutdown_signal() {
869 let ctrl_c = async {
870 tokio::signal::ctrl_c()
871 .await
872 .expect("failed to install Ctrl+C handler");
873 };
874
875 #[cfg(unix)]
876 let terminate = async {
877 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
878 .expect("failed to install signal handler")
879 .recv()
880 .await;
881 };
882
883 #[cfg(not(unix))]
884 let terminate = std::future::pending::<()>();
885
886 tokio::select! {
887 () = ctrl_c => {},
888 () = terminate => {},
889 }
890}
891
892pub struct TracingGuards {
893 _file: Option<TracingWorkerGuard>,
894 _otel: Option<OtelGuard>,
895}
896
897/// # Errors
898/// Returns [`TracingSetupError`] if subscriber or exporter initialization fails.
899pub fn setup_tracing(config: &Configuration) -> Result<TracingGuards, TracingSetupError> {
900 let tracing = cda_tracing::new();
901 let mut layers = vec![];
902 layers.push(cda_tracing::new_term_subscriber(&config.logging));
903 #[cfg(feature = "tokio-tracing")]
904 layers.push(cda_tracing::new_tokio_tracing(
905 &config.logging.tokio_tracing,
906 )?);
907 let otel_guard = if config.logging.otel.enabled {
908 println!(
909 "Starting OpenTelemetry tracing with {}",
910 config.logging.otel.endpoint
911 );
912 let (guard, metrics_layer, otel_layer) =
913 cda_tracing::new_otel_subscriber(&config.logging.otel)?;
914 layers.push(metrics_layer);
915 layers.push(otel_layer);
916 Some(guard)
917 } else {
918 None
919 };
920
921 let file_guard = if config.logging.log_file_config.enabled {
922 let (guard, file_layer) =
923 cda_tracing::new_file_subscriber(&config.logging.log_file_config)?;
924 layers.push(file_layer);
925 Some(guard)
926 } else {
927 None
928 };
929
930 #[cfg(feature = "dlt-tracing")]
931 if config.logging.dlt_tracing.enabled {
932 layers.push(cda_tracing::new_dlt_tracing(&config.logging.dlt_tracing)?);
933 }
934
935 cda_tracing::init_tracing(tracing.with(layers))?;
936 Ok(TracingGuards {
937 _file: file_guard,
938 _otel: otel_guard,
939 })
940}
941
942#[must_use]
943pub fn cda_version() -> &'static str {
944 env!("CARGO_PKG_VERSION")
945}
946
947/// Compute the effective [`ComParams`] for a single ECU.
948///
949/// Starts from the global `global` config and merges any per-ECU TOML overrides
950/// present in `ecu_table`.
951///
952/// Returns `None` (and emits a `tracing::error!`) if the TOML table cannot be
953/// serialised or if figment extraction fails - the caller should `continue` to
954/// the next ECU.
955pub fn resolve_com_params(
956 ecu_name: &str,
957 global: &ComParams,
958 ecu_overrides: Option<&config::configfile::EcuComParams>,
959) -> Option<ComParams> {
960 let params: ComParams = match ecu_overrides {
961 None => global.clone(),
962 Some(overrides) => {
963 let toml_str = match toml::to_string(overrides) {
964 Ok(s) => s,
965 Err(e) => {
966 tracing::error!(
967 ecu_name = %ecu_name,
968 error = %e,
969 "Failed to serialize per-ECU com_params TOML table; skipping ECU"
970 );
971 return None;
972 }
973 };
974 match Figment::from(Serialized::defaults(global))
975 .merge(Toml::string(&toml_str))
976 .extract::<ComParams>()
977 {
978 Ok(p) => p,
979 Err(e) => {
980 tracing::error!(
981 ecu_name = %ecu_name,
982 error = %e,
983 "Failed to merge per-ECU com_params overrides; skipping ECU"
984 );
985 return None;
986 }
987 }
988 }
989 };
990
991 Some(params)
992}
993
994#[cfg(test)]
995mod tests {
996 use super::*;
997
998 #[test]
999 fn resolve_com_params_returns_none_on_figment_extraction_failure() {
1000 let global = ComParams::default();
1001 // Put a string where figment expects a table (the `uds` key should map
1002 // to a struct, not a scalar); this reliably triggers an extraction error.
1003 let mut table = toml::Table::new();
1004 table.insert(
1005 "uds".to_owned(),
1006 toml::Value::String("not_a_struct".to_owned()),
1007 );
1008 let ecu_params = crate::config::configfile::EcuComParams(table);
1009 let result = resolve_com_params("BAD_ECU", &global, Some(&ecu_params));
1010 assert!(
1011 result.is_none(),
1012 "resolve_com_params must return None when figment extraction fails"
1013 );
1014 }
1015}