use std::net::Ipv6Addr;
use std::process::ExitStatus;
use anyhow::{anyhow, ensure, Context, Result};
use tempfile::TempDir;
use tokio::net::TcpListener;
use tokio::process::Command;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
pub mod nats;
pub fn tempdir() -> Result<TempDir> {
tempfile::tempdir().context("failed to create temporary directory")
}
pub async fn free_port() -> Result<u16> {
TcpListener::bind((Ipv6Addr::LOCALHOST, 0))
.await
.context("failed to start TCP listener")?
.local_addr()
.context("failed to query listener local address")
.map(|v| v.port())
}
pub struct BackgroundServer {
handle: JoinHandle<Result<ExitStatus>>,
stop_tx: oneshot::Sender<()>,
}
impl BackgroundServer {
pub async fn spawn(cmd: &mut Command) -> Result<Self> {
let mut child = cmd
.kill_on_drop(true)
.spawn()
.with_context(|| format!("failed to spawn child [{cmd:#?}]"))?;
let (stop_tx, stop_rx) = oneshot::channel();
let handle = tokio::spawn(async move {
tokio::select!(
res = stop_rx => {
res.context("failed to wait for shutdown")?;
child.kill().await.context("failed to kill child")?;
child.wait().await
}
status = child.wait() => {
status
}
)
.context("failed to wait for child")
});
Ok(Self { handle, stop_tx })
}
pub async fn stop(self) -> Result<()> {
self.stop_tx
.send(())
.map_err(|()| anyhow!("failed to send stop"))?;
let status = self
.handle
.await
.context("failed to wait for server to exit")?
.context("server failed to exit")?;
ensure!(status.code().is_none());
Ok(())
}
}