Skip to main content

daemonbit_rundir/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) $year Kamil Becmer
3
4//noinspection RsCompileErrorMacro
5//noinspection RsCompileErrorMacro
6#[cfg(all(not(unix), not(test)))]
7mod api {
8    compile_error!("unsupported platform");
9}
10
11#[cfg(all(unix, not(test)))]
12mod api {
13    use std::{
14        borrow::Cow,
15        ffi::OsStr,
16        io,
17        path::{Path, PathBuf},
18        sync::atomic::AtomicPtr,
19    };
20
21    use daemonbit_core::{DaemonScope, Global, Local, ScopeKey};
22
23    use super::Shared;
24
25    static GLOBAL_RUNDIR: AtomicPtr<Shared<Global>> = AtomicPtr::new(std::ptr::null_mut());
26    static LOCAL_RUNDIR: AtomicPtr<Shared<Local>> = AtomicPtr::new(std::ptr::null_mut());
27
28    pub fn atomic<'a, T: DaemonScope>() -> &'a AtomicPtr<Shared<T>> {
29        let ptr = match T::key() {
30            ScopeKey::Global => &raw const GLOBAL_RUNDIR as *const AtomicPtr<Shared<T>>,
31            ScopeKey::Local => &raw const LOCAL_RUNDIR as *const AtomicPtr<Shared<T>>,
32        };
33        unsafe { &*ptr }
34    }
35    pub fn daemon_name() -> Cow<'static, OsStr> {
36        Cow::Borrowed(OsStr::new(daemonbit_core::daemon_name()))
37    }
38    pub fn euid() -> u32 {
39        nix::unistd::Uid::effective().as_raw()
40    }
41    pub fn run_dir() -> Option<&'static Path> {
42        ["/run", "/var/run"]
43            .into_iter()
44            .map(Path::new)
45            .find(|p| p.is_dir())
46    }
47    pub fn xdg_runtime_dir() -> Option<PathBuf> {
48        std::env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from)
49    }
50    pub fn temp_dir() -> PathBuf {
51        std::env::temp_dir()
52    }
53    pub fn is_dir<P: AsRef<Path>>(path: P) -> bool {
54        path.as_ref().is_dir()
55    }
56    pub fn create_dir_all<P: AsRef<Path>>(path: P) -> io::Result<()> {
57        std::fs::create_dir_all(path)
58    }
59    pub fn remove_dir_all<P: AsRef<Path>>(path: P) -> io::Result<()> {
60        std::fs::remove_dir_all(path)
61    }
62}
63
64//noinspection RsUnresolvedPath,RsInherentImplDifferentCrate,RsInvalidFieldsInStructLiteral,RsNonExistentFieldAccess,RsReferenceIsNotPublic,RsUnresolvedMethod
65#[cfg(test)]
66mod api {
67    use std::{
68        borrow::Cow,
69        cell::RefCell,
70        ffi::OsStr,
71        io,
72        path::{Path, PathBuf},
73        sync::atomic::{AtomicPtr, Ordering::AcqRel},
74    };
75
76    use daemonbit_core::{DaemonScope, Global, Local, ScopeKey};
77
78    use super::Shared;
79
80    thread_local! {
81        static GLOBAL_RUNDIR: RefCell<AtomicPtr<Shared<Global>>> = RefCell::new(AtomicPtr::default());
82        static LOCAL_RUNDIR: RefCell<AtomicPtr<Shared<Local>>> = RefCell::new(AtomicPtr::default());
83        static NAME: RefCell<&'static str> = RefCell::new("");
84        static EUID: RefCell<u32> = RefCell::new(0);
85        static RUN_DIR: RefCell<Option<&'static str>> = RefCell::new(None);
86        static XDG_RUNTIME_DIR: RefCell<Option<&'static str>> = RefCell::new(None);
87    }
88    pub fn mock(
89        name: &'static str,
90        euid: u32,
91        run_dir: Option<&'static str>,
92        xdg_runtime_dir: Option<&'static str>,
93    ) {
94        reset::<Global>();
95        reset::<Local>();
96        NAME.set(name);
97        EUID.set(euid);
98        RUN_DIR.set(run_dir);
99        XDG_RUNTIME_DIR.set(xdg_runtime_dir);
100    }
101    #[allow(clippy::unnecessary_cast)]
102    pub fn atomic<'a, T: DaemonScope>() -> &'a AtomicPtr<Shared<T>> {
103        let ptr = match T::key() {
104            ScopeKey::Global => GLOBAL_RUNDIR.with_borrow(|v: &AtomicPtr<Shared<Global>>| {
105                &raw const *v as *const AtomicPtr<Shared<T>>
106            }),
107            ScopeKey::Local => LOCAL_RUNDIR.with_borrow(|v: &AtomicPtr<Shared<Local>>| {
108                &raw const *v as *const AtomicPtr<Shared<T>>
109            }),
110        };
111        unsafe { &*ptr }
112    }
113    fn reset<T: DaemonScope>() {
114        let _ = Shared::try_from_raw(atomic::<T>().swap(std::ptr::null_mut(), AcqRel));
115    }
116    pub fn daemon_name() -> Cow<'static, OsStr> {
117        let name = NAME.with(|v| *v.borrow());
118        Cow::Borrowed(OsStr::new(name))
119    }
120    pub fn euid() -> u32 {
121        EUID.with(|v| *v.borrow())
122    }
123    pub fn run_dir() -> Option<&'static Path> {
124        RUN_DIR.with_borrow(|v| *v).map(Path::new)
125    }
126    pub fn xdg_runtime_dir() -> Option<PathBuf> {
127        XDG_RUNTIME_DIR.with_borrow(|v| *v).map(PathBuf::from)
128    }
129    pub fn temp_dir() -> PathBuf {
130        PathBuf::from("/tmp")
131    }
132    pub fn is_dir<P: AsRef<Path>>(_: P) -> bool {
133        true
134    }
135    pub fn create_dir_all<P: AsRef<Path>>(_: P) -> io::Result<()> {
136        Ok(())
137    }
138    pub fn remove_dir_all<P: AsRef<Path>>(_: P) -> io::Result<()> {
139        Ok(())
140    }
141}
142
143use std::{
144    borrow::{Borrow, Cow},
145    cell::{OnceCell, UnsafeCell},
146    ffi::{OsStr, OsString},
147    io,
148    marker::PhantomData,
149    ops::Deref,
150    path::{Path, PathBuf},
151    sync::{
152        Arc, Weak,
153        atomic::{
154            AtomicPtr,
155            Ordering::{AcqRel, Acquire},
156        },
157    },
158};
159
160use daemonbit_core::{DaemonScope, Global, Local, ScopeKey};
161use tracing::warn;
162
163pub struct RuntimeDirectory<T> {
164    shared: Arc<Shared<T>>,
165}
166impl<T: DaemonScope> RuntimeDirectory<T> {
167    pub fn as_path(&self) -> &Path {
168        &self.shared.path
169    }
170    pub fn temporary(&self) -> bool {
171        self.shared.state.temporary()
172    }
173    pub fn get() -> Result<Self, (PathBuf, io::Error)> {
174        let shared = OnceCell::new();
175        let atomic: &AtomicPtr<Shared<T>> = api::atomic::<T>();
176        let mut current = atomic.load(Acquire);
177
178        loop {
179            if let Some(current) = Shared::try_from_raw(current) {
180                let _ = shared.into_inner();
181                return Ok(Self { shared: current });
182            }
183            let new = shared
184                .get_or_init(|| {
185                    let shared = Shared::<T>::new(api::daemon_name());
186                    let ptr = shared.to_raw();
187                    (shared, ptr)
188                })
189                .1;
190            if let Err(ptr) = atomic.compare_exchange(current, new, AcqRel, Acquire) {
191                current = ptr;
192                continue;
193            }
194            let shared = shared.into_inner().expect("already initialized").0;
195            return match shared.state.init(&shared.path) {
196                Ok(()) => Ok(Self { shared }),
197                Err(e) => Err((shared.path.clone(), e)),
198            };
199        }
200    }
201}
202impl<T: DaemonScope> AsRef<Path> for RuntimeDirectory<T> {
203    fn as_ref(&self) -> &Path {
204        &self.shared.path
205    }
206}
207impl<T: DaemonScope> Borrow<Path> for RuntimeDirectory<T> {
208    fn borrow(&self) -> &Path {
209        &self.shared.path
210    }
211}
212impl<T: DaemonScope> Deref for RuntimeDirectory<T> {
213    type Target = Path;
214    fn deref(&self) -> &Self::Target {
215        &self.shared.path
216    }
217}
218
219struct Shared<T> {
220    path: PathBuf,
221    state: State,
222    _scope: PhantomData<fn(T) -> T>,
223}
224impl<T: DaemonScope> Shared<T> {
225    fn new<S: AsRef<OsStr>>(name: S) -> Arc<Self> {
226        let scope = T::key();
227        let name = name.as_ref();
228        let (path, state) = match scope.rundir_path() {
229            Some(path) => (path.join(name), State::Persistent),
230            None => {
231                let path = api::temp_dir().join(scope.rundir_name(name));
232                (path, State::Temporary)
233            }
234        };
235        Arc::new(Self {
236            path,
237            state,
238            _scope: PhantomData,
239        })
240    }
241    fn to_raw(self: &Arc<Self>) -> *mut Self {
242        Arc::downgrade(self).into_raw().cast_mut()
243    }
244    fn try_from_raw(ptr: *mut Self) -> Option<Arc<Self>> {
245        if ptr.is_null() {
246            None
247        } else {
248            unsafe { Weak::from_raw(ptr.cast_const()) }
249                .clone()
250                .upgrade()
251        }
252    }
253}
254impl<T> Drop for Shared<T> {
255    fn drop(&mut self) {
256        if self.state.remove_on_drop() {
257            if let Err(error) = api::remove_dir_all(&self.path) {
258                warn!(rundir = %self.path.display(), ?error, "failed to cleanup temporary runtime directory");
259            }
260        }
261    }
262}
263struct State {
264    inner: UnsafeCell<StateInner>,
265}
266#[derive(Copy, Clone, Debug)]
267enum StateInner {
268    Persistent,
269    Temporary { remove_on_drop: bool },
270}
271impl State {
272    #[allow(non_upper_case_globals)]
273    const Persistent: Self = Self {
274        inner: UnsafeCell::new(StateInner::Persistent),
275    };
276    #[allow(non_upper_case_globals)]
277    const Temporary: Self = Self {
278        inner: UnsafeCell::new(StateInner::Temporary {
279            remove_on_drop: false,
280        }),
281    };
282    fn inner(&self) -> &StateInner {
283        unsafe { &*self.inner.get() }
284    }
285    fn inner_mut(&self) -> &mut StateInner {
286        unsafe { &mut *self.inner.get() }
287    }
288    fn init<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
289        if let StateInner::Temporary { remove_on_drop } = self.inner_mut() {
290            api::create_dir_all(path)?;
291            *remove_on_drop = true;
292        }
293        Ok(())
294    }
295    fn temporary(&self) -> bool {
296        matches!(self.inner(), StateInner::Temporary { .. })
297    }
298    fn remove_on_drop(&self) -> bool {
299        match unsafe { *self.inner.get() } {
300            StateInner::Temporary { remove_on_drop } => remove_on_drop,
301            _ => false,
302        }
303    }
304}
305
306trait ScopeExt {
307    fn rundir_path(&self) -> Option<Cow<'static, Path>>;
308    fn rundir_name<'a>(&self, name: &'a OsStr) -> Cow<'a, Path>;
309}
310impl ScopeExt for ScopeKey {
311    fn rundir_path(&self) -> Option<Cow<'static, Path>> {
312        match *self {
313            Self::Global => api::run_dir().map(Cow::Borrowed),
314            Self::Local => Some(Cow::Owned(match api::xdg_runtime_dir() {
315                Some(path) if api::is_dir(&path) => path,
316                _ => api::run_dir()?.join(format!("user/{}", api::euid())),
317            })),
318        }
319    }
320    fn rundir_name<'a>(&self, name: &'a OsStr) -> Cow<'a, Path> {
321        match *self {
322            Self::Global => Cow::Borrowed(Path::new(name)),
323            Self::Local => {
324                let uid = api::euid().to_string();
325                let mut path = OsString::with_capacity(name.len() + 1 + uid.len());
326                path.push(name);
327                path.push("-");
328                path.push(uid);
329                Cow::Owned(PathBuf::from(path))
330            }
331        }
332    }
333}
334
335pub enum ScopedRuntimeDirectory {
336    Global(RuntimeDirectory<Global>),
337    Local(RuntimeDirectory<Local>),
338}
339impl<T: DaemonScope> From<RuntimeDirectory<T>> for ScopedRuntimeDirectory {
340    fn from(rundir: RuntimeDirectory<T>) -> Self {
341        match T::key() {
342            ScopeKey::Global => {
343                ScopedRuntimeDirectory::Global(unsafe { std::mem::transmute(rundir) })
344            }
345            ScopeKey::Local => {
346                ScopedRuntimeDirectory::Local(unsafe { std::mem::transmute(rundir) })
347            }
348        }
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use claims::assert_ok;
355    use daemonbit_core::{Global, Local};
356
357    use super::*;
358
359    const DAEMON_NAME: &str = "daemon";
360    const USER: u32 = 1000;
361
362    #[test]
363    fn persistent_local_xdg_present() {
364        api::mock(DAEMON_NAME, USER, Some("/run"), Some("/home/user/.run"));
365
366        let rundir = assert_ok!(RuntimeDirectory::<Local>::get());
367        assert_eq!(rundir.as_path(), Path::new("/home/user/.run/daemon"));
368        assert!(!rundir.temporary());
369    }
370
371    #[test]
372    fn persistent_local_xdg_missing() {
373        api::mock(DAEMON_NAME, USER, Some("/run"), None);
374
375        let rundir = assert_ok!(RuntimeDirectory::<Local>::get());
376        assert_eq!(rundir.as_path(), Path::new("/run/user/1000/daemon"));
377        assert!(!rundir.temporary());
378    }
379
380    #[test]
381    fn temporary_local() {
382        api::mock(DAEMON_NAME, USER, None, None);
383
384        let rundir = assert_ok!(RuntimeDirectory::<Local>::get());
385        assert_eq!(rundir.as_path(), Path::new("/tmp/daemon-1000"));
386        assert!(rundir.temporary());
387    }
388
389    #[test]
390    fn persistent_global_xdg_present() {
391        api::mock(DAEMON_NAME, USER, Some("/run"), Some("/home/user/.run"));
392
393        let rundir = assert_ok!(RuntimeDirectory::<Global>::get());
394        assert_eq!(rundir.as_path(), Path::new("/run/daemon"));
395        assert!(!rundir.temporary());
396    }
397
398    #[test]
399    fn persistent_global_xdg_missing() {
400        api::mock(DAEMON_NAME, USER, Some("/run"), None);
401
402        let rundir = assert_ok!(RuntimeDirectory::<Global>::get());
403        assert_eq!(rundir.as_path(), Path::new("/run/daemon"));
404        assert!(!rundir.temporary());
405    }
406
407    #[test]
408    fn temporary_global() {
409        api::mock(DAEMON_NAME, USER, None, None);
410
411        let rundir = assert_ok!(RuntimeDirectory::<Global>::get());
412        assert_eq!(rundir.as_path(), Path::new("/tmp/daemon"));
413        assert!(rundir.temporary());
414    }
415}