use super::{Preprocessor, PreprocessorContext}; use crate::book::Book; use crate::errors::*; use log::{debug, trace, warn}; use shlex::Shlex; use std::io::{self, Read, Write}; use std::process::{Child, Command, Stdio}; /// A custom preprocessor which will shell out to a 3rd-party program. /// /// # Preprocessing Protocol /// /// When the `supports_renderer()` method is executed, `CmdPreprocessor` will /// execute the shell command `$cmd supports $renderer`. If the renderer is /// supported, custom preprocessors should exit with a exit code of `0`, /// any other exit code be considered as unsupported. /// /// The `run()` method is implemented by passing a `(PreprocessorContext, Book)` /// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors /// should then "return" a processed book by printing it to `stdout` as JSON. /// For convenience, the `CmdPreprocessor::parse_input()` function can be used /// to parse the input provided by `mdbook`. /// /// Exiting with a non-zero exit code while preprocessing is considered an /// error. `stderr` is passed directly through to the user, so it can be used /// for logging or emitting warnings if desired. /// /// # Examples /// /// An example preprocessor is available in this project's `examples/` /// directory. #[derive(Debug, Clone, PartialEq)] pub struct CmdPreprocessor { name: String, cmd: String, } impl CmdPreprocessor { /// Create a new `CmdPreprocessor`. pub fn new(name: String, cmd: String) -> CmdPreprocessor { CmdPreprocessor { name, cmd } } /// A convenience function custom preprocessors can use to parse the input /// written to `stdin` by a `CmdRenderer`. pub fn parse_input(reader: R) -> Result<(PreprocessorContext, Book)> { serde_json::from_reader(reader).with_context(|| "Unable to parse the input") } fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) { let stdin = child.stdin.take().expect("Child has stdin"); if let Err(e) = self.write_input(stdin, book, ctx) { // Looks like the backend hung up before we could finish // sending it the render context. Log the error and keep going warn!("Error writing the RenderContext to the backend, {}", e); } } fn write_input( &self, writer: W, book: &Book, ctx: &PreprocessorContext, ) -> Result<()> { serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into) } /// The command this `Preprocessor` will invoke. pub fn cmd(&self) -> &str { &self.cmd } fn command(&self) -> Result { let mut words = Shlex::new(&self.cmd); let executable = match words.next() { Some(e) => e, None => bail!("Command string was empty"), }; let mut cmd = Command::new(executable); for arg in words { cmd.arg(arg); } Ok(cmd) } } impl Preprocessor for CmdPreprocessor { fn name(&self) -> &str { &self.name } fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result { let mut cmd = self.command()?; let mut child = cmd .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .spawn() .with_context(|| { format!( "Unable to start the \"{}\" preprocessor. Is it installed?", self.name() ) })?; self.write_input_to_child(&mut child, &book, ctx); let output = child.wait_with_output().with_context(|| { format!( "Error waiting for the \"{}\" preprocessor to complete", self.name ) })?; trace!("{} exited with output: {:?}", self.cmd, output); ensure!( output.status.success(), format!( "The \"{}\" preprocessor exited unsuccessfully with {} status", self.name, output.status ) ); serde_json::from_slice(&output.stdout).with_context(|| { format!( "Unable to parse the preprocessed book from \"{}\" processor", self.name ) }) } fn supports_renderer(&self, renderer: &str) -> bool { debug!( "Checking if the \"{}\" preprocessor supports \"{}\"", self.name(), renderer ); let mut cmd = match self.command() { Ok(c) => c, Err(e) => { warn!( "Unable to create the command for the \"{}\" preprocessor, {}", self.name(), e ); return false; } }; let outcome = cmd .arg("supports") .arg(renderer) .stdin(Stdio::null()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .status() .map(|status| status.code() == Some(0)); if let Err(ref e) = outcome { if e.kind() == io::ErrorKind::NotFound { warn!( "The command wasn't found, is the \"{}\" preprocessor installed?", self.name ); warn!("\tCommand: {}", self.cmd); } } outcome.unwrap_or(false) } } #[cfg(test)] mod tests { use super::*; use crate::MDBook; use std::path::Path; fn guide() -> MDBook { let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide"); MDBook::load(example).unwrap() } #[test] fn round_trip_write_and_parse_input() { let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string()); let md = guide(); let ctx = PreprocessorContext::new( md.root.clone(), md.config.clone(), "some-renderer".to_string(), ); let mut buffer = Vec::new(); cmd.write_input(&mut buffer, &md.book, &ctx).unwrap(); let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap(); assert_eq!(got_book, md.book); assert_eq!(got_ctx, ctx); } }