1#[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#[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}