Skip to main content

retex_engine/
snapshot.rs

1//! Resuming a run from a snapshot of the process.
2//!
3//! The pass driver (in `retex-cli`) forks the process when LaTeX begins
4//! the document: everything before is the same in every pass, so later
5//! passes resume from that point instead of running the preamble again.
6//! The engine's part is the hook that marks the point
7//! ([`Engine::snapshot_hook`]) and the output files: they are flushed
8//! before the fork, and a resumed process reopens them, cut back to their
9//! length at the snapshot.
10//!
11//! With `--watch`, the driver also takes a checkpoint once the document
12//! has begun and after each page ([`Engine::page_hook`]): after an edit,
13//! the build resumes from the last checkpoint before it.
14
15use std::io::{Seek, SeekFrom};
16use std::path::PathBuf;
17use std::rc::Rc;
18
19use crate::Engine;
20use crate::input::{Level, LineSource};
21
22/// The snapshot hook (see [`Engine::snapshot_hook`]).
23pub type Hook = Box<dyn FnOnce(&mut Engine)>;
24
25/// New contents of the input files being read (see
26/// [`Engine::reloaded_inputs`]).
27pub struct Reload(Vec<(usize, Rc<[u8]>)>);
28
29/// An output file open when the snapshot was taken.
30#[derive(Clone, Debug)]
31pub struct OpenOutput {
32    /// The file.
33    pub path: PathBuf,
34    /// Its length at the snapshot.
35    pub len: u64,
36    /// The `\write` stream, or `None` for the log file.
37    stream: Option<u32>,
38}
39
40impl Engine {
41    /// Flushes all output and lists the output files that are open.
42    pub fn snapshot_outputs(&mut self) -> Vec<OpenOutput> {
43        self.printer.flush();
44        if let Some(b) = self.backend.as_mut() {
45            // A failure shows when the file is reopened.
46            let _ = b.suspend();
47        }
48        let mut out = Vec::new();
49        if self.printer.log_opened()
50            && let Some(path) = &self.files.log_name
51        {
52            out.push(OpenOutput {
53                path: path.clone(),
54                len: std::fs::metadata(path).map_or(0, |m| m.len()),
55                stream: None,
56            });
57        }
58        for n in self.printer.open_write_streams() {
59            if let Some(Some(path)) = self.write_paths.get(n as usize) {
60                out.push(OpenOutput {
61                    path: path.clone(),
62                    len: std::fs::metadata(path).map_or(0, |m| m.len()),
63                    stream: Some(n),
64                });
65            }
66        }
67        out
68    }
69
70    /// Whether the output files listed by
71    /// [`snapshot_outputs`](Self::snapshot_outputs) are still there, at
72    /// least as long as at the snapshot, so that a pass can resume from
73    /// it: a failed build may have removed one (the PDF file of a build
74    /// that shipped no page).
75    pub fn outputs_intact(&self, outputs: &[OpenOutput]) -> bool {
76        self.backend.as_ref().is_none_or(|b| b.resumable())
77            && outputs
78                .iter()
79                .all(|o| std::fs::metadata(&o.path).is_ok_and(|m| m.len() >= o.len))
80    }
81
82    /// In a process resumed from a snapshot: reopens the output files
83    /// listed by [`snapshot_outputs`](Self::snapshot_outputs), truncated to
84    /// their length at the snapshot.
85    pub fn resume_outputs(&mut self, outputs: &[OpenOutput]) -> std::io::Result<()> {
86        if let Some(b) = self.backend.as_mut() {
87            b.resume()?;
88        }
89        for o in outputs {
90            let mut f = std::fs::OpenOptions::new()
91                .write(true)
92                .open(&o.path)
93                .map_err(|e| std::io::Error::new(e.kind(), format!("{}: {e}", o.path.display())))?;
94            f.set_len(o.len)?;
95            f.seek(SeekFrom::End(0))?;
96            match o.stream {
97                None => self
98                    .printer
99                    .set_log(Box::new(std::io::BufWriter::with_capacity(1 << 16, f))),
100                Some(n) => {
101                    let w = crate::files::RecordingWriter::resume(
102                        f,
103                        o.path.clone(),
104                        self.written_sink.clone(),
105                    );
106                    self.printer.open_write(n, Box::new(w));
107                }
108            }
109        }
110        Ok(())
111    }
112
113    /// The input files being read, from the outermost.
114    pub fn open_input_files(&self) -> Vec<PathBuf> {
115        self.file_levels().map(|(_, path, _, _)| path).collect()
116    }
117
118    /// The text levels reading files: stack index, path, content and
119    /// number of bytes read.
120    fn file_levels(&self) -> impl Iterator<Item = (usize, PathBuf, &[u8], usize)> {
121        self.input
122            .stack
123            .iter()
124            .enumerate()
125            .filter_map(|(i, l)| match l {
126                Level::Text(t) => match &t.source {
127                    LineSource::File { data, pos } => {
128                        let path = PathBuf::from(String::from_utf8_lossy(&t.name).into_owned());
129                        Some((i, path, &data[..], *pos))
130                    }
131                    _ => None,
132                },
133                Level::Tokens(_) => None,
134            })
135    }
136
137    /// Reads the input files being read again, for a process resumed from
138    /// a snapshot after they were edited (`--watch`): the part already read
139    /// must be unchanged; the rest may differ. Returns the first file
140    /// whose read part changed (or that cannot be read) as the error.
141    pub fn reloaded_inputs(&self) -> Result<Reload, PathBuf> {
142        let mut out = Vec::new();
143        for (i, path, data, pos) in self.file_levels() {
144            match std::fs::read(&path) {
145                Ok(new) if new.get(..pos) == data.get(..pos) => out.push((i, Rc::from(new))),
146                _ => return Err(path),
147            }
148        }
149        Ok(Reload(out))
150    }
151
152    /// Continues reading the input files with the contents of `reload`.
153    pub fn reload_inputs(&mut self, reload: Reload) {
154        for (i, new) in reload.0 {
155            if let Some(Level::Text(t)) = self.input.stack.get_mut(i)
156                && let LineSource::File { data, .. } = &mut t.source
157            {
158                *data = new;
159            }
160        }
161    }
162
163    /// Runs the snapshot hook if `cs` is the control sequence it waits for,
164    /// expanded while the document's own file is read: packages expand
165    /// `\document` in the preamble to patch it (auxhook, loaded by zref and
166    /// mdframed, takes its replacement text with `\expandafter`).
167    #[inline]
168    pub(crate) fn check_snapshot(&mut self) {
169        if self.snapshot_cs.is_some_and(|cs| cs == self.cur_cs)
170            && self.file_levels().nth(1).is_none()
171        {
172            self.snapshot_cs = None;
173            if let Some(hook) = self.snapshot_hook.take() {
174                hook(self);
175            }
176            // What `\begin{document}` does (loading fonts, reading the
177            // auxiliary files) can take longer than the preamble: a page
178            // checkpoint follows once it is done, when the file it was read
179            // from goes on to its next line.
180            if self.page_hook.is_some() {
181                self.begun_level = self.file_levels().last().map(|(i, ..)| i);
182            }
183        }
184    }
185
186    /// Before the next line of the current text level is read: runs the
187    /// page hook if the document has just begun.
188    #[inline]
189    pub(crate) fn check_begun(&mut self) {
190        if self
191            .begun_level
192            .is_some_and(|i| i + 1 == self.input.stack.len())
193        {
194            self.begun_level = None;
195            if let Some(f) = self.page_hook {
196                f(self);
197            }
198        }
199    }
200}