document_graphics/images/
worker.rs1use super::{
3 Frame, Info, Pixels, Region, check,
4 protocol::{self, Operation, Response},
5};
6use crate::process::{RequestError, RequestGuard, kill_and_reap};
7#[cfg(unix)]
8use std::io::Write;
9use std::{fs::File, path::Path, sync::atomic::AtomicBool};
10use std::{
11 io::{self, Read},
12 process::{Child, ChildStdin, ChildStdout, Command, Stdio},
13 time::{Duration, Instant},
14};
15enum Answer {
16 Info(Info),
17 Frame(Frame),
18}
19
20const TIMEOUT: Duration = Duration::from_secs(10);
21const RSS_LIMIT: u64 = 256 * 1024 * 1024;
22pub struct Worker {
23 child: Child,
24 input: ChildStdin,
25 output: ChildStdout,
26 _source: File,
27 failed: bool,
28 info: Option<Info>,
29}
30impl Worker {
31 pub fn spawn(executable: &Path, argument: &str, source: &File) -> Result<Self, String> {
34 if !cfg!(unix) {
35 return Err("Isolated image previews are unavailable on this platform".into());
36 }
37 let mut command = Command::new(executable);
38 command.arg(argument);
39 Self::start(command, source.try_clone().map_err(|e| e.to_string())?)
40 }
41 fn start(mut command: Command, source_file: File) -> Result<Self, String> {
42 let metadata = source_file.metadata().map_err(|e| e.to_string())?;
43 if !metadata.is_file() || metadata.len() > super::MAX_FILE_BYTES {
44 return Err("Image exceeds the 256 MiB file limit or is not a regular file".into());
45 }
46 #[cfg(unix)]
47 {
48 use std::os::{fd::AsRawFd, unix::process::CommandExt};
49 let source = source_file.as_raw_fd();
50 unsafe {
52 command.pre_exec(move || {
53 if libc::dup2(source, 3) < 0 || libc::fcntl(3, libc::F_SETFD, 0) < 0 {
54 return Err(io::Error::last_os_error());
55 }
56 Ok(())
57 });
58 }
59 }
60 let mut child = command
61 .env_clear()
62 .stdin(Stdio::piped())
63 .stdout(Stdio::piped())
64 .stderr(Stdio::null())
65 .spawn()
66 .map_err(|e| e.to_string())?;
67 let input = child.stdin.take().unwrap();
68 let output = child.stdout.take().unwrap();
69 let worker = Self {
70 child,
71 input,
72 output,
73 _source: source_file,
74 failed: false,
75 info: None,
76 };
77 #[cfg(unix)]
78 {
79 use std::os::fd::AsRawFd;
80 let result = unsafe {
82 let fd = worker.output.as_raw_fd();
83 let flags = libc::fcntl(fd, libc::F_GETFL);
84 if flags < 0 {
85 -1
86 } else {
87 libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK)
88 }
89 };
90 if result < 0 {
91 return Err(io::Error::last_os_error().to_string());
92 }
93 }
94 Ok(worker)
95 }
96 pub fn info(&mut self, cancel: &AtomicBool) -> Result<Info, String> {
97 match self.run(Operation::Info, cancel)? {
98 Answer::Info(info) => Ok(info),
99 _ => Err("Unexpected image response".into()),
100 }
101 }
102 pub fn render(&mut self, regions: Vec<Region>, cancel: &AtomicBool) -> Result<Frame, String> {
103 match self.run(Operation::Render(regions), cancel)? {
104 Answer::Frame(frame) => Ok(frame),
105 _ => Err("Unexpected image response".into()),
106 }
107 }
108 fn run(&mut self, operation: Operation, cancel: &AtomicBool) -> Result<Answer, String> {
109 if self.failed {
110 return Err("Image worker stopped; start a new worker".into());
111 }
112 let result = self.request(operation, cancel);
113 if result.is_err() {
114 self.failed = true;
115 kill_and_reap(&mut self.child);
116 }
117 result
118 }
119 fn request(&mut self, operation: Operation, cancel: &AtomicBool) -> Result<Answer, String> {
120 check(cancel)?;
121 if let Operation::Render(regions) = &operation {
122 super::validate_regions(self.info.as_ref().ok_or("Image metadata missing")?, regions)?;
123 }
124 protocol::write(&mut self.input, &operation).map_err(|e| e.to_string())?;
125 let mut reader = Interruptible {
126 worker: self,
127 guard: RequestGuard::new(cancel, Instant::now() + TIMEOUT, RSS_LIMIT),
128 };
129 let response: Response = protocol::read(&mut reader).map_err(|e| e.to_string())?;
130 match response {
131 Response::Info(info) => {
132 if !matches!(operation, Operation::Info) {
133 return Err("Unexpected image metadata".into());
134 }
135 info.validate()?;
136 reader.resources(true).map_err(|e| e.to_string())?;
137 reader.worker.info = Some(info.clone());
138 Ok(Answer::Info(info))
139 }
140 Response::Pixels { regions, delay_ms } => {
141 let Operation::Render(wanted) = operation else {
142 return Err("Unexpected image pixels".into());
143 };
144 if regions != wanted || !(11..=60_000).contains(&delay_ms) {
145 return Err("Invalid image worker response".into());
146 }
147 let mut pixels = Vec::with_capacity(regions.len());
148 for region in regions {
149 let mut rgba = vec![0; region.bytes()];
150 reader.read_exact(&mut rgba).map_err(|e| e.to_string())?;
151 pixels.push(Pixels { region, rgba });
152 }
153 reader.resources(true).map_err(|e| e.to_string())?;
154 Ok(Answer::Frame(Frame { pixels, delay_ms }))
155 }
156 Response::Error(error) => Err(error),
157 }
158 }
159}
160impl Drop for Worker {
161 fn drop(&mut self) {
162 kill_and_reap(&mut self.child);
163 }
164}
165struct Interruptible<'a> {
166 worker: &'a mut Worker,
167 guard: RequestGuard<'a>,
168}
169impl Interruptible<'_> {
170 fn resources(&mut self, force: bool) -> io::Result<()> {
171 self.guard
172 .check(self.worker.child.id(), force)
173 .map_err(|error| {
174 io::Error::other(match error {
175 RequestError::Cancelled => super::CANCELLED,
176 RequestError::TimedOut => {
177 "Image decode exceeded 10 seconds; use Source/Binary or retry"
178 }
179 #[cfg(target_os = "macos")]
180 RequestError::MemoryUnavailable => "Cannot monitor image worker memory",
181 #[cfg(target_os = "macos")]
182 RequestError::MemoryExceeded => {
183 "Image decoder exceeded 256 MiB; overview or Binary remains available"
184 }
185 })
186 })
187 }
188}
189impl Read for Interruptible<'_> {
190 fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
191 loop {
192 self.resources(false)?;
193 match self.worker.output.read(bytes) {
194 Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
195 #[cfg(unix)]
196 {
197 use std::os::fd::AsRawFd;
198 let mut poll = libc::pollfd {
199 fd: self.worker.output.as_raw_fd(),
200 events: libc::POLLIN,
201 revents: 0,
202 };
203 if unsafe { libc::poll(&mut poll, 1, 10) } < 0 {
205 let e = io::Error::last_os_error();
206 if e.kind() != io::ErrorKind::Interrupted {
207 return Err(e);
208 }
209 }
210 }
211 #[cfg(not(unix))]
212 std::thread::sleep(Duration::from_millis(10));
213 }
214 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
215 result => return result,
216 }
217 }
218 }
219}
220pub fn worker_main() -> Result<(), String> {
221 #[cfg(unix)]
222 {
223 #[cfg(target_os = "macos")]
224 restrict_worker()?;
225 unsafe {
227 libc::setrlimit(
228 libc::RLIMIT_CORE,
229 &libc::rlimit {
230 rlim_cur: 0,
231 rlim_max: 0,
232 },
233 );
234 #[cfg(not(target_os = "macos"))]
235 libc::setrlimit(
236 libc::RLIMIT_AS,
237 &libc::rlimit {
238 rlim_cur: 512 * 1024 * 1024,
239 rlim_max: 512 * 1024 * 1024,
240 },
241 );
242 libc::alarm(12);
243 }
244 let document = super::backend::Decoder::open(std::path::Path::new("/dev/fd/3"));
245 unsafe {
246 libc::alarm(0);
247 }
248 let mut input = io::stdin().lock();
249 let mut output = io::stdout().lock();
250 loop {
251 let operation: Operation = match protocol::read(&mut input) {
252 Ok(op) => op,
253 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(()),
254 Err(e) => return Err(e.to_string()),
255 };
256 unsafe {
257 libc::alarm(12);
258 }
259 let result = (|| {
260 let decoder = document.as_ref().map_err(Clone::clone)?;
261 match operation {
262 Operation::Info => Ok(Answer::Info(decoder.info())),
263 Operation::Render(regions) => decoder.render(®ions).map(Answer::Frame),
264 }
265 })();
266 unsafe {
267 libc::alarm(0);
268 }
269 match result {
270 Ok(Answer::Info(info)) => protocol::write(&mut output, &Response::Info(info)),
271 Ok(Answer::Frame(frame)) => (|| {
272 protocol::write(
273 &mut output,
274 &Response::Pixels {
275 regions: frame.pixels.iter().map(|p| p.region).collect(),
276 delay_ms: frame.delay_ms,
277 },
278 )?;
279 for pixels in frame.pixels {
280 output.write_all(&pixels.rgba)?;
281 }
282 output.flush()
283 })(),
284 Err(error) => protocol::write(
285 &mut output,
286 &Response::Error(error.chars().take(2048).collect()),
287 ),
288 }
289 .map_err(|e| e.to_string())?;
290 }
291 }
292 #[cfg(not(unix))]
293 Err("Isolated image previews are unavailable on this platform".into())
294}
295#[cfg(target_os = "macos")]
296fn restrict_worker() -> Result<(), String> {
297 unsafe extern "C" {
298 fn sandbox_init(
299 profile: *const std::ffi::c_char,
300 flags: u64,
301 error: *mut *mut std::ffi::c_char,
302 ) -> i32;
303 fn sandbox_free_error(error: *mut std::ffi::c_char);
304 }
305 let profile = c"(version 1)(allow default)(deny network*)(deny file-write*)(deny process-exec)(deny process-fork)";
308 let mut error = std::ptr::null_mut();
309 let result = unsafe { sandbox_init(profile.as_ptr(), 0, &mut error) };
311 if result != 0 {
312 let message = if error.is_null() {
313 "Cannot restrict image worker".into()
314 } else {
315 unsafe { std::ffi::CStr::from_ptr(error) }
316 .to_string_lossy()
317 .into_owned()
318 };
319 unsafe {
320 sandbox_free_error(error);
321 }
322 return Err(message);
323 }
324 unsafe {
326 libc::setpriority(libc::PRIO_PROCESS, 0, 10);
327 libc::setrlimit(
328 libc::RLIMIT_CORE,
329 &libc::rlimit {
330 rlim_cur: 0,
331 rlim_max: 0,
332 },
333 );
334 }
335 Ok(())
336}
337
338#[cfg(all(test, unix))]
339mod tests {
340 use super::*;
341 use std::sync::{
342 Arc,
343 atomic::{AtomicBool, Ordering},
344 };
345 #[test]
346 fn cancellation_kills_and_reaps_a_stuck_decoder() {
347 let file = tempfile::NamedTempFile::new().unwrap();
348 let cancel = Arc::new(AtomicBool::new(false));
349 let source_file = file.reopen().unwrap();
350 let mut command = Command::new("/bin/sleep");
351 command.arg("60");
352 let mut worker = Worker::start(command, source_file).unwrap();
353 let pid = worker.child.id();
354 let flag = cancel.clone();
355 let trigger = std::thread::spawn(move || {
356 std::thread::sleep(Duration::from_millis(30));
357 flag.store(true, Ordering::Relaxed);
358 });
359 let start = Instant::now();
360 assert_eq!(
361 worker.run(Operation::Info, &cancel).err().unwrap(),
362 super::super::CANCELLED
363 );
364 assert!(worker.failed);
365 trigger.join().unwrap();
366 assert!(start.elapsed() < Duration::from_secs(1));
367 assert_eq!(unsafe { libc::kill(pid as i32, 0) }, -1);
369 }
370}