mod process;
mod sandbox;
use std::path::Path;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::pipeline::{BuildParams, build_tiled_document};
use crate::tile::{DocumentMeta, TilePngs};
pub use process::ChildProcess;
#[derive(Serialize, Deserialize)]
enum Request {
RenderTile(usize),
Shutdown,
}
#[derive(Serialize, Deserialize)]
enum Response {
Meta(DocumentMeta),
Tile(TilePngs),
Error(String),
}
pub struct TileRenderer {
tx: process::TypedWriter<Request>,
rx: process::TypedReader<Response>,
}
impl TileRenderer {
pub fn wait_for_meta(&mut self) -> Result<DocumentMeta> {
match self
.rx
.recv()
.context("failed to receive metadata from child")?
{
Response::Meta(m) => Ok(m),
Response::Error(e) => anyhow::bail!("child build error: {e}"),
Response::Tile(_) => anyhow::bail!("unexpected Tile response, expected Meta"),
}
}
pub fn render_tile_pair(&mut self, idx: usize) -> Result<TilePngs> {
self.tx.send(&Request::RenderTile(idx))?;
match self.rx.recv()? {
Response::Tile(pngs) => Ok(pngs),
Response::Error(e) => anyhow::bail!("{e}"),
Response::Meta(_) => anyhow::bail!("unexpected Meta response"),
}
}
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);
}
}
pub fn fork_renderer(
params: &BuildParams<'_>,
sandbox_read_base: Option<&Path>,
no_sandbox: bool,
) -> Result<(TileRenderer, ChildProcess)> {
let theme_name = params.theme_name.to_string();
let theme_text = params.theme_text.to_string();
let data_files = params.data_files;
let markdown = params.markdown.to_string();
let base_dir = params.base_dir.map(|p| p.to_path_buf());
let width_pt = params.width_pt;
let sidebar_width_pt = params.sidebar_width_pt;
let tile_height_pt = params.tile_height_pt;
let ppi = params.ppi;
let allow_remote_images = params.allow_remote_images;
let sandbox_read_base = sandbox_read_base.map(|p| p.to_path_buf());
let (tx, rx, child) = process::fork_with_channels::<Request, Response, _>(
move |mut req_rx: process::TypedReader<Request>,
mut resp_tx: process::TypedWriter<Response>| {
if !no_sandbox
&& let Err(e) =
sandbox::enforce_sandbox(sandbox_read_base.as_deref(), allow_remote_images)
{
log::warn!("child: sandbox failed: {e:#}");
}
let fonts = crate::pipeline::FontCache::new();
let doc = match build_tiled_document(&BuildParams {
theme_name: &theme_name,
theme_text: &theme_text,
data_files,
markdown: &markdown,
base_dir: base_dir.as_deref(),
width_pt,
sidebar_width_pt,
tile_height_pt,
ppi,
fonts: &fonts,
allow_remote_images,
}) {
Ok(doc) => doc,
Err(e) => {
log::error!("child: build failed: {e:#}");
let _ = resp_tx.send(&Response::Error(format!("{e:#}")));
return;
}
};
let meta = doc.metadata();
if resp_tx.send(&Response::Meta(meta)).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(pngs),
Ok(Err(e)) => Response::Error(format!("render tile {idx}: {e:#}")),
Err(_) => Response::Error(format!("render tile {idx}: panic")),
};
if resp_tx.send(&resp).is_err() {
break;
}
}
Request::Shutdown => break,
}
}
},
)?;
Ok((TileRenderer { tx, rx }, child))
}
pub fn fork_dump(
params: &BuildParams<'_>,
sandbox_read_base: Option<&Path>,
no_sandbox: bool,
) -> Result<ChildProcess> {
use crate::pipeline::build_and_dump;
let theme_name = params.theme_name.to_string();
let theme_text = params.theme_text.to_string();
let data_files = params.data_files;
let markdown = params.markdown.to_string();
let base_dir = params.base_dir.map(|p| p.to_path_buf());
let width_pt = params.width_pt;
let sidebar_width_pt = params.sidebar_width_pt;
let tile_height_pt = params.tile_height_pt;
let ppi = params.ppi;
let allow_remote_images = params.allow_remote_images;
let sandbox_read_base = sandbox_read_base.map(|p| p.to_path_buf());
let (_, _, child) = process::fork_with_channels::<(), (), _>(move |_, _| {
if !no_sandbox
&& let Err(e) = sandbox::enforce_read_only_sandbox(sandbox_read_base.as_deref())
{
log::warn!("child: sandbox failed: {e:#}");
}
let fonts = crate::pipeline::FontCache::new();
if let Err(e) = build_and_dump(&BuildParams {
theme_name: &theme_name,
theme_text: &theme_text,
data_files,
markdown: &markdown,
base_dir: base_dir.as_deref(),
width_pt,
sidebar_width_pt,
tile_height_pt,
ppi,
fonts: &fonts,
allow_remote_images,
}) {
eprintln!("{e:#}");
unsafe { nix::libc::_exit(1) }
}
})?;
Ok(child)
}
pub fn spawn_renderer(
params: &BuildParams<'_>,
sandbox_read_base: Option<&Path>,
no_sandbox: bool,
) -> Result<(DocumentMeta, TileRenderer, ChildProcess)> {
let (mut renderer, child) = fork_renderer(params, sandbox_read_base, no_sandbox)?;
let meta = renderer.wait_for_meta()?;
Ok((meta, renderer, 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));
}
#[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"),
}
}
}