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> {
80 let mut cmd = Command::new(program);
81 cmd.args(args)
82 .stdin(Stdio::null())
83 .stdout(Stdio::null())
84 .stderr(Stdio::piped());
85 #[cfg(windows)]
86 {
87 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
88 use std::os::windows::process::CommandExt;
89 cmd.creation_flags(CREATE_NO_WINDOW);
90 }
91 cmd.spawn()
92}
93
94fn stderr_tail(child: &mut Child) -> String {
96 let Some(mut pipe) = child.stderr.take() else {
97 return String::new();
98 };
99 use std::io::Read;
100 let mut buf = Vec::new();
101 let _ = pipe.read_to_end(&mut buf);
102 if buf.len() > STDERR_TAIL_LEN {
103 let tail = &buf[buf.len() - STDERR_TAIL_LEN..];
104 String::from_utf8_lossy(tail).to_string()
105 } else {
106 String::from_utf8_lossy(&buf).to_string()
107 }
108}
109
110pub fn wait_ready(
112 child: &mut Child,
113 marker: &std::path::Path,
114 timeout: Duration,
115) -> Result<(), LaunchFail> {
116 let deadline = Instant::now() + timeout;
117 loop {
118 if marker.is_file() {
119 return Ok(());
120 }
121 match child.try_wait() {
122 Ok(Some(status)) => {
123 let tail = stderr_tail(child);
124 return Err(LaunchFail::Exited {
125 code: status.code().unwrap_or(-1),
126 stderr_tail: tail,
127 });
128 }
129 Ok(None) => {}
130 Err(_) => {} }
132 if Instant::now() >= deadline {
133 let _ = child.kill();
134 return Err(LaunchFail::NoReadySignal { timeout });
135 }
136 std::thread::sleep(Duration::from_millis(50));
137 }
138}
139
140pub fn launch(
143 program: &std::path::Path,
144 args: &[String],
145 marker: &std::path::Path,
146 timeout: Duration,
147) -> Result<(), LaunchFail> {
148 let mut child = match spawn_detached(program, args) {
149 Ok(c) => c,
150 Err(e) => {
151 return Err(LaunchFail::Exited {
152 code: 0,
153 stderr_tail: format!("spawn failed: {e}"),
154 });
155 }
156 };
157 match wait_ready(&mut child, marker, timeout) {
158 Ok(()) => Ok(()), Err(e) => Err(e),
160 }
161}