use crate::Result;
use crate::cli::logs;
use crate::daemon_id::DaemonId;
use crate::daemon_status::DaemonStatus;
use crate::env;
use crate::ipc::client::IpcClient;
use crate::pitchfork_toml::PitchforkToml;
use crate::procs::PROCS;
use crate::settings::settings;
use crate::state_file::StateFile;
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::time;
#[cfg(windows)]
use tokio::signal;
#[cfg(unix)]
use tokio::signal::unix::{self, SignalKind};
#[derive(Debug, usage_rs::Args)]
#[usage(
verbatim_doc_comment,
long_about = "\
Wait for one or more daemons to stop, tailing the logs along the way
Blocks until every specified daemon stops running, while displaying its
log output in real-time. Already-finished daemons are evaluated without
waiting; their exit codes still count. With no daemon IDs and no
`--group`, shows an interactive picker of the currently running daemons.
With `--kill`, an incoming signal (SIGINT/SIGTERM/SIGHUP/SIGQUIT, or Ctrl-C
on Windows) first stops the waited daemons via the supervisor (graceful
SIGTERM then SIGKILL, hooks fire, reverse dependency order), then the
command exits with 128 + the signal number like the shell, so Ctrl-C
yields 130.
Exit code: 0 when every waited daemon stopped cleanly. Otherwise the exit
code of the first failing daemon (in the order given) is propagated;
unknown exit codes, failed daemons, and missing statuses map to 1.
Useful in scripts that need to wait for daemons to complete.
Examples:
pitchfork wait api Wait for 'api' to stop, exit with its status
pitchfork wait api worker Wait for 'api' and 'worker' to stop
pitchfork wait --group backend Wait for the whole 'backend' group
pitchfork wait --kill api Stop 'api' gracefully when a signal arrives
pitchfork w api Alias for 'wait'
pitchfork wait api && echo done Run command after the daemon stops"
)]
pub struct Wait {
id: Vec<String>,
#[usage(long, value_name = "GROUP")]
group: Option<String>,
#[usage(long)]
kill: bool,
}
impl Wait {
pub async fn run(&self) -> Result<()> {
let no_target = self.id.is_empty() && self.group.is_none();
let ids: Vec<DaemonId> = if no_target {
super::interactive::require_interactive_terminal()?;
let ipc = Arc::new(IpcClient::connect(false).await?);
let candidates = ipc.get_running_daemons().await?;
super::interactive::select_daemons_interactively(&candidates, "wait")?
} else {
PitchforkToml::resolve_ids_and_group(&self.id, self.group.as_deref())?
};
let sf = StateFile::get();
let mut watched_ids: Vec<DaemonId> = Vec::new();
let mut polled: Vec<(DaemonId, u32)> = Vec::new();
for id in &ids {
match sf.daemons.get(id) {
Some(daemon) if !is_terminal_status(&daemon.status) => {
watched_ids.push(id.clone());
if let Some(pid) = daemon.pid {
polled.push((id.clone(), pid));
}
}
Some(_) => {
watched_ids.push(id.clone());
}
None => {
warn!("{id} is not running");
}
}
}
if watched_ids.is_empty() {
return Ok(());
}
let ipc: Option<Arc<IpcClient>> = if self.kill {
Some(Arc::new(IpcClient::connect(false).await?))
} else {
None
};
let tail_names = watched_ids.clone();
tokio::spawn(async move {
logs::tail_logs(
&tail_names,
true,
false,
Vec::new(),
Vec::new(),
None,
settings().logs.timestamp,
false,
)
.await
.unwrap_or_default();
});
let mut signal_rx = if self.kill {
Some(register_signal_receiver()?)
} else {
None
};
if !polled.is_empty() {
let mut interval = time::interval(time::Duration::from_millis(100));
let mut remaining = polled;
loop {
tokio::select! {
signo = wait_for_signal(&mut signal_rx), if signal_rx.is_some() => {
match signo {
Some(signo) => {
let stop_ids: Vec<DaemonId> =
remaining.iter().map(|(id, _)| id.clone()).collect();
let ipc = ipc.as_ref().expect("--kill connects IPC upfront");
if let Err(e) = ipc.stop_daemons(&stop_ids).await {
warn!("failed to stop waited daemons on signal: {e}");
}
std::process::exit(128 + signo);
}
None => {
warn!("--kill signal handling is no longer active; continuing to wait");
signal_rx = None;
}
}
}
_ = interval.tick() => {
let mut i = 0;
while i < remaining.len() {
let (_, pid) = &remaining[i];
if !PROCS.is_running(*pid) {
remaining.remove(i);
} else {
i += 1;
}
}
if remaining.is_empty() {
break;
}
}
}
}
}
let statuses = read_terminal_statuses(&watched_ids).await;
if let Some(exit_code) = watched_ids
.iter()
.map(|id| daemon_exit_code(id, &statuses))
.find(|code| *code != 0)
{
std::process::exit(exit_code);
}
Ok(())
}
}
#[cfg(unix)]
fn register_signal_receiver() -> Result<mpsc::Receiver<i32>> {
let (tx, rx) = mpsc::channel(4);
let mut registered = 0;
for (kind, signo) in [
(SignalKind::interrupt(), libc::SIGINT),
(SignalKind::terminate(), libc::SIGTERM),
(SignalKind::hangup(), libc::SIGHUP),
(SignalKind::quit(), libc::SIGQUIT),
] {
let stream = match unix::signal(kind) {
Ok(s) => s,
Err(e) => {
warn!("Failed to register signal handler for {kind:?}: {e}");
continue;
}
};
registered += 1;
let tx = tx.clone();
tokio::spawn(async move {
let mut stream = stream;
if stream.recv().await.is_some() {
let _ = tx.send(signo).await;
}
});
}
if registered == 0 {
return Err(miette::miette!(
"failed to register any signal handler for --kill"
));
}
Ok(rx)
}
#[cfg(windows)]
fn register_signal_receiver() -> Result<mpsc::Receiver<i32>> {
let (tx, rx) = mpsc::channel(4);
tokio::spawn(async move {
if signal::ctrl_c().await.is_ok() {
let _ = tx.send(2).await;
}
});
Ok(rx)
}
async fn wait_for_signal(signal_rx: &mut Option<mpsc::Receiver<i32>>) -> Option<i32> {
signal_rx.as_mut()?.recv().await
}
fn daemon_exit_code(id: &DaemonId, statuses: &[(DaemonId, DaemonStatus)]) -> i32 {
statuses
.iter()
.find(|(status_id, _)| status_id == id)
.map_or(1, |(_, status)| status_exit_code(status))
}
fn status_exit_code(status: &DaemonStatus) -> i32 {
match status {
DaemonStatus::Stopped => 0,
DaemonStatus::Completed => 0,
DaemonStatus::Errored(code) if *code != -1 => *code,
DaemonStatus::Errored(_) => 1,
DaemonStatus::Failed(_) => 1,
_ => 1,
}
}
fn is_terminal_status(status: &DaemonStatus) -> bool {
!status.is_running() && !status.is_waiting() && !status.is_stopping()
}
fn fresh_statuses(ids: &[DaemonId]) -> Vec<(DaemonId, DaemonStatus)> {
StateFile::read(&*env::PITCHFORK_STATE_FILE)
.map(|sf| {
ids.iter()
.filter_map(|id| sf.daemons.get(id).map(|d| (id.clone(), d.status.clone())))
.collect()
})
.unwrap_or_default()
}
async fn read_terminal_statuses(ids: &[DaemonId]) -> Vec<(DaemonId, DaemonStatus)> {
for _ in 0..40 {
let statuses = fresh_statuses(ids);
if statuses.len() == ids.len()
&& statuses
.iter()
.all(|(_, status)| is_terminal_status(status))
{
return statuses;
}
time::sleep(time::Duration::from_millis(50)).await;
}
fresh_statuses(ids)
}