1/*
  2 * SPDX-License-Identifier: Apache-2.0
  3 * SPDX-FileCopyrightText: 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS)
  4 *
  5 * See the NOTICE file(s) distributed with this work for additional
  6 * information regarding copyright ownership.
  7 *
  8 * This program and the accompanying materials are made available under the
  9 * terms of the Apache License Version 2.0 which is available at
 10 * https://www.apache.org/licenses/LICENSE-2.0
 11 */
 12
 13use std::{path::PathBuf, sync::Arc};
 14
 15use cda_core::DiagServiceResponseStruct;
 16use cda_interfaces::dlt_ctx;
 17use cda_plugin_security::{DefaultSecurityPlugin, DefaultSecurityPluginData};
 18use clap::{Parser, Subcommand};
 19use futures::future::FutureExt;
 20use opensovd_cda_lib::{
 21    AppError, cda_version,
 22    config::configfile::{ConfigSanity, Configuration},
 23    setup_tracing, shutdown_signal,
 24};
 25
 26#[cfg(feature = "health")]
 27const MAIN_HEALTH_COMPONENT_KEY: &str = "main";
 28
 29#[derive(Subcommand, Debug)]
 30enum Command {
 31    /// Generate a reference TOML configuration file with all fields commented out
 32    GenerateConfig {
 33        /// Output file path (defaults to opensovd-cda.toml). Use "-" for stdout.
 34        #[arg(short, long)]
 35        output: Option<PathBuf>,
 36    },
 37}
 38
 39#[derive(Parser, Debug)]
 40#[command(version, about, long_about = None)]
 41struct AppArgs {
 42    #[arg(short, long, env = "CDA_CONFIG_FILE")]
 43    config: Option<String>,
 44
 45    #[command(subcommand)]
 46    command: Option<Command>,
 47
 48    #[arg(short, long)]
 49    databases_path: Option<String>,
 50
 51    #[arg(short, long)]
 52    tester_address: Option<String>,
 53
 54    #[arg(long)]
 55    tester_subnet: Option<String>,
 56
 57    #[arg(long)]
 58    gateway_port: Option<u16>,
 59
 60    /// Protocol name used for com-param lookups
 61    /// in the diagnostic database (matched case-insensitively).
 62    /// Examples: `UDS_Ethernet_DoIP`, `UDS_Ethernet_DoIP_DOBT`
 63    #[arg(long)]
 64    protocol_name: Option<String>,
 65
 66    #[arg(long)]
 67    listen_address: Option<String>,
 68
 69    #[arg(long)]
 70    listen_port: Option<u16>,
 71
 72    #[arg(short, long)]
 73    flash_files_path: Option<String>,
 74
 75    #[arg(long)]
 76    file_logging: Option<bool>,
 77
 78    #[arg(long)]
 79    log_file_dir: Option<String>,
 80
 81    #[arg(long)]
 82    log_file_name: Option<String>,
 83
 84    #[arg(long)]
 85    exit_no_database_loaded: Option<bool>,
 86
 87    #[arg(long)]
 88    fallback_to_base_variant: Option<bool>,
 89
 90    /// Set to true, to rewrite mdd files without compression, which
 91    /// reduces memory usage due to mmap significantly.
 92    // Could use Action::SetFalse here, as the default is false but then we would have
 93    // two different ways to set booleans (with and without `true`)
 94    #[arg(long)]
 95    mdd_decompress: Option<bool>,
 96}
 97
 98#[tokio::main]
 99#[tracing::instrument(
