1use 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 fn run_cleanup(&self) -> Result<Option<String>>;
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub enum Deletion {
43 File(Arc<Path>),
45 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 pub fn file(path: impl IntoArcPath + AsRef<Path>) -> Result<Self> {
75 Ok(Self::File(std::path::absolute(path)?.into()))
76 }
77
78 pub fn dir(path: impl IntoArcPath + AsRef<Path>) -> Result<Self> {
80 Ok(Self::Dir(std::path::absolute(path)?.into()))
81 }
82
83 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#[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(CleanupItem),
176 Cancel(CleanupItem),
178}
179
180#[derive(Debug)]
181pub struct CleanupDaemon {
182 _child_pid: Pid,
183 writer: NdJsonPipeWriter<CleanupMessage>,
184}
185
186impl CleanupDaemon {
187 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 let r = (|| -> Result<()> {
204 setsid()?;
209
210 let reader = pipe.into_reader();
211 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 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 pub fn start() -> Result<Self> {
269 let daemon = Mutex::new(CleanupDaemon::start()?).into();
270 Ok(Self { daemon })
271 }
272
273 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 fn cleanup_now(self) -> Result<Option<String>> {
323 let did = self.item.run_cleanup()?;
324 self.cancel_cleanup()?;
326 Ok(did)
327 }
328}
329
330pub 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}