use std::path::Path;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::fork_sandbox::process;
use crate::fork_sandbox::sandbox;
use crate::highlight::{HighlightRect, HighlightSpec};
use crate::image::LoadedImages;
use crate::log::{LogBuffer, LogEntry, WireLogEntry};
use crate::pipeline::{BuildParams, compile_and_dump, compile_and_tile};
use crate::tile::DocumentMeta;
use crate::tile_cache::TilePngs;
pub use crate::fork_sandbox::process::ChildProcess;
#[derive(Serialize, Deserialize)]
enum Request {
RenderTile(usize),
FindHighlightRects { idx: usize, spec: HighlightSpec },
Shutdown,
}
#[derive(Serialize, Deserialize)]
enum Response {
Meta(DocumentMeta),
Tile {
idx: usize,
pngs: TilePngs,
},
Rects {
idx: usize,
rects: Vec<HighlightRect>,
},
Error(String),
}
#[derive(Serialize, Deserialize)]
struct ChildMessage {
response: Response,
logs: Vec<WireLogEntry>,
}
#[derive(Debug)]
pub enum TileResponse {
Tile {
idx: usize,
pngs: TilePngs,
},
Rects {
idx: usize,
rects: Vec<HighlightRect>,
},
}
pub struct TileRenderer {
tx: process::TypedWriter<Request>,
rx: process::TypedReader<ChildMessage>,
log_buffer: LogBuffer,
}
impl TileRenderer {
pub fn wait_for_meta(&mut self) -> Result<DocumentMeta> {
match self
.recv_and_ingest()
.context("failed to receive metadata from child")?
{
Response::Meta(m) => Ok(m),
Response::Error(e) => anyhow::bail!("child build error: {e}"),
_ => anyhow::bail!("unexpected response, expected Meta"),
}
}
pub fn send_render_tile(&mut self, idx: usize) -> Result<()> {
self.tx.send(&Request::RenderTile(idx))
}
pub fn send_find_rects(&mut self, idx: usize, spec: &HighlightSpec) -> Result<()> {
self.tx.send(&Request::FindHighlightRects {
idx,
spec: spec.clone(),
})
}
pub fn try_recv(&mut self) -> Result<Option<TileResponse>> {
if !self.has_pending_data() {
return Ok(None);
}
self.recv().map(Some)
}
pub fn recv(&mut self) -> Result<TileResponse> {
match self.recv_and_ingest()? {
Response::Tile { idx, pngs } => Ok(TileResponse::Tile { idx, pngs }),
Response::Rects { idx, rects } => Ok(TileResponse::Rects { idx, rects }),
Response::Error(e) => anyhow::bail!("{e}"),
Response::Meta(_) => anyhow::bail!("unexpected Meta response"),
}
}
fn recv_and_ingest(&mut self) -> Result<Response> {
let msg = self.rx.recv()?;
for entry in msg.logs {
self.log_buffer.push(LogEntry::from(entry));
}
Ok(msg.response)
}
pub fn render_tile_pair(&mut self, idx: usize) -> Result<TilePngs> {
self.send_render_tile(idx)?;
match self.recv()? {
TileResponse::Tile { pngs, .. } => Ok(pngs),
_ => anyhow::bail!("unexpected response, expected Tile"),
}
}
pub fn find_highlight_rects(
&mut self,
idx: usize,
spec: &HighlightSpec,
) -> Result<Vec<HighlightRect>> {
self.send_find_rects(idx, spec)?;
match self.recv()? {
TileResponse::Rects { rects, .. } => Ok(rects),
_ => anyhow::bail!("unexpected response, expected Rects"),
}
}
pub fn has_pending_data(&self) -> bool {
use std::os::fd::AsRawFd;
let fd = self.rx.as_raw_fd();
let mut pfd = nix::libc::pollfd {
fd,
events: nix::libc::POLLIN,
revents: 0,
};
let ret = unsafe { nix::libc::poll(&mut pfd, 1, 0) };
ret > 0 && (pfd.revents & nix::libc::POLLIN) != 0
}
pub fn shutdown(mut self) {
let _ = self.tx.send(&Request::Shutdown);
}
}
fn send_with_logs(
tx: &mut process::TypedWriter<ChildMessage>,
response: Response,
log_buf: &LogBuffer,
) -> Result<()> {
let logs = log_buf
.drain()
.into_iter()
.map(WireLogEntry::from)
.collect();
tx.send(&ChildMessage { response, logs })
}
fn prepare_remote_images(
params: &BuildParams,
no_sandbox: bool,
log_buffer: &LogBuffer,
) -> Result<(crate::pipeline::Prescan, LoadedImages)> {
use crate::fork_sandbox::fork_compute;
let prescan_result = fork_compute(None, &[], no_sandbox, log_buffer, {
let md = params.markdown.clone();
move || crate::pipeline::prescan(&md)
})?;
let paths = &prescan_result.image_paths;
let remote_images = if params.allow_remote_images {
let remote_urls: Vec<String> = paths
.iter()
.filter(|p| p.starts_with("http://") || p.starts_with("https://"))
.cloned()
.collect();
if remote_urls.is_empty() {
LoadedImages::default()
} else {
let (images, errors) = crate::image::load_images(&remote_urls, None, true);
for err in &errors {
log::warn!("{err}");
}
images
}
} else {
LoadedImages::default()
};
Ok((prescan_result, remote_images))
}
fn sandbox_read_base(params: &BuildParams) -> Option<&Path> {
params.base_dir.as_deref()
}
pub fn build_renderer(
params: &BuildParams,
no_sandbox: bool,
log_buffer: &LogBuffer,
) -> Result<(TileRenderer, ChildProcess)> {
let (prescan, remote_images) = prepare_remote_images(params, no_sandbox, log_buffer)?;
let read_base = sandbox_read_base(params);
let font_dirs = params.fonts.font_dirs();
let params = params.clone();
let read_base = read_base.map(|p| p.to_path_buf());
let log_buf = log_buffer.clone();
let (tx, rx, child) = process::fork_with_channels::<Request, ChildMessage, _>(
move |mut req_rx: process::TypedReader<Request>,
mut resp_tx: process::TypedWriter<ChildMessage>| {
if !no_sandbox
&& let Err(e) = sandbox::enforce_sandbox(read_base.as_deref(), &font_dirs)
{
log::warn!("child: sandbox failed: {e:#}");
}
let (mut images, errors) =
crate::image::load_images(&prescan.image_paths, params.base_dir.as_deref(), false);
for err in &errors {
log::warn!("{err}");
}
images.extend(remote_images);
let doc = match compile_and_tile(¶ms, &prescan, images) {
Ok(doc) => doc,
Err(e) => {
log::error!("child: build failed: {e:#}");
let _ =
send_with_logs(&mut resp_tx, Response::Error(format!("{e:#}")), &log_buf);
return;
}
};
let meta = doc.metadata();
if send_with_logs(&mut resp_tx, Response::Meta(meta), &log_buf).is_err() {
return;
}
loop {
let req = match req_rx.recv() {
Ok(r) => r,
Err(_) => break, };
match req {
Request::RenderTile(idx) => {
let resp =
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
doc.render_tile_pair(idx)
})) {
Ok(Ok(pngs)) => Response::Tile { idx, pngs },
Ok(Err(e)) => Response::Error(format!("render tile {idx}: {e:#}")),
Err(_) => Response::Error(format!("render tile {idx}: panic")),
};
if send_with_logs(&mut resp_tx, resp, &log_buf).is_err() {
break;
}
}
Request::FindHighlightRects { idx, spec } => {
let resp =
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
doc.find_tile_highlight_rects(idx, &spec)
})) {
Ok(rects) => Response::Rects { idx, rects },
Err(_) => Response::Error(format!(
"find highlight rects tile {idx}: panic"
)),
};
if send_with_logs(&mut resp_tx, resp, &log_buf).is_err() {
break;
}
}
Request::Shutdown => break,
}
}
},
)?;
Ok((
TileRenderer {
tx,
rx,
log_buffer: log_buffer.clone(),
},
child,
))
}
pub fn build_renderer_blocking(
params: &BuildParams,
no_sandbox: bool,
log_buffer: &LogBuffer,
) -> Result<(DocumentMeta, TileRenderer, ChildProcess)> {
let (mut renderer, child) = build_renderer(params, no_sandbox, log_buffer)?;
let meta = renderer.wait_for_meta()?;
Ok((meta, renderer, child))
}
pub fn build_dump(
params: &BuildParams,
no_sandbox: bool,
log_buffer: &LogBuffer,
) -> Result<ChildProcess> {
let (prescan, remote_images) = prepare_remote_images(params, no_sandbox, log_buffer)?;
let read_base = sandbox_read_base(params);
let font_dirs = params.fonts.font_dirs();
let params = params.clone();
let read_base = read_base.map(|p| p.to_path_buf());
let (_, _, child) = process::fork_with_channels::<(), (), _>(move |_, _| {
if !no_sandbox && let Err(e) = sandbox::enforce_sandbox(read_base.as_deref(), &font_dirs) {
log::warn!("child: sandbox failed: {e:#}");
}
let (mut images, errors) =
crate::image::load_images(&prescan.image_paths, params.base_dir.as_deref(), false);
for err in &errors {
log::warn!("{err}");
}
images.extend(remote_images);
if let Err(e) = compile_and_dump(¶ms, &prescan, images) {
eprintln!("{e:#}");
unsafe { nix::libc::_exit(1) }
}
})?;
Ok(child)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_response_serde_roundtrip() {
let req = Request::RenderTile(42);
let encoded = bincode::serde::encode_to_vec(&req, bincode::config::standard()).unwrap();
let (decoded, _): (Request, _) =
bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap();
match decoded {
Request::RenderTile(idx) => assert_eq!(idx, 42),
_ => panic!("wrong variant"),
}
let req2 = Request::Shutdown;
let encoded2 = bincode::serde::encode_to_vec(&req2, bincode::config::standard()).unwrap();
let (decoded2, _): (Request, _) =
bincode::serde::decode_from_slice(&encoded2, bincode::config::standard()).unwrap();
assert!(matches!(decoded2, Request::Shutdown));
let req3 = Request::FindHighlightRects {
idx: 7,
spec: HighlightSpec {
target_ranges: vec![10..20, 30..40],
active_ranges: vec![10..20],
},
};
let encoded3 = bincode::serde::encode_to_vec(&req3, bincode::config::standard()).unwrap();
let (decoded3, _): (Request, _) =
bincode::serde::decode_from_slice(&encoded3, bincode::config::standard()).unwrap();
match decoded3 {
Request::FindHighlightRects { idx, spec } => {
assert_eq!(idx, 7);
assert_eq!(spec.target_ranges.len(), 2);
assert_eq!(spec.target_ranges[0], 10..20);
assert_eq!(spec.target_ranges[1], 30..40);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn response_error_serde_roundtrip() {
let resp = Response::Error("test error".into());
let encoded = bincode::serde::encode_to_vec(&resp, bincode::config::standard()).unwrap();
let (decoded, _): (Response, _) =
bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap();
match decoded {
Response::Error(msg) => assert_eq!(msg, "test error"),
_ => panic!("wrong variant"),
}
}
#[test]
fn child_message_serde_roundtrip() {
let msg = ChildMessage {
response: Response::Error("oops".into()),
logs: vec![WireLogEntry {
timestamp_ms: 1234567890,
level: 2,
target: "mlux::test".into(),
message: "warning".into(),
}],
};
let encoded = bincode::serde::encode_to_vec(&msg, bincode::config::standard()).unwrap();
let (decoded, _): (ChildMessage, _) =
bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap();
assert_eq!(decoded.logs.len(), 1);
assert_eq!(decoded.logs[0].level, 2);
assert_eq!(decoded.logs[0].message, "warning");
match decoded.response {
Response::Error(e) => assert_eq!(e, "oops"),
_ => panic!("wrong variant"),
}
}
}