isabelle_client/client/
mod.rs

1/// Contains the arguments data types for the Isabelle server commands
2pub mod args;
3/// Contains the result data types the Isabelle servers responses with
4pub mod results;
5
6use serde::Deserialize;
7use serde::Serialize;
8
9use crate::server::IsabelleServer;
10
11use std::fmt::Display;
12use std::io;
13use std::{
14    io::{BufRead, BufReader, BufWriter, Write},
15    net::TcpStream,
16};
17
18use self::args::*;
19use self::results::*;
20
21/// A command to be sent to the Isabelle server.
22/// It consists of a `name` and optional arguments `args` which are serialized as JSON.
23struct Command<T: serde::Serialize> {
24    pub name: String,
25    pub args: Option<T>,
26}
27
28impl<T: serde::Serialize> Command<T> {
29    /// Converts the command to a `\n`-terminated string the Isabelle server understands
30    fn as_string(&self) -> String {
31        let args = match &self.args {
32            Some(arg) => serde_json::to_string(&arg).expect("Could not serialize"),
33            None => "".to_owned(),
34        };
35        format!("{} {}\n", self.name, args)
36    }
37
38    /// Converts the command to a `\n`-terminated sequence of Bytes the Isabelle server understands
39    fn as_bytes(&self) -> Vec<u8> {
40        self.as_string().as_bytes().to_owned()
41    }
42}
43
44impl<T: serde::Serialize> Display for Command<T> {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        write!(f, "{}", self.as_string().trim())
47    }
48}
49
50/// Result of a synchronous command sent to the Isabelle server.
51#[derive(Debug)]
52pub enum SyncResult<T, E> {
53    /// If the command was successful (server returned `OK`), contains the result value of type `T`.
54    Ok(T),
55    /// If the command was unsuccessful (server returned `ERROR`), contains an error value of type `E`.
56    Error(E),
57}
58
59impl<T, E> SyncResult<T, E> {
60    /// Returns the result value if the command was successful, panics otherwise.
61    pub fn ok(&self) -> &T {
62        match self {
63            SyncResult::Ok(t) => t,
64            SyncResult::Error(_) => panic!("Called unwrap on error value"),
65        }
66    }
67}
68
69/// Result of an asynchronous command sent to the Isabelle server.
70#[derive(Debug)]
71pub enum AsyncResult<T, F> {
72    /// If the task associated with the command was successful (server returned `FINISHED`), contains the result value of type `T`.
73    Finished(T),
74    /// If the task associated with the command failed (server returned `FAILED`), contains a [FailedResult] value of type `F`.
75    Failed(FailedResult<F>),
76    /// If the async command fails immediately, contains the message
77    Error(Message),
78}
79
80impl<T, F> AsyncResult<T, F> {
81    /// Returns the result value if the task was successful, panics otherwise.
82    pub fn finished(&self) -> &T {
83        match self {
84            AsyncResult::Finished(t) => t,
85            AsyncResult::Failed(_) => panic!("Called unwrap on Failed result"),
86            AsyncResult::Error(_) => panic!("Called unwrap on Error result"),
87        }
88    }
89}
90
91/// Result of a failed asynchronous task.
92#[derive(Serialize, Deserialize, Debug)]
93pub struct FailedResult<T> {
94    /// Task identifier
95    #[serde(flatten)]
96    task: Task,
97    /// Information about the error as returned from the server
98    #[serde(flatten)]
99    pub message: Message,
100    /// Context information returned from the server
101    #[serde(flatten)]
102    pub context: Option<T>,
103}
104
105/// Provides interaction with Isabelle servers.
106pub struct IsabelleClient {
107    /// The address of the Isabelle server
108    addr: String,
109    /// The password used to authenticate with the Isabelle server
110    pass: String,
111}
112
113impl IsabelleClient {
114    /// Connect to an Isabelle server.
115    ///
116    /// # Arguments
117    ///
118    /// - `address`: specifies the server address. If it is `None`, "127.0.0.1" is use as a default
119    /// - `port`: specifies the server port
120    /// - `pass`: the password
121    pub fn connect(address: Option<&str>, port: u32, pass: &str) -> Self {
122        let addr = format!("{}:{}", address.unwrap_or("127.0.0.1"), port);
123
124        Self {
125            addr,
126            pass: pass.to_owned(),
127        }
128    }
129
130    /// Connect to an Isabelle server using the information from a [IsabelleServer] instance.
131    pub fn for_server(server: &IsabelleServer) -> Self {
132        Self::connect(None, server.port(), server.password())
133    }
134
135    /// Performs the initial password exchange(i.e. password exchange) between a new client client and server.
136    /// Returns a `Result` indicating the success or failure of the handshake.
137    fn handshake(&self, stream: &TcpStream) -> io::Result<()> {
138        let mut writer = BufWriter::new(stream.try_clone().unwrap());
139        let mut reader = BufReader::new(stream.try_clone().unwrap());
140
141        writer.write_all(format!("{}\n", self.pass).as_bytes())?;
142        writer.flush()?;
143
144        if let Some(e) = stream.take_error()? {
145            return Err(e);
146        }
147
148        let mut res = String::new();
149        reader.read_line(&mut res)?;
150        log::trace!("Handshake result: {}", res.trim());
151        if !res.starts_with("OK") {
152            return Err(io::Error::new(
153                io::ErrorKind::PermissionDenied,
154                "Handshake failed",
155            ));
156        }
157        log::trace!("Handshake ok");
158        Ok(())
159    }
160
161    /// Facility to parse JSON responses from the Isabelle server into Rust types
162    fn parse_response<T: serde::de::DeserializeOwned>(
163        &self,
164        mut res: &str,
165    ) -> Result<T, io::Error> {
166        if res.is_empty() {
167            // Workaround for json compliance, unit type is `null` not empty string
168            res = "null";
169        }
170        match serde_json::from_str::<T>(res) {
171            Ok(r) => Ok(r),
172            Err(e) => Err(io::Error::new(
173                io::ErrorKind::InvalidData,
174                format!("{}: {}", e, res),
175            )),
176        }
177    }
178
179    /// Creates a new connection to the server and performs the initial password exchange
180    /// handshake. Returns a tuple of buffered reader and writer wrapped around the TcpStream
181    /// connection.
182    fn new_connection(&self) -> io::Result<(BufReader<TcpStream>, BufWriter<TcpStream>)> {
183        let con = TcpStream::connect(&self.addr)?;
184
185        // Perform password exchange
186        self.handshake(&con)?;
187
188        let writer = BufWriter::new(con.try_clone().unwrap());
189        let reader = BufReader::new(con.try_clone().unwrap());
190
191        Ok((reader, writer))
192    }
193
194    /// Dispatches asynchronous [Command] `cmd` to start the task on the server.
195    ///
196    /// The method dispatches the `cmd` which starts an asynchronous task at the server.
197    /// The method then waits for the task to finish or fail by reading the response and returns the result
198    /// as an `AsyncResult<R, F>` where `R` is the type of the response when the task is finished and
199    /// `F` is the type of the response when the task fails.
200    ///
201    /// Notes printed by the server are logged and cannot be accessed.
202    ///
203    /// Returns an `io::Error` if communication with the server failed.
204    async fn dispatch_async<
205        T: Serialize,
206        R: serde::de::DeserializeOwned,
207        F: serde::de::DeserializeOwned,
208    >(
209        &self,
210        cmd: &Command<T>,
211        reader: &mut BufReader<TcpStream>,
212        writer: &mut BufWriter<TcpStream>,
213    ) -> Result<AsyncResult<R, F>, io::Error> {
214        // Dispatch the command as sync to start the task. Return Error if it failed
215        if let SyncResult::Error(e) = self
216            .dispatch_sync::<T, Task, Message>(cmd, reader, writer)
217            .await?
218        {
219            // Cast to async result
220            return Ok(AsyncResult::Error(e));
221        };
222
223        // Wait for the task to finish or fail, and collect notes along the way
224        let mut res = String::new();
225        loop {
226            res.clear();
227            reader.read_line(&mut res)?;
228            let res = res.trim();
229            if let Some(finish_response) = res.strip_prefix("FINISHED") {
230                // If the task has finished, parse the response
231                let parsed = self.parse_response(finish_response.trim())?;
232                return Ok(AsyncResult::Finished(parsed));
233            } else if let Some(failed_response) = res.strip_prefix("FAILED") {
234                // If the task has failed, parse the response
235                let parsed = self.parse_response(failed_response.trim())?;
236                return Ok(AsyncResult::Failed(parsed));
237            } else if let Some(note) = res.strip_prefix("NOTE") {
238                // If it's a note, log it and continue the loop
239                log::trace!("{}", note);
240            } else {
241                // Occasionally the server omits some seemingly random numeric logs.
242                // Log and discard them, then continue the loop.
243                log::trace!("Unknown message format: {}", res);
244            }
245        }
246    }
247
248    /// Dispatches synchronous [Command] `cmd` to the server in and return the result.
249    ///
250    /// Sends the `cmd` to the server and reads the response, which is either "OK" or "ERROR".
251    /// Returns the corresponding result wrapped in a [SyncResult] enum.
252    ///
253    /// Returns an `io::Error` if communication with the server failed.
254    async fn dispatch_sync<
255        T: Serialize,
256        R: serde::de::DeserializeOwned,
257        E: serde::de::DeserializeOwned,
258    >(
259        &self,
260        cmd: &Command<T>,
261        reader: &mut BufReader<TcpStream>,
262        writer: &mut BufWriter<TcpStream>,
263    ) -> Result<SyncResult<R, E>, io::Error> {
264        writer.write_all(&cmd.as_bytes())?;
265        writer.flush()?;
266        loop {
267            let mut res = String::new();
268            reader.read_line(&mut res)?;
269            let res = res.trim();
270            if let Some(response_ok) = res.strip_prefix("OK") {
271                let res = self.parse_response(response_ok.trim())?;
272                return Ok(SyncResult::Ok(res));
273            } else if let Some(response_er) = res.strip_prefix("ERROR") {
274                let res = self.parse_response(response_er.trim())?;
275                return Ok(SyncResult::Error(res));
276            } else {
277                // Occasionally the server omits some seemingly random numeric logs.
278                // Log and discard them, then continue the loop.
279                log::trace!("Unknown message format: {}", res);
280            }
281        }
282    }
283
284    /// Identity function: Returns its argument as result
285    pub async fn echo(&mut self, echo: &str) -> Result<SyncResult<String, String>, io::Error> {
286        let cmd = Command {
287            name: "echo".to_owned(),
288            args: Some(echo.to_owned()),
289        };
290        let (mut reader, mut writer) = self.new_connection()?;
291        self.dispatch_sync(&cmd, &mut reader, &mut writer).await
292    }
293
294    /// Forces a shut- down of the connected server process, stopping all open sessions and closing the server socket.
295    /// This may disrupt pending commands on other connections.
296    pub async fn shutdown(&mut self) -> Result<SyncResult<(), String>, io::Error> {
297        let cmd: Command<()> = Command {
298            name: "shutdown".to_owned(),
299            args: None,
300        };
301        let (mut reader, mut writer) = self.new_connection()?;
302        self.dispatch_sync(&cmd, &mut reader, &mut writer).await
303    }
304
305    /// Attempts to cancel the specified task.
306    /// Cancellation is merely a hint that the client prefers an ongoing process to be stopped.
307    pub async fn cancel(&mut self, task_id: String) -> Result<SyncResult<(), ()>, io::Error> {
308        let cmd = Command {
309            name: "cancel".to_owned(),
310            args: Some(CancelArgs { task: task_id }),
311        };
312        let (mut reader, mut writer) = self.new_connection()?;
313        self.dispatch_sync(&cmd, &mut reader, &mut writer).await
314    }
315
316    /// Prepares a session image for interactive use of theories.
317    pub async fn session_build(
318        &mut self,
319        args: &SessionBuildArgs,
320    ) -> Result<AsyncResult<SessionBuildResults, SessionBuildResults>, io::Error> {
321        let cmd = Command {
322            name: "session_build".to_owned(),
323            args: Some(args),
324        };
325        let (mut reader, mut writer) = self.new_connection()?;
326        self.dispatch_async(&cmd, &mut reader, &mut writer).await
327    }
328
329    /// Starts a new Isabelle/PIDE session with underlying Isabelle/ML process, based on a session image that it produces on demand using `session_build`.
330    /// Returns the `session_id`, which provides the internal identification of the session object within the server process.
331    pub async fn session_start(
332        &mut self,
333        args: &SessionBuildArgs,
334    ) -> Result<AsyncResult<SessionStartResult, ()>, io::Error> {
335        let cmd = Command {
336            name: "session_start".to_owned(),
337            args: Some(args),
338        };
339
340        let (mut reader, mut writer) = self.new_connection()?;
341        self.dispatch_async(&cmd, &mut reader, &mut writer).await
342    }
343
344    /// Forces a shutdown of the identified session.
345    pub async fn session_stop(
346        &mut self,
347        args: &SessionStopArgs,
348    ) -> Result<AsyncResult<SessionStopResult, SessionStopResult>, io::Error> {
349        let cmd = Command {
350            name: "session_stop".to_owned(),
351            args: Some(args),
352        };
353
354        let (mut reader, mut writer) = self.new_connection()?;
355        self.dispatch_async(&cmd, &mut reader, &mut writer).await
356    }
357
358    /// Updates the identified session by adding the current version of theory files to it, while dependencies are resolved implicitly.
359    pub async fn use_theories(
360        &mut self,
361        args: &UseTheoriesArgs,
362    ) -> Result<AsyncResult<UseTheoryResults, ()>, io::Error> {
363        let cmd = Command {
364            name: "use_theories".to_owned(),
365            args: Some(args),
366        };
367
368        let (mut reader, mut writer) = self.new_connection()?;
369        self.dispatch_async(&cmd, &mut reader, &mut writer).await
370    }
371
372    /// Updates the identified session by removing theories.
373    /// Theories that are used in pending use_theories tasks or imported by other theories are retained.
374    pub async fn purge_theories(
375        &mut self,
376        args: PurgeTheoryArgs,
377    ) -> Result<SyncResult<PurgeTheoryResults, ()>, io::Error> {
378        let cmd = Command {
379            name: "purge_theories".to_owned(),
380            args: Some(args),
381        };
382
383        let (mut reader, mut writer) = self.new_connection()?;
384        self.dispatch_sync(&cmd, &mut reader, &mut writer).await
385    }
386}
387
388#[cfg(test)]
389mod test {
390
391    use super::*;
392    use crate::server::run_server;
393    use serial_test::serial;
394
395    struct TestContext {
396        server: IsabelleServer,
397        client: IsabelleClient,
398    }
399
400    impl Drop for TestContext {
401        fn drop(&mut self) {
402            self.server.exit().unwrap();
403        }
404    }
405
406    fn setup_test() -> TestContext {
407        let server = run_server(Some("test")).unwrap();
408        let client = IsabelleClient::for_server(&server);
409        TestContext { server, client }
410    }
411
412    #[tokio::test]
413    #[serial]
414    async fn test_echo() {
415        let client = &mut setup_test().client;
416
417        let res = client.echo("echo").await.unwrap();
418
419        match res {
420            SyncResult::Ok(r) => assert_eq!(r, "echo".to_owned()),
421            SyncResult::Error(_) => unreachable!(),
422        }
423    }
424
425    #[tokio::test]
426    #[serial]
427    async fn test_shutdown() {
428        let client = &mut setup_test().client;
429
430        let res = client.shutdown().await.unwrap();
431        assert!(matches!(res, SyncResult::Ok(())));
432    }
433
434    #[tokio::test]
435    #[serial]
436    async fn test_session_build_hol() {
437        let client = &mut setup_test().client;
438
439        let arg = SessionBuildArgs::session("HOL");
440
441        let res = client.session_build(&arg).await.unwrap();
442        match res {
443            AsyncResult::Finished(res) => {
444                assert!(res.ok);
445                for s in res.sessions {
446                    assert!(s.ok);
447                    assert!(s.return_code == 0);
448                }
449            }
450            AsyncResult::Failed(_) | AsyncResult::Error(_) => unreachable!(),
451        }
452    }
453
454    #[tokio::test]
455    #[serial]
456    async fn test_session_build_unknown() {
457        let client = &mut setup_test().client;
458        let arg = SessionBuildArgs::session("unknown_sessions");
459
460        let res = client.session_build(&arg).await.unwrap();
461
462        assert!(matches!(res, AsyncResult::Failed(_)));
463    }
464
465    #[tokio::test]
466    #[serial]
467    async fn test_session_start_hol() {
468        let client = &mut setup_test().client;
469
470        let arg = SessionBuildArgs::session("HOL");
471
472        let res = client.session_start(&arg).await.unwrap();
473        assert!(matches!(res, AsyncResult::Finished(_)));
474    }
475
476    #[tokio::test]
477    #[serial]
478    async fn test_session_start_unknown() {
479        let client = &mut setup_test().client;
480        let arg = SessionBuildArgs::session("unknown_sessions");
481
482        let res = client.session_start(&arg).await.unwrap();
483
484        assert!(matches!(res, AsyncResult::Failed(_)));
485    }
486
487    #[tokio::test]
488    #[serial]
489    async fn test_session_stop_active() {
490        let client = &mut setup_test().client;
491
492        let arg = SessionBuildArgs::session("HOL");
493        let res = client.session_start(&arg).await.unwrap();
494        if let AsyncResult::Finished(res) = res {
495            let arg = SessionStopArgs {
496                session_id: res.session_id,
497            };
498            if let AsyncResult::Finished(stop_res) = client.session_stop(&arg).await.unwrap() {
499                assert!(stop_res.ok);
500            } else {
501                unreachable!();
502            }
503        } else {
504            unreachable!()
505        }
506    }
507
508    #[tokio::test]
509    #[serial]
510    async fn test_session_stop_inactive() {
511        let client = &mut setup_test().client;
512
513        let arg = SessionStopArgs {
514            session_id: "03202b1a-bde6-4d84-926b-d435aac365fe".to_owned(),
515        };
516        let got = client.session_stop(&arg).await.unwrap();
517        assert!(matches!(got, AsyncResult::Failed(_)));
518    }
519
520    #[tokio::test]
521    #[serial]
522    async fn test_session_stop_invalid() {
523        let client = &mut setup_test().client;
524
525        let arg = SessionStopArgs {
526            session_id: "abc".to_owned(),
527        };
528        let got = client.session_stop(&arg).await.unwrap();
529        assert!(matches!(got, AsyncResult::Error(_)));
530    }
531
532    #[tokio::test]
533    #[serial]
534    async fn use_theory_in_hol() {
535        let client = &mut setup_test().client;
536
537        let arg = SessionBuildArgs::session("HOL");
538        let res = client.session_start(&arg).await.unwrap();
539        if let AsyncResult::Finished(res) = res {
540            let arg =
541                UseTheoriesArgs::for_session(&res.session_id, &["~~/src/HOL/Examples/Drinker"]);
542
543            match client.use_theories(&arg).await.unwrap() {
544                AsyncResult::Error(e) => unreachable!("{:?}", e),
545                AsyncResult::Finished(got) => assert!(got.ok),
546                AsyncResult::Failed(f) => unreachable!("{:?}", f),
547            }
548        } else {
549            unreachable!()
550        }
551    }
552
553    #[tokio::test]
554    #[serial]
555    async fn use_theory_unknown() {
556        let client = &mut setup_test().client;
557
558        let arg = SessionBuildArgs::session("HOL");
559        let res = client.session_start(&arg).await.unwrap();
560        if let AsyncResult::Finished(res) = res {
561            let arg = UseTheoriesArgs::for_session(&res.session_id, &["~~/src/HOL/foo"]);
562            let got = client.use_theories(&arg).await.unwrap();
563
564            assert!(matches!(got, AsyncResult::Failed(_)));
565        } else {
566            unreachable!()
567        }
568    }
569}