hyperion/global/
hook_runner.rs1use std::{collections::BTreeMap, fmt::Display, sync::Arc};
2
3use tokio::sync::broadcast;
4
5use super::{Event, InstanceEvent, InstanceEventKind};
6use crate::models::Hooks;
7
8const INSTANCE_ID: &str = "HYPERION_INSTANCE_ID";
9
10struct HookBuilder<'s> {
11 variables: BTreeMap<&'static str, String>,
12 command: &'s Vec<String>,
13}
14
15impl<'s> HookBuilder<'s> {
16 pub fn new(command: &'s Vec<String>) -> Self {
17 Self {
18 variables: Default::default(),
19 command,
20 }
21 }
22
23 pub fn arg(mut self, k: &'static str, v: impl Display) -> Self {
24 self.variables.insert(k, v.to_string());
25 self
26 }
27
28 pub async fn run(self) -> Option<Result<(), std::io::Error>> {
29 if self.command.is_empty() {
30 return None;
31 }
32
33 let mut process = tokio::process::Command::new(&self.command[0]);
34 process.args(&self.command[1..]);
35 process.envs(self.variables);
36
37 debug!(command = ?self.command, "spawning hook");
38
39 Some(process.spawn().map(|_| {
40 }))
42 }
43}
44
45#[derive(Debug)]
46pub struct HookRunner {
47 event_rx: broadcast::Receiver<Event>,
48 config: Arc<Hooks>,
49}
50
51impl HookRunner {
52 pub fn new(hooks: Hooks, event_rx: broadcast::Receiver<Event>) -> Self {
53 Self {
54 config: Arc::new(hooks),
55 event_rx,
56 }
57 }
58
59 async fn handle_message(&self, message: &Event) -> Option<Result<(), std::io::Error>> {
60 match message {
61 Event::Start => HookBuilder::new(&self.config.start).run(),
62 Event::Stop => HookBuilder::new(&self.config.stop).run(),
63 Event::Instance(InstanceEvent { id, kind }) => match kind {
64 InstanceEventKind::Start => HookBuilder::new(&self.config.instance_start),
65 InstanceEventKind::Stop => HookBuilder::new(&self.config.instance_stop),
66 InstanceEventKind::Activate => HookBuilder::new(&self.config.instance_activate),
67 InstanceEventKind::Deactivate => HookBuilder::new(&self.config.instance_deactivate),
68 }
69 .arg(INSTANCE_ID, id)
70 .run(),
71 }
72 .await
73 }
74
75 pub async fn run(mut self) {
76 loop {
77 match self.event_rx.recv().await {
78 Ok(message) => {
79 match self.handle_message(&message).await {
80 Some(result) => {
81 match result {
82 Ok(()) => { }
84 Err(error) => {
85 warn!(error = %error, event = ?message, "hook error");
86 }
87 }
88 }
89 None => {
90 }
92 }
93 }
94 Err(error) => match error {
95 broadcast::error::RecvError::Closed => {
96 break;
97 }
98 broadcast::error::RecvError::Lagged(skipped) => {
99 warn!(skipped = %skipped, "hook runner missed events");
100 }
101 },
102 }
103 }
104 }
105}