Skip to main content

winx_code_agent/runtime/
selection.rs

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::{default_socket_path, DaemonClient, DaemonShellRuntime};
12use crate::errors::{Result, WinxError};
13
14use super::{EmbeddedShellRuntime, ShellRuntime};
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum RuntimeMode {
18    Daemon,
19    Embedded,
20}
21
22fn enabled(value: Option<&str>) -> bool {
23    value.is_some_and(|value| matches!(value, "1" | "true" | "yes" | "on"))
24}
25
26/// Pure runtime-mode selection, kept separate from process spawning so its
27/// default and kill switches are deterministic under test.
28pub fn select_runtime_mode(
29    embedded: Option<&str>,
30    runtime: Option<&str>,
31    sandbox: Option<&str>,
32) -> Result<RuntimeMode> {
33    select_runtime_mode_for_platform(cfg!(unix), embedded, runtime, sandbox)
34}
35
36fn select_runtime_mode_for_platform(
37    daemon_supported: bool,
38    embedded: Option<&str>,
39    runtime: Option<&str>,
40    sandbox: Option<&str>,
41) -> Result<RuntimeMode> {
42    if enabled(sandbox) || enabled(embedded) {
43        return Ok(RuntimeMode::Embedded);
44    }
45    match runtime {
46        None | Some("") if daemon_supported => Ok(RuntimeMode::Daemon),
47        None | Some("") => Ok(RuntimeMode::Embedded),
48        Some("daemon") if daemon_supported => Ok(RuntimeMode::Daemon),
49        Some("daemon") => Err(WinxError::ConfigurationError(
50            "WINX_RUNTIME=\"daemon\" requires a Unix platform; use `embedded` on this OS"
51                .to_string(),
52        )),
53        Some("embedded") => Ok(RuntimeMode::Embedded),
54        Some(other) => Err(WinxError::ConfigurationError(format!(
55            "invalid WINX_RUNTIME={other:?}; expected `daemon` or `embedded`"
56        ))),
57    }
58}
59
60pub fn configured_runtime_mode() -> Result<RuntimeMode> {
61    let embedded = std::env::var("WINX_EMBEDDED").ok();
62    let runtime = std::env::var("WINX_RUNTIME").ok();
63    let sandbox = std::env::var("WINX_SANDBOX").ok();
64    select_runtime_mode(embedded.as_deref(), runtime.as_deref(), sandbox.as_deref())
65}
66
67pub async fn configured_shell_runtime() -> Result<Arc<dyn ShellRuntime>> {
68    match configured_runtime_mode()? {
69        RuntimeMode::Embedded => Ok(Arc::new(EmbeddedShellRuntime)),
70        #[cfg(unix)]
71        RuntimeMode::Daemon => {
72            let socket = default_socket_path();
73            let binary = daemon_binary()?;
74            ensure_daemon_at(&socket, &binary).await?;
75            Ok(Arc::new(DaemonShellRuntime::new(socket)))
76        }
77        #[cfg(not(unix))]
78        RuntimeMode::Daemon => Err(WinxError::ConfigurationError(
79            "the daemon runtime requires a Unix platform".to_string(),
80        )),
81    }
82}
83
84#[cfg(unix)]
85fn daemon_binary() -> Result<PathBuf> {
86    if let Some(path) = std::env::var_os("WINXD_BIN") {
87        return Ok(PathBuf::from(path));
88    }
89    let executable = std::env::current_exe().map_err(|error| {
90        WinxError::ConfigurationError(format!("cannot locate current Winx executable: {error}"))
91    })?;
92    let sibling = executable.with_file_name("winxd");
93    if sibling.is_file() {
94        Ok(sibling)
95    } else {
96        Err(WinxError::ConfigurationError(format!(
97            "winxd not found beside {} (set WINXD_BIN or WINX_EMBEDDED=1)",
98            executable.display()
99        )))
100    }
101}
102
103/// Ensure a compatible daemon is reachable, starting `daemon_binary` only when
104/// the socket cannot be reached. A reachable incompatible daemon is never
105/// replaced or killed.
106#[cfg(unix)]
107pub async fn ensure_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<()> {
108    let client = DaemonClient::new(socket);
109    match client.hello().await {
110        Ok(_) => return Ok(()),
111        Err(error @ WinxError::ConfigurationError(_)) => return Err(error),
112        Err(_) => {}
113    }
114
115    let mut command = Command::new(daemon_binary);
116    command
117        .arg("--socket")
118        .arg(socket)
119        .stdin(Stdio::null())
120        .stdout(Stdio::null())
121        .stderr(Stdio::null());
122    #[cfg(unix)]
123    {
124        use std::os::unix::process::CommandExt;
125        // SAFETY: the post-fork hook invokes only the async-signal-safe setsid(2)
126        // before exec, without touching allocator-backed state.
127        unsafe {
128            command.pre_exec(|| {
129                if libc::setsid() == -1 {
130                    Err(std::io::Error::last_os_error())
131                } else {
132                    Ok(())
133                }
134            });
135        }
136    }
137    let mut child = command.spawn().map_err(|error| {
138        WinxError::ShellInitializationError(format!(
139            "failed to auto-start {}: {error}",
140            daemon_binary.display()
141        ))
142    })?;
143
144    let deadline = Instant::now() + Duration::from_secs(3);
145    while Instant::now() < deadline {
146        match client.hello().await {
147            Ok(_) => {
148                std::thread::spawn(move || {
149                    let _ = child.wait();
150                });
151                return Ok(());
152            }
153            Err(error @ WinxError::ConfigurationError(_)) => return Err(error),
154            Err(_) => {}
155        }
156        if let Some(status) = child.try_wait()? {
157            return Err(WinxError::ShellInitializationError(format!(
158                "winxd exited before accepting connections: {status}"
159            )));
160        }
161        tokio::time::sleep(Duration::from_millis(25)).await;
162    }
163    Err(WinxError::ShellInitializationError(format!(
164        "timed out waiting for winxd at {}",
165        socket.display()
166    )))
167}
168
169#[cfg(test)]
170mod tests {
171    use super::{select_runtime_mode_for_platform, RuntimeMode};
172
173    #[test]
174    fn platform_without_unix_sockets_defaults_to_embedded() {
175        assert_eq!(
176            select_runtime_mode_for_platform(false, None, None, None).unwrap(),
177            RuntimeMode::Embedded
178        );
179        assert!(select_runtime_mode_for_platform(false, None, Some("daemon"), None).is_err());
180    }
181}