use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use crate::pick::PickErr;
pub const READY_TEXT: &str = "ready\n";
pub const DEFAULT_READY_TIMEOUT: Duration = Duration::from_secs(30);
const STDERR_TAIL_LEN: usize = 2048;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LaunchFail {
Exited { code: i32, stderr_tail: String },
NoReadySignal { timeout: Duration },
}
impl std::fmt::Display for LaunchFail {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Exited { code, stderr_tail } => write!(
f,
"child exited (code {code}) without ready signal; stderr tail: {stderr_tail}"
),
Self::NoReadySignal { timeout } => {
write!(f, "no ready signal within {:?} (child killed)", timeout)
}
}
}
}
impl std::error::Error for LaunchFail {}
impl From<PickErr> for LaunchFail {
fn from(e: PickErr) -> Self {
Self::Exited {
code: 0,
stderr_tail: e.to_string(),
}
}
}
#[must_use]
pub fn launch_workdir() -> std::path::PathBuf {
std::env::temp_dir().join("ek-launch")
}
#[must_use]
pub fn unique_marker(dir: &std::path::Path) -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let _ = std::fs::create_dir_all(dir);
dir.join(format!("ek-ready-{}{}-.marker", std::process::id(), nanos))
}
pub fn spawn_detached(program: &std::path::Path, args: &[String]) -> std::io::Result<Child> {
let mut cmd = Command::new(program);
cmd.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped());
#[cfg(windows)]
{
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
use std::os::windows::process::CommandExt;
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd.spawn()
}
fn stderr_tail(child: &mut Child) -> String {
let Some(mut pipe) = child.stderr.take() else {
return String::new();
};
use std::io::Read;
let mut buf = Vec::new();
let _ = pipe.read_to_end(&mut buf);
if buf.len() > STDERR_TAIL_LEN {
let tail = &buf[buf.len() - STDERR_TAIL_LEN..];
String::from_utf8_lossy(tail).to_string()
} else {
String::from_utf8_lossy(&buf).to_string()
}
}
pub fn wait_ready(
child: &mut Child,
marker: &std::path::Path,
timeout: Duration,
) -> Result<(), LaunchFail> {
let deadline = Instant::now() + timeout;
loop {
if marker.is_file() {
return Ok(());
}
match child.try_wait() {
Ok(Some(status)) => {
let tail = stderr_tail(child);
return Err(LaunchFail::Exited {
code: status.code().unwrap_or(-1),
stderr_tail: tail,
});
}
Ok(None) => {}
Err(_) => {} }
if Instant::now() >= deadline {
let _ = child.kill();
return Err(LaunchFail::NoReadySignal { timeout });
}
std::thread::sleep(Duration::from_millis(50));
}
}
pub fn launch(
program: &std::path::Path,
args: &[String],
marker: &std::path::Path,
timeout: Duration,
) -> Result<(), LaunchFail> {
let mut child = match spawn_detached(program, args) {
Ok(c) => c,
Err(e) => {
return Err(LaunchFail::Exited {
code: 0,
stderr_tail: format!("spawn failed: {e}"),
});
}
};
match wait_ready(&mut child, marker, timeout) {
Ok(()) => Ok(()), Err(e) => Err(e),
}
}