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
11use std::io::{Seek, SeekFrom};
12use std::path::PathBuf;
13use std::rc::Rc;
14
15use crate::Engine;
16use crate::input::{Level, LineSource};
17
18/// The snapshot hook (see [`Engine::snapshot_hook`]).
19pub type Hook = Box<dyn FnOnce(&mut Engine)>;
20
21/// New contents of the input files being read (see
22/// [`Engine::reloaded_inputs`]).
23pub struct Reload(Vec<(usize, Rc<[u8]>)>);
24
25/// An output file open when the snapshot was taken.
26#[derive(Clone, Debug)]
27pub struct OpenOutput {
28    /// The file.
29    pub path: PathBuf,
30    /// Its length at the snapshot.
31    pub len: u64,
32    /// The `\write` stream, or `None` for the log file.
33    stream: Option<u32>,
34}
35
36impl Engine {
37    /// Flushes all output and lists the output files that are open.
38    pub fn snapshot_outputs(&mut self) -> Vec<OpenOutput> {
39        self.printer.flush();
40        if let Some(b) = self.backend.as_mut() {
41            // A failure shows when the file is reopened.
42            let _ = b.suspend();
43        }
44        let mut out = Vec::new();
45        if self.printer.log_opened()
46            && let Some(path) = &self.files.log_name
47        {
48            out.push(OpenOutput {
49                path: path.clone(),
50                len: std::fs::metadata(path).map_or(0, |m| m.len()),
51                stream: None,
52            });
53        }
54        for n in self.printer.open_write_streams() {
55            if let Some(Some(path)) = self.write_paths.get(n as usize) {
56                out.push(OpenOutput {
57                    path: path.clone(),
58                    len: std::fs::metadata(path).map_or(0, |m| m.len()),
59                    stream: Some(n),
60                });
61            }
62        }
63        out
64    }
65
66    /// In a process resumed from a snapshot: reopens the output files
67    /// listed by [`snapshot_outputs`](Self::snapshot_outputs), truncated to
68    /// their length at the snapshot.
69    pub fn resume_outputs(&mut self, outputs: &[OpenOutput]) -> std::io::Result<()> {
70        if let Some(b) = self.backend.as_mut() {
71            b.resume()?;
72        }
73        for o in outputs {
74            let mut f = std::fs::OpenOptions::new().write(true).open(&o.path)?;
75            f.set_len(o.len)?;
76            f.seek(SeekFrom::End(0))?;
77            match o.stream {
78                None => self
79                    .printer
80                    .set_log(Box::new(std::io::BufWriter::with_capacity(1 << 16, f))),
81                Some(n) => {
82                    let w = crate::files::RecordingWriter::resume(
83                        f,
84                        o.path.clone(),
85                        self.written_sink.clone(),
86                    );
87                    self.printer.open_write(n, Box::new(w));
88                }
89            }
90        }
91        Ok(())
92    }
93
94    /// The input files being read, from the outermost.
95    pub fn open_input_files(&self) -> Vec<PathBuf> {
96        self.file_levels().map(|(_, path, _, _)| path).collect()
97    }
98
99    /// The text levels reading files: stack index, path, content and
100    /// number of bytes read.
101    fn file_levels(&self) -> impl Iterator<Item = (usize, PathBuf, &[u8], usize)> {
102        self.input
103            .stack
104            .iter()
105            .enumerate()
106            .filter_map(|(i, l)| match l {
107                Level::Text(t) => match &t.source {
108                    LineSource::File { data, pos } => {
109                        let path = PathBuf::from(String::from_utf8_lossy(&t.name).into_owned());
110                        Some((i, path, &data[..], *pos))
111                    }
112                    _ => None,
113                },
114                Level::Tokens(_) => None,
115            })
116    }
117
118    /// Reads the input files being read again, for a process resumed from
119    /// a snapshot after they were edited (`--watch`): the part already read
120    /// must be unchanged; the rest may differ. Returns the first file
121    /// whose read part changed (or that cannot be read) as the error.
122    pub fn reloaded_inputs(&self) -> Result<Reload, PathBuf> {
123        let mut out = Vec::new();
124        for (i, path, data, pos) in self.file_levels() {
125            match std::fs::read(&path) {
126                Ok(new) if new.get(..pos) == data.get(..pos) => out.push((i, Rc::from(new))),
127                _ => return Err(path),
128            }
129        }
130        Ok(Reload(out))
131    }
132
133    /// Continues reading the input files with the contents of `reload`.
134    pub fn reload_inputs(&mut self, reload: Reload) {
135        for (i, new) in reload.0 {
136            if let Some(Level::Text(t)) = self.input.stack.get_mut(i)
137                && let LineSource::File { data, .. } = &mut t.source
138            {
139                *data = new;
140            }
141        }
142    }
143
144    /// Runs the snapshot hook if `cs` is the control sequence it waits for.
145    #[inline]
146    pub(crate) fn check_snapshot(&mut self) {
147        if self.snapshot_cs.is_some_and(|cs| cs == self.cur_cs) {
148            self.snapshot_cs = None;
149            if let Some(hook) = self.snapshot_hook.take() {
150                hook(self);
151            }
152        }
153    }
154}