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, DaemonProcessRole, DaemonShellRuntime, HelloResult,
13 BUILD_IDENTITY_CAPABILITY, 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 hello.process_role == Some(DaemonProcessRole::Control)
121 && [
122 GUARDIAN_LIFECYCLE_CAPABILITY,
123 TYPED_ACTION_RESULT_CAPABILITY,
124 SESSION_NEGOTIATE_CAPABILITY,
125 BUILD_IDENTITY_CAPABILITY,
126 ]
127 .into_iter()
128 .all(|capability| has_capability(hello, capability))
129 && hello.build_matches(&crate::build_info::BuildIdentity::current())
130}
131
132#[cfg(unix)]
136pub async fn ensure_control_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<()> {
137 ensure_daemon_at(socket, daemon_binary).await?;
138 let client = DaemonClient::new(socket);
139 let hello = client.hello().await?;
140 if control_is_current(&hello) {
141 return Ok(());
142 }
143 if !has_capability(&hello, PLANNED_CONTROL_RESTART_CAPABILITY) {
144 return Err(WinxError::ConfigurationError(format!(
145 "the running winxd (pid {}) lacks the current guardian lifecycle controls and cannot be safely \
146 restarted automatically; stop it manually, then retry",
147 hello.daemon_pid
148 )));
149 }
150
151 tracing::info!(
152 pid = hello.daemon_pid,
153 "restarting older winxd control plane while preserving session guardians"
154 );
155 let restarted = restart_control_daemon_from_hello(socket, daemon_binary, hello).await?;
156 for capability in [
157 GUARDIAN_LIFECYCLE_CAPABILITY,
158 TYPED_ACTION_RESULT_CAPABILITY,
159 SESSION_NEGOTIATE_CAPABILITY,
160 BUILD_IDENTITY_CAPABILITY,
161 ] {
162 if !has_capability(&restarted, capability) {
163 return Err(WinxError::ConfigurationError(format!(
164 "restarted winxd at {} still lacks required capability {capability}",
165 socket.display()
166 )));
167 }
168 }
169 let expected = crate::build_info::BuildIdentity::current();
170 if restarted.process_role != Some(DaemonProcessRole::Control)
171 || !restarted.build_matches(&expected)
172 {
173 return Err(WinxError::ConfigurationError(format!(
174 "restarted daemon at {} is not the expected winxd build {}",
175 socket.display(),
176 expected.display
177 )));
178 }
179 Ok(())
180}
181
182#[cfg(unix)]
184pub async fn restart_control_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<HelloResult> {
185 let hello = DaemonClient::new(socket).hello().await?;
186 restart_control_daemon_from_hello(socket, daemon_binary, hello).await
187}
188
189#[cfg(unix)]
190async fn restart_control_daemon_from_hello(
191 socket: &Path,
192 daemon_binary: &Path,
193 previous: HelloResult,
194) -> Result<HelloResult> {
195 if !has_capability(&previous, PLANNED_CONTROL_RESTART_CAPABILITY) {
196 return Err(WinxError::ConfigurationError(format!(
197 "winxd pid {} does not advertise safe planned control restarts",
198 previous.daemon_pid
199 )));
200 }
201 signal_control_shutdown(previous.daemon_pid)?;
202
203 let client = DaemonClient::new(socket);
204 let deadline = Instant::now() + DAEMON_TRANSITION_TIMEOUT;
205 while Instant::now() < deadline {
206 match client.hello().await {
207 Ok(current) if current.daemon_epoch != previous.daemon_epoch => return Ok(current),
208 Ok(_) => tokio::time::sleep(Duration::from_millis(25)).await,
209 Err(_) => break,
210 }
211 }
212 if let Ok(current) = client.hello().await {
213 if current.daemon_epoch == previous.daemon_epoch {
214 return Err(WinxError::ShellInitializationError(format!(
215 "winxd pid {} did not stop within {} seconds",
216 previous.daemon_pid,
217 DAEMON_TRANSITION_TIMEOUT.as_secs()
218 )));
219 }
220 return Ok(current);
221 }
222
223 ensure_daemon_at(socket, daemon_binary).await?;
224 let restarted = client.hello().await?;
225 if restarted.daemon_epoch == previous.daemon_epoch {
226 return Err(WinxError::ShellInitializationError(
227 "winxd restart returned the previous daemon epoch".to_string(),
228 ));
229 }
230 Ok(restarted)
231}
232
233#[cfg(unix)]
234fn signal_control_shutdown(pid: u32) -> Result<()> {
235 crate::os::unix::signal_process(pid, libc::SIGTERM).map_err(Into::into)
236}
237
238#[cfg(unix)]
242pub async fn ensure_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<()> {
243 let client = DaemonClient::new(socket);
244 match client.hello().await {
245 Ok(_) => return Ok(()),
246 Err(error @ WinxError::ConfigurationError(_)) => return Err(error),
247 Err(_) => {}
248 }
249
250 let mut command = Command::new(daemon_binary);
251 command
252 .arg("--socket")
253 .arg(socket)
254 .stdin(Stdio::null())
255 .stdout(Stdio::null())
256 .stderr(Stdio::null());
257 crate::os::unix::configure_detached(&mut command);
258 let mut child = command.spawn().map_err(|error| {
259 WinxError::ShellInitializationError(format!(
260 "failed to auto-start {}: {error}",
261 daemon_binary.display()
262 ))
263 })?;
264
265 let deadline = Instant::now() + DAEMON_TRANSITION_TIMEOUT;
266 while Instant::now() < deadline {
267 match client.hello().await {
268 Ok(_) => {
269 std::thread::spawn(move || {
270 let _ = child.wait();
271 });
272 return Ok(());
273 }
274 Err(error @ WinxError::ConfigurationError(_)) => return Err(error),
275 Err(_) => {}
276 }
277 if let Some(status) = child.try_wait()? {
278 return Err(WinxError::ShellInitializationError(format!(
279 "winxd exited before accepting connections: {status}"
280 )));
281 }
282 tokio::time::sleep(Duration::from_millis(25)).await;
283 }
284 Err(WinxError::ShellInitializationError(format!(
285 "timed out waiting for winxd at {}",
286 socket.display()
287 )))
288}
289
290#[cfg(test)]
291mod tests {
292 use super::{select_runtime_mode_for_platform, RuntimeMode};
293
294 #[test]
295 fn platform_without_unix_sockets_defaults_to_embedded() {
296 assert!(matches!(
297 select_runtime_mode_for_platform(false, None, None, None),
298 Ok(RuntimeMode::Embedded)
299 ));
300 assert!(select_runtime_mode_for_platform(false, None, Some("daemon"), None).is_err());
301 }
302
303 #[cfg(unix)]
304 #[test]
305 fn protocol_1_4_control_without_session_negotiate_requires_upgrade() {
306 let hello = crate::daemon::HelloResult {
307 protocol_major: 1,
308 protocol_minor: 4,
309 capabilities: vec![
310 super::GUARDIAN_LIFECYCLE_CAPABILITY.to_string(),
311 super::TYPED_ACTION_RESULT_CAPABILITY.to_string(),
312 super::PLANNED_CONTROL_RESTART_CAPABILITY.to_string(),
313 ],
314 max_frame_bytes: 1024,
315 daemon_epoch: "control-14".to_string(),
316 daemon_pid: 14,
317 process_role: None,
318 build: None,
319 };
320 assert!(!super::control_is_current(&hello));
321 assert!(super::has_capability(&hello, super::PLANNED_CONTROL_RESTART_CAPABILITY));
322 }
323
324 #[cfg(unix)]
325 #[test]
326 fn current_control_requires_the_exact_role_and_build() {
327 let capabilities = vec![
328 super::GUARDIAN_LIFECYCLE_CAPABILITY.to_string(),
329 super::TYPED_ACTION_RESULT_CAPABILITY.to_string(),
330 super::SESSION_NEGOTIATE_CAPABILITY.to_string(),
331 super::BUILD_IDENTITY_CAPABILITY.to_string(),
332 ];
333 let hello = crate::daemon::HelloResult::current(
334 crate::daemon::DaemonProcessRole::Control,
335 capabilities,
336 "current",
337 );
338 assert!(super::control_is_current(&hello));
339
340 let mut wrong_role = hello.clone();
341 wrong_role.process_role = Some(crate::daemon::DaemonProcessRole::Guardian);
342 assert!(!super::control_is_current(&wrong_role));
343
344 let mut wrong_build = hello;
345 if let Some(build) = wrong_build.build.as_mut() {
346 build.display.push_str("-different");
347 }
348 assert!(!super::control_is_current(&wrong_build));
349 }
350}