1use anyhow::{Context, Result};
7use std::path::Path;
8use tracing::{info, warn};
9
10pub 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
41pub 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#[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}