1use std::process::{Child, Command, Stdio};
10use std::time::{Duration, Instant};
11
12use crate::pick::PickErr;
13
14pub const READY_TEXT: &str = "ready\n";
16
17pub const DEFAULT_READY_TIMEOUT: Duration = Duration::from_secs(30);
19
20const STDERR_TAIL_LEN: usize = 2048;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum LaunchFail {
26 Exited { code: i32, stderr_tail: String },
28 NoReadySignal { timeout: Duration },
30}
31
32impl std::fmt::Display for LaunchFail {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 Self::Exited { code, stderr_tail } => write!(
36 f,
37 "child exited (code {code}) without ready signal; stderr tail: {stderr_tail}"
38 ),
39 Self::NoReadySignal { timeout } => {
40 write!(f, "no ready signal within {:?} (child killed)", timeout)
41 }
42 }
43 }
44}
45
46impl std::error::Error for LaunchFail {}
47
48impl From<PickErr> for LaunchFail {
49 fn from(e: PickErr) -> Self {
50 Self::Exited {
52 code: 0,
53 stderr_tail: e.to_string(),
54 }
55 }
56}
57
58#[must_use]
60pub fn launch_workdir() -> std::path::PathBuf {
61 std::env::temp_dir().join("ek-launch")
62}
63
64#[must_use]
66pub fn unique_marker(dir: &std::path::Path) -> std::path::PathBuf {
67 let nanos = std::time::SystemTime::now()
68 .duration_since(std::time::UNIX_EPOCH)
69 .map(|d| d.as_nanos() as u64)
70 .unwrap_or(0);
71 let _ = std::fs::create_dir_all(dir);
72 dir.join(format!("ek-ready-{}{}-.marker", std::process::id(), nanos))
73}
74
75pub fn spawn_detached(program: &std::path::Path, args: &[String]) -> std::io::Result<Child> {
77 let mut cmd = Command::new(program);
78 cmd.args(args)
79 .stdin(Stdio::null())
80 .stdout(Stdio::null())
81 .stderr(Stdio::piped());
82 #[cfg(windows)]
83 {
84 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
85 use std::os::windows::process::CommandExt;
86 cmd.creation_flags(CREATE_NO_WINDOW);
87 }
88 cmd.spawn()
89}
90
91fn stderr_tail(child: &mut Child) -> String {
93 let Some(mut pipe) = child.stderr.take() else {
94 return String::new();
95 };
96 use std::io::Read;
97 let mut buf = Vec::new();
98 let _ = pipe.read_to_end(&mut buf);
99 if buf.len() > STDERR_TAIL_LEN {
100 let tail = &buf[buf.len() - STDERR_TAIL_LEN..];
101 String::from_utf8_lossy(tail).to_string()
102 } else {
103 String::from_utf8_lossy(&buf).to_string()
104 }
105}
106
107pub fn wait_ready(
109 child: &mut Child,
110 marker: &std::path::Path,
111 timeout: Duration,
112) -> Result<(), LaunchFail> {
113 let deadline = Instant::now() + timeout;
114 loop {
115 if marker.is_file() {
116 return Ok(());
117 }
118 match child.try_wait() {
119 Ok(Some(status)) => {
120 let tail = stderr_tail(child);
121 return Err(LaunchFail::Exited {
122 code: status.code().unwrap_or(-1),
123 stderr_tail: tail,
124 });
125 }
126 Ok(None) => {}
127 Err(_) => {} }
129 if Instant::now() >= deadline {
130 let _ = child.kill();
131 return Err(LaunchFail::NoReadySignal { timeout });
132 }
133 std::thread::sleep(Duration::from_millis(50));
134 }
135}
136
137pub fn launch(
140 program: &std::path::Path,
141 args: &[String],
142 marker: &std::path::Path,
143 timeout: Duration,
144) -> Result<(), LaunchFail> {
145 let mut child = match spawn_detached(program, args) {
146 Ok(c) => c,
147 Err(e) => {
148 return Err(LaunchFail::Exited {
149 code: 0,
150 stderr_tail: format!("spawn failed: {e}"),
151 });
152 }
153 };
154 match wait_ready(&mut child, marker, timeout) {
155 Ok(()) => Ok(()), Err(e) => Err(e),
157 }
158}