monoloop_testkit/
grok_serve.rs1use std::net::SocketAddr;
7use std::path::{Path, PathBuf};
8use std::process::Stdio;
9use std::time::Duration;
10use tokio::net::TcpStream;
11use tokio::process::{Child, Command};
12use tokio::time::{sleep, timeout};
13
14#[derive(Clone, Debug)]
16pub struct GrokServeOptions {
17 pub port: Option<u16>,
19 pub secret: String,
21 pub grok_bin: PathBuf,
23 pub ready_timeout: Duration,
25 pub log_path: Option<PathBuf>,
27}
28
29impl Default for GrokServeOptions {
30 fn default() -> Self {
31 Self {
32 port: Some(2419),
33 secret: "monoloop-live-test".into(),
34 grok_bin: PathBuf::from("grok"),
35 ready_timeout: Duration::from_secs(15),
36 log_path: None,
37 }
38 }
39}
40
41#[derive(Debug)]
46pub struct ManagedGrokServe {
47 child: Child,
48 port: u16,
49 secret: String,
50 log_path: Option<PathBuf>,
51}
52
53impl ManagedGrokServe {
54 pub async fn start(opts: GrokServeOptions) -> Result<Self, String> {
56 let port = match opts.port {
57 Some(p) => p,
58 None => free_loopback_port()?,
59 };
60 if port_is_listening(port).await {
61 return Err(format!(
62 "port {port} already in use — stop the other listener or choose another port"
63 ));
64 }
65
66 if let Some(ref log) = opts.log_path {
67 if let Some(parent) = log.parent() {
68 if !parent.as_os_str().is_empty() {
69 std::fs::create_dir_all(parent)
70 .map_err(|e| format!("create log dir {}: {e}", parent.display()))?;
71 }
72 }
73 }
74
75 let bind = format!("127.0.0.1:{port}");
76 let mut cmd = Command::new(&opts.grok_bin);
77 cmd.arg("agent")
78 .arg("--always-approve")
79 .arg("serve")
80 .arg("--bind")
81 .arg(&bind)
82 .arg("--secret")
83 .arg(&opts.secret)
84 .kill_on_drop(true)
85 .stdin(Stdio::null());
86
87 if let Some(ref log) = opts.log_path {
88 let f = std::fs::File::create(log)
89 .map_err(|e| format!("open log {}: {e}", log.display()))?;
90 let f2 = f
91 .try_clone()
92 .map_err(|e| format!("clone log handle: {e}"))?;
93 cmd.stdout(Stdio::from(f)).stderr(Stdio::from(f2));
94 } else {
95 cmd.stdout(Stdio::null()).stderr(Stdio::null());
96 }
97
98 let child = cmd.spawn().map_err(|e| {
99 format!(
100 "failed to spawn `{} agent serve`: {e} (is grok on PATH?)",
101 opts.grok_bin.display()
102 )
103 })?;
104
105 let serve = Self {
106 child,
107 port,
108 secret: opts.secret,
109 log_path: opts.log_path,
110 };
111
112 match timeout(opts.ready_timeout, wait_until_listening(port)).await {
113 Ok(Ok(())) => Ok(serve),
114 Ok(Err(e)) => {
115 let _ = serve.stop().await;
116 Err(e)
117 }
118 Err(_) => {
119 let _ = serve.stop().await;
120 Err(format!(
121 "grok serve not listening on 127.0.0.1:{port} within {:?}",
122 opts.ready_timeout
123 ))
124 }
125 }
126 }
127
128 pub fn port(&self) -> u16 {
130 self.port
131 }
132
133 pub fn secret(&self) -> &str {
135 &self.secret
136 }
137
138 pub fn log_path(&self) -> Option<&Path> {
140 self.log_path.as_deref()
141 }
142
143 pub fn pid(&self) -> Option<u32> {
145 self.child.id()
146 }
147
148 pub async fn stop(mut self) -> Result<(), String> {
150 self.kill_inner().await
151 }
152
153 async fn kill_inner(&mut self) -> Result<(), String> {
154 let _ = self.child.start_kill();
156 match timeout(Duration::from_secs(3), self.child.wait()).await {
157 Ok(Ok(status)) => {
158 if !status.success() {
159 }
161 Ok(())
162 }
163 Ok(Err(e)) => Err(format!("wait for grok serve child: {e}")),
164 Err(_) => {
165 let _ = self.child.start_kill();
166 let _ = timeout(Duration::from_secs(2), self.child.wait()).await;
167 Ok(())
168 }
169 }
170 }
171}
172
173impl Drop for ManagedGrokServe {
174 fn drop(&mut self) {
175 let _ = self.child.start_kill();
177 }
178}
179
180async fn wait_until_listening(port: u16) -> Result<(), String> {
181 loop {
182 if port_is_listening(port).await {
183 return Ok(());
184 }
185 sleep(Duration::from_millis(50)).await;
186 }
187}
188
189async fn port_is_listening(port: u16) -> bool {
190 let addr = SocketAddr::from(([127, 0, 0, 1], port));
191 timeout(Duration::from_millis(100), TcpStream::connect(addr))
192 .await
193 .map(|r| r.is_ok())
194 .unwrap_or(false)
195}
196
197fn free_loopback_port() -> Result<u16, String> {
198 let listener = std::net::TcpListener::bind("127.0.0.1:0")
199 .map_err(|e| format!("bind ephemeral port: {e}"))?;
200 let port = listener
201 .local_addr()
202 .map_err(|e| format!("local_addr: {e}"))?
203 .port();
204 drop(listener);
205 Ok(port)
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn default_options_use_loopback_secret() {
214 let o = GrokServeOptions::default();
215 assert_eq!(o.port, Some(2419));
216 assert_eq!(o.secret, "monoloop-live-test");
217 }
218
219 #[test]
220 fn free_port_is_nonzero() {
221 let p = free_loopback_port().expect("port");
222 assert!(p > 0);
223 }
224}