evobench_tools/utillib/
cleanup_daemon.rs

1//! A daemon process that clean ups open files when the main process
2//! is killed.
3
4use std::{
5    collections::HashSet,
6    fs::{remove_dir_all, remove_file},
7    ops::Deref,
8    path::Path,
9    process::{Command, exit},
10    sync::{Arc, Mutex},
11};
12
13use anyhow::{Result, bail};
14use chj_unix_util::unix::easy_fork;
15use derive_more::From;
16use nix::unistd::{Pid, setsid};
17use serde::{Deserialize, Serialize};
18
19use crate::{
20    debug, info,
21    utillib::{
22        arc::CloneArc,
23        escaped_display::{AsEscapedString, DebugForDisplay},
24        into_arc_path::IntoArcPath,
25        ndjson_pipe::{NdJsonPipe, NdJsonPipeWriter},
26    },
27    warn,
28};
29
30trait RunCleanup {
31    /// If successful, returns a string describing what was done (if
32    /// nothing was to be done then None is returned; if there was an
33    /// error carrying out what was to be done then an error is
34    /// returned)
35    fn run_cleanup(&self) -> Result<Option<String>>;
36}
37
38/// Note: paths need to be absolute (or canonical), or using chdir in
39/// the app after starting the daemon will lead to breakage!  Use the
40/// constructor functions rather than constructing this type directly!
41#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub enum Deletion {
43    /// A single file, deleted via `remove_file`, does not work for dirs
44    File(Arc<Path>),
45    /// A directory, deleted via `remove_dir_all`, also works on
46    /// single files but is more dangerous.
47    Dir(Arc<Path>),
48}
49
50impl Deref for Deletion {
51    type Target = Arc<Path>;
52
53    fn deref(&self) -> &Self::Target {
54        match self {
55            Self::File(path) => path,
56            Self::Dir(path) => path,
57        }
58    }
59}
60
61impl AsRef<Path> for Deletion {
62    fn as_ref(&self) -> &Path {
63        match self {
64            Self::File(path) => path,
65            Self::Dir(path) => path,
66        }
67    }
68}
69
70impl Deletion {
71    /// Construct a single-file deletion action from a path; the path
72    /// is made absolute based on the current cwd if needed, errors
73    /// during that process are reported
74    pub fn file(path: impl IntoArcPath + AsRef<Path>) -> Result<Self> {
75        Ok(Self::File(std::path::absolute(path)?.into()))
76    }
77
78    /// Same as `file` but to delete dir trees
79    pub fn dir(path: impl IntoArcPath + AsRef<Path>) -> Result<Self> {
80        Ok(Self::Dir(std::path::absolute(path)?.into()))
81    }
82
83    /// Note that this could be a path to an executable, something to
84    /// run, not delete!
85    pub fn path(&self) -> &Arc<Path> {
86        match self {
87            Deletion::File(path) => path,
88            Deletion::Dir(path) => path,
89        }
90    }
91}
92
93impl RunCleanup for Deletion {
94    fn run_cleanup(&self) -> Result<Option<String>> {
95        let raise_errors = |r: Result<(), std::io::Error>,
96                            doing_msg: &str,
97                            did_msg: &str,
98                            path: &Path|
99         -> Result<_> {
100            match r {
101                Ok(()) => Ok(Some(format!("{did_msg} {path:?}"))),
102                Err(e) => match e.kind() {
103                    std::io::ErrorKind::NotFound => Ok(None),
104                    _ => {
105                        bail!("{doing_msg} {path:?}: {e:#}");
106                    }
107                },
108            }
109        };
110        match self {
111            Deletion::File(path) => {
112                raise_errors(remove_file(path), "deleting file", "deleted file", path)
113            }
114            Deletion::Dir(path) => raise_errors(
115                remove_dir_all(path),
116                "deleting dir tree",
117                "deleted dir tree",
118                path,
119            ),
120        }
121    }
122}
123
124/// A command to run (with current working directory, but
125/// otherwise unchanged environment from the time of starting the
126/// daemon)
127#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
128pub struct CleanupCommand {
129    pub path: Arc<Path>,
130    pub args: Arc<[String]>,
131    pub cwd: Option<Arc<Path>>,
132}
133
134impl RunCleanup for CleanupCommand {
135    fn run_cleanup(&self) -> Result<Option<String>> {
136        let Self { path, args, cwd } = self;
137        let mut cmd = Command::new(&**path);
138        cmd.args(&**args);
139        if let Some(cwd) = cwd {
140            cmd.current_dir(cwd);
141        }
142        match cmd.status() {
143            Ok(status) => {
144                if status.success() {
145                    Ok(Some(format!("successfully executed command {cmd:?}")))
146                } else {
147                    bail!("error: command {cmd:?} exited with status {status}")
148                }
149            }
150            Err(e) => {
151                bail!("error: could not run command {path:?}: {e:#}")
152            }
153        }
154    }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, From)]
158pub enum CleanupItem {
159    Deletion(Deletion),
160    CleanupCommand(CleanupCommand),
161}
162
163impl RunCleanup for CleanupItem {
164    fn run_cleanup(&self) -> Result<Option<String>> {
165        match self {
166            CleanupItem::Deletion(d) => d.run_cleanup(),
167            CleanupItem::CleanupCommand(c) => c.run_cleanup(),
168        }
169    }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub enum CleanupMessage {
174    /// Add an item for clean up on exit
175    Add(CleanupItem),
176    /// Remove an item from being cleaned up
177    Cancel(CleanupItem),
178}
179
180#[derive(Debug)]
181pub struct CleanupDaemon {
182    _child_pid: Pid,
183    writer: NdJsonPipeWriter<CleanupMessage>,
184}
185
186impl CleanupDaemon {
187    /// Create a daemon that can receive information about open
188    /// files. This forks a separate process into a new unix session
189    /// so that it is not killed. Must be run while there are no
190    /// additional threads, otherwise this panics! When the process
191    /// holding the `CleanupDaemon` struct (the parent) exits (in
192    /// whatever way), the forked daemon process detects the pipe
193    /// filehandles closing, and then deletes all files that haven't
194    /// been cancelled.
195    pub fn start() -> Result<Self> {
196        let pipe = NdJsonPipe::<CleanupMessage>::new()?;
197
198        if let Some(_child_pid) = easy_fork()? {
199            let writer = pipe.into_writer();
200            Ok(Self { _child_pid, writer })
201        } else {
202            // Child
203            let r = (|| -> Result<()> {
204                // Do we have to double fork? Things work without so
205                // far--the parent will get signals, but that may
206                // actually be interesting to register daemon errors.
207
208                setsid()?;
209
210                let reader = pipe.into_reader();
211                // True means it's dir deletion
212                let mut items: HashSet<CleanupItem> = Default::default();
213                for msg in reader {
214                    let msg = msg?;
215                    debug!("got message {msg:?}");
216                    match msg {
217                        CleanupMessage::Add(item) => {
218                            items.insert(item);
219                        }
220                        CleanupMessage::Cancel(path) => {
221                            items.remove(&path);
222                        }
223                    }
224                }
225                for item in items {
226                    match item.run_cleanup() {
227                        Ok(None) => (),
228                        Ok(Some(did)) => {
229                            info!("{did}")
230                        }
231                        Err(e) => {
232                            warn!("{e:#}")
233                        }
234                    }
235                }
236                Ok(())
237            })();
238            match r {
239                Ok(()) => {
240                    debug!("exiting cleanly");
241                    exit(0);
242                }
243                Err(e) => {
244                    warn!("terminating due to error: {e:#}");
245                    exit(1);
246                }
247            }
248        }
249    }
250
251    /// Warning: only send absolute paths, or if you don't, make sure
252    /// that you don't use chdir in the parent after starting the
253    /// daemon!
254    pub fn send(&mut self, message: CleanupMessage) -> Result<()> {
255        self.writer.send(message)
256    }
257}
258
259#[derive(Debug, Clone)]
260pub struct CleanupHandler {
261    daemon: Arc<Mutex<CleanupDaemon>>,
262}
263
264impl CleanupHandler {
265    /// See the warning on `CleanupDaemon::start`, i.e. you must only
266    /// run this while there are no additional threads or this will
267    /// panic!
268    pub fn start() -> Result<Self> {
269        let daemon = Mutex::new(CleanupDaemon::start()?).into();
270        Ok(Self { daemon })
271    }
272
273    /// Note: if you are looking for auto-cleanup you want to use
274    /// `register_temporary_file` or `register_temporary_command`
275    /// instead. (Should this method even be public?)
276    fn register_cleanup<Item: Clone + Into<CleanupItem>>(
277        &self,
278        item: Item,
279    ) -> Result<ItemWithCleanup<Item>> {
280        {
281            let mut daemon = self.daemon.lock().expect("no panics");
282            daemon.send(CleanupMessage::Add(item.clone().into()))?;
283        }
284        Ok(ItemWithCleanup {
285            item,
286            daemon: self.daemon.clone_arc(),
287        })
288    }
289
290    pub fn register_temporary_file(&self, deletion: Deletion) -> Result<TemporaryFile> {
291        Ok(TemporaryFile(Some(self.register_cleanup(deletion)?)))
292    }
293
294    pub fn register_temporary_command(&self, deletion: CleanupCommand) -> Result<TemporaryCommand> {
295        Ok(TemporaryCommand(Some(self.register_cleanup(deletion)?)))
296    }
297}
298
299struct ItemWithCleanup<Item: Into<CleanupItem>> {
300    item: Item,
301    daemon: Arc<Mutex<CleanupDaemon>>,
302}
303
304impl<Item: Into<CleanupItem> + RunCleanup> ItemWithCleanup<Item> {
305    fn cancel_cleanup(self) -> Result<()> {
306        let Self { item, daemon } = self;
307        {
308            let mut daemon = daemon.lock().expect("no panics");
309            daemon.send(CleanupMessage::Cancel(item.into()))?;
310        }
311        Ok(())
312    }
313
314    // /// Does *not* cancel the cleanup! Is this unintuitive, I guess?
315    // /// Use `cancel_cleanup` for when you want to get rid of the
316    // /// cleanup "wrapping".
317    // fn into_inner(self) -> Item {
318    //     self.item
319    // }
320
321    /// Deletes the file (if present) then cancels the cleanup
322    fn cleanup_now(self) -> Result<Option<String>> {
323        let did = self.item.run_cleanup()?;
324        // XX should we ignore errors here? OK for now.
325        self.cancel_cleanup()?;
326        Ok(did)
327    }
328}
329
330// Have to provide Drop *separate* from a type that offers
331// ownership-accepting methods that expect to destructure the type
332// (and its Drop to not be called any more). I.e. layer the
333// concerns. Thus, TemporaryFile.
334
335pub struct TemporaryFile(Option<ItemWithCleanup<Deletion>>);
336
337impl Deref for TemporaryFile {
338    type Target = Arc<Path>;
339
340    fn deref(&self) -> &Self::Target {
341        self.0
342            .as_ref()
343            .expect("only Drop can take it out and then Deref can't be called anymore")
344            .item
345            .deref()
346    }
347}
348
349impl AsRef<Arc<Path>> for TemporaryFile {
350    fn as_ref(&self) -> &Arc<Path> {
351        &**self
352    }
353}
354
355impl AsRef<Path> for TemporaryFile {
356    fn as_ref(&self) -> &Path {
357        &**self
358    }
359}
360
361impl AsEscapedString for TemporaryFile {
362    type ViewableType<'t>
363        = DebugForDisplay<&'t Path>
364    where
365        Self: 't;
366
367    fn as_escaped_string<'s>(&'s self) -> Self::ViewableType<'s> {
368        DebugForDisplay(self.as_ref())
369    }
370}
371
372impl Drop for TemporaryFile {
373    fn drop(&mut self) {
374        if let Some(iwc) = self.0.take() {
375            match iwc.cleanup_now() {
376                Ok(_) => (),
377                Err(e) => {
378                    warn!("TemporaryFile: error in drop: {e:#}");
379                }
380            }
381        }
382    }
383}
384
385pub struct TemporaryCommand(Option<ItemWithCleanup<CleanupCommand>>);
386
387impl Drop for TemporaryCommand {
388    fn drop(&mut self) {
389        if let Some(iwc) = self.0.take() {
390            match iwc.cleanup_now() {
391                Ok(_) => (),
392                Err(e) => {
393                    warn!("TemporaryCommand: error in drop: {e:#}");
394                }
395            }
396        }
397    }
398}