use std::process::Stdio;
use thiserror::Error;
use tokio::sync::watch;
use tracing::Instrument;
use zebra_chain::block;
use zebra_state::ChainTipChange;
use crate::components::sync::SyncStatus;
#[cfg(test)]
mod tests;
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
pub block_notify_command: Option<String>,
}
#[allow(unknown_lints)]
#[allow(clippy::derivable_impls)]
impl Default for Config {
fn default() -> Self {
Self {
block_notify_command: None,
}
}
}
#[derive(Error, Debug)]
pub enum BlockNotifyError {
#[error("chain tip sender was dropped")]
TipChange(watch::error::RecvError),
#[error("sync status sender was dropped")]
SyncStatus(watch::error::RecvError),
}
pub async fn run_block_notify(
command: String,
mut sync_status: SyncStatus,
mut chain_tip_change: ChainTipChange,
) -> Result<(), BlockNotifyError> {
info!("initializing block notify task");
loop {
let tip_action = chain_tip_change
.wait_for_tip_change()
.await
.map_err(BlockNotifyError::TipChange)?;
sync_status
.wait_until_close_to_tip()
.await
.map_err(BlockNotifyError::SyncStatus)?;
let (hash, height) = chain_tip_change
.last_tip_change()
.unwrap_or(tip_action)
.best_tip_hash_and_height();
spawn_notify_command(&command, hash, height);
}
}
fn spawn_notify_command(command: &str, hash: block::Hash, height: block::Height) {
let rendered = render_command(command, hash);
#[cfg(not(target_os = "windows"))]
let mut cmd = {
let mut cmd = tokio::process::Command::new("/bin/sh");
cmd.arg("-c").arg(&rendered);
cmd
};
#[cfg(target_os = "windows")]
let mut cmd = {
let mut cmd = tokio::process::Command::new("cmd");
cmd.arg("/C").arg(&rendered);
cmd
};
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(unix)]
cmd.process_group(0);
let span = info_span!("block_notify_command", %hash, ?height);
match cmd.spawn() {
Ok(mut child) => {
tokio::spawn(
async move {
match child.wait().await {
Ok(status) if !status.success() => {
warn!(?rendered, ?status, "block notify command exited non-zero");
}
Ok(_) => {}
Err(error) => {
warn!(?rendered, ?error, "failed to wait on block notify command");
}
}
}
.instrument(span),
);
}
Err(error) => warn!(?rendered, ?error, "failed to spawn block notify command"),
}
}
fn render_command(command: &str, hash: block::Hash) -> String {
command.replace("%s", &hash.to_string())
}