hyperion/global/
hook_runner.rs

1use 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            // Drop child
41        }))
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(()) => { // Nothing to notify, hook spawned successfully
83                                }
84                                Err(error) => {
85                                    warn!(error = %error, event = ?message, "hook error");
86                                }
87                            }
88                        }
89                        None => {
90                            // No hook for this event
91                        }
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}