Skip to main content

geop/
session.rs

1//! One editor (see `geop_core_editor::editor`) answering commands of the
2//! JSON protocol, one line each: what `geop serve` runs over stdin/stdout
3//! and `geop open` over a WebSocket.
4//!
5//! Every command is answered by exactly one line: the update, or
6//! `{"fatal": "..."}` if the command could not be read at all, or
7//! `{"crashed": "<what panicked, where>"}` if the kernel panicked running
8//! it. A panic leaves the editor in an unknown state: the session is then
9//! started afresh, and the front end gives it the program it holds again
10//! (`web/src/backend.ts`, `vscode-extension/src/server.ts`).
11
12use geop_cad_base::disk::DiskCache;
13use geop_cad_base::{Editor, editor_with};
14use std::{cell::RefCell, path::PathBuf, sync::Once};
15
16thread_local! {
17    /// What last panicked on this thread and where, as the default hook
18    /// prints it: the panic hook runs on the thread that panics, so each
19    /// session reads its own.
20    static PANICKED: RefCell<String> = const { RefCell::new(String::new()) };
21}
22
23/// Note every panic's message in [`PANICKED`], besides printing it.
24fn note_panics() {
25    static HOOK: Once = Once::new();
26    HOOK.call_once(|| {
27        let default_hook = std::panic::take_hook();
28        std::panic::set_hook(Box::new(move |info| {
29            PANICKED.with(|p| *p.borrow_mut() = info.to_string());
30            default_hook(info);
31        }));
32    });
33}
34
35/// What a session answered to one command.
36pub struct Answer {
37    /// The line to send back.
38    pub reply: String,
39    /// Whether the kernel panicked: the editor answering it is gone.
40    pub crashed: bool,
41}
42
43/// An editor and where what it makes of files is kept.
44pub struct Session {
45    editor: Editor,
46    cache_root: Option<PathBuf>,
47}
48
49impl Session {
50    /// A fresh editor, keeping what it makes of the files under
51    /// `cache_root` beside each (see [`DiskCache`]), or in memory only.
52    pub fn new(cache_root: Option<PathBuf>) -> Self {
53        note_panics();
54        let editor = editor_with(DiskCache::cache(cache_root.clone().and_then(DiskCache::of)));
55        Self { editor, cache_root }
56    }
57
58    /// The answer to the command `line`.
59    pub fn answer(&mut self, line: &str) -> Answer {
60        let answer = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
61            self.editor.handle_json(line)
62        }));
63        match answer {
64            Ok(Ok(update)) => Answer {
65                reply: update,
66                crashed: false,
67            },
68            Ok(Err(message)) => Answer {
69                reply: serde_json::json!({ "fatal": message }).to_string(),
70                crashed: false,
71            },
72            Err(_) => {
73                let message = PANICKED.with(|p| p.borrow().clone());
74                Answer {
75                    reply: serde_json::json!({ "crashed": message }).to_string(),
76                    crashed: true,
77                }
78            }
79        }
80    }
81
82    /// Start afresh, as [`Session::new`] did: after a crash.
83    pub fn restart(&mut self) {
84        *self = Self::new(self.cache_root.take());
85    }
86}