#![warn(missing_docs)]
use scripting::{FFiData, FFiStr};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{
env,
path::Path,
process::{Child, Stdio},
};
pub use semver::Version;
pub use semver::VersionReq;
pub mod scripting;
mod error;
pub use error::Error;
use crate::scripting::DynamicScript;
#[derive(Serialize, Deserialize, Debug)]
pub struct ScriptInfo {
pub name: String,
pub script_type: ScriptType,
pub hooks: Box<[String]>,
pub version_requirement: VersionReq,
}
impl ScriptInfo {
pub fn new(
name: &'static str,
script_type: ScriptType,
hooks: &'static [&'static str],
version_requirement: VersionReq,
) -> Self {
Self {
name: name.into(),
script_type,
hooks: hooks.iter().map(|hook| String::from(*hook)).collect(),
version_requirement,
}
}
pub fn into_ffi_data(self) -> FFiData {
FFiData::serialize_from(&self).expect("ScriptInfo is always serialize-able")
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum ScriptType {
OneShot,
Daemon,
DynamicLib,
}
#[derive(Default)]
pub struct ScriptManager {
scripts: Vec<Script>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub(crate) enum Message {
Greeting,
Execute,
}
impl ScriptManager {
pub fn add_scripts_by_path<P: AsRef<Path>>(
&mut self,
path: P,
version: Version,
) -> Result<(), Error> {
fn start_script(path: &Path, version: &Version) -> Result<Script, 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)?;
if !metadata.version_requirement.matches(version) {
return Err(Error::ScriptVersionMismatch {
program_actual_version: version.clone(),
program_required_version: metadata.version_requirement,
});
}
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() {
if let Some(ext) = path.extension() {
if ext == env::consts::DLL_EXTENSION {
continue;
}
}
self.scripts.push(start_script(&path, &version)?);
}
}
Ok(())
}
pub unsafe fn add_dynamic_scripts_by_path<P: AsRef<Path>>(
&mut self,
path: P,
version: Version,
) -> Result<(), Error> {
fn load_dynamic_library(path: &Path, version: &Version) -> Result<Script, Error> {
let lib = unsafe { libloading::Library::new(path)? };
let script: libloading::Symbol<&DynamicScript> =
unsafe { lib.get(DynamicScript::NAME)? };
let metadata: ScriptInfo = (script.script_info)().deserialize()?;
if !metadata.version_requirement.matches(version) {
return Err(Error::ScriptVersionMismatch {
program_actual_version: version.clone(),
program_required_version: metadata.version_requirement,
});
}
Ok(Script {
script: ScriptTypeInternal::DynamicLib(lib),
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() {
if let Some(ext) = path.extension() {
if ext == env::consts::DLL_EXTENSION {
self.scripts.push(load_dynamic_library(&path, &version)?);
}
}
}
}
Ok(())
}
pub fn trigger<'a, H: 'static + Hook>(
&'a mut self,
hook: H,
) -> impl Iterator<Item = Result<<H as Hook>::Output, 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),
DynamicLib(libloading::Library),
}
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)
} else {
Err(Error::ScriptIsNotListeningForHook)
}
}
}
impl Script {
fn trigger_internal<H: Hook>(&mut self, hook: &H) -> Result<<H as Hook>::Output, Error> {
let trigger_hook_common =
|script: &mut Child| -> Result<<H as Hook>::Output, bincode::Error> {
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)
};
Ok(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()?,
)?,
ScriptTypeInternal::DynamicLib(lib) => unsafe {
let script: libloading::Symbol<&DynamicScript> = lib.get(DynamicScript::NAME)?;
let output = (script.script)(FFiStr::new(H::NAME), FFiData::serialize_from(hook)?);
output.deserialize()?
},
})
}
fn end(&mut self) {
if let ScriptTypeInternal::Daemon(ref mut script) = self.script {
let _ = script.kill();
}
}
}
pub trait Hook: Serialize + DeserializeOwned {
const NAME: &'static str;
type Output: Serialize + DeserializeOwned;
}