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}