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