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}