use std::sync::Arc;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use crate::daemon::{
default_socket_path, DaemonClient, DaemonProcessRole, DaemonShellRuntime, HelloResult,
BUILD_IDENTITY_CAPABILITY, TYPED_ACTION_RESULT_CAPABILITY,
};
use crate::errors::{Result, WinxError};
use super::{EmbeddedShellRuntime, ShellRuntime};
const GUARDIAN_LIFECYCLE_CAPABILITY: &str = "guardian_activity_clock";
const PLANNED_CONTROL_RESTART_CAPABILITY: &str = "planned_control_restart";
const SESSION_NEGOTIATE_CAPABILITY: &str = "session.negotiate";
const DAEMON_TRANSITION_TIMEOUT: Duration = Duration::from_secs(3);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RuntimeMode {
Daemon,
Embedded,
}
fn enabled(value: Option<&str>) -> bool {
value.and_then(crate::config::parse_bool).unwrap_or(false)
}
pub fn select_runtime_mode(
embedded: Option<&str>,
runtime: Option<&str>,
sandbox: Option<&str>,
) -> Result<RuntimeMode> {
select_runtime_mode_for_platform(cfg!(any(unix, windows)), embedded, runtime, sandbox)
}
fn select_runtime_mode_for_platform(
daemon_supported: bool,
embedded: Option<&str>,
runtime: Option<&str>,
sandbox: Option<&str>,
) -> Result<RuntimeMode> {
if enabled(sandbox) || enabled(embedded) {
return Ok(RuntimeMode::Embedded);
}
match runtime {
None | Some("" | "daemon") if daemon_supported => Ok(RuntimeMode::Daemon),
None | Some("" | "embedded") => Ok(RuntimeMode::Embedded),
Some("daemon") => Err(WinxError::ConfigurationError(
"WINX_RUNTIME=\"daemon\" requires Unix or Windows; use `embedded` on this OS"
.to_string(),
)),
Some(other) => Err(WinxError::ConfigurationError(format!(
"invalid WINX_RUNTIME={other:?}; expected `daemon` or `embedded`"
))),
}
}
pub fn configured_runtime_mode() -> Result<RuntimeMode> {
let embedded = std::env::var("WINX_EMBEDDED").ok();
let runtime = std::env::var("WINX_RUNTIME").ok();
let sandbox = std::env::var("WINX_SANDBOX").ok();
select_runtime_mode(embedded.as_deref(), runtime.as_deref(), sandbox.as_deref())
}
pub async fn configured_shell_runtime() -> Result<Arc<dyn ShellRuntime>> {
match configured_runtime_mode()? {
RuntimeMode::Embedded => Ok(Arc::new(EmbeddedShellRuntime)),
RuntimeMode::Daemon => {
let socket = default_socket_path();
let binary = configured_daemon_binary()?;
ensure_control_daemon_at(&socket, &binary).await?;
Ok(Arc::new(DaemonShellRuntime::new(socket)))
}
}
}
pub fn configured_daemon_binary() -> Result<PathBuf> {
if let Some(path) = std::env::var_os("WINXD_BIN") {
return Ok(PathBuf::from(path));
}
let executable = std::env::current_exe().map_err(|error| {
WinxError::ConfigurationError(format!("cannot locate current Winx executable: {error}"))
})?;
let sibling = executable.with_file_name(if cfg!(windows) { "winxd.exe" } else { "winxd" });
if sibling.is_file() {
Ok(sibling)
} else {
Err(WinxError::ConfigurationError(format!(
"winxd not found beside {} (set WINXD_BIN or WINX_EMBEDDED=1)",
executable.display()
)))
}
}
fn has_capability(hello: &HelloResult, name: &str) -> bool {
hello.capabilities.iter().any(|capability| capability == name)
}
fn control_is_current(hello: &HelloResult) -> bool {
hello.process_role == Some(DaemonProcessRole::Control)
&& [
GUARDIAN_LIFECYCLE_CAPABILITY,
TYPED_ACTION_RESULT_CAPABILITY,
SESSION_NEGOTIATE_CAPABILITY,
BUILD_IDENTITY_CAPABILITY,
]
.into_iter()
.all(|capability| has_capability(hello, capability))
&& hello.build_matches(&crate::build_info::BuildIdentity::current())
}
pub async fn ensure_control_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<()> {
ensure_daemon_at(socket, daemon_binary).await?;
let client = DaemonClient::new(socket);
let hello = client.hello().await?;
if control_is_current(&hello) {
return Ok(());
}
if !has_capability(&hello, PLANNED_CONTROL_RESTART_CAPABILITY) {
return Err(WinxError::ConfigurationError(format!(
"the running winxd (pid {}) lacks the current guardian lifecycle controls and cannot be safely \
restarted automatically; stop it manually, then retry",
hello.daemon_pid
)));
}
tracing::info!(
pid = hello.daemon_pid,
"restarting older winxd control plane while preserving session guardians"
);
let restarted = restart_control_daemon_from_hello(socket, daemon_binary, hello).await?;
for capability in [
GUARDIAN_LIFECYCLE_CAPABILITY,
TYPED_ACTION_RESULT_CAPABILITY,
SESSION_NEGOTIATE_CAPABILITY,
BUILD_IDENTITY_CAPABILITY,
] {
if !has_capability(&restarted, capability) {
return Err(WinxError::ConfigurationError(format!(
"restarted winxd at {} still lacks required capability {capability}",
socket.display()
)));
}
}
let expected = crate::build_info::BuildIdentity::current();
if restarted.process_role != Some(DaemonProcessRole::Control)
|| !restarted.build_matches(&expected)
{
return Err(WinxError::ConfigurationError(format!(
"restarted daemon at {} is not the expected winxd build {}",
socket.display(),
expected.display
)));
}
Ok(())
}
pub async fn restart_control_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<HelloResult> {
let hello = DaemonClient::new(socket).hello().await?;
restart_control_daemon_from_hello(socket, daemon_binary, hello).await
}
async fn restart_control_daemon_from_hello(
socket: &Path,
daemon_binary: &Path,
previous: HelloResult,
) -> Result<HelloResult> {
if !has_capability(&previous, PLANNED_CONTROL_RESTART_CAPABILITY) {
return Err(WinxError::ConfigurationError(format!(
"winxd pid {} does not advertise safe planned control restarts",
previous.daemon_pid
)));
}
signal_control_shutdown(socket, previous.daemon_pid).await?;
let client = DaemonClient::new(socket);
let deadline = Instant::now() + DAEMON_TRANSITION_TIMEOUT;
while Instant::now() < deadline {
match client.hello().await {
Ok(current) if current.daemon_epoch != previous.daemon_epoch => return Ok(current),
Ok(_) => tokio::time::sleep(Duration::from_millis(25)).await,
Err(_) => break,
}
}
if let Ok(current) = client.hello().await {
if current.daemon_epoch == previous.daemon_epoch {
return Err(WinxError::ShellInitializationError(format!(
"winxd pid {} did not stop within {} seconds",
previous.daemon_pid,
DAEMON_TRANSITION_TIMEOUT.as_secs()
)));
}
return Ok(current);
}
ensure_daemon_at(socket, daemon_binary).await?;
let restarted = client.hello().await?;
if restarted.daemon_epoch == previous.daemon_epoch {
return Err(WinxError::ShellInitializationError(
"winxd restart returned the previous daemon epoch".to_string(),
));
}
Ok(restarted)
}
#[cfg_attr(unix, allow(clippy::unused_async))] async fn signal_control_shutdown(socket: &Path, pid: u32) -> Result<()> {
#[cfg(unix)]
{
let _ = socket;
crate::os::unix::signal_process(pid, libc::SIGTERM).map_err(Into::into)
}
#[cfg(not(unix))]
{
let _ = pid;
DaemonClient::new(socket).shutdown().await
}
}
fn spawn_detached(daemon_binary: &Path, socket: &Path) -> Result<std::process::Child> {
let build = |breakaway: bool| {
let mut command = Command::new(daemon_binary);
command
.arg("--socket")
.arg(socket)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(unix)]
{
let _ = breakaway;
crate::os::unix::configure_detached(&mut command);
}
#[cfg(windows)]
crate::os::windows::configure_detached(&mut command, breakaway);
command
};
let failure = |error: std::io::Error| {
WinxError::ShellInitializationError(format!(
"failed to auto-start {}: {error}",
daemon_binary.display()
))
};
match build(true).spawn() {
Ok(child) => Ok(child),
Err(error) if cfg!(windows) => {
tracing::debug!(%error, "detached spawn with job breakaway refused; retrying inside the job");
build(false).spawn().map_err(failure)
}
Err(error) => Err(failure(error)),
}
}
pub async fn ensure_daemon_at(socket: &Path, daemon_binary: &Path) -> Result<()> {
let client = DaemonClient::new(socket);
match client.hello().await {
Ok(_) => return Ok(()),
Err(error @ WinxError::ConfigurationError(_)) => return Err(error),
Err(_) => {}
}
let mut child = spawn_detached(daemon_binary, socket)?;
let deadline = Instant::now() + DAEMON_TRANSITION_TIMEOUT;
let mut child_exit = None;
while Instant::now() < deadline {
match client.hello().await {
Ok(_) => {
if child_exit.is_none() {
std::thread::spawn(move || {
let _ = child.wait();
});
}
return Ok(());
}
Err(error @ WinxError::ConfigurationError(_)) => return Err(error),
Err(_) => {}
}
if child_exit.is_none() {
child_exit = child.try_wait()?;
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
Err(WinxError::ShellInitializationError(match child_exit {
Some(status) => format!(
"winxd exited ({status}) and nothing answered at {} within {} seconds",
socket.display(),
DAEMON_TRANSITION_TIMEOUT.as_secs()
),
None => format!("timed out waiting for winxd at {}", socket.display()),
}))
}
#[cfg(test)]
mod tests {
use super::{select_runtime_mode_for_platform, RuntimeMode};
#[test]
fn platform_without_unix_sockets_defaults_to_embedded() {
assert!(matches!(
select_runtime_mode_for_platform(false, None, None, None),
Ok(RuntimeMode::Embedded)
));
assert!(select_runtime_mode_for_platform(false, None, Some("daemon"), None).is_err());
}
#[test]
fn protocol_1_4_control_without_session_negotiate_requires_upgrade() {
let hello = crate::daemon::HelloResult {
protocol_major: 1,
protocol_minor: 4,
capabilities: vec![
super::GUARDIAN_LIFECYCLE_CAPABILITY.to_string(),
super::TYPED_ACTION_RESULT_CAPABILITY.to_string(),
super::PLANNED_CONTROL_RESTART_CAPABILITY.to_string(),
],
max_frame_bytes: 1024,
daemon_epoch: "control-14".to_string(),
daemon_pid: 14,
process_role: None,
build: None,
};
assert!(!super::control_is_current(&hello));
assert!(super::has_capability(&hello, super::PLANNED_CONTROL_RESTART_CAPABILITY));
}
#[test]
fn current_control_requires_the_exact_role_and_build() {
let capabilities = vec![
super::GUARDIAN_LIFECYCLE_CAPABILITY.to_string(),
super::TYPED_ACTION_RESULT_CAPABILITY.to_string(),
super::SESSION_NEGOTIATE_CAPABILITY.to_string(),
super::BUILD_IDENTITY_CAPABILITY.to_string(),
];
let hello = crate::daemon::HelloResult::current(
crate::daemon::DaemonProcessRole::Control,
capabilities,
"current",
);
assert!(super::control_is_current(&hello));
let mut wrong_role = hello.clone();
wrong_role.process_role = Some(crate::daemon::DaemonProcessRole::Guardian);
assert!(!super::control_is_current(&wrong_role));
let mut wrong_build = hello;
if let Some(build) = wrong_build.build.as_mut() {
build.display.push_str("-different");
}
assert!(!super::control_is_current(&wrong_build));
}
}