ghci/lib.rs
1#![deny(missing_docs)]
2#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
3#![doc(html_root_url = "https://docs.rs/ghci/0.1.0")]
4
5//! A crate to manage and communicate with `ghci` sessions
6//!
7//! ```
8//! # use ghci::Ghci;
9//! #
10//! # fn main() -> ghci::Result<()> {
11//! let mut ghci = Ghci::new()?;
12//! let out = ghci.eval("putStrLn \"Hello world\"")?;
13//! assert_eq!(&out.stdout, "Hello world\n");
14//! #
15//! # Ok(())
16//! # }
17//! ```
18//!
19//! See [`Ghci`] documentation for more examples
20
21use core::time::Duration;
22use nix::poll::{poll, PollFd, PollFlags};
23use nonblock::NonBlockingReader;
24use std::io::{ErrorKind, LineWriter, Read, Write};
25use std::os::fd::{AsRawFd, RawFd};
26use std::path::Path;
27use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, Stdio};
28
29/// A ghci session handle
30///
31/// The session is stateful, so the order of interaction matters
32pub struct Ghci {
33 /// `ghci` process
34 child: Child,
35 /// Buffered child stdin writer
36 stdin: LineWriter<ChildStdin>,
37 /// Non-blocking child stdout reader
38 stdout: NonBlockingReader<ChildStdout>,
39 /// Raw fd for child stdout used to wait for events
40 stdout_fd: RawFd,
41 /// Non-blocking child stderr reader
42 stderr: NonBlockingReader<ChildStderr>,
43 /// Raw fd for child stderr used to wait for events
44 stderr_fd: RawFd,
45 /// Current timeout value
46 timeout: Option<Duration>,
47}
48
49#[derive(Debug)]
50#[non_exhaustive]
51/// Result for a ghci evaluation
52pub struct EvalOutput {
53 /// stdout for the result of the ghci evaluation
54 pub stdout: String,
55 /// stderr for the result of the ghci evaluation
56 pub stderr: String,
57}
58
59#[derive(Debug, thiserror::Error)]
60#[non_exhaustive]
61/// Errors associated with a [`Ghci`] session
62pub enum GhciError {
63 /// The evaluation timed out
64 ///
65 /// Note: The Ghci session is not be in a good state and needs to be closed
66 #[error("ghci session timed out waiting on output")]
67 Timeout,
68 /// IO error from the underlying child process management
69 #[error("IO error: {0}")]
70 IOError(#[from] std::io::Error),
71 /// Poll error when waiting on ghci stdout/stderr
72 #[error("Poll error: {0}")]
73 PollError(#[from] nix::errno::Errno),
74}
75
76/// A convenient alias for [`std::result::Result`] using a [`GhciError`]
77pub type Result<T> = std::result::Result<T, GhciError>;
78
79// Use a prompt that is unlikely to be part of the stdout of the ghci session
80const PROMPT: &str = "__ghci_rust_prompt__>\n";
81
82impl Ghci {
83 /// Create a new ghci session
84 ///
85 /// It will use `ghci` on your `PATH` by default, but can be overridden to use any `ghci` by
86 /// setting the `GHCI_PATH` environment variable pointing at the binary to use
87 ///
88 /// # Errors
89 ///
90 /// Returns [`IOError`] when it encounters IO errors as part of spawning the `ghci` subprocess
91 ///
92 /// [`IOError`]: GhciError::IOError
93 pub fn new() -> Result<Self> {
94 const PIPE_ERR: &str = "pipe should be present";
95
96 let ghci = std::env::var("GHCI_PATH").unwrap_or_else(|_| "ghci".to_string());
97
98 let mut child = Command::new(ghci)
99 .stdin(Stdio::piped())
100 .stdout(Stdio::piped())
101 .stderr(Stdio::piped())
102 .spawn()?;
103
104 let mut stdin = LineWriter::new(child.stdin.take().expect(PIPE_ERR));
105 let mut stdout = child.stdout.take().expect(PIPE_ERR);
106 let stderr = child.stderr.take().expect(PIPE_ERR);
107
108 clear_blocking_reader_until(&mut stdout, b"> ")?;
109
110 // Setup a known prompt/multi-line prompt
111 stdin.write_all(b":set prompt \"")?;
112 stdin.write_all(PROMPT[..PROMPT.len() - 1].as_bytes())?;
113 stdin.write_all(b"\\n\"\n")?;
114 clear_blocking_reader_until(&mut stdout, PROMPT.as_bytes())?;
115
116 stdin.write_all(b":set prompt-cont \"\"\n")?;
117 clear_blocking_reader_until(&mut stdout, PROMPT.as_bytes())?;
118
119 Ok(Self {
120 stdin,
121 stdout_fd: stdout.as_raw_fd(),
122 stdout: NonBlockingReader::from_fd(stdout)?,
123 stderr_fd: stderr.as_raw_fd(),
124 stderr: NonBlockingReader::from_fd(stderr)?,
125 child,
126 timeout: None,
127 })
128 }
129
130 /// Evaluate/run a statement
131 ///
132 /// ```
133 /// # use ghci::Ghci;
134 /// #
135 /// # fn main() -> ghci::Result<()> {
136 /// let mut ghci = Ghci::new()?;
137 /// let out = ghci.eval("putStrLn \"Hello world\"")?;
138 /// assert_eq!(&out.stdout, "Hello world\n");
139 /// #
140 /// # Ok(())
141 /// # }
142 /// ```
143 ///
144 /// Multi-line inputs are also supported. The evaluation output may contain both stdout and
145 /// stderr:
146 ///
147 /// ```
148 /// # use ghci::Ghci;
149 /// #
150 /// # fn main() -> ghci::Result<()> {
151 /// let mut ghci = Ghci::new()?;
152 /// ghci.import(&["System.IO"]); // imports not supported as part of multi-line inputs
153 ///
154 /// let out = ghci.eval(r#"
155 /// do
156 /// hPutStrLn stdout "Output on stdout"
157 /// hPutStrLn stderr "Output on stderr"
158 /// hPutStrLn stdout "And a bit more on stdout"
159 /// "#)?;
160 ///
161 /// assert_eq!(&out.stderr, "Output on stderr\n");
162 /// assert_eq!(&out.stdout, "Output on stdout\nAnd a bit more on stdout\n");
163 /// #
164 /// # Ok(())
165 /// # }
166 /// ```
167 ///
168 /// # Errors
169 ///
170 /// - Returns a [`Timeout`] if the evaluation timeout (set by [`Ghci::set_timeout`])
171 /// is reached before the evaluation completes.
172 /// - Returns a [`IOError`] when encounters an IO error on the `ghci` subprocess
173 /// `stdin`, `stdout`, or `stderr`.
174 /// - Returns a [`PollError`] when waiting for output, if the `ghci` subprocess
175 /// `stdout` or `stderr` is closed (upon a crash for example)
176 ///
177 /// [`Timeout`]: GhciError::Timeout
178 /// [`IOError`]: GhciError::IOError
179 /// [`PollError`]: GhciError::PollError
180 pub fn eval(&mut self, input: &str) -> Result<EvalOutput> {
181 self.stdin.write_all(b":{\n")?;
182 self.stdin.write_all(input.as_bytes())?;
183 self.stdin.write_all(b"\n:}\n")?;
184
185 let mut stdout = String::new();
186 let mut stderr = String::new();
187 let timeout = self
188 .timeout
189 .and_then(|d| d.as_millis().try_into().ok())
190 .unwrap_or(-1);
191
192 loop {
193 let mut poll_fds = [
194 PollFd::new(self.stderr_fd, PollFlags::POLLIN),
195 PollFd::new(self.stdout_fd, PollFlags::POLLIN),
196 ];
197
198 let ret = poll(&mut poll_fds, timeout)?;
199
200 if ret == 0 {
201 return Err(GhciError::Timeout);
202 }
203
204 if poll_fds[0].any() == Some(true) {
205 self.stderr.read_available_to_string(&mut stderr)?;
206 }
207
208 if poll_fds[1].any() == Some(true) {
209 self.stdout.read_available_to_string(&mut stdout)?;
210
211 if stdout.ends_with(PROMPT) {
212 stdout.truncate(stdout.len() - PROMPT.len());
213 break;
214 }
215 }
216 }
217
218 Ok(EvalOutput { stdout, stderr })
219 }
220
221 /// Set a timeout for evaluations
222 ///
223 /// ```
224 /// # use ghci::{Ghci, GhciError};
225 /// # use std::time::Duration;
226 /// #
227 /// # fn main() -> ghci::Result<()> {
228 /// let mut ghci = Ghci::new()?;
229 /// ghci.import(&["Control.Concurrent"])?;
230 ///
231 /// let res = ghci.eval("threadDelay 50000");
232 /// assert!(matches!(res, Ok(_)));
233 ///
234 /// ghci.set_timeout(Some(Duration::from_millis(20)));
235 ///
236 /// let res = ghci.eval("threadDelay 50000");
237 /// assert!(matches!(res, Err(GhciError::Timeout)));
238 /// #
239 /// # Ok(())
240 /// # }
241 /// ```
242 ///
243 /// By default, no timeout is set.
244 ///
245 /// Note: When a [`Timeout`] error is triggered, the `ghci` session **must** be closed with
246 /// [`Ghci::close`] or [`Drop`]ed in order to properly stop the corresponding evaluation.
247 /// If the evaluation is left to finish after a timeout occurs, the session is then left in a
248 /// bad state that is not recoverable.
249 ///
250 /// [`Timeout`]: GhciError::Timeout
251 #[inline]
252 pub fn set_timeout(&mut self, timeout: Option<Duration>) {
253 self.timeout = timeout;
254 }
255
256 /// Import multiple modules
257 ///
258 /// ```
259 /// # use ghci::Ghci;
260 /// #
261 /// # fn main() -> ghci::Result<()> {
262 /// let mut ghci = Ghci::new()?;
263 /// ghci.import(&["Data.Char", "Control.Applicative"])?;
264 /// #
265 /// # Ok(())
266 /// # }
267 /// ```
268 ///
269 /// # Errors
270 ///
271 /// Same as [`Ghci::eval`]
272 #[inline]
273 pub fn import(&mut self, modules: &[&str]) -> Result<()> {
274 let mut line = String::from(":module ");
275 line.push_str(&modules.join(" "));
276
277 self.eval(&line)?;
278
279 Ok(())
280 }
281
282 /// Load multiple modules by file path
283 ///
284 /// # Errors
285 ///
286 /// Same as [`Ghci::eval`]
287 #[inline]
288 pub fn load(&mut self, paths: &[&Path]) -> Result<()> {
289 let mut line = String::from(":load");
290
291 for path in paths {
292 line.push_str(&format!(" {}", path.display()));
293 }
294
295 self.eval(&line)?;
296
297 Ok(())
298 }
299
300 /// Close the ghci session
301 ///
302 /// Closing explicitly is not necessary as the [`Drop`] impl will take care of it. This
303 /// function does however give the possibility to properly handle errors on close.
304 ///
305 /// # Errors
306 ///
307 /// If the underlying child process has already exited a [`IOError`] with
308 /// [`InvalidInput`] error is returned
309 ///
310 /// [`IOError`]: GhciError::IOError
311 /// [`InvalidInput`]: std::io::ErrorKind::InvalidInput
312 #[inline]
313 pub fn close(mut self) -> Result<()> {
314 Ok(self.child.kill()?)
315 }
316}
317
318impl Drop for Ghci {
319 fn drop(&mut self) {
320 if self.child.try_wait().unwrap().is_none() {
321 self.child.kill().unwrap();
322 }
323 }
324}
325
326// Helper function to clear data from a blocking reader until a pattern is seen
327// - the pattern is also cleared
328// - the pattern has to be at the end of a given read (otherwise it will hang)
329// - limited to 1024 bytes
330fn clear_blocking_reader_until(mut r: impl Read, expected_end: &[u8]) -> std::io::Result<()> {
331 let mut buffer = [0; 1024];
332 let mut end = 0;
333 loop {
334 match r.read(&mut buffer[end..]) {
335 Ok(0) => return Ok(()),
336 Ok(bytes) => {
337 end += bytes;
338 if buffer[..end].ends_with(expected_end) {
339 return Ok(());
340 }
341 }
342 Err(err) if err.kind() == ErrorKind::Interrupted => {}
343 Err(err) => return Err(err),
344 }
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn parse_error() {
354 let mut ghci = Ghci::new().unwrap();
355 let res = ghci.eval("x ::").unwrap();
356 assert!(res.stderr.contains("parse error"));
357 }
358}