@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "ipc"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rule = { path = "../rule" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["net", "io-util"] }
|
||||
@@ -0,0 +1,63 @@
|
||||
use crate::{
|
||||
SOCKET_NAME,
|
||||
repr::{Error, Request, de_reply},
|
||||
};
|
||||
use rule::parser::When;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
os::{
|
||||
linux::net::SocketAddrExt,
|
||||
unix::net::{SocketAddr, UnixStream},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
stream: UnixStream,
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
impl Client {
|
||||
pub fn connect() -> std::io::Result<Self> {
|
||||
let addr = SocketAddr::from_abstract_name(SOCKET_NAME)?;
|
||||
Ok(Self {
|
||||
stream: UnixStream::connect_addr(&addr)?,
|
||||
buf: Vec::with_capacity(128),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send(&mut self, req: &Request) -> std::io::Result<()> {
|
||||
self.buf.clear();
|
||||
serde_json::to_writer(&mut self.buf, req).unwrap();
|
||||
self.stream
|
||||
.write_all(&(self.buf.len() as u16).to_le_bytes())?;
|
||||
self.stream.write_all(&self.buf)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn recv<T: DeserializeOwned>(&mut self) -> std::io::Result<Result<T, Error>> {
|
||||
let mut len = [0u8; size_of::<u16>()];
|
||||
self.stream.read_exact(&mut len)?;
|
||||
let len = u16::from_le_bytes(len);
|
||||
self.buf.resize(len as usize, 0);
|
||||
self.stream.read_exact(&mut self.buf)?;
|
||||
Ok(de_reply(&String::from_utf8_lossy(&self.buf)))
|
||||
}
|
||||
|
||||
pub fn invoke<T: DeserializeOwned>(&mut self, req: &Request) -> Result<T, Error> {
|
||||
self.send(req).map_err(|_| Error::Delivery)?;
|
||||
self.recv().map_err(|_| Error::Delivery).flatten()
|
||||
}
|
||||
|
||||
pub fn trigger(&mut self, set: String, action: String) -> Result<(), Error> {
|
||||
self.invoke(&Request::Trigger(set, action))
|
||||
}
|
||||
|
||||
pub fn reload_rules(&mut self) -> Result<(), Error> {
|
||||
self.invoke(&Request::ReloadRules)
|
||||
}
|
||||
|
||||
pub fn list_devices(&mut self, when: When) -> Result<Vec<String>, Error> {
|
||||
self.invoke(&Request::ListDevices(when))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod client;
|
||||
pub mod repr;
|
||||
pub mod server;
|
||||
|
||||
pub const SOCKET_NAME: &str = "verified:org.semilabs.os/lxdeviced";
|
||||
@@ -0,0 +1,64 @@
|
||||
use rule::parser::When;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "method", content = "params")]
|
||||
pub enum Request {
|
||||
#[serde(rename = "/org.semilabs.os/LxDeviced/Trigger@LXDEVICED_0.1.0")]
|
||||
Trigger(String, String),
|
||||
|
||||
#[serde(rename = "/org.semilabs.os/LxDeviced/ReloadRules@LXDEVICED_0.1.0")]
|
||||
ReloadRules,
|
||||
|
||||
#[serde(rename = "/org.semilabs.os/LxDeviced/ListDevices@LXDEVICED_0.1.0")]
|
||||
ListDevices(When),
|
||||
}
|
||||
|
||||
pub fn ser_reply<T: Serialize>(result: Result<T, Error>) -> String {
|
||||
const FALLBACK: &str = r#"{"error": "/AnyCall/BadReply@ANYCALL_LITE_1"}"#;
|
||||
|
||||
let reply = match result {
|
||||
Ok(x) => Reply::Ok(x),
|
||||
Err(e) => Reply::Err(e),
|
||||
};
|
||||
serde_json::to_string(&reply).unwrap_or_else(|_| FALLBACK.into())
|
||||
}
|
||||
|
||||
pub fn de_reply<T: DeserializeOwned>(s: &str) -> Result<T, Error> {
|
||||
let reply: Reply<T> = serde_json::from_str(s).map_err(|_| Error::BadReply)?;
|
||||
match reply {
|
||||
Reply::Ok(x) => Ok(x),
|
||||
Reply::Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Reply<T> {
|
||||
Err(Error),
|
||||
Ok(T),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
|
||||
#[serde(tag = "error")]
|
||||
pub enum Error {
|
||||
#[serde(rename = "/AnyCall/BadRequest@ANYCALL_LITE_1")]
|
||||
#[error("Invalid Arguments")]
|
||||
BadRequest,
|
||||
|
||||
#[serde(rename = "/AnyCall/BadReply@ANYCALL_LITE_1")]
|
||||
#[error("Bad Reply")]
|
||||
BadReply,
|
||||
|
||||
#[serde(rename = "/AnyCall/NotImplemented@ANYCALL_LITE_1")]
|
||||
#[error("Not Implemented")]
|
||||
NotImplemented,
|
||||
|
||||
#[serde(rename = "/AnyCall/PermissionDenied@ANYCALL_LITE_1")]
|
||||
#[error("Permission Denied")]
|
||||
PermissionDenied,
|
||||
|
||||
#[serde(rename = "/AnyCall/DeliveryError@ANYCALL_LITE_1")]
|
||||
#[error("Delivery Error")]
|
||||
Delivery,
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use crate::{
|
||||
SOCKET_NAME,
|
||||
repr::{Error, Request, ser_reply},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::os::{linux::net::SocketAddrExt, unix::net::SocketAddr as StdSocketAddr};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::{UnixListener, UnixStream, unix::SocketAddr},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Listener(UnixListener);
|
||||
impl Listener {
|
||||
pub fn bind() -> std::io::Result<Self> {
|
||||
let addr = SocketAddr::from(StdSocketAddr::from_abstract_name(SOCKET_NAME)?);
|
||||
UnixListener::bind_addr(&addr).map(Self)
|
||||
}
|
||||
|
||||
pub async fn accept(&self) -> std::io::Result<Connection> {
|
||||
let stream = self.0.accept().await?.0;
|
||||
let cred = stream.peer_cred()?;
|
||||
Ok(Connection {
|
||||
stream,
|
||||
buf: Vec::with_capacity(128),
|
||||
uid: cred.uid(),
|
||||
gid: cred.gid(),
|
||||
pid: cred.pid().unwrap_or_default() as _,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Connection {
|
||||
stream: UnixStream,
|
||||
buf: Vec<u8>,
|
||||
pub uid: u32,
|
||||
pub gid: u32,
|
||||
pub pid: u32,
|
||||
}
|
||||
impl Connection {
|
||||
pub async fn recv(&mut self) -> std::io::Result<Request> {
|
||||
loop {
|
||||
let size = self.stream.read_u16_le().await?;
|
||||
self.buf.resize(size as usize, 0);
|
||||
self.stream.read_exact(&mut self.buf).await?;
|
||||
let Ok(request) = serde_json::from_slice(&self.buf) else {
|
||||
self.send::<()>(Err(Error::BadRequest)).await?;
|
||||
continue;
|
||||
};
|
||||
break Ok(request);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send<T: Serialize>(&mut self, reply: Result<T, Error>) -> std::io::Result<()> {
|
||||
let reply = ser_reply(reply);
|
||||
self.stream.write_u16_le(reply.len() as u16).await?;
|
||||
self.stream.write_all(reply.as_bytes()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "kobject-uevent"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
netlink-sys = { version = "0.9", features = ["tokio_socket"] }
|
||||
rustc-hash = "2"
|
||||
@@ -0,0 +1,87 @@
|
||||
use netlink_sys::{
|
||||
AsyncSocket, AsyncSocketExt, SocketAddr, TokioSocket, protocols::NETLINK_KOBJECT_UEVENT,
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
pub const MULTICAST_GROUP_KERNEL: u32 = 1;
|
||||
|
||||
pub struct KobjectUeventSocket(TokioSocket);
|
||||
impl KobjectUeventSocket {
|
||||
pub fn connect(group: u32) -> std::io::Result<Self> {
|
||||
let mut socket = TokioSocket::new(NETLINK_KOBJECT_UEVENT)?;
|
||||
let addr = SocketAddr::new(0, group);
|
||||
socket.socket_mut().bind(&addr)?;
|
||||
Ok(Self(socket))
|
||||
}
|
||||
|
||||
pub async fn recv(&self) -> std::io::Result<KobjectUevent> {
|
||||
let (data, sender) = self.0.recv_from_full().await?;
|
||||
if sender.port_number() != 0 {
|
||||
return Err(std::io::ErrorKind::PermissionDenied.into());
|
||||
}
|
||||
Ok(KobjectUevent::parse(data.as_slice()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KobjectUevent(FxHashMap<String, String>);
|
||||
impl KobjectUevent {
|
||||
pub fn inner(&self) -> &FxHashMap<String, String> {
|
||||
&self.0
|
||||
}
|
||||
pub fn parse(s: &[u8]) -> Self {
|
||||
let mut map = FxHashMap::default();
|
||||
for kvp in s.split(|x| *x == b'\0') {
|
||||
let Some(sep) = kvp.iter().position(|x| *x == b'=') else {
|
||||
continue;
|
||||
};
|
||||
let (key, val) = kvp.split_at(sep);
|
||||
let val = &val[1..];
|
||||
map.insert(
|
||||
String::from_utf8_lossy_owned(key.into()),
|
||||
String::from_utf8_lossy_owned(val.into()),
|
||||
);
|
||||
}
|
||||
Self(map)
|
||||
}
|
||||
|
||||
pub fn action(&self) -> Option<&str> {
|
||||
self.0.get("ACTION").map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn devpath(&self) -> Option<&str> {
|
||||
self.0.get("DEVPATH").map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn subsystem(&self) -> Option<&str> {
|
||||
self.0.get("SUBSYSTEM").map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn synth_uuid(&self) -> Option<&str> {
|
||||
self.0.get("SYNTH_UUID").map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn major(&self) -> Option<u32> {
|
||||
let major = self.0.get("MAJOR")?;
|
||||
major.parse().ok()
|
||||
}
|
||||
|
||||
pub fn minor(&self) -> Option<u32> {
|
||||
let minor = self.0.get("MINOR")?;
|
||||
minor.parse().ok()
|
||||
}
|
||||
|
||||
pub fn devname(&self) -> Option<&str> {
|
||||
self.0.get("DEVNAME").map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn devmode(&self) -> Option<u16> {
|
||||
let devmode = self.0.get("DEVMODE")?;
|
||||
let devmode = devmode.strip_prefix("0")?;
|
||||
u16::from_str_radix(devmode, 8).ok()
|
||||
}
|
||||
|
||||
pub fn seqnum(&self) -> Option<&str> {
|
||||
self.0.get("SEQNUM").map(String::as_str)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "rule"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
regex = "1"
|
||||
ron = "0.12"
|
||||
rustc-hash = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["process", "time"] }
|
||||
tracing = "0.1"
|
||||
@@ -0,0 +1,359 @@
|
||||
use crate::{
|
||||
Error,
|
||||
parser::{Action, OnError, Rule, RuleFile, Str, When},
|
||||
};
|
||||
use regex::Regex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{
|
||||
path::Path,
|
||||
process::{ExitStatus, Stdio},
|
||||
sync::{Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{io::AsyncReadExt, process::Child};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RuleSet(Arc<Vec<RuleFile>>);
|
||||
impl RuleSet {
|
||||
pub fn empty() -> Self {
|
||||
Self(Arc::default())
|
||||
}
|
||||
|
||||
pub fn open_dirs(dirs: impl Iterator<Item = impl AsRef<Path>>) -> Self {
|
||||
let mut rule_files = Vec::with_capacity(32);
|
||||
for dir in dirs {
|
||||
let Ok(read_dir) = std::fs::read_dir(dir.as_ref()) else {
|
||||
tracing::debug!(
|
||||
"Did not read directory \"{}\" for rule files.",
|
||||
dir.as_ref().display()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
for file in read_dir {
|
||||
let Ok(file) = file else {
|
||||
tracing::warn!(
|
||||
"Error while reading directory \"{}\" for rule files.",
|
||||
dir.as_ref().display()
|
||||
);
|
||||
break;
|
||||
};
|
||||
if file.file_name().as_encoded_bytes().starts_with(b".") {
|
||||
continue;
|
||||
}
|
||||
let rule_file = match RuleFile::parse(file.path()) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Failed to parse rule file \"{}\": {}",
|
||||
file.path().display(),
|
||||
e,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
tracing::debug!("Activating rule file \"{}\"", file.path().display());
|
||||
rule_files.push(rule_file);
|
||||
}
|
||||
}
|
||||
Self(Arc::new(rule_files))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Engine {
|
||||
properties: RwLock<FxHashMap<String, String>>,
|
||||
command_policy: CommandPolicy,
|
||||
}
|
||||
impl Engine {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
properties: Default::default(),
|
||||
command_policy: CommandPolicy::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_property(&self, key: &str) -> Option<String> {
|
||||
self.properties.read().unwrap().get(key).cloned()
|
||||
}
|
||||
|
||||
pub fn set_property(&self, key: String, val: String) {
|
||||
self.properties.write().unwrap().insert(key, val);
|
||||
}
|
||||
|
||||
pub fn remove_property(&self, key: &str) {
|
||||
self.properties.write().unwrap().remove(key);
|
||||
}
|
||||
|
||||
pub fn get_list(&self, key: &str) -> Vec<String> {
|
||||
self.get_property(key)
|
||||
.unwrap_or_default()
|
||||
.split('\0')
|
||||
.filter(|x| !x.is_empty())
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn list_add(&self, key: String, val: &str) {
|
||||
let orig = self.get_property(&key).unwrap_or_default();
|
||||
self.set_property(key, format!("{orig}{val}\0"));
|
||||
}
|
||||
|
||||
pub async fn exec(&self, rule_set: &RuleSet) {
|
||||
for rule_file in rule_set.0.iter() {
|
||||
for rule in rule_file.rules.iter() {
|
||||
if let Err(err) = self.exec_rule(rule).await {
|
||||
if !matches!(rule.on_error, OnError::Continue) {
|
||||
tracing::warn!("Rule \"{}\" failed: {err}", rule_file.path.display());
|
||||
}
|
||||
match rule.on_error {
|
||||
OnError::Continue => {}
|
||||
OnError::Log => {}
|
||||
OnError::StopFile => break,
|
||||
OnError::StopAll => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn exec_rule(&self, rule: &Rule) -> Result<(), Error> {
|
||||
if self.exec_condition(&rule.when)? {
|
||||
for action in rule.actions.iter() {
|
||||
self.exec_action(&action).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_condition(&self, when: &When) -> Result<bool, Error> {
|
||||
match when {
|
||||
When::All(children) => {
|
||||
let mut result = true;
|
||||
for child in children {
|
||||
result &= self.exec_condition(child)?;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
When::Any(children) => {
|
||||
let mut result = false;
|
||||
for child in children {
|
||||
result |= self.exec_condition(child)?;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
When::Not(child) => self.exec_condition(child).map(|x| !x),
|
||||
When::True => Ok(true),
|
||||
When::False => Ok(false),
|
||||
When::Match(key, regex) => {
|
||||
let key = self.bake_str(key)?;
|
||||
let regex = self.bake_str(regex)?;
|
||||
let regex = format!("^{regex}$");
|
||||
let regex = Regex::new(®ex).map_err(Error::Regex)?;
|
||||
let Some(val) = self.get_property(&key) else {
|
||||
return Ok(false);
|
||||
};
|
||||
Ok(regex.is_match(&val))
|
||||
}
|
||||
When::PartMatch(key, regex) => {
|
||||
let key = self.bake_str(key)?;
|
||||
let regex = self.bake_str(regex)?;
|
||||
let regex = Regex::new(®ex).map_err(Error::Regex)?;
|
||||
let Some(val) = self.get_property(&key) else {
|
||||
return Ok(false);
|
||||
};
|
||||
Ok(regex.is_match(&val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn exec_action(&self, action: &Action) -> Result<(), Error> {
|
||||
match action {
|
||||
Action::Set(key, val) => {
|
||||
let key = self.bake_str(key)?;
|
||||
let val = self.bake_str(val)?;
|
||||
self.set_property(key, val);
|
||||
}
|
||||
Action::Add(key, val) => {
|
||||
let key = self.bake_str(key)?;
|
||||
let val = self.bake_str(val)?;
|
||||
self.list_add(key, &val);
|
||||
}
|
||||
Action::Unset(key) => {
|
||||
self.remove_property(&self.bake_str(key)?);
|
||||
}
|
||||
Action::Command(cmd) => {
|
||||
let cmd = self.bake_str(cmd)?;
|
||||
self.command_policy.run(&cmd).await?;
|
||||
}
|
||||
Action::UseProvider(key, cmd) => {
|
||||
let key = self.bake_str(key)?;
|
||||
let cmd = self.bake_str(cmd)?;
|
||||
let val = self.command_policy.run_provider(&cmd).await?;
|
||||
self.set_property(key, val);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bake_str(&self, s: &Str) -> Result<String, Error> {
|
||||
match s {
|
||||
Str::Direct(x) => Ok(x.into()),
|
||||
Str::Format(template) => {
|
||||
let mut result = String::with_capacity(template.len());
|
||||
let mut current_property: Option<String> = None;
|
||||
let mut right_bracket = false;
|
||||
for ch in template.chars() {
|
||||
if right_bracket {
|
||||
if ch == '}' {
|
||||
right_bracket = false;
|
||||
result.push('}');
|
||||
} else {
|
||||
return Err(Error::InvalidStringTemplate);
|
||||
}
|
||||
} else if let Some(property) = &mut current_property {
|
||||
if property.is_empty() && ch == '{' {
|
||||
result.push('{');
|
||||
current_property = None;
|
||||
} else if ch == '}' {
|
||||
let value = self
|
||||
.get_property(property)
|
||||
.ok_or_else(|| Error::PropertyNotFound(property.clone()))?;
|
||||
result.push_str(&value);
|
||||
current_property = None;
|
||||
} else {
|
||||
property.push(ch);
|
||||
}
|
||||
} else if ch == '{' {
|
||||
current_property = Some(String::with_capacity(16));
|
||||
} else if ch == '}' {
|
||||
right_bracket = true;
|
||||
} else {
|
||||
result.push(ch);
|
||||
}
|
||||
}
|
||||
if current_property.is_some() || right_bracket {
|
||||
return Err(Error::InvalidStringTemplate);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Default for Engine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CommandPolicy {
|
||||
prefix: Vec<String>,
|
||||
pub timeout: Duration,
|
||||
pub timeout_behavior: CommandTimeoutBehavior,
|
||||
pub info_limit: usize,
|
||||
}
|
||||
impl CommandPolicy {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
prefix: vec![String::from("/bin/sh"), String::from("-c")],
|
||||
timeout: Duration::from_secs(30),
|
||||
timeout_behavior: CommandTimeoutBehavior::Ignore,
|
||||
info_limit: 1024,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&self, cmd: &str) -> Result<(), Error> {
|
||||
let mut child = self
|
||||
._command(cmd)
|
||||
.spawn()
|
||||
.map_err(|e| Error::Command(cmd.into(), e.to_string()))?;
|
||||
let timeout_wait = tokio::time::timeout(self.timeout, child.wait()).await;
|
||||
match timeout_wait {
|
||||
Ok(Ok(wait)) => self._wait_success(cmd, wait),
|
||||
Ok(Err(_)) => Err(Error::Command(cmd.into(), "wait() failure".into())),
|
||||
Err(_) => self._timeout(cmd, child),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_provider(&self, cmd: &str) -> Result<String, Error> {
|
||||
let mut child = self
|
||||
._command(cmd)
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| Error::Command(cmd.into(), e.to_string()))?;
|
||||
let mut stdout = String::with_capacity(128);
|
||||
let timeout_wait = tokio::time::timeout(self.timeout, async {
|
||||
child
|
||||
.stdout
|
||||
.take()
|
||||
.unwrap()
|
||||
.take(self.info_limit as _)
|
||||
.read_to_string(&mut stdout)
|
||||
.await?;
|
||||
child.wait().await
|
||||
})
|
||||
.await;
|
||||
match timeout_wait {
|
||||
Ok(Ok(wait)) => {
|
||||
self._wait_success(cmd, wait)?;
|
||||
Ok(stdout.trim().into())
|
||||
}
|
||||
Ok(Err(_)) => Err(Error::Command(cmd.into(), "wait() failure".into())),
|
||||
Err(_) => {
|
||||
self._timeout(cmd, child)?;
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _wait_success(&self, cmd: &str, wait: ExitStatus) -> Result<(), Error> {
|
||||
if wait.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Command(
|
||||
cmd.into(),
|
||||
format!(
|
||||
"process exited with code {}",
|
||||
wait.code().unwrap_or(i32::MIN),
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn _command(&self, cmd: &str) -> tokio::process::Command {
|
||||
let mut command = tokio::process::Command::new(&self.prefix[0]);
|
||||
command.args(&self.prefix[1..]).arg(cmd);
|
||||
command
|
||||
}
|
||||
|
||||
fn _timeout(&self, cmd: &str, mut child: Child) -> Result<(), Error> {
|
||||
match self.timeout_behavior {
|
||||
CommandTimeoutBehavior::Ignore => {}
|
||||
CommandTimeoutBehavior::SoftKill => unsafe {
|
||||
if let Some(pid) = child.id() {
|
||||
libc::kill(pid as _, libc::SIGTERM);
|
||||
}
|
||||
},
|
||||
CommandTimeoutBehavior::HardKill => _ = child.start_kill(),
|
||||
};
|
||||
Err(Error::Command(cmd.into(), "Timed out".into()))
|
||||
}
|
||||
}
|
||||
impl Default for CommandPolicy {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub enum CommandTimeoutBehavior {
|
||||
#[default]
|
||||
Ignore,
|
||||
SoftKill,
|
||||
HardKill,
|
||||
}
|
||||
impl CommandTimeoutBehavior {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("{0}")]
|
||||
Read(std::io::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
Parse(ron::de::SpannedError),
|
||||
|
||||
#[error("Property \"{0}\" not found")]
|
||||
PropertyNotFound(String),
|
||||
|
||||
#[error("Invalid format string template")]
|
||||
InvalidStringTemplate,
|
||||
|
||||
#[error("Regex error: {0}")]
|
||||
Regex(regex::Error),
|
||||
|
||||
#[error("Failed to execute command \"{0}\": {1}")]
|
||||
Command(String, String),
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod engine;
|
||||
pub mod error;
|
||||
pub mod parser;
|
||||
|
||||
pub use error::Error;
|
||||
@@ -0,0 +1,124 @@
|
||||
use crate::Error;
|
||||
use serde::{Deserialize, Serialize, ser::SerializeTupleVariant};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RuleFile {
|
||||
pub path: PathBuf,
|
||||
pub rules: Vec<Rule>,
|
||||
}
|
||||
impl RuleFile {
|
||||
pub fn parse(path: impl Into<PathBuf>) -> Result<Self, Error> {
|
||||
let path = path.into();
|
||||
let s = std::fs::read_to_string(&path).map_err(Error::Read)?;
|
||||
let rules = ron::from_str::<Vec<Rule>>(&s).map_err(Error::Parse)?;
|
||||
Ok(Self { path, rules })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Rule {
|
||||
pub when: When,
|
||||
|
||||
#[serde(alias = "do")]
|
||||
pub actions: Vec<Action>,
|
||||
|
||||
#[serde(default)]
|
||||
pub on_error: OnError,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum When {
|
||||
All(Vec<When>),
|
||||
Any(Vec<When>),
|
||||
Not(Box<When>),
|
||||
True,
|
||||
False,
|
||||
Match(Str, Str),
|
||||
PartMatch(Str, Str),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Action {
|
||||
Command(Str),
|
||||
Add(Str, Str),
|
||||
Set(Str, Str),
|
||||
Unset(Str),
|
||||
UseProvider(Str, Str),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
pub enum OnError {
|
||||
/// Continue the workflow, without logging messages.
|
||||
Continue,
|
||||
|
||||
/// Continue the workflow, but outputs a warning log.
|
||||
#[default]
|
||||
Log,
|
||||
|
||||
/// Stop the file.
|
||||
StopFile,
|
||||
|
||||
/// Stop the workflow.
|
||||
StopAll,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Str {
|
||||
Direct(String),
|
||||
Format(String),
|
||||
}
|
||||
impl Serialize for Str {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Direct(s) => s.serialize(serializer),
|
||||
Self::Format(s) => {
|
||||
let mut ser = serializer.serialize_tuple_variant("Str", 1, "f", 1)?;
|
||||
ser.serialize_field(s)?;
|
||||
ser.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for Str {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct StrVisitor;
|
||||
impl<'de> serde::de::Visitor<'de> for StrVisitor {
|
||||
type Value = Str;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("string or array with a single string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(Str::Direct(v.to_string()))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let value = seq
|
||||
.next_element::<String>()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
|
||||
|
||||
if seq.next_element::<String>()?.is_some() {
|
||||
return Err(serde::de::Error::invalid_length(2, &self));
|
||||
}
|
||||
|
||||
Ok(Str::Format(value))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(StrVisitor)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user