evobench_tools/run/
bench_tmp_dir.rs

1use std::{
2    collections::{HashMap, hash_map::Entry},
3    env::temp_dir,
4    fs::File,
5    io::Write,
6    mem::swap,
7    ops::Deref,
8    os::unix::fs::MetadataExt,
9    path::{Path, PathBuf},
10    sync::{Arc, Mutex},
11    thread::JoinHandle,
12    time::Duration,
13};
14
15use anyhow::{Result, anyhow, bail};
16use cj_path_util::path_util::AppendToPath;
17use nix::unistd::{getpid, getuid};
18use rand::Rng;
19
20use crate::{
21    ctx, info,
22    utillib::{into_arc_path::IntoArcPath, linux_mounts::MountPoints, user::get_username},
23    warn,
24};
25
26/// The path to a temporary directory, and [on Linux (because of
27/// systems using systemd--Debian from trixie onwards will delete
28/// it),] a thread that keeps updating its mtime to prevent
29/// deletion. Implements `AsRef<Path>` and `Deref<Target = Path>`.
30#[derive(Debug, PartialEq, Eq)]
31pub struct BenchTmpDir {
32    path: Arc<Path>,
33}
34
35impl AsRef<Path> for BenchTmpDir {
36    fn as_ref(&self) -> &Path {
37        &self.path
38    }
39}
40
41impl Deref for BenchTmpDir {
42    type Target = Path;
43
44    fn deref(&self) -> &Self::Target {
45        &self.path
46    }
47}
48
49fn _start_daemon(path: Arc<Path>, is_tmpfs: bool) -> Result<JoinHandle<()>, std::io::Error> {
50    let sleep_time = Duration::from_millis(if is_tmpfs { 100 } else { 10000 });
51
52    let th = std::thread::Builder::new().name("tmp-keep-alive".into());
53    th.spawn(move || -> () {
54        let pid = getpid();
55
56        let mut file_path_0 = path.append(format!(".{pid}.tmp-keep-alive-dir-0"));
57        {
58            if let Ok(mut f) = File::create(&file_path_0) {
59                _ = f.write_all(
60                    "This file stays to ensure there is always a file--or now replaced\n"
61                        .as_bytes(),
62                );
63            } else {
64                info!("could not create touch file {file_path_0:?}");
65            }
66        }
67        let mut file_path = path.append(format!(".{pid}.tmp-keep-alive-dir"));
68        _ = File::create(&file_path);
69        let mut rnd = rand::thread_rng();
70        while Arc::strong_count(&path) > 1 {
71            if rnd.gen_range(0..2) == 0 {
72                swap(&mut file_path_0, &mut file_path);
73                _ = std::fs::remove_file(&file_path);
74            }
75            match File::create(&file_path) {
76                Ok(mut f) => {
77                    for _ in 0..20 {
78                        let n = rnd.gen_range(0..10000000);
79                        use std::io::Write;
80                        if let Err(e) = write!(&mut f, "{n}\n") {
81                            info!("could not write to touch file {file_path:?}: {e:#}");
82                            break;
83                        }
84
85                        if !path.exists() {
86                            // COPY-PASTE from below
87                            match std::fs::create_dir(&path) {
88                                Ok(_) => {
89                                    warn!(
90                                        "recreated directory {path:?}, worst of all \
91                                         total hacks with race condition"
92                                    );
93                                }
94                                Err(e) => {
95                                    warn!("could not even recreate directory {path:?}: {e:#}");
96                                    break;
97                                }
98                            }
99                        }
100
101                        std::thread::sleep(sleep_time);
102                    }
103                }
104                Err(e) => {
105                    warn!("could not create touch file {file_path:?}: {e:#}");
106                    match std::fs::create_dir(&path) {
107                        Ok(_) => {
108                            warn!(
109                                "recreated directory {path:?}, worst of all \
110                                 total hacks with race condition"
111                            );
112                        }
113                        Err(e) => {
114                            warn!("could not even recreate directory {path:?}: {e:#}");
115                            break;
116                        }
117                    }
118                }
119            }
120            std::thread::sleep(sleep_time);
121        }
122        // Remove ourselves, right?
123        let mut daemons_guard = DAEMONS.lock().expect("no panics in this scope");
124        let daemons = daemons_guard
125            .as_mut()
126            .expect("already has hashmap since that happens before this thread is started");
127        daemons.remove(&path);
128    })
129}
130
131static DAEMONS: Mutex<Option<HashMap<Arc<Path>, Result<JoinHandle<()>, std::io::Error>>>> =
132    Mutex::new(None);
133
134fn start_daemon(path: Arc<Path>, is_tmpfs: bool) -> Result<()> {
135    let mut daemons = DAEMONS.lock().expect("no panics in this scope");
136    let m = daemons.get_or_insert_with(|| HashMap::new());
137    let r = match m.entry(path.clone()) {
138        Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
139        Entry::Vacant(vacant_entry) => vacant_entry.insert(_start_daemon(path, is_tmpfs)),
140    };
141    r.as_ref()
142        .map(|_| ())
143        .map_err(|e| anyhow!("start_daemon: {e:#}"))
144}
145
146/// Try to find the best place for putting the evobench.log, while
147/// avoiding tmp file cleaners like systemd and staying portable. Also
148/// returns if the path is pointing to a tmpfs.
149pub fn get_fast_and_large_temp_dir_base() -> Result<(PathBuf, bool)> {
150    // XX use src/installation/binaries_repo.rs from xmlhub-indexer
151    // instead once that's separated?
152    match std::env::consts::OS {
153        "linux" => {
154            let mount_points = MountPoints::read()?;
155
156            if let Some(tmp) = mount_points.get_by_path("/tmp") {
157                if tmp.is_tmpfs() {
158                    let tmp_metadata = tmp.path_metadata()?;
159                    if let Some(dev_shm) = mount_points.get_by_path("/dev/shm") {
160                        let dev_shm_metadata = dev_shm.path_metadata()?;
161                        if tmp_metadata.ino() == dev_shm_metadata.ino() {
162                            return Ok((tmp.path_buf(), true));
163                        }
164                    }
165                    // XX todo check if large enough
166                    return Ok((tmp.path_buf(), true));
167                } else {
168                    if let Some(dev_shm) = mount_points.get_by_path("/dev/shm") {
169                        // XX todo check Debian release? oldstable is OK, stable not.
170                        return Ok((dev_shm.path_buf(), dev_shm.is_tmpfs()));
171                    }
172                    // XX ?
173                    return Ok((tmp.path_buf(), false));
174                }
175            } else {
176                if let Some(dev_shm) = mount_points.get_by_path("/dev/shm") {
177                    // XX todo check Debian release? oldstable is OK, stable not.
178                    return Ok((dev_shm.path_buf(), dev_shm.is_tmpfs()));
179                }
180                return Ok((temp_dir(), false));
181            }
182        }
183        _ => Ok((temp_dir(), false)),
184    }
185}
186
187/// Returns the path to a temporary directory, creating it if
188/// necessary and checking ownership if it already exists. The
189/// directory is not unique for all processes, but shared for all
190/// evobench instances--which is OK both because we only do 1 run
191/// at the same time (and take a lock to ensure that), but also
192/// because we're now currently actually also adding the pid to the
193/// file paths inside. It is wrapped since it comes with a daemon that
194/// keeps updating the directory mtime to prevent deletion by tmp
195/// cleaners.
196pub fn bench_tmp_dir() -> Result<BenchTmpDir> {
197    let (base, is_tmpfs) = get_fast_and_large_temp_dir_base()?;
198    let user = get_username()?;
199    let path = base.append(&user).into_arc_path();
200
201    match std::fs::create_dir(&path) {
202        Ok(()) => {
203            start_daemon(path.clone(), is_tmpfs)?;
204            Ok(BenchTmpDir { path })
205        }
206        Err(e) => match e.kind() {
207            std::io::ErrorKind::AlreadyExists => {
208                let m = std::fs::metadata(&path)?;
209                let dir_uid = m.uid();
210                let uid: u32 = getuid().into();
211                if dir_uid == uid {
212                    start_daemon(path.clone(), is_tmpfs)?;
213                    Ok(BenchTmpDir { path })
214                } else {
215                    bail!(
216                        "bench_tmp_dir: directory {path:?} should be owned by \
217                         the user {user:?} which is set in the USER env var, \
218                         but the uid owning that directory is {dir_uid} whereas \
219                         the current process is running as {uid}"
220                    )
221                }
222            }
223            _ => Err(e).map_err(ctx!("create_dir {path:?}")),
224        },
225    }
226}