Skip to main content

pai_engine/
bootstrap.rs

1//! Composition root helpers: domain crate visibility, config load, shutdown signals.
2//!
3//! Concrete adapters for vision/audio/api/… are feature-gated in their crates; this module
4//! ensures each domain crate is part of the engine binary and emits structured bootstrap logs.
5
6use anyhow::{Context, Result};
7use std::path::Path;
8use tracing::{info, warn};
9
10/// Load optional TOML/JSON config from disk when the path exists; otherwise continue with defaults.
11pub fn load_config(path: Option<&Path>) -> Result<()> {
12    match path {
13        Some(p) if p.exists() => {
14            let _cfg = config::Config::builder()
15                .add_source(config::File::from(p))
16                .build()
17                .with_context(|| format!("failed to load config from {}", p.display()))?;
18            info!(
19                target: "pai_engine::config",
20                "loaded configuration from {}",
21                p.display()
22            );
23        }
24        Some(p) => {
25            info!(
26                target: "pai_engine::config",
27                "config path {} not found; using defaults",
28                p.display()
29            );
30        }
31        None => {
32            info!(
33                target: "pai_engine::config",
34                "no configuration file; using built-in defaults"
35            );
36        }
37    }
38    Ok(())
39}
40
41/// Emit structured logs and resolve optional domain crate dependencies for the active feature set.
42pub fn log_domain_stack_init() {
43    use common as _;
44
45    info!(
46        target: "pai_engine::bootstrap",
47        "common: shared domain types and ports (linked)"
48    );
49    info!(
50        target: "pai_engine::bootstrap",
51        "pai-core: SessionManager, EventBus, FlowRunner (active)"
52    );
53
54    #[cfg(any(
55        feature = "vision_mock",
56        feature = "vision_v4l2",
57        feature = "vision_rga",
58        feature = "vision_image"
59    ))]
60    {
61        use vision as _;
62        info!(target: "pai_engine::bootstrap", "vision: crate linked");
63    }
64
65    #[cfg(any(
66        feature = "audio_mock",
67        feature = "audio_cpal",
68        feature = "audio_webrtc"
69    ))]
70    {
71        use audio as _;
72        info!(
73            target: "pai_engine::bootstrap",
74            "audio: crate linked"
75        );
76    }
77
78    #[cfg(any(
79        feature = "infer_mock",
80        feature = "infer_llamacpp_cpu",
81        feature = "infer_rknn",
82        feature = "infer_rkllm",
83        feature = "infer_sherpa",
84        feature = "infer_hailo",
85        feature = "infer_mcp_client"
86    ))]
87    {
88        use inference as _;
89        info!(
90            target: "pai_engine::bootstrap",
91            "inference: crate linked"
92        );
93    }
94
95    #[cfg(any(
96        feature = "api_mock",
97        feature = "api_grpc_uds",
98        feature = "api_grpc_tcp",
99        feature = "api_http",
100        feature = "api_mcp_server"
101    ))]
102    {
103        use api as _;
104        info!(target: "pai_engine::bootstrap", "api: crate linked");
105    }
106
107    #[cfg(any(
108        feature = "periph_mock",
109        feature = "periph_desktop",
110        feature = "periph_desktop_hid",
111        feature = "periph_gpio",
112        feature = "periph_evdev",
113        feature = "periph_usb_hid"
114    ))]
115    {
116        use peripherals as _;
117        info!(
118            target: "pai_engine::bootstrap",
119            "peripherals: crate linked"
120        );
121    }
122}
123
124/// Block until Ctrl-C or SIGTERM (Unix). Used as the engine main-loop shutdown trigger.
125#[allow(
126    clippy::expect_used,
127    reason = "OS signal registration is infallible on supported platforms"
128)]
129pub async fn wait_for_shutdown_signal() {
130    #[cfg(unix)]
131    {
132        use tokio::signal::unix::{signal, SignalKind};
133        let mut sigterm = signal(SignalKind::terminate()).expect("register SIGTERM");
134        tokio::select! {
135            res = tokio::signal::ctrl_c() => {
136                if let Err(err) = res {
137                    warn!(
138                        target: "pai_engine::bootstrap",
139                        error = %err,
140                        "failed to listen for Ctrl-C; continuing shutdown wait on other signals"
141                    );
142                }
143            }
144            _ = sigterm.recv() => {},
145        }
146    }
147    #[cfg(not(unix))]
148    {
149        if let Err(err) = tokio::signal::ctrl_c().await {
150            warn!(
151                target: "pai_engine::bootstrap",
152                error = %err,
153                "failed to listen for Ctrl-C; exiting shutdown wait"
154            );
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
162
163    use super::*;
164    use std::time::Duration;
165
166    #[test]
167    fn load_config_none_ok() {
168        load_config(None).expect("built-in defaults path");
169    }
170
171    #[test]
172    fn load_config_missing_file_ok() {
173        let path = Path::new("/nonexistent/pai-engine-config-does-not-exist.toml");
174        load_config(Some(path)).expect("defaults when missing");
175    }
176
177    #[test]
178    fn load_config_existing_file_ok() {
179        let dir = tempfile::tempdir().expect("tempdir");
180        let path = dir.path().join("app.toml");
181        std::fs::write(&path, "key = \"value\"\n").expect("write config");
182        load_config(Some(path.as_path())).expect("load existing");
183    }
184
185    #[test]
186    fn log_domain_stack_init_runs_without_panic() {
187        log_domain_stack_init();
188    }
189
190    #[tokio::test]
191    async fn wait_for_shutdown_signal_does_not_complete_without_signal() {
192        let result =
193            tokio::time::timeout(Duration::from_millis(50), wait_for_shutdown_signal()).await;
194        assert!(
195            result.is_err(),
196            "expected timeout before any shutdown signal"
197        );
198    }
199}