Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,187 @@
|
||||
//! Framed requests between an app and its engine worker.
|
||||
|
||||
mod relay;
|
||||
pub use relay::{Relay, relay};
|
||||
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
/// A frame: u32 length, u8 tag, u64 request id, payload.
|
||||
pub const MAX_FRAME: usize = 64 * 1024 * 1024;
|
||||
|
||||
/// Frames no request asked for, such as pushed events.
|
||||
pub const UNASKED: u64 = 0;
|
||||
|
||||
/// Writes one frame and flushes; replies carry the request's id.
|
||||
pub fn write_frame(out: &mut impl Write, tag: u8, id: u64, payload: &[u8]) -> io::Result<()> {
|
||||
// The reader's cap, checked before a byte goes out.
|
||||
if payload.len() > MAX_FRAME {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("a {} byte frame is over the {MAX_FRAME} byte cap", payload.len()),
|
||||
));
|
||||
}
|
||||
let len = payload.len() as u32;
|
||||
out.write_all(&len.to_le_bytes())?;
|
||||
out.write_all(&[tag])?;
|
||||
out.write_all(&id.to_le_bytes())?;
|
||||
out.write_all(payload)?;
|
||||
out.flush()
|
||||
}
|
||||
|
||||
/// A reply with another request's id means the pairing broke.
|
||||
pub fn check_reply(asked: u64, got: u64) -> io::Result<()> {
|
||||
match asked == got {
|
||||
true => Ok(()),
|
||||
false => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("the worker answered request {got}, not {asked}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads (tag, id, payload); None at a clean stream end.
|
||||
pub fn read_frame(input: &mut impl Read) -> io::Result<Option<(u8, u64, Vec<u8>)>> {
|
||||
let mut len = [0u8; 4];
|
||||
// Only a stream ending between frames ends cleanly.
|
||||
let mut got = 0;
|
||||
while got < len.len() {
|
||||
match input.read(&mut len[got..]) {
|
||||
Ok(0) if got == 0 => return Ok(None),
|
||||
Ok(0) => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "a cut frame header")),
|
||||
Ok(read) => got += read,
|
||||
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
let len = u32::from_le_bytes(len) as usize;
|
||||
if len > MAX_FRAME {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"frame over the cap",
|
||||
));
|
||||
}
|
||||
let mut tag = [0u8; 1];
|
||||
input.read_exact(&mut tag)?;
|
||||
let mut id = [0u8; 8];
|
||||
input.read_exact(&mut id)?;
|
||||
let mut payload = vec![0u8; len];
|
||||
input.read_exact(&mut payload)?;
|
||||
Ok(Some((tag[0], u64::from_le_bytes(id), payload)))
|
||||
}
|
||||
|
||||
/// A payload writer.
|
||||
#[derive(Default)]
|
||||
pub struct Pack {
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Pack {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn str(self, text: &str) -> Self {
|
||||
self.blob(text.as_bytes())
|
||||
}
|
||||
|
||||
/// Length-prefixed raw bytes.
|
||||
pub fn blob(mut self, data: &[u8]) -> Self {
|
||||
self.bytes
|
||||
.extend_from_slice(&(data.len() as u32).to_le_bytes());
|
||||
self.bytes.extend_from_slice(data);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn u64(mut self, value: u64) -> Self {
|
||||
self.bytes.extend_from_slice(&value.to_le_bytes());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn i64(mut self, value: i64) -> Self {
|
||||
self.bytes.extend_from_slice(&value.to_le_bytes());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bytes(self) -> Vec<u8> {
|
||||
self.bytes
|
||||
}
|
||||
}
|
||||
|
||||
/// A payload reader.
|
||||
pub struct Unpack<'a> {
|
||||
bytes: &'a [u8],
|
||||
at: usize,
|
||||
}
|
||||
|
||||
impl<'a> Unpack<'a> {
|
||||
pub fn new(bytes: &'a [u8]) -> Self {
|
||||
Self { bytes, at: 0 }
|
||||
}
|
||||
|
||||
fn take(&mut self, len: usize) -> io::Result<&'a [u8]> {
|
||||
let end = self.at + len;
|
||||
let Some(slice) = self.bytes.get(self.at..end) else {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "a short frame"));
|
||||
};
|
||||
self.at = end;
|
||||
Ok(slice)
|
||||
}
|
||||
|
||||
pub fn str(&mut self) -> io::Result<String> {
|
||||
String::from_utf8(self.blob()?)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "payload is no text"))
|
||||
}
|
||||
|
||||
/// Length-prefixed raw bytes.
|
||||
pub fn blob(&mut self) -> io::Result<Vec<u8>> {
|
||||
let len = u32::from_le_bytes(self.take(4)?.try_into().expect("a u32")) as usize;
|
||||
Ok(self.take(len)?.to_vec())
|
||||
}
|
||||
|
||||
pub fn u64(&mut self) -> io::Result<u64> {
|
||||
Ok(u64::from_le_bytes(self.take(8)?.try_into().expect("a u64")))
|
||||
}
|
||||
|
||||
pub fn i64(&mut self) -> io::Result<i64> {
|
||||
Ok(i64::from_le_bytes(
|
||||
self.take(8)?.try_into().expect("an i64"),
|
||||
))
|
||||
}
|
||||
|
||||
/// The unread tail.
|
||||
pub fn rest(&self) -> &'a [u8] {
|
||||
&self.bytes[self.at..]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn frames_and_payloads_round_trip_and_short_ones_fail() {
|
||||
let payload = Pack::new().str("页 one").u64(7).i64(-3).blob(&[1, 2]).bytes();
|
||||
let mut wire = Vec::new();
|
||||
write_frame(&mut wire, 9, 41, &payload).expect("writes");
|
||||
let mut read = &wire[..];
|
||||
let (tag, id, back) = read_frame(&mut read).expect("reads").expect("a frame");
|
||||
assert_eq!((tag, id), (9, 41));
|
||||
assert!(check_reply(41, id).is_ok());
|
||||
assert_eq!(check_reply(42, id).expect_err("another request").kind(), io::ErrorKind::InvalidData);
|
||||
let mut unpack = Unpack::new(&back);
|
||||
assert_eq!(unpack.str().expect("text"), "页 one");
|
||||
assert_eq!((unpack.u64().expect("u64"), unpack.i64().expect("i64")), (7, -3));
|
||||
assert_eq!(unpack.blob().expect("blob"), [1, 2]);
|
||||
assert!(unpack.rest().is_empty());
|
||||
assert!(unpack.u64().is_err(), "a short payload is an error");
|
||||
assert!(read_frame(&mut &[][..]).expect("a clean end").is_none());
|
||||
let mut nothing = Vec::new();
|
||||
let over = write_frame(&mut nothing, 9, 1, &vec![0; MAX_FRAME + 1]).expect_err("over the cap");
|
||||
assert_eq!(over.kind(), io::ErrorKind::InvalidInput);
|
||||
assert!(nothing.is_empty(), "nothing written");
|
||||
for cut in 1..4 {
|
||||
let failed = read_frame(&mut &wire[..cut]).expect_err("a cut header");
|
||||
assert_eq!(failed.kind(), io::ErrorKind::UnexpectedEof, "{cut} bytes");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//! A worker's stderr, carried into the app's log.
|
||||
|
||||
use std::{
|
||||
io::{BufRead as _, BufReader, Read},
|
||||
sync::{Arc, Mutex},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
/// The relaying thread, and the worker's last error record.
|
||||
pub struct Relay {
|
||||
last: Arc<Mutex<Option<String>>>,
|
||||
thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Relay {
|
||||
/// After the worker exits: its last error record.
|
||||
pub fn last_error(&mut self) -> Option<String> {
|
||||
if let Some(thread) = self.thread.take() {
|
||||
let _ = thread.join();
|
||||
}
|
||||
self.last.lock().expect("the relay lock").clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs the worker's own records; engine text stays out.
|
||||
pub fn relay(stderr: impl Read + Send + 'static, who: &'static str) -> Relay {
|
||||
let last = Arc::new(Mutex::new(None));
|
||||
let kept = Arc::clone(&last);
|
||||
let thread = std::thread::spawn(move || {
|
||||
let mut withheld = 0u64;
|
||||
for line in BufReader::new(stderr).lines() {
|
||||
let line = match line {
|
||||
Ok(line) => line,
|
||||
Err(error) => return log::warn!(target: who, "stderr unreadable: {error}"),
|
||||
};
|
||||
match parse(&line) {
|
||||
Some((level, target, message)) => {
|
||||
if level == log::Level::Error {
|
||||
*kept.lock().expect("the relay lock") = Some(message.to_string());
|
||||
}
|
||||
log::log!(target: target, level, "{message}")
|
||||
}
|
||||
// A crash stack names code, never documents.
|
||||
None if frame(&line) => log::error!(target: who, "{line}"),
|
||||
None => {
|
||||
withheld += 1;
|
||||
log::debug!(target: who, "native output withheld ({withheld} lines)");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Relay { last, thread: Some(thread) }
|
||||
}
|
||||
|
||||
/// `[time LEVEL target] message`, as env_logger writes it.
|
||||
fn parse(line: &str) -> Option<(log::Level, &str, &str)> {
|
||||
let (head, message) = line.strip_prefix('[')?.split_once("] ")?;
|
||||
let mut words = head.split_whitespace();
|
||||
let (_, level, target) = (words.next()?, words.next()?, words.next()?);
|
||||
Some((level.parse().ok()?, target, message))
|
||||
}
|
||||
|
||||
/// `3 ely-lok-worker 0x0001 symbol + 12` from backtrace_symbols_fd.
|
||||
fn frame(line: &str) -> bool {
|
||||
let mut words = line.split_whitespace();
|
||||
words.next().is_some_and(|n| n.parse::<u32>().is_ok())
|
||||
&& words.next().is_some()
|
||||
&& words.next().is_some_and(|address| address.starts_with("0x"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{frame, parse};
|
||||
|
||||
#[test]
|
||||
fn a_record_keeps_its_level_and_target() {
|
||||
let line = "[2026-10-08T12:00:00Z WARN ely_lok::worker] request failed";
|
||||
assert_eq!(parse(line), Some((log::Level::Warn, "ely_lok::worker", "request failed")));
|
||||
assert_eq!(parse("warning: syntax error: unknown keyword: 'Zq'"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_frames_pass_and_engine_text_does_not() {
|
||||
assert!(frame("3 ely-lok-worker 0x0000000104f3c960 report + 52"));
|
||||
assert!(!frame("syntax error: unknown keyword: 'ZqxPrivate42'"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user