mdbook_aquascope/
preprocessor.rs1use std::{
4 collections::HashMap,
5 fs,
6 path::PathBuf,
7 process::{Command, Stdio},
8 sync::RwLock,
9 time::Duration,
10};
11
12use anyhow::{bail, Result};
13use aquascope_workspace_utils::{miri_sysroot, run_and_get_output, rustc};
14use mdbook_preprocessor_utils::HtmlElementBuilder;
15use rayon::prelude::*;
16use tempfile::tempdir;
17use wait_timeout::ChildExt;
18
19use crate::{block::AquascopeBlock, cache::Cache};
20
21pub struct AquascopePreprocessor {
22 miri_sysroot: PathBuf,
23 target_libdir: PathBuf,
24 cache: RwLock<Cache<AquascopeBlock, String>>,
25}
26
27impl AquascopePreprocessor {
28 pub fn new() -> Result<Self> {
29 let miri_sysroot = miri_sysroot()?;
30 let rustc = rustc()?;
31 let output = run_and_get_output(
32 Command::new(rustc).args(["--print", "target-libdir"]),
33 )?;
34 let target_libdir = PathBuf::from(output);
35
36 let cache = RwLock::new(Cache::load()?);
37 Ok(AquascopePreprocessor {
38 miri_sysroot,
39 target_libdir,
40 cache,
41 })
42 }
43
44 fn run_aquascope(&self, block: &AquascopeBlock) -> Result<String> {
46 let tempdir = tempdir()?;
49 let root = tempdir.path();
50 let status = Command::new("cargo")
51 .args(["new", "--bin", "example"])
52 .current_dir(root)
53 .stdout(Stdio::null())
54 .stderr(Stdio::null())
55 .status()?;
56 if !status.success() {
57 bail!("Cargo failed");
58 }
59
60 fs::write(root.join("example/src/main.rs"), &block.code)?;
61
62 let mut responses = HashMap::new();
63 for operation in &block.operations {
64 let mut cmd = Command::new("cargo");
65 cmd
66 .arg("aquascope")
67 .env("SYSROOT", &self.miri_sysroot)
68 .env("MIRI_SYSROOT", &self.miri_sysroot)
69 .env("DYLD_LIBRARY_PATH", &self.target_libdir)
70 .env("LD_LIBRARY_PATH", &self.target_libdir)
71 .env("RUST_BACKTRACE", "1")
72 .current_dir(root.join("example"));
73
74 let should_fail = block.config.iter().any(|(k, _)| k == "shouldFail");
75 if should_fail {
76 cmd.arg("--should-fail");
77 }
78
79 cmd.arg(operation);
80
81 let show_flows = block.config.iter().any(|(k, _)| k == "showFlows");
82 if show_flows {
83 cmd.arg("--show-flows");
84 }
85
86 let mut child =
87 cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?;
88 if child.wait_timeout(Duration::from_secs(10))?.is_none() {
89 child.kill()?;
90 bail!("Aquascope timed out on program:\n{}", block.code)
91 };
92
93 let output = child.wait_with_output()?;
94
95 if !output.status.success() {
96 let error = String::from_utf8(output.stderr)?;
97 bail!(
98 "Aquascope failed for program:\n{}\nwith error:\n{error}",
99 block.code
100 )
101 }
102
103 let response = String::from_utf8(output.stdout)?;
104 let response_json: serde_json::Value = serde_json::from_str(&response)?;
105 let is_err = match (response_json.as_object(), response_json.as_array()) {
106 (Some(obj), _) => obj.get("Err").is_some(),
107 (_, Some(arr)) => arr.iter().any(|obj| obj.get("Err").is_some()),
108 _ => false,
109 };
110 if is_err {
111 let stderr = String::from_utf8(output.stderr)?;
112 bail!(
113 "Aquascope failed for program:\n{}\nwith error:\n{stderr}",
114 block.code,
115 )
116 }
117
118 if let Some("BuildError") =
119 response_json.get("type").and_then(|ty| ty.as_str())
120 {
121 bail!("Aquascope failed for program:\n{}", block.code)
122 }
123
124 responses.insert(operation, response_json);
125 }
126
127 Ok(serde_json::to_string(&responses)?)
128 }
129
130 fn process_code(&self, block: AquascopeBlock) -> Result<String> {
132 let cached_response = {
133 let cache = self.cache.read().unwrap();
134 cache.get(&block).cloned()
135 };
136 let response_str = match cached_response {
137 Some(response) => response,
138 None => {
139 let response = self.run_aquascope(&block)?;
140 self
141 .cache
142 .write()
143 .unwrap()
144 .set(block.clone(), response.clone());
145 response
146 }
147 };
148 let response: serde_json::Value =
149 serde_json::from_str(response_str.trim_end())?;
150
151 let mut html = HtmlElementBuilder::new();
152 html
153 .attr("class", "aquascope-embed")
154 .data("code", &block.code)?
155 .data("annotations", &block.annotations)?
156 .data("operations", &block.operations)?
157 .data("responses", response)?;
158
159 let config = block
160 .config
161 .iter()
162 .map(|(k, v)| (k, v))
163 .collect::<HashMap<_, _>>();
164 html.data("config", &config)?;
165
166 html.data("no-interact", true)?;
168
169 Ok(html.finish())
173 }
174
175 pub fn replacements(
176 &self,
177 content: &str,
178 ) -> Result<Vec<(std::ops::Range<usize>, String)>> {
179 let to_process = AquascopeBlock::parse_all(content);
180 to_process
181 .into_par_iter()
182 .map(|(range, block)| {
183 let html = self.process_code(block)?;
184 Ok((range, html))
185 })
186 .chain(crate::permissions::parse_perms(content).par_bridge())
187 .collect()
188 }
189
190 pub fn save_cache(&mut self) {
191 self.cache.write().unwrap().save().unwrap();
192 }
193}