#![warn(missing_docs)]
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{
path::Path,
process::{Child, Stdio},
};
pub mod scripting;
mod error;
pub use error::Error;
#[derive(Serialize, Deserialize, Debug)]
pub struct ScriptInfo {
pub name: String,
pub script_type: ScriptType,
pub hooks: Box<[String]>,
}
impl ScriptInfo {
pub fn new(
name: &'static str,
script_type: ScriptType,
hooks: &'static [&'static str],
) -> Self {
Self {
name: name.into(),
script_type,
hooks: hooks.iter().map(|hook| String::from(*hook)).collect(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum ScriptType {
OneShot,
Daemon,
}
#[derive(Default)]
pub struct ScriptManager {
scripts: Vec<Script>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum Message {
Greeting,
Execute,
}
impl ScriptManager {
pub fn add_scripts_by_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), bincode::Error> {
fn start_script(path: &Path) -> Result<Script, bincode::Error> {
let mut script = std::process::Command::new(path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let stdin = script.stdin.as_mut().expect("stdin is piped");
bincode::serialize_into(stdin, &Message::Greeting)?;
let stdout = script.stdout.as_mut().expect("stdout is piped");
let metadata: ScriptInfo = bincode::deserialize_from(stdout)?;
let script = if matches!(metadata.script_type, ScriptType::Daemon) {
ScriptTypeInternal::Daemon(script)
} else {
ScriptTypeInternal::OneShot(path.to_path_buf())
};
Ok(Script {
script,
metadata,
state: State::Active,
})
}
let path = path.as_ref();
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
self.scripts.push(start_script(&path)?);
}
}
Ok(())
}
pub fn trigger<'a, H: 'static + Hook>(
&'a mut self,
hook: H,
) -> impl Iterator<Item = Result<<H as Hook>::Output, bincode::Error>> + 'a {
self.scripts.iter_mut().filter_map(move |script| {
if script.is_active() && script.is_listening_for::<H>() {
Some(script.trigger_internal(&hook))
} else {
None
}
})
}
pub fn scripts(&self) -> &[Script] {
&self.scripts
}
pub fn scripts_mut(&mut self) -> &mut [Script] {
&mut self.scripts
}
}
impl Drop for ScriptManager {
fn drop(&mut self) {
self.scripts.iter_mut().for_each(|script| script.end());
}
}
#[derive(Debug)]
pub struct Script {
metadata: ScriptInfo,
script: ScriptTypeInternal,
state: State,
}
#[derive(Debug)]
enum State {
Active,
Inactive,
}
#[derive(Debug)]
enum ScriptTypeInternal {
Daemon(Child),
OneShot(std::path::PathBuf),
}
impl Script {
pub fn metadata(&self) -> &ScriptInfo {
&self.metadata
}
pub fn activate(&mut self) {
self.state = State::Active;
}
pub fn deactivate(&mut self) {
self.state = State::Inactive;
}
pub fn is_active(&self) -> bool {
matches!(self.state, State::Active)
}
pub fn is_listening_for<H: Hook>(&self) -> bool {
self.metadata
.hooks
.iter()
.any(|hook| hook.as_str() == H::NAME)
}
pub fn trigger<H: Hook>(&mut self, hook: &H) -> Result<<H as Hook>::Output, Error> {
if self.is_listening_for::<H>() {
self.trigger_internal(hook).map_err(Error::Bincode)
} else {
Err(Error::ScriptIsNotListeningForHook)
}
}
}
impl Script {
fn trigger_internal<H: Hook>(
&mut self,
hook: &H,
) -> Result<<H as Hook>::Output, bincode::Error> {
let trigger_hook_common = |script: &mut Child| {
let mut stdin = script.stdin.as_mut().expect("stdin is piped");
let stdout = script.stdout.as_mut().expect("stdout is piped");
bincode::serialize_into(&mut stdin, &Message::Execute)?;
bincode::serialize_into(&mut stdin, H::NAME)?;
bincode::serialize_into(stdin, hook)?;
bincode::deserialize_from(stdout)
};
match &mut self.script {
ScriptTypeInternal::Daemon(ref mut script) => trigger_hook_common(script),
ScriptTypeInternal::OneShot(script_path) => trigger_hook_common(
&mut std::process::Command::new(script_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?,
),
}
}
fn end(&mut self) {
if let ScriptTypeInternal::Daemon(ref mut script) = self.script {
let _ = script.kill();
}
}
}
pub trait Hook: Serialize {
const NAME: &'static str;
type Output: DeserializeOwned;
}