1use std::sync::Arc;
2
3#[cfg(unix)]
4use std::path::{Path, PathBuf};
5#[cfg(unix)]
6use std::process::{Command, Stdio};
7#[cfg(unix)]
8use std::time::{Duration, Instant};
9
10#[cfg(unix)]
11use crate::daemon::{
12 default_socket_path, DaemonClient, DaemonShellRuntime, HelloResult,
13 TYPED_ACTION_RESULT_CAPABILITY,
14};
15use crate::errors::{Result, WinxError};
16
17use super::{EmbeddedShellRuntime, ShellRuntime};
18
19#[cfg(unix)]
20const GUARDIAN_LIFECYCLE_CAPABILITY: &str = "guardian_activity_clock";
21#[cfg(unix)]
22const PLANNED_CONTROL_RESTART_CAPABILITY: &str = "planned_control_restart";
23#[cfg(unix)]
24const SESSION_NEGOTIATE_CAPABILITY: &str = "session.negotiate";
25#[cfg(unix)]
26const DAEMON_TRANSITION_TIMEOUT: Duration = Duration::from_secs(3);
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum RuntimeMode {
30 Daemon,
31 Embedded,
32}
33
34fn enabled(value: Option<&str>) -> bool {
35 value.and_then(crate::config::parse_bool).unwrap_or(false)
36}
37
38pub fn select_runtime_mode(
41 embedded: Option<&str>,
42 runtime: Option<&str>,
43 sandbox: Option<&str>,
44) -> Result<RuntimeMode> {
45 select_runtime_mode_for_platform(cfg!(unix), embedded, runtime, sandbox)
46}
47
48fn select_runtime_mode_for_platform(
49 daemon_supported: bool,
50 embedded: Option<&str>,
51 runtime: Option<&str>,
52 sandbox: Option<&str>,
53) -> Result<RuntimeMode> {
54 if enabled(sandbox) || enabled(embedded) {
55 return Ok(RuntimeMode::Embedded);
56 }
57 match runtime {
58 None | Some("" | "daemon") if daemon_supported => Ok(RuntimeMode::Daemon),
59 None | Some("" | "embedded") => Ok(RuntimeMode::Embedded),
60 Some("daemon") => Err(WinxError::ConfigurationError(
61 "WINX_RUNTIME=\"daemon\" requires a Unix platform; use `embedded` on this OS"
62 .to_string(),
63 )),
64 Some(other) => Err(WinxError::ConfigurationError(format!(
65 "invalid WINX_RUNTIME={other:?}; expected `daemon` or `embedded`"
66 ))),
67 }
68}
69
70pub fn configured_runtime_mode() -> Result<RuntimeMode> {
71 let embedded = std::env::var("WINX_EMBEDDED").ok();
72 let runtime = std::env::var("WINX_RUNTIME").ok();
73 let sandbox = std::env::var("WINX_SANDBOX").ok();
74 select_runtime_mode(embedded.as_deref(), runtime.as_deref(), sandbox.as_deref())
75}
76
77pub async fn configured_shell_runtime() -> Result<Arc<dyn ShellRuntime>> {
78 match configured_runtime_mode()? {
79 RuntimeMode::Embedded => Ok(Arc::new(EmbeddedShellRuntime)),
80 #[cfg(unix)]
81 RuntimeMode::Daemon => {
82 let socket = default_socket_path();
83 let binary = configured_daemon_binary()?;
84 ensure_control_daemon_at(&socket, &binary).await?;
85 Ok(Arc::new(DaemonShellRuntime::new(socket)))
86 }
87 #[cfg(not(unix))]
88 RuntimeMode::Daemon => Err(WinxError::ConfigurationError(
89 "the daemon runtime requires a Unix platform".to_string(),
90 )),
91 }
92}
93
94#[cfg(unix)]
95pub fn configured_daemon_binary() -> Result<PathBuf> {
96 if let Some(path) = std::env::var_os("WINXD_BIN") {
97 return Ok(PathBuf::from(path));
98 }
99 let executable = std::env::current_exe().map_err(|error| {
100 WinxError::ConfigurationError(format!("cannot locate current Winx executable: {error}"))
101 })?;
102 let sibling = executable.with_file_name("winxd");
103 if sibling.is_file() {
104 Ok(sibling)
105 } else {
106 Err(WinxError::ConfigurationError(format!(
107 "winxd not found beside {} (set WINXD_BIN or WINX_EMBEDDED=1)",
108 executable.display()
109 )))
110 }
111}
112
113#[cfg(unix)]
114fn has_capability(hello: &HelloResult, name: &str) -> bool {
115 hello.capabilities.iter().any(|capability| capability == name)
116}
117
118#[cfg(unix)]
119fn control_is_current(hello: &HelloResult) -> bool {
120 [GUARDIAN_LIFECYCLE_CAPABILITY, TYPED_ACTION_RESULT_CAPABILITY, SESSION_NEGOTIATE_CAPABILITY]
121 .into_iter()
122 .all(|capability| has_capability(hello, capability))
123}
124
125#[cfg(unix)]
129pub async fn ensure_control_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<()> {
130 ensure_daemon_at(socket, daemon_binary).await?;
131 let client = DaemonClient::new(socket);
132 let hello = client.hello().await?;
133 if control_is_current(&hello) {
134 return Ok(());
135 }
136 if !has_capability(&hello, PLANNED_CONTROL_RESTART_CAPABILITY) {
137 return Err(WinxError::ConfigurationError(format!(
138 "the running winxd (pid {}) lacks the current guardian lifecycle controls and cannot be safely \
139 restarted automatically; stop it manually, then retry",
140 hello.daemon_pid
141 )));
142 }
143
144 tracing::info!(
145 pid = hello.daemon_pid,
146 "restarting older winxd control plane while preserving session guardians"
147 );
148 let restarted = restart_control_daemon_from_hello(socket, daemon_binary, hello).await?;
149 for capability in [
150 GUARDIAN_LIFECYCLE_CAPABILITY,
151 TYPED_ACTION_RESULT_CAPABILITY,
152 SESSION_NEGOTIATE_CAPABILITY,
153 ] {
154 if !has_capability(&restarted, capability) {
155 return Err(WinxError::ConfigurationError(format!(
156 "restarted winxd at {} still lacks required capability {capability}",
157 socket.display()
158 )));
159 }
160 }
161 Ok(())
162}
163
164#[cfg(unix)]
166pub async fn restart_control_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<HelloResult> {
167 let hello = DaemonClient::new(socket).hello().await?;
168 restart_control_daemon_from_hello(socket, daemon_binary, hello).await
169}
170
171#[cfg(unix)]
172async fn restart_control_daemon_from_hello(
173 socket: &Path,
174 daemon_binary: &Path,
175 previous: HelloResult,
176) -> Result<HelloResult> {
177 if !has_capability(&previous, PLANNED_CONTROL_RESTART_CAPABILITY) {
178 return Err(WinxError::ConfigurationError(format!(
179 "winxd pid {} does not advertise safe planned control restarts",
180 previous.daemon_pid
181 )));
182 }
183 signal_control_shutdown(previous.daemon_pid)?;
184
185 let client = DaemonClient::new(socket);
186 let deadline = Instant::now() + DAEMON_TRANSITION_TIMEOUT;
187 while Instant::now() < deadline {
188 match client.hello().await {
189 Ok(current) if current.daemon_epoch != previous.daemon_epoch => return Ok(current),
190 Ok(_) => tokio::time::sleep(Duration::from_millis(25)).await,
191 Err(_) => break,
192 }
193 }
194 if let Ok(current) = client.hello().await {
195 if current.daemon_epoch == previous.daemon_epoch {
196 return Err(WinxError::ShellInitializationError(format!(
197 "winxd pid {} did not stop within {} seconds",
198 previous.daemon_pid,
199 DAEMON_TRANSITION_TIMEOUT.as_secs()
200 )));
201 }
202 return Ok(current);
203 }
204
205 ensure_daemon_at(socket, daemon_binary).await?;
206 let restarted = client.hello().await?;
207 if restarted.daemon_epoch == previous.daemon_epoch {
208 return Err(WinxError::ShellInitializationError(
209 "winxd restart returned the previous daemon epoch".to_string(),
210 ));
211 }
212 Ok(restarted)
213}
214
215#[cfg(unix)]
216fn signal_control_shutdown(pid: u32) -> Result<()> {
217 crate::os::unix::signal_process(pid, libc::SIGTERM).map_err(Into::into)
218}
219
220#[cfg(unix)]
224pub async fn ensure_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<()> {
225 let client = DaemonClient::new(socket);
226 match client.hello().await {
227 Ok(_) => return Ok(()),
228 Err(error @ WinxError::ConfigurationError(_)) => return Err(error),
229 Err(_) => {}
230 }
231
232 let mut command = Command::new(daemon_binary);
233 command
234 .arg("--socket")
235 .arg(socket)
236 .stdin(Stdio::null())
237 .stdout(Stdio::null())
238 .stderr(Stdio::null());
239 crate::os::unix::configure_detached(&mut command);
240 let mut child = command.spawn().map_err(|error| {
241 WinxError::ShellInitializationError(format!(
242 "failed to auto-start {}: {error}",
243 daemon_binary.display()
244 ))
245 })?;
246
247 let deadline = Instant::now() + DAEMON_TRANSITION_TIMEOUT;
248 while Instant::now() < deadline {
249 match client.hello().await {
250 Ok(_) => {
251 std::thread::spawn(move || {
252 let _ = child.wait();
253 });
254 return Ok(());
255 }
256 Err(error @ WinxError::ConfigurationError(_)) => return Err(error),
257 Err(_) => {}
258 }
259 if let Some(status) = child.try_wait()? {
260 return Err(WinxError::ShellInitializationError(format!(
261 "winxd exited before accepting connections: {status}"
262 )));
263 }
264 tokio::time::sleep(Duration::from_millis(25)).await;
265 }
266 Err(WinxError::ShellInitializationError(format!(
267 "timed out waiting for winxd at {}",
268 socket.display()
269 )))
270}
271
272#[cfg(test)]
273mod tests {
274 use super::{select_runtime_mode_for_platform, RuntimeMode};
275
276 #[test]
277 fn platform_without_unix_sockets_defaults_to_embedded() {
278 assert!(matches!(
279 select_runtime_mode_for_platform(false, None, None, None),
280 Ok(RuntimeMode::Embedded)
281 ));
282 assert!(select_runtime_mode_for_platform(false, None, Some("daemon"), None).is_err());
283 }
284
285 #[cfg(unix)]
286 #[test]
287 fn protocol_1_4_control_without_session_negotiate_requires_upgrade() {
288 let hello = crate::daemon::HelloResult {
289 protocol_major: 1,
290 protocol_minor: 4,
291 capabilities: vec![
292 super::GUARDIAN_LIFECYCLE_CAPABILITY.to_string(),
293 super::TYPED_ACTION_RESULT_CAPABILITY.to_string(),
294 super::PLANNED_CONTROL_RESTART_CAPABILITY.to_string(),
295 ],
296 max_frame_bytes: 1024,
297 daemon_epoch: "control-14".to_string(),
298 daemon_pid: 14,
299 };
300 assert!(!super::control_is_current(&hello));
301 assert!(super::has_capability(&hello, super::PLANNED_CONTROL_RESTART_CAPABILITY));
302 }
303}