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 {
83 gears_cfg: Arc::new(config),
84 db: db_options,
85 shutdown: ShutdownOptions::Token(cancel.clone()),
86 clients: vec![],
87 instance_id,
88 oop: oop_options,
89 shutdown_deadline: None,
90 };
91
92 let result = run(run_options).await;
93
94 #[cfg(feature = "otel")]
96 tracing_shutdown();
97
98 result
99}
100
101pub async fn run_migrate(config: AppConfig) -> Result<()> {
125 init_procedure(&config).map_err(|e| {
126 tracing::error!(error = %e, "Initialization failed");
127 e
128 })?;
129 tracing::info!("Starting migration mode...");
130
131 let instance_id = uuid::Uuid::new_v4();
133 tracing::info!(instance_id = %instance_id, "Generated migration instance ID");
134
135 let cancel = CancellationToken::new();
137
138 spawn_signal_handler(cancel.clone(), "migration");
140
141 let db_options = resolve_db_options(&config)?;
143
144 if matches!(db_options, DbOptions::None) {
146 anyhow::bail!("Cannot run migrations: no database configuration found");
147 }
148
149 let registry = crate::registry::GearRegistry::discover_and_build()?;
151 tracing::info!(
152 gear_count = registry.gears().len(),
153 "Discovered gears for migration"
154 );
155
156 let host = crate::runtime::HostRuntime::new(
158 registry,
159 Arc::new(config),
160 db_options,
161 Arc::new(crate::client_hub::ClientHub::new()),
162 cancel,
163 instance_id,
164 None, );
166
167 let result = host.run_migration_phases().await;
169
170 #[cfg(feature = "otel")]
172 tracing_shutdown();
173
174 result?;
175
176 tracing::info!("All migrations completed successfully");
177 Ok(())
178}
179
180fn resolve_db_options(config: &AppConfig) -> Result<DbOptions> {
181 if config.database.is_none() {
182 tracing::warn!("No global database section found; running without databases");
183 return Ok(DbOptions::None);
184 }
185
186 tracing::info!("Using DbManager with Figment-based configuration");
187 let figment = Figment::new().merge(Serialized::defaults(config));
188 let db_manager = Arc::new(toolkit_db::DbManager::from_figment(
189 figment,
190 config.server.home_dir.clone(),
191 )?);
192 Ok(DbOptions::Manager(db_manager))
193}
194
195fn build_oop_spawn_options(
200 config: &AppConfig,
201 backend: LocalProcessBackend,
202) -> Result<Option<OopSpawnOptions>> {
203 let home_dir = PathBuf::from(&config.server.home_dir);
204 let mut gears = Vec::new();
205
206 for gear_name in config.gears.keys() {
207 if let Some(spawn_config) = try_build_oop_gear_config(config, gear_name, &home_dir)? {
208 gears.push(spawn_config);
209 }
210 }
211
212 if gears.is_empty() {
213 Ok(None)
214 } else {
215 tracing::info!(count = gears.len(), "Prepared OoP gears for spawning");
216 Ok(Some(OopSpawnOptions {
217 gears,
218 backend: Box::new(backend),
219 }))
220 }
221}
222
223fn try_build_oop_gear_config(
225 config: &AppConfig,
226 gear_name: &str,
227 home_dir: &Path,
228) -> Result<Option<OopGearSpawnConfig>> {
229 let Some(runtime_cfg) = get_gear_runtime_config(config, gear_name)? else {
230 return Ok(None);
231 };
232
233 if !matches!(runtime_cfg.mod_type, RuntimeKind::Oop) {
234 return Ok(None);
235 }
236
237 let exec_cfg = runtime_cfg.execution.as_ref().ok_or_else(|| {
238 anyhow::anyhow!("gear '{gear_name}' is type=oop but execution config is missing")
239 })?;
240
241 let binary = normalize_path(&exec_cfg.executable_path)?;
242 let spawn_args = exec_cfg.args.clone();
243 let env = exec_cfg.environment.clone();
244
245 let rendered_config = render_gear_config_for_oop(config, gear_name, home_dir)?;
247 let rendered_json = rendered_config.to_json()?;
248
249 tracing::debug!(
250 gear = %gear_name,
251 "Prepared OoP gear config: db={}",
252 rendered_config.database.is_some()
253 );
254
255 Ok(Some(OopGearSpawnConfig {
256 gear_name: gear_name.to_owned(),
257 binary,
258 args: spawn_args,
259 env,
260 working_directory: exec_cfg.working_directory.clone(),
261 rendered_config_json: rendered_json,
262 }))
263}
264
265#[cfg_attr(not(feature = "otel"), allow(clippy::unnecessary_wraps))]
290pub fn init_procedure(config: &AppConfig) -> Result<()> {
291 super::init_crypto_provider()?;
295
296 #[cfg(feature = "otel")]
298 let otel_layer = if config.opentelemetry.tracing.enabled {
299 Some(crate::telemetry::init::init_tracing(&config.opentelemetry)?)
300 } else {
301 None
302 };
303 #[cfg(not(feature = "otel"))]
304 let otel_layer = None;
305
306 init_logging_unified(&config.logging, &config.server.home_dir, otel_layer);
308
309 init_panic_tracing();
311
312 #[cfg(feature = "otel")]
314 if let Err(e) = crate::telemetry::init::init_metrics_provider(&config.opentelemetry) {
315 tracing::error!(error = %e, "OpenTelemetry metrics not initialized");
316 }
317
318 #[cfg(feature = "otel")]
320 if config.opentelemetry.tracing.enabled
321 && let Err(e) = crate::telemetry::init::otel_connectivity_probe(&config.opentelemetry)
322 {
323 tracing::error!(error = %e, "OTLP connectivity probe failed");
324 }
325
326 tracing::info_span!("startup_check", app = config.server.name).in_scope(|| {
328 tracing::info!("startup span alive - traces should be visible in Jaeger");
329 });
330
331 tracing::info!(
332 version = env!("CARGO_PKG_VERSION"),
333 rust_version = env!("CARGO_PKG_RUST_VERSION"),
334 "{} Server starting",
335 config.server.name,
336 );
337
338 Ok(())
339}
340
341#[cfg(feature = "otel")]
342pub fn tracing_shutdown() {
347 crate::telemetry::init::shutdown_metrics();
348 crate::telemetry::init::shutdown_tracing();
349}