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 backon::Retryable;
 26use cda_interfaces::{config::ConfigSanity, dlt_ctx, storage_api::StorageError};
 27use cda_plugin_communication_management::plugin::CommunicationPluginBuilder;
 28use cda_plugin_security::{
 29    DefaultSecurityPlugin, DefaultSecurityPluginData, SecurityPlugin, SecurityPluginLoader,
 30};
 31use cda_storage::LocalStorage;
 32use cda_tracing::{OtelGuard, TracingSetupError, TracingWorkerGuard};
 33use clap::{Parser, Subcommand};
 34use tracing_subscriber::layer::SubscriberExt;
 35
 36use crate::{
 37    config::{configfile::Configuration, generate::generate_config_cmd},
 38    setup::PreLoadHook,
 39    update::{UpdatePluginBuilder, create_default_update_plugin, update_plugin_fn},
 40};
 41
 42pub mod cda_factory;
 43pub mod config;
 44pub mod error;
 45pub mod mdd;
 46pub mod setup;
 47pub mod update;
 48pub mod vehicle;
 49
 50pub use error::AppError;
 51pub use setup::Setup;
 52
 53// Valgrind and other profing tools intercept the system allocator, whereas mimalloc
 54// manages allocations internally. Keep mimalloc in normal builds but omit it
 55// from profiling builds so allocation call stacks remain visible.
 56#[cfg(not(feature = "heap-profiling"))]
 57#[global_allocator]
 58static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
 59
 60/// Environment variable to override the purge delay of mimalloc in milliseconds.
 61#[cfg(not(feature = "heap-profiling"))]
 62const MIMALLOC_PURGE_DELAY_ENV: &str = "MIMALLOC_PURGE_DELAY";
 63
 64/// Configures mimalloc to return unused memory to the OS immediately.
 65///
 66/// mimalloc only executes delayed purges when the owning thread calls into the
 67/// allocator again. Idle tokio worker threads therefore keep freed memory (e.g. from
 68/// startup or from serving `openapi.json`) resident indefinitely. Purging immediately
 69/// cuts the resident memory after startup roughly in half.
 70/// Can be overridden with the `MIMALLOC_PURGE_DELAY` environment variable.
 71fn configure_allocator() {
 72    #[cfg(not(feature = "heap-profiling"))]
 73    if std::env::var_os(MIMALLOC_PURGE_DELAY_ENV).is_none() {
 74        mimalloc::options::option_set(mimalloc::options::MiOption::PurgeDelay, 0);
 75    }
 76}
 77
 78const DOIP_HEALTH_COMPONENT_KEY: &str = "doip";
 79
 80#[cfg(feature = "health")]
 81const MAIN_HEALTH_COMPONENT_KEY: &str = "main";
 82
 83#[derive(Subcommand, Debug)]
 84pub enum Command {
 85    /// Generate a reference TOML configuration file with all fields commented out
 86    GenerateConfig {
 87        /// Output file path (defaults to opensovd-cda.toml). Use "-" for stdout.
 88        #[arg(short, long)]
 89        output: Option<PathBuf>,
 90    },
 91}
 92
 93#[derive(Parser, Debug)]
 94#[command(version, about, long_about = None)]
 95pub struct AppArgs {
 96    #[arg(short, long, env = "CDA_CONFIG_FILE")]
 97    pub config: Option<PathBuf>,
 98
 99    #[command(subcommand)]
100    pub command: Option<Command>,
101
102    /// Directory with diagnostic databases to load, if none have
103    /// been loaded into storage before via the Update plugin.
104    #[arg(short = 'd', long)]
105    pub databases_dir: Option<String>,
106
107    #[arg(short, long)]
108    pub tester_address: Option<String>,
109
110    #[arg(long)]
111    pub tester_subnet: Option<String>,
112
113    #[arg(long)]
114    pub gateway_port: Option<u16>,
115
116    /// Protocol name used for com-param lookups
117    /// in the diagnostic database (matched case-insensitively).
118    /// Examples: `UDS_Ethernet_DoIP`, `UDS_Ethernet_DoIP_DOBT`
119    #[arg(long)]
120    pub protocol_name: Option<String>,
121
122    #[arg(long)]
123    pub listen_address: Option<String>,
124
125    #[arg(long)]
126    pub listen_port: Option<u16>,
127
128    /// Path to a Unix domain socket to bind the server to. Takes priority over
129    /// `listen_address`/`listen_port` when set.
130    #[arg(long)]
131    pub unix_socket: Option<String>,
132
133    #[arg(short, long)]
134    pub flash_files_path: Option<String>,
135
136    #[arg(long)]
137    pub file_logging: Option<bool>,
138
139    #[arg(long)]
140    pub log_file_dir: Option<String>,
141
142    #[arg(long)]
143    pub log_file_name: Option<String>,
144
145    #[arg(long)]
146    pub exit_no_database_loaded: Option<bool>,
147
148    #[arg(long)]
149    pub fallback_to_base_variant: Option<bool>,
150
151    /// Set to true, to rewrite mdd files without compression, which
152    /// reduces memory usage due to mmap significantly.
153    // Could use Action::SetFalse here, as the default is false but then we would have
154    // two different ways to set booleans (with and without `true`)
155    #[arg(long)]
156    pub mdd_decompress: Option<bool>,
157}
158
159impl AppArgs {
160    #[tracing::instrument(skip(self, config),
161        fields(
162            dlt_context = dlt_ctx!("MAIN"),
163        )
164    )]
165    pub fn update_config(self, config: &mut Configuration) {
166        if let Some(databases_dir) = self.databases_dir {
167            config.database.dir = databases_dir;
168        }
169        if let Some(exit_no_database_loaded) = self.exit_no_database_loaded {
170            config.database.exit_no_database_loaded = exit_no_database_loaded;
171        }
172        if let Some(fallback_to_base_variant) = self.fallback_to_base_variant {
173            config.database.fallback_to_base_variant = fallback_to_base_variant;
174        }
175        if let Some(flash_files_path) = self.flash_files_path {
176            config.flash_files_path = flash_files_path;
177        }
178        if let Some(tester_address) = self.tester_address {
179            config.doip.tester_address = tester_address;
180        }
181        if let Some(tester_subnet) = self.tester_subnet {
182            config.doip.tester_subnet = tester_subnet;
183        }
184        if let Some(gateway_port) = self.gateway_port {
185            config.doip.gateway_port = gateway_port;
186        }
187        if let Some(protocol_name) = self.protocol_name {
188            config.doip.protocol_name = protocol_name;
189        }
190        if self.listen_address.is_some() || self.listen_port.is_some() || self.unix_socket.is_some()
191        {
192            use crate::config::configfile::ServerTransport;
193
194            config.server = if let Some(unix_socket) = self.unix_socket {
195                // `--unix-socket` takes priority over `--listen-address`/`--listen-port`,
196                // matching the config file's documented priority.
197                ServerTransport::UnixSocket {
198                    unix_socket,
199                    address: crate::config::configfile::default_server_address(),
200                    port: crate::config::configfile::default_server_port(),
201                }
202            } else {
203                let mut address = config.server.address().to_owned();
204                let mut port = config.server.port();
205                if let Some(listen_address) = self.listen_address {
206                    address = listen_address;
207                }
208                if let Some(listen_port) = self.listen_port {
209                    port = listen_port;
210                }
211                ServerTransport::Tcp {
212                    address,
213                    port,
214                    unix_socket: None,
215                }
216            };
217        }
218
219        if let Some(file_logging) = self.file_logging {
220            config.logging.log_file_config.enabled = file_logging;
221        }
222        if let Some(log_file_dir) = self.log_file_dir {
223            config.logging.log_file_config.path = log_file_dir;
224        }
225        if let Some(log_file_name) = self.log_file_name {
226            config.logging.log_file_config.name = log_file_name;
227        }
228        if let Some(mdd_decompress) = self.mdd_decompress {
229            config.flat_buf.mdd_decompress = mdd_decompress;
230        }
231    }
232}
233
234/// Parse CLI arguments and start the CDA with the default startup flow.
235///
236/// # Errors
237/// Returns [`AppError`] if configuration loading, validation, or startup fails.
238pub async fn run_from_cli() -> Result<(), AppError> {
239    // Box is needed because it's a large future with a size of 16392 bytes
240    Box::pin(run(AppArgs::parse())).await
241}
242
243#[tracing::instrument(
244    skip(args, setup),
245    fields(
246        dlt_context = dlt_ctx!("MAIN"),
247    )
248)]
249/// Run the CDA from parsed CLI arguments with a custom [`Setup`].
250///
251/// This is the primary setup-aware entry point. Pass a [`Setup`] created with
252/// [`Setup::new`] and optionally configured with
253/// [`Setup::with_preload`] / [`Setup::with_update_plugin`].
254///
255/// # Errors
256/// Returns [`AppError`] if configuration loading, validation, or startup fails.
257pub async fn run_with_ext<SP, SL, UPB, CPB>(
258    args: AppArgs,
259    setup: Setup<SP, SL, UPB, CPB>,
260) -> Result<(), AppError>
261where
262    SP: SecurityPlugin,
263    SL: SecurityPluginLoader,
264    UPB: UpdatePluginBuilder<SP>,
265    CPB: CommunicationPluginBuilder,
266{
267    if let Some(Command::GenerateConfig { output }) = args.command.as_ref() {
268        // Exiting after generating config is on purpose.
269        return generate_config_cmd(output.as_ref());
270    }
271
272    configure_allocator();
273
274    let config_file = match &args.config {
275        Some(config_file) => {
276            if config_file.exists() {
277                config_file
278            } else {
279                // ignore `config-optional` feature here, because it's specifically for when no config was specified
280                return Err(AppError::ConfigurationError {
281                    message: format!(
282                        "Specified configuration file {} does not exist",
283                        config_file.display()
284                    ),
285                    source: None,
286                });
287            }
288        }
289        None => Path::new("opensovd-cda.toml"),
290    };
291
292    let (mut config, disk_loaded) = config::load_config_with_fallback(config_file);
293    if !disk_loaded {
294        config::require_config_source()?;
295    }
296
297    // Command line arguments always take precedence over file configuration.
298    args.update_config(&mut config);
299
300    config.validate_sanity().map_err(AppError::from)?;
301
302    run_with_ext_from_config(config, setup).await
303}
304
305/// Waits until a stage during startup is done or shutdown is requested.
306/// This makes sure we can cancel the startup.
307async fn await_startup_stage<T>(
308    stage: impl Future<Output = Result<T, AppError>>,
309    shutdown_signal: cda_interfaces::ShutdownSignal,
310) -> Result<Option<T>, AppError> {
311    tokio::select! {
312        result = stage => result.map(Some),
313        () = shutdown_signal => Ok(None),
314    }
315}
316
317/// Start the CDA runtime from a prepared configuration with a custom [`Setup`].
318///
319/// This is the setup-aware version of [`run_with_config`]. Supply a [`Setup`] to
320/// configure a custom update plugin and/or a preload hook:
321///
322/// ```rust,ignore
323/// use opensovd_cda_lib::{Setup, run_with_ext_from_config, update::update_plugin_fn};
324/// use opensovd_cda_lib::config::configfile::Configuration;
325///
326/// let config: Configuration = // ... load or construct ...
327/// # todo!();
328///
329/// run_with_ext_from_config::<MySecurityPlugin, MySecurityLoader, _, _>(
330///     config,
331///     Setup::new().with_update_plugin(update_plugin_fn(|infra| async move {
332///         Ok(MyPlugin::new(infra))
333///     })),
334/// ).await?;
335/// ```
336///
337/// # Errors
338/// Returns [`AppError`] if tracing setup, webserver startup, data loading, or route setup fails.
339pub async fn run_with_ext_from_config<SP, SL, UPB, CPB>(
340    config: Configuration,
341    setup: Setup<SP, SL, UPB, CPB>,
342) -> Result<(), AppError>
343where
344    SP: SecurityPlugin,
345    SL: SecurityPluginLoader,
346    UPB: UpdatePluginBuilder<SP>,
347    CPB: CommunicationPluginBuilder,
348{
349    let tracing_guards = if setup.initialize_tracing {
350        setup_tracing(&config)?
351    } else {
352        TracingGuards {
353            _file: None,
354            _otel: None,
355        }
356    };
357    tracing::info!("Starting CDA - version {}", cda_version());
358    // Emitted here rather than in `validate_sanity`, which runs before tracing is set up.
359    config
360        .database
361        .naming_convention
362        .warn_on_non_standard_categories();
363
364    let webserver_state = init_webserver(
365        &config,
366        setup.pre_load,
367        tracing_guards,
368        setup.shutdown_signal,
369    )
370    .await?;
371
372    tracing::debug!("Webserver is running. Loading SOVD routes...");
373
374    // After the webserver, so health is served while waiting for the storage.
375    // Before loading, so nothing reads from storage that is not recovered yet.
376    let Some(storage) = await_startup_stage(
377        initialize_storage(&config),
378        webserver_state.shutdown_signal.clone(),
379    )
380    .await?
381    else {
382        tracing::info!("Shutdown requested while waiting for storage, exiting cleanly");
383        return Ok(());
384    };
385
386    let Some(vehicle_data) = await_startup_stage(
387        vehicle::load_vehicle_data::<SP>(
388            &config,
389            webserver_state.health_state.as_ref(),
390            &storage,
391            setup.lock_priority_policy,
392        ),
393        webserver_state.shutdown_signal.clone(),
394    )
395    .await?
396    else {
397        tracing::info!("Shutdown requested during database load, exiting cleanly");
398        return Ok(());
399    };
400
401    if vehicle_data.databases.is_empty() && config.database.exit_no_database_loaded {
402        return Err(AppError::ResourceError(
403            "No database loaded, exiting as configured".to_string(),
404        ));
405    }
406
407    // Retained for the full server lifetime, so its event dispatcher keeps
408    // running until explicit shutdown.
409    let Some(communication_runtime) = await_startup_stage(
410        setup::setup_runtime_routes::<SP, SL, UPB, CPB>(
411            config,
412            vehicle_data,
413            &webserver_state,
414            setup.build_update_plugin,
415            setup.build_communication_plugin,
416            storage,
417        ),
418        webserver_state.shutdown_signal.clone(),
419    )
420    .await?
421    else {
422        tracing::info!("Shutdown requested during runtime setup, exiting cleanly");
423        return Ok(());
424    };
425
426    tracing::info!("CDA fully initialized and ready to serve requests");
427    if let Some(provider) = &webserver_state.main_health_provider {
428        provider.update_status(cda_health::Status::Up).await;
429    }
430
431    // signal readiness only once loading has finished, so a `Type=notify` unit
432    // doesn't consider the CDA started while it is still loading databases.
433    #[cfg(feature = "systemd-notify")]
434    cda_extra::notify_ready();
435
436    // Wait for shutdown signal
437    webserver_state.shutdown_signal.clone().await;
438    tracing::info!("Shutting down...");
439    cda_interfaces::Shutdown::shutdown(&*communication_runtime.plugin).await;
440    webserver_state.join().await
441}
442
443/// Run the CDA from parsed CLI arguments.
444///
445/// Uses the default security, lock-priority, and runtime update plugins.
446/// To customize startup behavior, use [`run_with_ext`] instead.
447///
448/// # Errors
449/// Returns [`AppError`] if configuration loading, validation, or startup fails.
450pub async fn run(args: AppArgs) -> Result<(), AppError> {
451    Box::pin(run_with_ext::<
452        DefaultSecurityPluginData,
453        DefaultSecurityPlugin,
454        _,
455        _,
456    >(
457        args,
458        Setup::new().with_update_plugin(update_plugin_fn(|infra| async move {
459            create_default_update_plugin::<DefaultSecurityPluginData, DefaultSecurityPlugin>(infra)
460                .await
461        })),
462    ))
463    .await
464}
465
466/// Start the CDA runtime from a prepared configuration.
467///
468/// Uses the default security, lock-priority, and runtime update plugins.
469/// To customize startup behavior, use [`run_with_ext_from_config`] instead.
470///
471/// # Errors
472/// Returns [`AppError`] if tracing setup, webserver startup, data loading, or route setup fails.
473pub async fn run_with_config(config: Configuration) -> Result<(), AppError> {
474    Box::pin(run_with_ext_from_config::<
475        DefaultSecurityPluginData,
476        DefaultSecurityPlugin,
477        _,
478        _,
479    >(
480        config,
481        Setup::new().with_update_plugin(update_plugin_fn(
482            |infra: setup::CdaRuntime<DefaultSecurityPluginData>| async move {
483                create_default_update_plugin::<DefaultSecurityPluginData, DefaultSecurityPlugin>(
484                    infra,
485                )
486                .await
487            },
488        )),
489    ))
490    .await
491}
492
493async fn init_webserver(
494    config: &Configuration,
495    pre_load: Option<PreLoadHook>,
496    tracing_guards: TracingGuards,
497    shutdown_signal: Option<cda_interfaces::ShutdownSignal>,
498) -> Result<ApplicationState, AppError> {
499    // Vendor overrides are registered via `linkme` distributed slices, whose
500    // final contents are only known after linking; this checks that at most
501    // one override is linked in per overridable function before any of them
502    // are used. Every crate that defines vendor-overridable functions must
503    // be listed here.
504    if let Err(errors) = cda_core::validate_vendor_overrides() {
505        return Err(AppError::InitializationFailed(format!(
506            "Vendor override configuration error(s): {}",
507            errors.join("; ")
508        )));
509    }
510
511    let webserver_config = match config.server.clone() {
512        crate::config::configfile::ServerTransport::Tcp { address, port, .. } => {
513            cda_sovd::WebServerConfig::Tcp {
514                host: address,
515                port,
516            }
517        }
518        crate::config::configfile::ServerTransport::UnixSocket { unix_socket, .. } => {
519            cda_sovd::WebServerConfig::UnixSocket { path: unix_socket }
520        }
521    };
522
523    let clonable_shutdown_signal = shutdown_signal
524        .unwrap_or_else(|| cda_interfaces::shutdown_signal(crate::shutdown_signal()));
525
526    let (dynamic_router, webserver_task) =
527        cda_sovd::launch_webserver(webserver_config.clone(), clonable_shutdown_signal.clone())
528            .await?;
529
530    let mut webserver_state = ApplicationState {
531        _tracing_guards: tracing_guards,
532        dynamic_router,
533        webserver_task: Some(webserver_task),
534        shutdown_signal: clonable_shutdown_signal,
535        health_state: None,
536        main_health_provider: None,
537    };
538
539    #[cfg(feature = "health")]
540    let (health_state, main_health_provider) = if config.health.enabled {
541        let health_state = cda_health::add_health_routes(
542            &webserver_state.dynamic_router,
543            cda_version().to_owned(),
544        )
545        .await;
546        let main_health_provider = Arc::new(cda_health::StatusHealthProvider::new(
547            cda_health::Status::Starting,
548        ));
549        let registration = health_state
550            .register_provider(
551                MAIN_HEALTH_COMPONENT_KEY,
552                Arc::clone(&main_health_provider) as Arc<dyn cda_health::HealthProvider>,
553            )
554            .await
555            .map_err(|e| AppError::InitializationFailed(e.to_string()));
556        registration?;
557        (Some(health_state), Some(main_health_provider))
558    } else {
559        (None, None)
560    };
561
562    #[cfg(not(feature = "health"))]
563    let (health_state, main_health_provider): (
564        Option<cda_health::HealthState>,
565        Option<Arc<cda_health::StatusHealthProvider>>,
566    ) = (None, None);
567
568    webserver_state.health_state = health_state;
569    webserver_state.main_health_provider = main_health_provider;
570
571    #[cfg(feature = "systemd-notify")]
572    let _sd_notify_task = cda_extra::create_sd_notify_task(
573        webserver_state.health_state.clone(),
574        webserver_state.shutdown_signal.clone(),
575    );
576
577    register_version_endpoints(&webserver_state.dynamic_router).await;
578    cda_sovd::add_openapi_routes(&webserver_state.dynamic_router).await;
579
580    if let Some(hook) = pre_load {
581        hook(webserver_state.dynamic_router.clone()).await?;
582    }
583
584    Ok(webserver_state)
585}
586
587async fn register_version_endpoints(dynamic_router: &cda_sovd::dynamic_router::DynamicRouter) {
[docs]588    // [[ dimpl~sovd-api-version-endpoint, Register Version Endpoint ]]
589    let serde_json::Value::Object(version_info) = serde_json::json!({
590        "id": "version",
591        "data": {
592            "name": "Eclipse OpenSOVD Classic Diagnostic Adapter",
593            "api": {
594                "version": "1.1"
595            },
596            "implementation": {
597                "version": cda_version(),
598                "commit": env!("GIT_COMMIT_HASH").to_owned(),
599                "build_date": env!("BUILD_DATE").to_owned(),
600            }
601        }
602    }) else {
603        tracing::error!("Failed to build version information");
604        return;
605    };
606    cda_sovd::add_static_data_endpoint(
607        dynamic_router,
608        version_info.clone(),
609        "/vehicle/v15/apps/sovd2uds/data/version",
610    )
611    .await;
612    cda_sovd::add_static_data_endpoint(dynamic_router, version_info, "/vehicle/v15/data/version")
613        .await;
614}
615
616/// Collected webserver state produced by [`init_webserver`].
617///
618/// Passed to [`setup::setup_runtime_routes`] and [`run_with_ext_from_config`] so that
619/// the shutdown signal and health provider are accessible after the webserver is started.
620///
621/// Dropping this value aborts the webserver task, so all error paths are covered
622/// automatically without any explicit cleanup calls.
623pub(crate) struct ApplicationState {
624    _tracing_guards: TracingGuards,
625    pub dynamic_router: cda_sovd::dynamic_router::DynamicRouter,
626    webserver_task: Option<tokio::task::JoinHandle<()>>,
627    pub shutdown_signal: cda_interfaces::ShutdownSignal,
628    health_state: Option<cda_health::HealthState>,
629    main_health_provider: Option<Arc<cda_health::StatusHealthProvider>>,
630}
631
632impl Drop for ApplicationState {
633    fn drop(&mut self) {
634        if let Some(task) = self.webserver_task.take() {
635            task.abort();
636        }
637    }
638}
639
640impl ApplicationState {
641    /// Waits for the normally signaled webserver task to finish.
642    async fn join(mut self) -> Result<(), AppError> {
643        if let Some(task) = self.webserver_task.take() {
644            task.await
645                .map_err(|e| AppError::RuntimeError(format!("Webserver task join error: {e}")))?;
646        }
647        Ok(())
648    }
649}
650
651/// # Panics
652/// Panics if the OS signal handlers cannot be installed.
653pub async fn shutdown_signal() {
654    let ctrl_c = async {
655        tokio::signal::ctrl_c()
656            .await
657            .expect("failed to install Ctrl+C handler");
658    };
659
660    #[cfg(unix)]
661    let terminate = async {
662        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
663            .expect("failed to install signal handler")
664            .recv()
665            .await;
666    };
667
668    #[cfg(not(unix))]
669    let terminate = std::future::pending::<()>();
670
671    tokio::select! {
672        () = ctrl_c => {},
673        () = terminate => {},
674    }
675}
676
677pub struct TracingGuards {
678    _file: Option<TracingWorkerGuard>,
679    _otel: Option<OtelGuard>,
680}
681
682/// # Errors
683/// Returns [`TracingSetupError`] if subscriber or exporter initialization fails.
684pub fn setup_tracing(config: &Configuration) -> Result<TracingGuards, TracingSetupError> {
685    let tracing = cda_tracing::new();
686    let mut layers = vec![];
687    layers.push(cda_tracing::new_term_subscriber(&config.logging));
688    #[cfg(feature = "tokio-tracing")]
689    layers.push(cda_tracing::new_tokio_tracing(
690        &config.logging.tokio_tracing,
691    )?);
692    let otel_guard = if config.logging.otel.enabled {
693        println!(
694            "Starting OpenTelemetry tracing with {}",
695            config.logging.otel.endpoint
696        );
697        let (guard, metrics_layer, otel_layer) =
698            cda_tracing::new_otel_subscriber(&config.logging.otel)?;
699        layers.push(metrics_layer);
700        layers.push(otel_layer);
701        Some(guard)
702    } else {
703        None
704    };
705
706    let file_guard = if config.logging.log_file_config.enabled {
707        let (guard, file_layer) =
708            cda_tracing::new_file_subscriber(&config.logging.log_file_config)?;
709        layers.push(file_layer);
710        Some(guard)
711    } else {
712        None
713    };
714
715    #[cfg(feature = "dlt-tracing")]
716    if config.logging.dlt_tracing.enabled {
717        layers.push(cda_tracing::new_dlt_tracing(&config.logging.dlt_tracing)?);
718    }
719
720    cda_tracing::init_tracing(tracing.with(layers))?;
721    Ok(TracingGuards {
722        _file: file_guard,
723        _otel: otel_guard,
724    })
725}
726
727/// Opens the storage, retrying while its directory does not exist yet, e.g.
728/// because the storage is not mounted yet.
729///
730/// Opening recovers an interrupted transaction, so that must not happen before
731/// the storage is there: there would be nothing to recover, and the storage
732/// would later be used in an indeterminate state.
733///
734/// # Errors
735/// Returns [`AppError::InitializationFailed`] if the storage directory does not
736/// appear within the retry budget, or opening fails otherwise, e.g. because
737/// recovery fails.
738async fn initialize_storage(config: &Configuration) -> Result<Arc<LocalStorage>, AppError> {
739    let storage_dir = &config.runtime_update_config.storage_dir;
740    let delay = Duration::from_millis(config.runtime_update_config.storage_dir_load_retry_delay_ms);
741    let attempts = config.runtime_update_config.storage_dir_load_retry_attempts;
742
743    let storage = (async || LocalStorage::new(storage_dir))
744        .retry(
745            backon::ConstantBuilder::new()
746                .with_delay(delay)
747                .with_max_times(attempts),
748        )
749        .when(|error| {
750            matches!(error, StorageError::Io(e) if e.kind() == std::io::ErrorKind::NotFound)
751        })
752        .notify(|error, delay: Duration| {
753            tracing::warn!(
754                storage_dir = %storage_dir,
755                error = %error,
756                "Storage not available, retrying in {delay:?}"
757            );
758        })
759        .await
760        .map_err(|e| {
761            AppError::InitializationFailed(format!(
762                "Failed to open storage {storage_dir}: {e}"
763            ))
764        })?;
765
766    Ok(Arc::new(storage))
767}
768
769/// Returns the CDA version string, which is either
770/// the value of the `CDA_VERSION` environment variable (if set)
771/// or the Cargo package version.
772#[must_use]
773pub fn cda_version() -> &'static str {
774    option_env!("CDA_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
775}
776
777#[cfg(test)]
778mod cli_args_tests {
779    use clap::Parser;
780
781    use super::*;
782
783    #[test]
784    fn unix_socket_flag_updates_server_config() {
785        let args = AppArgs::parse_from(["cda", "--unix-socket", "/run/cda.sock"]);
786        let mut config = crate::config::configfile::Configuration::default();
787        args.update_config(&mut config);
788
789        assert!(matches!(
790            config.server,
791            crate::config::configfile::ServerTransport::UnixSocket { ref unix_socket, .. }
792                if unix_socket == "/run/cda.sock"
793        ));
794    }
795
796    #[test]
797    fn without_unix_socket_flag_server_config_unix_socket_stays_none() {
798        let args = AppArgs::parse_from(["cda"]);
799        let mut config = crate::config::configfile::Configuration::default();
800        args.update_config(&mut config);
801
802        assert!(matches!(
803            config.server,
804            crate::config::configfile::ServerTransport::Tcp {
805                unix_socket: None,
806                ..
807            }
808        ));
809    }
810}
811
812#[cfg(test)]
813mod webserver_lifecycle_tests {
814    use super::*;
815
816    #[tokio::test]
817    async fn startup_stage_is_cancelled_when_shutdown_arrives() {
818        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
819        let shutdown_signal = cda_interfaces::shutdown_signal(async move {
820            let _ = shutdown_rx.await;
821        });
822        shutdown_tx.send(()).unwrap();
823
824        let result = await_startup_stage(
825            std::future::pending::<Result<(), AppError>>(),
826            shutdown_signal,
827        )
828        .await
829        .unwrap();
830
831        assert!(result.is_none());
832    }
833
834    #[tokio::test]
835    async fn startup_stage_completes_before_shutdown() {
836        let result = await_startup_stage(
837            std::future::ready(Ok(42)),
838            cda_interfaces::shutdown_signal(std::future::pending()),
839        )
840        .await
841        .unwrap();
842
843        assert_eq!(result, Some(42));
844    }
845
846    #[tokio::test]
847    async fn drop_aborts_webserver_task() {
848        let task = tokio::spawn(std::future::pending::<()>());
849        let abort_handle = task.abort_handle();
850
851        let state = ApplicationState {
852            _tracing_guards: TracingGuards {
853                _file: None,
854                _otel: None,
855            },
856            dynamic_router: cda_sovd::dynamic_router::DynamicRouter::new(),
857            webserver_task: Some(task),
858            shutdown_signal: cda_interfaces::shutdown_signal(std::future::pending()),
859            health_state: None,
860            main_health_provider: None,
861        };
862
863        drop(state);
864
865        // abort() is asynchronous; yield to let the cancellation propagate.
866        tokio::task::yield_now().await;
867        assert!(abort_handle.is_finished());
868    }
869
870    #[tokio::test]
871    async fn cda_stays_running_when_no_database_loaded_and_exit_flag_is_false() {
872        fn available_port() -> u16 {
873            std::net::TcpListener::bind("127.0.0.1:0")
874                .expect("Bind temporary port")
875                .local_addr()
876                .expect("Read temporary port")
877                .port()
878        }
879
880        let database_dir = tempfile::tempdir().expect("Create empty database directory");
881        let storage_dir = tempfile::tempdir().expect("Create empty storage directory");
882        let mut config = Configuration::default();
883        config.server = crate::config::configfile::ServerTransport::Tcp {
884            address: "0.0.0.0".to_owned(),
885            port: available_port(),
886            unix_socket: None,
887        };
888        config.doip.tester_address = "127.0.0.1".to_owned();
889        config.doip.gateway_port = available_port();
890        config.database.dir = database_dir.path().to_string_lossy().into_owned();
891        config.database.exit_no_database_loaded = false;
892        config.runtime_update_config.storage_dir =
893            storage_dir.path().to_string_lossy().into_owned();
894
895        let task = tokio::spawn(run_with_ext_from_config(
896            config,
897            Setup::<DefaultSecurityPluginData, DefaultSecurityPlugin>::new()
898                .with_existing_tracing()
899                .with_update_plugin(update_plugin_fn(
900                    |infra: setup::CdaRuntime<DefaultSecurityPluginData>| async move {
901                        create_default_update_plugin::<
902                            DefaultSecurityPluginData,
903                            DefaultSecurityPlugin,
904                        >(infra)
905                        .await
906                    },
907                )),
908        ));
909
910        cda_interfaces::util::tokio_ext::sleep_for(Duration::from_millis(250)).await;
911        assert!(
912            !task.is_finished(),
913            "CDA exited without any loaded database"
914        );
915
916        task.abort();
917        task.await.expect_err("CDA task must be cancelled");
918    }
919}
920
921#[cfg(test)]
922mod storage_init_tests {
923    use super::*;
924
925    fn config_for(storage_dir: &Path, attempts: usize) -> Configuration {
926        let mut config = Configuration::default();
927        config.runtime_update_config.storage_dir = storage_dir.to_string_lossy().into_owned();
928        config.runtime_update_config.storage_dir_load_retry_attempts = attempts;
929        config.runtime_update_config.storage_dir_load_retry_delay_ms = 10;
930        config
931    }
932
933    #[tokio::test]
934    async fn recovers_storage_that_appears_after_startup() {
935        let mount_point = tempfile::tempdir().unwrap();
936        let storage_dir = mount_point.path().join("cda");
937        let config = config_for(&storage_dir, 100);
938
939        // Mounted late, carrying leftovers of an interrupted transaction.
940        let mount = {
941            let storage_dir = storage_dir.clone();
942            tokio::spawn(async move {
943                cda_interfaces::util::tokio_ext::sleep_for(Duration::from_millis(50)).await;
944                let staging = storage_dir.join("journal").join("staging");
945                std::fs::create_dir_all(&staging).unwrap();
946                std::fs::write(staging.join("orphan.tmp"), b"partial").unwrap();
947            })
948        };
949
950        let storage = initialize_storage(&config).await;
951        mount.await.unwrap();
952
953        assert!(storage.is_ok());
954        assert!(
955            !storage_dir.join("journal/staging/orphan.tmp").exists(),
956            "recovery must run on the storage once it is present"
957        );
958    }
959
960    #[tokio::test]
961    async fn storage_that_never_appears_fails_startup() {
962        let mount_point = tempfile::tempdir().unwrap();
963        let config = config_for(&mount_point.path().join("cda"), 2);
964
965        let storage = initialize_storage(&config).await;
966
967        assert!(matches!(storage, Err(AppError::InitializationFailed(_))));
968    }
969}