100    fields(
101        dlt_context = dlt_ctx!("MAIN"),
102    )
103)]
104async fn main() -> Result<(), AppError> {
105    let args = AppArgs::parse();
106    if let Some(Command::GenerateConfig { ref output }) = args.command {
107        // Exiting after generating config is on purpose
108        return generate_config_cmd(output.as_ref());
109    }
110
111    let mut config =
112        opensovd_cda_lib::config::load_config(args.config.as_deref()).unwrap_or_else(|e| {
113            println!("Failed to load configuration: {e}");
114            println!("Using default values");
115            opensovd_cda_lib::config::default_config()
116        });
117    config.validate_sanity()?;
118
119    args.update_config(&mut config);
120
121    let _tracing_guards = setup_tracing(&config)?;
122    tracing::info!("Starting CDA - version {}", cda_version());
123
124    let webserver_config = cda_sovd::WebServerConfig {
125        host: config.server.address.clone(),
126        port: config.server.port,
127    };
128
129    let clonable_shutdown_signal = shutdown_signal().shared();
130
131    let (dynamic_router, webserver_task) =
132        cda_sovd::launch_webserver(webserver_config.clone(), clonable_shutdown_signal.clone())
133            .await?;
134
135    #[cfg(feature = "health")]
136    let (health_state, main_health_provider) = if config.health.enabled {
137        let health_state =
138            cda_health::add_health_routes(&dynamic_router, cda_version().to_owned()).await;
139        let main_health_provider = Arc::new(cda_health::StatusHealthProvider::new(
140            cda_health::Status::Starting,
141        ));
142
143        if let Err(e) = health_state
144            .register_provider(
145                MAIN_HEALTH_COMPONENT_KEY,
146                Arc::clone(&main_health_provider) as Arc<dyn cda_health::HealthProvider>,
147            )
148            .await
149        {
150            tracing::warn!(error = %e, "Failed to register main health provider");
151        }
152        (Some(health_state), Some(main_health_provider))
153    } else {
154        (None, None)
155    };
156
157    #[cfg(not(feature = "health"))]
158    let (health_state, main_health_provider): (
159        Option<cda_health::HealthState>,
160        Option<Arc<cda_health::StatusHealthProvider>>,
161    ) = (None, None);
162
163    #[cfg(feature = "systemd-notify")]
164    let _sd_notify_task =
165        cda_extra::create_sd_notify_task(health_state.clone(), clonable_shutdown_signal.clone());
166
167    tracing::debug!("Webserver is running. Loading sovd routes...");
168
169    let vehicle_data = match opensovd_cda_lib::load_vehicle_data::<_, DefaultSecurityPluginData>(
170        &config,
171        clonable_shutdown_signal.clone(),
172        health_state.as_ref(),
173    )
174    .await
175    {
176        Ok(data) => data,
177        Err(AppError::ShutdownRequested) => {
178            tracing::info!("Shutdown requested during database load, exiting cleanly");
179            return Ok(());
180        }
181        Err(e) => return Err(e),
182    };
183
184    if vehicle_data.databases.is_empty() && config.database.exit_no_database_loaded {
185        return Err(AppError::ResourceError(
186            "No database loaded, exiting as configured".to_string(),
187        ));
188    }
189
190    cda_sovd::add_vehicle_routes::<DiagServiceResponseStruct, _, _, DefaultSecurityPlugin>(
191        &dynamic_router,
192        vehicle_data.uds_manager,
193        config.flash_files_path.clone(),
194        vehicle_data.file_managers,
195        vehicle_data.locks,
196        config.functional_description,
197        config.components,
198    )
199    .await?;
200
[docs]201    // [[ dimpl~sovd-api-version-endpoint, Register Version Endpoint ]]
202    if let serde_json::Value::Object(version_info) = serde_json::json!({
203        "id": "version",
204        "data": {
205            "name": "Eclipse OpenSOVD Classic Diagnostic Adapter",
206            "api": {
207                // 1.1 to match the sovd standard version
208                "version": "1.1"
209            },
210            "implementation": {
211                "version": cda_version(),
212                "commit": env!("GIT_COMMIT_HASH").to_owned(),
213                "build_date": env!("BUILD_DATE").to_owned(),
214            }
215        }
216    }) {
217        cda_sovd::add_static_data_endpoint(
218            &dynamic_router,
219            version_info.clone(),
220            "/vehicle/v15/apps/sovd2uds/data/version",
221        )
222        .await;
223        // For now, both version endpoints serve the same data. This might change in the future.
224        cda_sovd::add_static_data_endpoint(
225            &dynamic_router,
226            version_info,
227            "/vehicle/v15/data/version",
228        )
229        .await;
230    } else {
231        tracing::error!("Failed to build version information");
232    }
233
234    cda_sovd::add_openapi_routes(&dynamic_router, &webserver_config).await;
235
236    tracing::info!("CDA fully initialized and ready to serve requests");
237    if let Some(provider) = main_health_provider {
238        provider.update_status(cda_health::Status::Up).await;
239    }
240
241    // Wait for shutdown signal
242    clonable_shutdown_signal.await;
243    tracing::info!("Shutting down...");
244    webserver_task
245        .await
246        .map_err(|e| AppError::RuntimeError(format!("Webserver task join error: {e}")))?;
247
248    Ok(())
249}
250
251fn generate_config_cmd(output: Option<&PathBuf>) -> Result<(), AppError> {
252    let content = opensovd_cda_lib::config::generate::generate_reference_config()
253        .map_err(|e| AppError::RuntimeError(format!("Failed to generate config: {e}")))?;
254
255    match output.map(|p| p.as_os_str()) {
256        Some(p) if p == "-" => {
257            use std::io::Write;
258            std::io::stdout()
259                .write_all(content.as_bytes())
260                .map_err(|e| AppError::RuntimeError(format!("Failed to write stdout: {e}")))?;
261        }
262        Some(path) => {
263            std::fs::write(path, &content)
264                .map_err(|e| AppError::RuntimeError(format!("Failed to write config: {e}")))?;
265        }
266        None => {
267            std::fs::write("opensovd-cda.toml", &content)
268                .map_err(|e| AppError::RuntimeError(format!("Failed to write config: {e}")))?;
269        }
270    }
271    Ok(())
272}
273
274impl AppArgs {
275    #[tracing::instrument(skip(self, config),
276        fields(
277            dlt_context = dlt_ctx!("MAIN"),
278        )
279    )]
280    fn update_config(self, config: &mut Configuration) {
281        if let Some(databases_path) = self.databases_path {
282            config.database.path = databases_path;
283        }
284        if let Some(exit_no_database_loaded) = self.exit_no_database_loaded {
285            config.database.exit_no_database_loaded = exit_no_database_loaded;
286        }
287        if let Some(fallback_to_base_variant) = self.fallback_to_base_variant {
288            config.database.fallback_to_base_variant = fallback_to_base_variant;
289        }
290        if let Some(flash_files_path) = self.flash_files_path {
291            config.flash_files_path = flash_files_path;
292        }
293        if let Some(tester_address) = self.tester_address {
294            config.doip.tester_address = tester_address;
295        }
296        if let Some(tester_subnet) = self.tester_subnet {
297            config.doip.tester_subnet = tester_subnet;
298        }
299        if let Some(gateway_port) = self.gateway_port {
300            config.doip.gateway_port = gateway_port;
301        }
302        if let Some(protocol_name) = self.protocol_name {
303            config.doip.protocol_name = protocol_name;
304        }
305        if let Some(listen_address) = self.listen_address {
306            config.server.address = listen_address;
307        }
308        if let Some(listen_port) = self.listen_port {
309            config.server.port = listen_port;
310        }
311        if let Some(file_logging) = self.file_logging {
312            config.logging.log_file_config.enabled = file_logging;
313        }
314        if let Some(log_file_dir) = self.log_file_dir {
315            config.logging.log_file_config.path = log_file_dir;
316        }
317        if let Some(log_file_name) = self.log_file_name {
318            config.logging.log_file_config.name = log_file_name;
319        }
320        if let Some(mdd_decompress) = self.mdd_decompress {
321            config.flat_buf.mdd_decompress = mdd_decompress;
322        }
323    }
324}