1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
use std::{collections::BTreeMap, fmt::Display, sync::Arc};

use tokio::sync::broadcast;

use super::{Event, InstanceEvent, InstanceEventKind};
use crate::models::Hooks;

const INSTANCE_ID: &str = "HYPERION_INSTANCE_ID";

struct HookBuilder<'s> {
    variables: BTreeMap<&'static str, String>,
    command: &'s Vec<String>,
}

impl<'s> HookBuilder<'s> {
    pub fn new(command: &'s Vec<String>) -> Self {
        Self {
            variables: Default::default(),
            command,
        }
    }

    pub fn arg(mut self, k: &'static str, v: impl Display) -> Self {
        self.variables.insert(k, v.to_string());
        self
    }

    pub async fn run(self) -> Option<Result<(), std::io::Error>> {
        if self.command.is_empty() {
            return None;
        }

        let mut process = tokio::process::Command::new(&self.command[0]);
        process.args(&self.command[1..]);
        process.envs(self.variables);

        debug!(command = ?self.command, "spawning hook");

        Some(process.spawn().map(|_| {
            // Drop child
        }))
    }
}

#[derive(Debug)]
pub struct HookRunner {
    event_rx: broadcast::Receiver<Event>,
    config: Arc<Hooks>,
}

impl HookRunner {
    pub fn new(hooks: Hooks, event_rx: broadcast::Receiver<Event>) -> Self {
        Self {
            config: Arc::new(hooks),
            event_rx,
        }
    }

    async fn handle_message(&self, message: &Event) -> Option<Result<(), std::io::Error>> {
        match message {
            Event::Start => HookBuilder::new(&self.config.start).run(),
            Event::Stop => HookBuilder::new(&self.config.stop).run(),
            Event::Instance(InstanceEvent { id, kind }) => match kind {
                InstanceEventKind::Start => HookBuilder::new(&self.config.instance_start),
                InstanceEventKind::Stop => HookBuilder::new(&self.config.instance_stop),
                InstanceEventKind::Activate => HookBuilder::new(&self.config.instance_activate),
                InstanceEventKind::Deactivate => HookBuilder::new(&self.config.instance_deactivate),
            }
            .arg(INSTANCE_ID, id)
            .run(),
        }
        .await
    }

    pub async fn run(mut self) {
        loop {
            match self.event_rx.recv().await {
                Ok(message) => {
                    match self.handle_message(&message).await {
                        Some(result) => {
                            match result {
                                Ok(()) => { // Nothing to notify, hook spawned successfully
                                }
                                Err(error) => {
                                    warn!(error = %error, event = ?message, "hook error");
                                }
                            }
                        }
                        None => {
                            // No hook for this event
                        }
                    }
                }
                Err(error) => match error {
                    broadcast::error::RecvError::Closed => {
                        break;
                    }
                    broadcast::error::RecvError::Lagged(skipped) => {
                        warn!(skipped = %skipped, "hook runner missed events");
                    }
                },
            }
        }
    }
}