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 /// In a process resumed from a snapshot: reopens the output files
71 /// listed by [`snapshot_outputs`](Self::snapshot_outputs), truncated to
72 /// their length at the snapshot.
73 pub fn resume_outputs(&mut self, outputs: &[OpenOutput]) -> std::io::Result<()> {
74 if let Some(b) = self.backend.as_mut() {
75 b.resume()?;
76 }
77 for o in outputs {
78 let mut f = std::fs::OpenOptions::new().write(true).open(&o.path)?;
79 f.set_len(o.len)?;
80 f.seek(SeekFrom::End(0))?;
81 match o.stream {
82 None => self
83 .printer
84 .set_log(Box::new(std::io::BufWriter::with_capacity(1 << 16, f))),
85 Some(n) => {
86 let w = crate::files::RecordingWriter::resume(
87 f,
88 o.path.clone(),
89 self.written_sink.clone(),
90 );
91 self.printer.open_write(n, Box::new(w));
92 }
93 }
94 }
95 Ok(())
96 }
97
98 /// The input files being read, from the outermost.
99 pub fn open_input_files(&self) -> Vec<PathBuf> {
100 self.file_levels().map(|(_, path, _, _)| path).collect()
101 }
102
103 /// The text levels reading files: stack index, path, content and
104 /// number of bytes read.
105 fn file_levels(&self) -> impl Iterator<Item = (usize, PathBuf, &[u8], usize)> {
106 self.input
107 .stack
108 .iter()
109 .enumerate()
110 .filter_map(|(i, l)| match l {
111 Level::Text(t) => match &t.source {
112 LineSource::File { data, pos } => {
113 let path = PathBuf::from(String::from_utf8_lossy(&t.name).into_owned());
114 Some((i, path, &data[..], *pos))
115 }
116 _ => None,
117 },
118 Level::Tokens(_) => None,
119 })
120 }
121
122 /// Reads the input files being read again, for a process resumed from
123 /// a snapshot after they were edited (`--watch`): the part already read
124 /// must be unchanged; the rest may differ. Returns the first file
125 /// whose read part changed (or that cannot be read) as the error.
126 pub fn reloaded_inputs(&self) -> Result<Reload, PathBuf> {
127 let mut out = Vec::new();
128 for (i, path, data, pos) in self.file_levels() {
129 match std::fs::read(&path) {
130 Ok(new) if new.get(..pos) == data.get(..pos) => out.push((i, Rc::from(new))),
131 _ => return Err(path),
132 }
133 }
134 Ok(Reload(out))
135 }
136
137 /// Continues reading the input files with the contents of `reload`.
138 pub fn reload_inputs(&mut self, reload: Reload) {
139 for (i, new) in reload.0 {
140 if let Some(Level::Text(t)) = self.input.stack.get_mut(i)
141 && let LineSource::File { data, .. } = &mut t.source
142 {
143 *data = new;
144 }
145 }
146 }
147
148 /// Runs the snapshot hook if `cs` is the control sequence it waits for,
149 /// expanded while the document's own file is read: packages expand
150 /// `\document` in the preamble to patch it (auxhook, loaded by zref and
151 /// mdframed, takes its replacement text with `\expandafter`).
152 #[inline]
153 pub(crate) fn check_snapshot(&mut self) {
154 if self.snapshot_cs.is_some_and(|cs| cs == self.cur_cs)
155 && self.file_levels().nth(1).is_none()
156 {
157 self.snapshot_cs = None;
158 if let Some(hook) = self.snapshot_hook.take() {
159 hook(self);
160 }
161 // What `\begin{document}` does (loading fonts, reading the
162 // auxiliary files) can take longer than the preamble: a page
163 // checkpoint follows once it is done, when the file it was read
164 // from goes on to its next line.
165 if self.page_hook.is_some() {
166 self.begun_level = self.file_levels().last().map(|(i, ..)| i);
167 }
168 }
169 }
170
171 /// Before the next line of the current text level is read: runs the
172 /// page hook if the document has just begun.
173 #[inline]
174 pub(crate) fn check_begun(&mut self) {
175 if self
176 .begun_level
177 .is_some_and(|i| i + 1 == self.input.stack.len())
178 {
179 self.begun_level = None;
180 if let Some(f) = self.page_hook {
181 f(self);
182 }
183 }
184 }
185}