use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use crate::hooks::{HookCommandExecutor, HookCommandOutput, HookExecutors, HookWebhookSender};
use tokio_util::sync::CancellationToken;
use crate::tools::exec::{KillReason, run_with_kill_on_timeout_or_cancel};
pub fn daemon_executors() -> HookExecutors {
HookExecutors {
command: Some(command_executor()),
webhook: Some(webhook_sender()),
}
}
fn command_executor() -> HookCommandExecutor {
Arc::new(|command, cwd, envs, timeout, cancel| {
Box::pin(run_hook_command(command, cwd, envs, timeout, cancel))
})
}
async fn run_hook_command(
command: String,
cwd: PathBuf,
envs: BTreeMap<String, String>,
timeout: Duration,
cancel: CancellationToken,
) -> anyhow::Result<HookCommandOutput> {
let outcome = run_with_kill_on_timeout_or_cancel(
&command,
Some(timeout),
Some(&cwd),
Some(&envs),
&cancel,
)
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
match outcome.kill_reason {
Some(KillReason::Cancelled) => anyhow::bail!("cancelled"),
Some(KillReason::TimedOut { .. }) => {
anyhow::bail!("timed out after {}ms", timeout.as_millis())
}
None => {
let code = outcome
.exit_code
.ok_or_else(|| anyhow::anyhow!("command failed to produce an exit code"))?;
if code != 0 {
anyhow::bail!("command exited {code}: {}", outcome.stderr.trim());
}
Ok(HookCommandOutput {
stdout: outcome.stdout,
stderr: outcome.stderr,
})
}
}
}
fn webhook_sender() -> HookWebhookSender {
Arc::new(|url, payload_json, headers, timeout, cancel| {
Box::pin(run_hook_webhook(
url,
payload_json,
headers,
timeout,
cancel,
))
})
}
async fn run_hook_webhook(
url: String,
payload_json: String,
headers: BTreeMap<String, String>,
timeout: Duration,
cancel: CancellationToken,
) -> anyhow::Result<()> {
let client = reqwest::Client::builder()
.user_agent(format!("theway/{}", env!("CARGO_PKG_VERSION")))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
let mut req = client
.post(url)
.timeout(timeout)
.header("Content-Type", "application/json")
.body(payload_json);
for (k, v) in headers {
req = req.header(k, v);
}
let resp = tokio::select! {
r = req.send() => r?,
_ = cancel.cancelled() => {
anyhow::bail!("cancelled");
}
};
let status = resp.status();
if !status.is_success() {
let text = resp.text().await.unwrap_or_default();
anyhow::bail!(
"webhook status {status}: {}",
text.chars().take(500).collect::<String>()
);
}
Ok(())
}