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}