use std::future::Future;
use std::net::SocketAddr;
use std::time::Duration;
use tokio::time::Instant;
use tokio::net::TcpStream;
use crate::error::{Error, Result};
use super::RunningProcess;
const READINESS_POLL: Duration = Duration::from_millis(50);
const CONNECT_ATTEMPT_CAP: Duration = Duration::from_secs(1);
impl RunningProcess {
pub async fn wait_for_line(
&mut self,
predicate: impl Fn(&str) -> bool + Send,
within: Duration,
) -> Result<String> {
use tokio_stream::StreamExt;
let mut lines = self.drain_stdout_lines()?;
let search = async {
while let Some(line) = lines.next().await {
if predicate(&line) {
return Some(line);
}
}
None };
match tokio::time::timeout(within, search).await {
Ok(Some(line)) => Ok(line),
Ok(None) | Err(_) => Err(self.not_ready(within)),
}
}
pub async fn wait_for<F, Fut>(&mut self, check: F, within: Duration) -> Result<()>
where
F: FnMut() -> Fut + Send,
Fut: Future<Output = bool> + Send,
{
self.poll_until(check, within).await
}
pub async fn wait_for_port(&mut self, addr: SocketAddr, within: Duration) -> Result<()> {
let deadline = Instant::now() + within.min(crate::MAX_DEADLINE);
self.poll_until(
move || {
let remaining = deadline.saturating_duration_since(Instant::now());
async move {
let cap = CONNECT_ATTEMPT_CAP
.min(remaining)
.max(Duration::from_millis(1));
matches!(
tokio::time::timeout(cap, TcpStream::connect(addr)).await,
Ok(Ok(_))
)
}
},
within,
)
.await
}
async fn poll_until<F, Fut>(&mut self, mut check: F, within: Duration) -> Result<()>
where
F: FnMut() -> Fut,
Fut: Future<Output = bool>,
{
self.ensure_background_drains();
let deadline = Instant::now() + within.min(crate::MAX_DEADLINE);
loop {
if check().await {
return Ok(());
}
if self.has_exited_now() {
return Err(self.not_ready(within));
}
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(self.not_ready(within));
}
tokio::time::sleep(READINESS_POLL.min(remaining)).await;
}
}
fn not_ready(&self, within: Duration) -> Error {
Error::NotReady {
program: self.program.clone(),
timeout: within,
}
}
}
#[cfg(test)]
#[allow(dead_code)]
fn probe_futures_are_send(rp: &mut RunningProcess) {
fn assert_send<T: Send>(_: &T) {}
assert_send(&rp.wait_for_line(|line| line.is_empty(), Duration::ZERO));
assert_send(&rp.wait_for(|| async { true }, Duration::ZERO));
let addr: SocketAddr = ([127, 0, 0, 1], 0).into();
assert_send(&rp.wait_for_port(addr, Duration::ZERO));
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use crate::command::Command;
use crate::doubles::{Reply, ScriptedRunner};
use crate::runner::ProcessRunner;
#[tokio::test]
async fn probe_background_drains_stderr_when_stdout_is_not_piped() {
let mut run = ScriptedRunner::new()
.fallback(Reply::ok("").with_stderr("warn-1\nwarn-2\n"))
.start(&Command::new("tool").stdout(crate::StdioMode::Null))
.await
.expect("scripted start");
run.wait_for(|| async { true }, Duration::from_millis(50))
.await
.expect("an always-true check passes immediately");
assert!(
run.stderr_sink.is_some(),
"the probe must background-drain piped stderr even with a non-piped stdout"
);
let finished = run.finish().await.expect("finish the scripted run");
assert_eq!(
finished.stderr, "warn-1\nwarn-2",
"the stderr the probe drained is handed back by finish"
);
}
#[tokio::test]
async fn wait_for_port_probe_also_drains_stderr_when_stdout_is_not_piped() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral listener");
let addr = listener.local_addr().expect("local addr");
let mut run = ScriptedRunner::new()
.fallback(Reply::ok("").with_stderr("boom\n"))
.start(&Command::new("tool").stdout(crate::StdioMode::Null))
.await
.expect("scripted start");
run.wait_for_port(addr, Duration::from_secs(1))
.await
.expect("the bound port is immediately ready");
assert!(
run.stderr_sink.is_some(),
"wait_for_port must background-drain piped stderr even with a non-piped stdout"
);
}
#[tokio::test]
async fn probe_after_streaming_does_not_reinstall_the_drains() {
let mut run = ScriptedRunner::new()
.fallback(Reply::ok("out-1\n").with_stderr("err-1\n"))
.start(&Command::new("tool"))
.await
.expect("scripted start");
let _stream = run.stdout_lines().expect("stream stdout");
let stderr_before =
std::sync::Arc::clone(run.stderr_sink.as_ref().expect("streaming armed stderr"));
run.wait_for(|| async { true }, Duration::from_millis(50))
.await
.expect("an always-true check passes immediately");
assert!(
std::sync::Arc::ptr_eq(
&stderr_before,
run.stderr_sink.as_ref().expect("stderr still armed"),
),
"the probe must not replace the stderr sink an earlier stream installed"
);
}
}