466 lines
16 KiB
Rust
466 lines
16 KiB
Rust
use std::{
|
|
io::{self, BufReader, BufWriter},
|
|
path::{Path, PathBuf},
|
|
process::{Child, ChildStdin, Command, Stdio},
|
|
sync::mpsc,
|
|
};
|
|
|
|
use crate::ipc::{self, Pack, Unpack, tag};
|
|
|
|
pub use crate::events::{A11y, Cell, Events, Function, MIXED, Mark, Rect, Request};
|
|
|
|
/// The pinned LibreOffice build the worker must report (LOK-01).
|
|
pub const BUILD_ID: &str = "8221e31b3ac356a1623c672912a3d2b492f7e3d1";
|
|
|
|
mod doc;
|
|
mod feed;
|
|
mod frames;
|
|
mod install;
|
|
mod load;
|
|
pub use install::{install_dir, templates};
|
|
use install::worker_bin;
|
|
pub use doc::{Pdf, Sheet, Sheets};
|
|
pub use feed::Feed;
|
|
#[cfg(test)]
|
|
pub(crate) mod tests;
|
|
|
|
/// The LOK worker process: spawn, talk frames, shut down (LOK-01).
|
|
pub struct Worker {
|
|
pub(crate) child: Child,
|
|
stdin: BufWriter<ChildStdin>,
|
|
/// Answers to requests; None once the worker is gone.
|
|
replies: mpsc::Receiver<io::Result<(u8, u64, Vec<u8>)>>,
|
|
feed: Feed,
|
|
down: bool,
|
|
/// The last request's id; each reply must carry it.
|
|
asked: u64,
|
|
/// This worker among all spawned; its documents carry it.
|
|
session: u64,
|
|
stderr: ely_ipc::Relay,
|
|
check: Check,
|
|
/// The name new comments sign with, once set.
|
|
author: Option<String>,
|
|
}
|
|
|
|
/// The start-up check: to run, passed, or why it failed.
|
|
enum Check {
|
|
Pending(PathBuf),
|
|
Passed,
|
|
Failed(String),
|
|
}
|
|
|
|
/// One painted tile and the generation it matches (LOK-03).
|
|
pub struct Tile {
|
|
pub generation: u64,
|
|
pub pixels: Vec<u8>,
|
|
}
|
|
|
|
/// One LOK mouse event; coordinates in twips.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct Mouse {
|
|
/// 0 down, 1 up, 2 move.
|
|
pub kind: i32,
|
|
pub x: i32,
|
|
pub y: i32,
|
|
pub count: i32,
|
|
/// 1 left, 2 middle, 4 right.
|
|
pub buttons: i32,
|
|
/// vcl modifier bits.
|
|
pub modifiers: i32,
|
|
}
|
|
|
|
/// One open document: its worker, number and revision read.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Doc {
|
|
worker: u64,
|
|
id: u64,
|
|
/// The document's revision when this handle was taken.
|
|
pub(crate) rev: u64,
|
|
/// LOK_DOCTYPE: 0 text, 1 spreadsheet, 2 presentation.
|
|
kind: u64,
|
|
}
|
|
|
|
impl Doc {
|
|
/// Whether this is a spreadsheet.
|
|
pub fn is_sheet(&self) -> bool {
|
|
self.kind == 1
|
|
}
|
|
|
|
/// Whether this handle saw a later state than `other`.
|
|
pub fn newer_than(&self, other: &Doc) -> bool {
|
|
self.rev > other.rev
|
|
}
|
|
}
|
|
|
|
/// Whether `error` refused a request made for an older state.
|
|
pub fn is_stale(error: &io::Error) -> bool {
|
|
error.to_string().starts_with(ipc::STALE)
|
|
}
|
|
|
|
static SESSIONS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
|
|
|
impl Worker {
|
|
/// Spawns the worker against `install` with an isolated profile (LOK-01).
|
|
pub fn spawn(install: &Path, profile_dir: &Path) -> io::Result<Self> {
|
|
let mut worker = Self::spawn_mode(install, profile_dir, false)?;
|
|
worker.ready()?;
|
|
Ok(worker)
|
|
}
|
|
|
|
/// Starts the process only; the first request checks it.
|
|
pub fn launch(install: &Path, profile_dir: &Path) -> io::Result<Self> {
|
|
Self::spawn_mode(install, profile_dir, false)
|
|
}
|
|
|
|
/// Spawns with the dev-only probes on, for tests.
|
|
pub fn spawn_dev(install: &Path, profile_dir: &Path) -> io::Result<Self> {
|
|
let mut worker = Self::spawn_mode(install, profile_dir, true)?;
|
|
worker.ready()?;
|
|
Ok(worker)
|
|
}
|
|
|
|
/// Checks the pinned build and tiles, once (LOK-01).
|
|
pub fn ready(&mut self) -> io::Result<()> {
|
|
let profile = match &self.check {
|
|
Check::Passed => return Ok(()),
|
|
Check::Failed(why) => return Err(io::Error::other(format!("the worker failed its check: {why}"))),
|
|
Check::Pending(profile) => profile.clone(),
|
|
};
|
|
// Marked first: the checks' own requests pass through here.
|
|
self.check = Check::Passed;
|
|
let checked = self.version_checked().and_then(|build| self.verify(&build, &profile).map(|()| build));
|
|
match checked {
|
|
Ok(build) => {
|
|
log::info!("lok: worker up, {build}");
|
|
Ok(())
|
|
}
|
|
Err(error) => {
|
|
self.check = Check::Failed(error.to_string());
|
|
Err(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Spawns with or without the dev probes.
|
|
fn spawn_mode(install: &Path, profile_dir: &Path, dev: bool) -> io::Result<Self> {
|
|
std::fs::create_dir_all(profile_dir)?;
|
|
let worker_bin = worker_bin()?;
|
|
log::info!("lok: worker from {}", worker_bin.display());
|
|
let mut command = Command::new(worker_bin);
|
|
command
|
|
.arg(install)
|
|
.arg(profile_dir)
|
|
.env("SAL_FORCE_HIDPI_SCALING", "1")
|
|
.env("SAL_SKIA", "raster");
|
|
// Calc reports in document twips, as its geometry does.
|
|
let options = match cfg!(target_os = "macos") {
|
|
true => "unipoll:sc_print_twips_msgs",
|
|
false => "sc_print_twips_msgs",
|
|
};
|
|
command.env("SAL_LOK_OPTIONS", options);
|
|
if dev {
|
|
command.arg("--dev");
|
|
}
|
|
if let Ok(sal_log) = std::env::var("ELY_LOK_SAL_LOG") {
|
|
command.env("SAL_LOG", sal_log);
|
|
}
|
|
let temp = profile_dir.join("tmp");
|
|
std::fs::create_dir_all(&temp)?;
|
|
command.env("TMPDIR", &temp);
|
|
let mut child = command
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.map_err(|error| io::Error::new(error.kind(), format!("lok-worker spawn: {error}")))?;
|
|
let stderr = ely_ipc::relay(child.stderr.take().expect("piped stderr"), "lok-worker");
|
|
let (stdin, stdout) = (
|
|
child.stdin.take().expect("piped stdin"),
|
|
child.stdout.take().expect("piped stdout"),
|
|
);
|
|
let (feed, (sender, replies)) = (Feed::default(), mpsc::channel());
|
|
let reading = feed.clone();
|
|
std::thread::spawn(move || frames::read_frames(BufReader::new(stdout), &reading, &sender));
|
|
let worker = Self {
|
|
child,
|
|
stdin: BufWriter::new(stdin),
|
|
replies,
|
|
feed,
|
|
down: false,
|
|
asked: 0,
|
|
session: SESSIONS.fetch_add(1, std::sync::atomic::Ordering::SeqCst),
|
|
stderr,
|
|
check: Check::Pending(profile_dir.to_path_buf()),
|
|
author: None,
|
|
};
|
|
Ok(worker)
|
|
}
|
|
|
|
/// The hooked library's BuildId.
|
|
pub fn build_id(&mut self) -> io::Result<String> {
|
|
let payload = self.request(tag::BUILD_ID, &[])?;
|
|
Unpack::new(&payload).str()
|
|
}
|
|
|
|
/// The name new comments and tracked changes carry.
|
|
pub fn set_author(&mut self, name: &str) -> io::Result<()> {
|
|
log::info!("lok: the author is set");
|
|
// Wanted even if this worker fails; a restart sends it.
|
|
self.author = Some(name.to_string());
|
|
self.request(tag::SET_AUTHOR, &Pack::new().str(name).bytes())?;
|
|
Ok(())
|
|
}
|
|
|
|
/// The name last asked for; a restart signs with it.
|
|
pub fn author(&self) -> Option<&str> {
|
|
self.author.as_deref()
|
|
}
|
|
|
|
/// Runs a macro by its URL (LOK-11 probe).
|
|
pub fn run_macro(&mut self, url: &str) -> io::Result<i32> {
|
|
let payload = self.request(tag::RUN_MACRO, &Pack::new().str(url).bytes())?;
|
|
Ok(Unpack::new(&payload).u64()? as i32)
|
|
}
|
|
|
|
/// Posts a key press and release (LOK-06).
|
|
pub fn post_key(&mut self, doc: &Doc, char_code: u32, key_code: i32) -> io::Result<()> {
|
|
self.request(
|
|
tag::POST_KEY,
|
|
&Pack::new()
|
|
.u64(self.doc_id(doc)?)
|
|
.i64(char_code as i64)
|
|
.i64(key_code as i64)
|
|
.bytes(),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Drags a selected object's handle, start then end.
|
|
pub fn drag_graphic(&mut self, doc: &Doc, from: (i32, i32), to: (i32, i32)) -> io::Result<()> {
|
|
for (kind, (x, y)) in [(0, from), (1, to)] {
|
|
let pack = Pack::new()
|
|
.u64(self.doc_id(doc)?)
|
|
.i64(kind)
|
|
.i64(x.into())
|
|
.i64(y.into());
|
|
self.request(tag::GRAPHIC, &pack.bytes())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Posts one mouse event in twips (LOK-04).
|
|
pub fn post_mouse(&mut self, doc: &Doc, event: Mouse) -> io::Result<()> {
|
|
let pack = [
|
|
event.kind as i64,
|
|
event.x as i64,
|
|
event.y as i64,
|
|
event.count as i64,
|
|
event.buttons as i64,
|
|
event.modifiers as i64,
|
|
]
|
|
.into_iter()
|
|
.fold(Pack::new().u64(self.doc_id(doc)?), Pack::i64);
|
|
self.request(tag::POST_MOUSE, &pack.bytes())?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Updates the IME composition, or commits it (LOK-07).
|
|
pub fn post_text(&mut self, doc: &Doc, commit: bool, text: &str) -> io::Result<()> {
|
|
let kind = if commit { 2 } else { 0 };
|
|
self.request(
|
|
tag::POST_TEXT,
|
|
&Pack::new().u64(self.doc_id(doc)?).i64(kind).str(text).bytes(),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Copies the selection; one (mime, bytes) per flavor (LOK-06).
|
|
pub fn copy(&mut self, doc: &Doc) -> io::Result<Vec<(String, Vec<u8>)>> {
|
|
let payload = self.request(tag::COPY, &Pack::new().u64(self.doc_id(doc)?).bytes())?;
|
|
let mut unpack = Unpack::new(&payload);
|
|
let count = unpack.u64()?;
|
|
(0..count)
|
|
.map(|_| Ok((unpack.str()?, unpack.blob()?)))
|
|
.collect()
|
|
}
|
|
|
|
/// Cuts the selection after a copy went out (LOK-06).
|
|
pub fn cut(&mut self, doc: &Doc) -> io::Result<()> {
|
|
self.request(tag::CUT, &Pack::new().u64(self.doc_id(doc)?).bytes())?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Pastes one flavor into the document (LOK-06).
|
|
pub fn paste(&mut self, doc: &Doc, mime: &str, data: &[u8]) -> io::Result<()> {
|
|
self.request(
|
|
tag::PASTE,
|
|
&Pack::new().u64(self.doc_id(doc)?).str(mime).blob(data).bytes(),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Runs a table command; the worker confirms it took (LOK-02).
|
|
pub fn run(&mut self, doc: &Doc, id: &str, values: &[&str]) -> io::Result<()> {
|
|
self.run_all(doc, &[(id, values)])
|
|
}
|
|
|
|
/// Commands made from one read; the state is checked once.
|
|
pub fn run_all(&mut self, doc: &Doc, commands: &[(&str, &[&str])]) -> io::Result<()> {
|
|
let mut addressed = false;
|
|
for (id, values) in commands {
|
|
let command = crate::commands::find(id)?;
|
|
command.args_json(values)?;
|
|
addressed |= command.addressed(values);
|
|
}
|
|
// Numbers hold only in the state they were read.
|
|
if addressed {
|
|
self.request(tag::EXPECT, &Pack::new().u64(self.doc_id(doc)?).u64(doc.rev).bytes())?;
|
|
}
|
|
for (id, values) in commands {
|
|
let pack = values.iter().fold(
|
|
Pack::new().u64(self.doc_id(doc)?).str(id).u64(values.len() as u64),
|
|
|pack, value| pack.str(value),
|
|
);
|
|
self.request(tag::POST_UNO, &pack.bytes())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// `doc` as of now: commands by number use this revision.
|
|
pub fn now(&mut self, doc: &Doc) -> io::Result<Doc> {
|
|
let payload = self.request(tag::REVISION, &Pack::new().u64(self.doc_id(doc)?).bytes())?;
|
|
Ok(Doc { rev: Unpack::new(&payload).u64()?, ..doc.clone() })
|
|
}
|
|
|
|
/// `doc`'s number here; another worker's document is refused.
|
|
pub(crate) fn doc_id(&self, doc: &Doc) -> io::Result<u64> {
|
|
match doc.worker == self.session {
|
|
true => Ok(doc.id),
|
|
false => Err(io::Error::new(io::ErrorKind::NotFound, "a document of another lok worker")),
|
|
}
|
|
}
|
|
|
|
/// The kernel's JSON list for `command`: fonts or styles (LOK-05).
|
|
pub fn values(&mut self, doc: &Doc, command: &str) -> io::Result<String> {
|
|
let payload = self.request(tag::VALUES, &Pack::new().u64(self.doc_id(doc)?).str(command).bytes())?;
|
|
Unpack::new(&payload).str()
|
|
}
|
|
|
|
/// Events past seq `known`, waiting up to `timeout_ms`.
|
|
pub fn poll_events(&self, doc: &Doc, known: u64, timeout_ms: u64) -> io::Result<Events> {
|
|
self.feed.wait(doc, known, timeout_ms)
|
|
}
|
|
|
|
/// The pushed events, waited on without this worker's lock.
|
|
pub fn feed(&self) -> Feed {
|
|
self.feed.clone()
|
|
}
|
|
|
|
/// A worker whose output failed: stopped, reaped, and why.
|
|
fn broken(&mut self, request: u8, error: io::Error) -> io::Error {
|
|
let _ = self.child.kill();
|
|
let status = self.child.wait().map(|status| status.to_string()).unwrap_or_else(|wait| wait.to_string());
|
|
log::error!("lok: the worker failed on request {request} ({status})");
|
|
let said = self.stderr.last_error().map_or_else(String::new, |said| format!(": {said}"));
|
|
io::Error::new(error.kind(), format!("the lok worker failed: {error} ({status}){said}"))
|
|
}
|
|
|
|
/// One round trip per request.
|
|
pub(crate) fn request(&mut self, request: u8, payload: &[u8]) -> io::Result<Vec<u8>> {
|
|
// Nothing reaches a worker that has not passed its checks.
|
|
self.ready()?;
|
|
self.asked += 1;
|
|
ipc::write_frame(&mut self.stdin, request, self.asked, payload)?;
|
|
let (reply, id, body) = match self.replies.recv() {
|
|
Ok(Ok(frame)) => frame,
|
|
Ok(Err(error)) => return Err(self.broken(request, error)),
|
|
Err(_) => return Err(self.broken(request, io::Error::other("the reply reader stopped"))),
|
|
};
|
|
if let Err(error) = ipc::check_reply(self.asked, id) {
|
|
return Err(self.broken(request, error));
|
|
}
|
|
match reply {
|
|
tag::OK => Ok(body),
|
|
tag::ERR => Err(io::Error::other(Unpack::new(&body).str()?)),
|
|
other => Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!("unknown reply {other}"),
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Why the worker process ended, if it has.
|
|
pub fn ended(&mut self) -> Option<String> {
|
|
match self.child.try_wait() {
|
|
Ok(status) => status.map(|status| status.to_string()),
|
|
Err(error) => Some(error.to_string()),
|
|
}
|
|
}
|
|
|
|
/// Closes the document in the worker.
|
|
pub fn close_doc(&mut self, doc: Doc) -> io::Result<()> {
|
|
self.request(tag::CLOSE_DOC, &Pack::new().u64(self.doc_id(&doc)?).bytes())?;
|
|
self.feed.closed(doc.id);
|
|
Ok(())
|
|
}
|
|
|
|
/// Shuts the worker down and reaps it.
|
|
pub fn shutdown(&mut self) -> io::Result<()> {
|
|
if self.down {
|
|
return Ok(());
|
|
}
|
|
self.down = true;
|
|
let mut outcome = self.answer_shutdown();
|
|
if outcome.is_err() {
|
|
let _ = self.child.kill();
|
|
}
|
|
let status = match self.child.wait() {
|
|
Ok(status) => status,
|
|
Err(error) => {
|
|
let _ = self.child.kill();
|
|
return Err(io::Error::new(
|
|
error.kind(),
|
|
format!("the lok worker could not be reaped: {error}"),
|
|
));
|
|
}
|
|
};
|
|
if outcome.is_ok() && !status.success() {
|
|
outcome = Err(io::Error::other(format!(
|
|
"the lok worker exited as {status}"
|
|
)));
|
|
}
|
|
if outcome.is_ok() {
|
|
log::info!("lok: worker down ({status})");
|
|
}
|
|
outcome
|
|
}
|
|
|
|
/// The shutdown round trip alone.
|
|
fn answer_shutdown(&mut self) -> io::Result<()> {
|
|
self.asked += 1;
|
|
ipc::write_frame(&mut self.stdin, tag::SHUTDOWN, self.asked, &[])?;
|
|
let reply = match self.replies.recv() {
|
|
Ok(Ok(frame)) => frame,
|
|
Ok(Err(error)) => return Err(self.broken(tag::SHUTDOWN, error)),
|
|
Err(_) => return Err(self.broken(tag::SHUTDOWN, io::Error::other("the reply reader stopped"))),
|
|
};
|
|
ipc::check_reply(self.asked, reply.1)?;
|
|
match reply {
|
|
(tag::OK, _, _) => Ok(()),
|
|
(tag::ERR, _, body) => Err(io::Error::other(Unpack::new(&body).str()?)),
|
|
other => Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!("bad shutdown answer: {other:?}"),
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for Worker {
|
|
fn drop(&mut self) {
|
|
if let Err(error) = self.shutdown() {
|
|
log::warn!("lok: shutdown failed: {error}");
|
|
}
|
|
}
|
|
}
|