winx_code_agent/runtime/
selection.rs1use 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
26pub 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#[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 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}