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}