1 | use super::{Preprocessor, PreprocessorContext}; |
2 | use crate::book::Book; |
3 | use crate::errors::*; |
4 | use log::{debug, trace, warn}; |
5 | use shlex::Shlex; |
6 | use std::io::{self, Read, Write}; |
7 | use std::process::{Child, Command, Stdio}; |
8 | |
9 | /// A custom preprocessor which will shell out to a 3rd-party program. |
10 | /// |
11 | /// # Preprocessing Protocol |
12 | /// |
13 | /// When the `supports_renderer()` method is executed, `CmdPreprocessor` will |
14 | /// execute the shell command `$cmd supports $renderer`. If the renderer is |
15 | /// supported, custom preprocessors should exit with a exit code of `0`, |
16 | /// any other exit code be considered as unsupported. |
17 | /// |
18 | /// The `run()` method is implemented by passing a `(PreprocessorContext, Book)` |
19 | /// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors |
20 | /// should then "return" a processed book by printing it to `stdout` as JSON. |
21 | /// For convenience, the `CmdPreprocessor::parse_input()` function can be used |
22 | /// to parse the input provided by `mdbook`. |
23 | /// |
24 | /// Exiting with a non-zero exit code while preprocessing is considered an |
25 | /// error. `stderr` is passed directly through to the user, so it can be used |
26 | /// for logging or emitting warnings if desired. |
27 | /// |
28 | /// # Examples |
29 | /// |
30 | /// An example preprocessor is available in this project's `examples/` |
31 | /// directory. |
32 | #[derive (Debug, Clone, PartialEq)] |
33 | pub struct CmdPreprocessor { |
34 | name: String, |
35 | cmd: String, |
36 | } |
37 | |
38 | impl CmdPreprocessor { |
39 | /// Create a new `CmdPreprocessor`. |
40 | pub fn new(name: String, cmd: String) -> CmdPreprocessor { |
41 | CmdPreprocessor { name, cmd } |
42 | } |
43 | |
44 | /// A convenience function custom preprocessors can use to parse the input |
45 | /// written to `stdin` by a `CmdRenderer`. |
46 | pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> { |
47 | serde_json::from_reader(reader).with_context(|| "Unable to parse the input" ) |
48 | } |
49 | |
50 | fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) { |
51 | let stdin = child.stdin.take().expect("Child has stdin" ); |
52 | |
53 | if let Err(e) = self.write_input(stdin, book, ctx) { |
54 | // Looks like the backend hung up before we could finish |
55 | // sending it the render context. Log the error and keep going |
56 | warn!("Error writing the RenderContext to the backend, {}" , e); |
57 | } |
58 | } |
59 | |
60 | fn write_input<W: Write>( |
61 | &self, |
62 | writer: W, |
63 | book: &Book, |
64 | ctx: &PreprocessorContext, |
65 | ) -> Result<()> { |
66 | serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into) |
67 | } |
68 | |
69 | /// The command this `Preprocessor` will invoke. |
70 | pub fn cmd(&self) -> &str { |
71 | &self.cmd |
72 | } |
73 | |
74 | fn command(&self) -> Result<Command> { |
75 | let mut words = Shlex::new(&self.cmd); |
76 | let executable = match words.next() { |
77 | Some(e) => e, |
78 | None => bail!("Command string was empty" ), |
79 | }; |
80 | |
81 | let mut cmd = Command::new(executable); |
82 | |
83 | for arg in words { |
84 | cmd.arg(arg); |
85 | } |
86 | |
87 | Ok(cmd) |
88 | } |
89 | } |
90 | |
91 | impl Preprocessor for CmdPreprocessor { |
92 | fn name(&self) -> &str { |
93 | &self.name |
94 | } |
95 | |
96 | fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> { |
97 | let mut cmd = self.command()?; |
98 | |
99 | let mut child = cmd |
100 | .stdin(Stdio::piped()) |
101 | .stdout(Stdio::piped()) |
102 | .stderr(Stdio::inherit()) |
103 | .spawn() |
104 | .with_context(|| { |
105 | format!( |
106 | "Unable to start the \"{}\" preprocessor. Is it installed?" , |
107 | self.name() |
108 | ) |
109 | })?; |
110 | |
111 | self.write_input_to_child(&mut child, &book, ctx); |
112 | |
113 | let output = child.wait_with_output().with_context(|| { |
114 | format!( |
115 | "Error waiting for the \"{}\" preprocessor to complete" , |
116 | self.name |
117 | ) |
118 | })?; |
119 | |
120 | trace!(" {} exited with output: {:?}" , self.cmd, output); |
121 | ensure!( |
122 | output.status.success(), |
123 | format!( |
124 | "The \"{}\" preprocessor exited unsuccessfully with {} status" , |
125 | self.name, output.status |
126 | ) |
127 | ); |
128 | |
129 | serde_json::from_slice(&output.stdout).with_context(|| { |
130 | format!( |
131 | "Unable to parse the preprocessed book from \"{}\" processor" , |
132 | self.name |
133 | ) |
134 | }) |
135 | } |
136 | |
137 | fn supports_renderer(&self, renderer: &str) -> bool { |
138 | debug!( |
139 | "Checking if the \"{}\" preprocessor supports \"{}\"" , |
140 | self.name(), |
141 | renderer |
142 | ); |
143 | |
144 | let mut cmd = match self.command() { |
145 | Ok(c) => c, |
146 | Err(e) => { |
147 | warn!( |
148 | "Unable to create the command for the \"{}\" preprocessor, {}" , |
149 | self.name(), |
150 | e |
151 | ); |
152 | return false; |
153 | } |
154 | }; |
155 | |
156 | let outcome = cmd |
157 | .arg("supports" ) |
158 | .arg(renderer) |
159 | .stdin(Stdio::null()) |
160 | .stdout(Stdio::inherit()) |
161 | .stderr(Stdio::inherit()) |
162 | .status() |
163 | .map(|status| status.code() == Some(0)); |
164 | |
165 | if let Err(ref e) = outcome { |
166 | if e.kind() == io::ErrorKind::NotFound { |
167 | warn!( |
168 | "The command wasn't found, is the \"{}\" preprocessor installed?" , |
169 | self.name |
170 | ); |
171 | warn!(" \tCommand: {}" , self.cmd); |
172 | } |
173 | } |
174 | |
175 | outcome.unwrap_or(false) |
176 | } |
177 | } |
178 | |
179 | #[cfg (test)] |
180 | mod tests { |
181 | use super::*; |
182 | use crate::MDBook; |
183 | use std::path::Path; |
184 | |
185 | fn guide() -> MDBook { |
186 | let example = Path::new(env!("CARGO_MANIFEST_DIR" )).join("guide" ); |
187 | MDBook::load(example).unwrap() |
188 | } |
189 | |
190 | #[test ] |
191 | fn round_trip_write_and_parse_input() { |
192 | let cmd = CmdPreprocessor::new("test" .to_string(), "test" .to_string()); |
193 | let md = guide(); |
194 | let ctx = PreprocessorContext::new( |
195 | md.root.clone(), |
196 | md.config.clone(), |
197 | "some-renderer" .to_string(), |
198 | ); |
199 | |
200 | let mut buffer = Vec::new(); |
201 | cmd.write_input(&mut buffer, &md.book, &ctx).unwrap(); |
202 | |
203 | let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap(); |
204 | |
205 | assert_eq!(got_book, md.book); |
206 | assert_eq!(got_ctx, ctx); |
207 | } |
208 | } |
209 | |