1use super::config::{get_gear_runtime_config, render_gear_config_for_oop};
2use super::host::{init_logging_unified, init_panic_tracing, normalize_path};
3use super::{AppConfig, RuntimeKind};
4use crate::backends::LocalProcessBackend;
5use crate::runtime::{
6 DbOptions, OopGearSpawnConfig, OopSpawnOptions, RunOptions, ShutdownOptions, run, shutdown,
7};
8use anyhow::Result;
9use figment::Figment;
10use figment::providers::Serialized;
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13use tokio_util::sync::CancellationToken;
14
15fn spawn_signal_handler(cancel: CancellationToken, context: &str) {
20 let context_owned = context.to_owned();
21 tokio::spawn(async move {
22 match shutdown::wait_for_shutdown().await {
23 Ok(()) => {
24 tracing::info!(target: "", "------------------");
25 tracing::info!("{}: shutdown signal received", context_owned);
26 }
27 Err(e) => {
28 tracing::warn!(
29 error = %e,
30 "{}: signal handler failed, falling back to ctrl_c()",
31 context_owned
32 );
33 _ = tokio::signal::ctrl_c().await;
34 }
35 }
36 cancel.cancel();
37 });
38}
39
40pub async fn run_server(config: AppConfig) -> Result<()> {
51 init_procedure(&config).map_err(|e| {
52 tracing::error!(error = %e, "Initialization failed");
53 e
54 })?;
55 tracing::info!("Initializing gears...");
56
57 let instance_id = uuid::Uuid::new_v4();
60 tracing::info!(instance_id = %instance_id, "Generated process instance ID");
61
62 let cancel = CancellationToken::new();
65
66 spawn_signal_handler(cancel.clone(), "server");
69
70 let db_options = resolve_db_options(&config)?;
72
73 let oop_backend = LocalProcessBackend::new(cancel.clone());
75
76 let oop_options = build_oop_spawn_options(&config, oop_backend)?;
78
79 let run_options = RunOptions::new(
93 Arc::new(config),
94 db_options,
95 ShutdownOptions::Token(cancel.clone()),
96 instance_id,
97 )
98 .with_oop(oop_options);
99
100 let result = run(run_options).await;
101
102 #[cfg(feature = "otel")]
104 tracing_shutdown();
105
106 result
107}
108
109pub async fn run_migrate(config: AppConfig) -> Result<()> {
133 init_procedure(&config).map_err(|e| {
134 tracing::error!(error = %e, "Initialization failed");
135 e
136 })?;
137 tracing::info!("Starting migration mode...");
138
139 let instance_id = uuid::Uuid::new_v4();
141 tracing::info!(instance_id = %instance_id, "Generated migration instance ID");
142
143 let cancel = CancellationToken::new();
145
146 spawn_signal_handler(cancel.clone(), "migration");
148
149 let db_options = resolve_db_options(&config)?;
151
152 if matches!(db_options, DbOptions::None) {
154 anyhow::bail!("Cannot run migrations: no database configuration found");
155 }
156
157 let registry = crate::registry::GearRegistry::discover_and_build()?;
159 tracing::info!(
160 gear_count = registry.gears().len(),
161 "Discovered gears for migration"
162 );
163
164 let host = crate::runtime::HostRuntime::new(
166 registry,
167 Arc::new(config),
168 db_options,
169 Arc::new(crate::client_hub::ClientHub::new()),
170 cancel,
171 instance_id,
172 None, );
174
175 let result = host.run_migration_phases().await;
177
178 #[cfg(feature = "otel")]
180 tracing_shutdown();
181
182 result?;
183
184 tracing::info!("All migrations completed successfully");
185 Ok(())
186}
187
188fn resolve_db_options(config: &AppConfig) -> Result<DbOptions> {
189 if config.database.is_none() {
190 tracing::warn!("No global database section found; running without databases");
191 return Ok(DbOptions::None);
192 }
193
194 tracing::info!("Using DbManager with Figment-based configuration");
195 let figment = Figment::new().merge(Serialized::defaults(config));
196 let db_manager = Arc::new(toolkit_db::DbManager::from_figment(
197 figment,
198 config.server.home_dir.clone(),
199 )?);
200 Ok(DbOptions::Manager(db_manager))
201}
202
203fn build_oop_spawn_options(
208 config: &AppConfig,
209 backend: LocalProcessBackend,
210) -> Result<Option<OopSpawnOptions>> {
211 let home_dir = PathBuf::from(&config.server.home_dir);
212 let mut gears = Vec::new();
213
214 for gear_name in config.gears.keys() {
215 if let Some(spawn_config) = try_build_oop_gear_config(config, gear_name, &home_dir)? {
216 gears.push(spawn_config);
217 }
218 }
219
220 if gears.is_empty() {
221 Ok(None)
222 } else {
223 tracing::info!(count = gears.len(), "Prepared OoP gears for spawning");
224 Ok(Some(OopSpawnOptions {
225 gears,
226 backend: Box::new(backend),
227 }))
228 }
229}
230
231fn try_build_oop_gear_config(
233 config: &AppConfig,
234 gear_name: &str,
235 home_dir: &Path,
236) -> Result<Option<OopGearSpawnConfig>> {
237 let Some(runtime_cfg) = get_gear_runtime_config(config, gear_name)? else {
238 return Ok(None);
239 };
240
241 if !matches!(runtime_cfg.mod_type, RuntimeKind::Oop) {
242 return Ok(None);
243 }
244
245 let exec_cfg = runtime_cfg.execution.as_ref().ok_or_else(|| {
246 anyhow::anyhow!("gear '{gear_name}' is type=oop but execution config is missing")
247 })?;
248
249 let binary = normalize_path(&exec_cfg.executable_path)?;
250 let spawn_args = exec_cfg.args.clone();
251 let env = exec_cfg.environment.clone();
252
253 let rendered_config = render_gear_config_for_oop(config, gear_name, home_dir)?;
255 let rendered_json = rendered_config.to_json()?;
256
257 tracing::debug!(
258 gear = %gear_name,
259 "Prepared OoP gear config: db={}",
260 rendered_config.database.is_some()
261 );
262
263 Ok(Some(OopGearSpawnConfig {
264 gear_name: gear_name.to_owned(),
265 binary,
266 args: spawn_args,
267 env,
268 working_directory: exec_cfg.working_directory.clone(),
269 rendered_config_json: rendered_json,
270 }))
271}
272
273#[cfg_attr(not(feature = "otel"), allow(clippy::unnecessary_wraps))]
298pub fn init_procedure(config: &AppConfig) -> Result<()> {
299 super::init_crypto_provider()?;
303
304 #[cfg(feature = "otel")]
306 let otel_layer = if config.opentelemetry.tracing.enabled {
307 Some(crate::telemetry::init::init_tracing(&config.opentelemetry)?)
308 } else {
309 None
310 };
311 #[cfg(not(feature = "otel"))]
312 let otel_layer = None;
313
314 init_logging_unified(&config.logging, &config.server.home_dir, otel_layer);
316
317 init_panic_tracing();
319
320 #[cfg(feature = "otel")]
322 if let Err(e) = crate::telemetry::init::init_metrics_provider(&config.opentelemetry) {
323 tracing::error!(error = %e, "OpenTelemetry metrics not initialized");
324 }
325
326 #[cfg(feature = "otel")]
328 if config.opentelemetry.tracing.enabled
329 && let Err(e) = crate::telemetry::init::otel_connectivity_probe(&config.opentelemetry)
330 {
331 tracing::error!(error = %e, "OTLP connectivity probe failed");
332 }
333
334 tracing::info_span!("startup_check", app = config.server.name).in_scope(|| {
336 tracing::info!("startup span alive - traces should be visible in Jaeger");
337 });
338
339 tracing::info!(
340 version = env!("CARGO_PKG_VERSION"),
341 rust_version = env!("CARGO_PKG_RUST_VERSION"),
342 "{} Server starting",
343 config.server.name,
344 );
345
346 Ok(())
347}
348
349#[cfg(feature = "otel")]
350pub fn tracing_shutdown() {
355 crate::telemetry::init::shutdown_metrics();
356 crate::telemetry::init::shutdown_tracing();
357}