use anyhow::{anyhow, Context, Result};
use log::{debug, warn};
use std::ffi::OsStr;
use std::fmt::{self, Display, Formatter};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use crate::mdev::*;
#[derive(Clone, Copy, PartialEq)]
pub enum Event {
Pre,
Post,
Notify,
Get,
}
impl Display for Event {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Event::Pre => {
write!(f, "pre")
}
Event::Post => {
write!(f, "post")
}
Event::Notify => {
write!(f, "notify")
}
Event::Get => {
write!(f, "get")
}
}
}
}
#[derive(Clone, Copy)]
#[allow(dead_code)]
pub enum Action {
Start,
Stop,
Define,
Undefine,
Modify,
Attributes,
Test, }
impl Display for Action {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Action::Start => write!(f, "start"),
Action::Stop => write!(f, "stop"),
Action::Define => write!(f, "define"),
Action::Undefine => write!(f, "undefine"),
Action::Modify => write!(f, "modify"),
Action::Attributes => write!(f, "attributes"),
Action::Test => write!(f, "test"),
}
}
}
#[derive(Clone, Copy)]
pub enum State {
None,
Success,
Failure,
}
impl Display for State {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
State::None => write!(f, "none"),
State::Success => write!(f, "success"),
State::Failure => write!(f, "failure"),
}
}
}
pub struct Callout {
state: State,
script: Option<PathBuf>,
}
impl Callout {
fn new() -> Callout {
Callout {
state: State::None,
script: None,
}
}
pub fn invoke<F>(dev: &mut MDev, action: Action, func: F) -> Result<()>
where
F: Fn(&mut MDev) -> Result<()>,
{
let mut c = Callout::new();
let res = c.callout(dev, Event::Pre, action).and_then(|_| {
let tmp_res = func(dev);
c.state = match tmp_res {
Ok(_) => State::Success,
Err(_) => State::Failure,
};
let post_res = c.callout(dev, Event::Post, action);
if post_res.is_err() {
debug!("Error occurred when executing post callout script");
}
tmp_res
});
let _ = c.notify(dev, action);
res
}
pub fn get_attributes(dev: &mut MDev) -> Result<serde_json::Value> {
let event = Event::Get;
let action = Action::Attributes;
let c = Callout::new();
let dir = dev.env.callout_dir();
if !dir.is_dir() {
return Ok(serde_json::Value::Null);
}
match c.invoke_first_matching_script(dev, dir, event, action) {
Some((path, output)) => {
if output.status.success() {
debug!("Get attributes successfully from callout script");
let mut st = String::from_utf8_lossy(&output.stdout).to_string();
if st.is_empty() {
return Ok(serde_json::Value::Null);
}
if &st == "[{}]" {
debug!(
"Attribute field for {} is empty",
dev.uuid.to_hyphenated().to_string()
);
st = "[]".to_string();
}
serde_json::from_str(&st)
.with_context(|| anyhow!("Unable to parse attributes from JSON"))
} else {
c.print_err(&output, &path);
return Err(anyhow!("failed to get attributes from {:?}", path));
}
}
None => {
debug!(
"Device type {} unmatched by callout script",
dev.mdev_type.as_ref().unwrap()
);
Ok(serde_json::Value::Null)
}
}
}
fn invoke_script<P: AsRef<Path>>(
&self,
dev: &mut MDev,
script: P,
event: Event,
action: Action,
) -> Result<Output> {
debug!(
"{}-{}: executing {:?}",
event,
action,
script.as_ref().as_os_str()
);
let mut cmd = Command::new(script.as_ref().as_os_str());
cmd.arg("-t")
.arg(dev.mdev_type()?)
.arg("-e")
.arg(event.to_string())
.arg("-a")
.arg(action.to_string())
.arg("-s")
.arg(self.state.to_string())
.arg("-u")
.arg(dev.uuid.to_string())
.arg("-p")
.arg(dev.parent()?)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn()?;
if event != Event::Get {
let conf = dev.to_json(false)?.to_string();
if let Some(mut child_stdin) = child.stdin.take() {
child_stdin
.write_all(conf.as_bytes())
.with_context(|| "Failed to write to stdin of command")?;
}
}
child.wait_with_output().map_err(anyhow::Error::from)
}
fn print_err<P: AsRef<Path>>(&self, output: &Output, script: P) {
let sname = script
.as_ref()
.file_name()
.unwrap_or_else(|| OsStr::new("unknown script name"))
.to_string_lossy();
let st = String::from_utf8_lossy(&output.stderr);
if !st.is_empty() {
eprint!("{}: {}", &sname, st);
}
}
fn invoke_first_matching_script<P: AsRef<Path>>(
&self,
dev: &mut MDev,
dir: P,
event: Event,
action: Action,
) -> Option<(PathBuf, Output)> {
debug!(
"{}-{}: looking for a matching callout script for dev type '{}'",
event,
action,
dev.mdev_type.as_ref()?
);
if dir.as_ref().read_dir().ok()?.count() == 0 {
return None;
}
for s in dir.as_ref().read_dir().ok()? {
let path = s.ok()?.path();
match self.invoke_script(dev, &path, event, action) {
Ok(res) => {
if res.status.code().is_none() {
warn!("callout script {:?} was terminated by a signal", path);
continue;
} else if res.status.code() != Some(2) {
debug!("found callout script {:?}", path);
return Some((path, res));
} else {
debug!(
"device type {} unmatched by callout script",
dev.mdev_type().ok()?
);
}
}
Err(e) => {
debug!("failed to execute callout script {:?}: {:?}", path, e);
continue;
}
}
}
None
}
fn callout(&mut self, dev: &mut MDev, event: Event, action: Action) -> Result<()> {
let rc = match self.script {
Some(ref s) => self
.invoke_script(dev, s, event, action)
.ok()
.and_then(|output| {
self.print_err(&output, s);
output.status.code()
}),
_ => {
let dir = dev.env.callout_dir();
if !dir.is_dir() {
return Ok(());
}
self.invoke_first_matching_script(dev, dir, event, action)
.and_then(|(path, output)| {
self.print_err(&output, &path);
self.script = Some(path);
output.status.code()
})
}
};
match rc {
Some(0) | None => Ok(()),
Some(n) => Err(anyhow!(
"callout script {:?} failed with return code {}",
self.script.as_ref().unwrap(),
n
)),
}
}
fn notify(&mut self, dev: &mut MDev, action: Action) -> Result<()> {
let event = Event::Notify;
let dir = dev.env.notification_dir();
debug!(
"{}-{}: executing notification scripts for device {}",
event, action, dev.uuid
);
if !dir.is_dir() {
return Ok(());
}
for s in dir.read_dir()? {
let path = s?.path();
match self.invoke_script(dev, &path, event, action) {
Ok(output) => {
if !output.status.success() {
debug!("Error occurred when executing notify script {:?}", path);
}
}
_ => {
debug!("Failed to execute callout script {:?}", path);
continue;
}
}
}
Ok(())
}
}