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}