Skip to main content

rightkit_daemon/
wake.rs

1//! A cross-process wake signal (rightkit#404): `post` from any process of the current user, and
2//! every [`WakeListener`] on the same name in any process wakes.
3//!
4//! The wake carries no data (it says "look again": a hub, a notch and an engine use it to tell each
5//! other that shared state changed, and each then reads that state). Posts coalesce: any number of
6//! posts before a listener looks is one wake. Nothing polls.
7//!
8//! * **Unix**: each listener binds a datagram socket in a 0700 directory made for the name; `post`
9//!   sends one byte to every socket there and removes the sockets nobody listens on any more. The
10//!   directory is under the per-user temporary directory (a socket path is limited to about 100
11//!   bytes, which a suite root often exceeds), named from the suite root and the name, and must be
12//!   owned by the current user and not group or world writable, or the call fails closed.
13//! * **Windows**: each listener creates an auto-reset named event, with an owner-only DACL, and
14//!   writes its name to a registration file under the suite's runtime directory; `post` opens every
15//!   registered event and sets it, and removes the registrations that no longer open.
16//!
17//! A listener that is dropped removes its endpoint. A crashed listener leaves a stale endpoint that
18//! the next `post` removes.
19
20use crate::suite::{valid_service_name, Suite};
21use std::io;
22use std::time::Duration;
23
24/// A name that is not a valid service name (the wake directory is derived from it).
25fn invalid_name(name: &str) -> io::Error {
26    io::Error::new(
27        io::ErrorKind::InvalidInput,
28        format!("{name:?} is not a valid wake name"),
29    )
30}
31
32fn hash_hex(text: &str) -> String {
33    // FNV-1a, 64 bit: a short stable directory name, not a security property.
34    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
35    for b in text.bytes() {
36        h ^= u64::from(b);
37        h = h.wrapping_mul(0x0000_0100_0000_01b3);
38    }
39    format!("{h:016x}")
40}
41
42/// Wake every listener on `name` in `suite`. Returns how many listeners were signalled; zero when
43/// nobody listens (not an error).
44pub fn post(suite: &Suite, name: &str) -> io::Result<usize> {
45    if !valid_service_name(name) {
46        return Err(invalid_name(name));
47    }
48    imp::post(suite, name)
49}
50
51/// Listen for wakes on `name` in `suite`.
52pub fn listen(suite: &Suite, name: &str) -> io::Result<WakeListener> {
53    if !valid_service_name(name) {
54        return Err(invalid_name(name));
55    }
56    imp::listen(suite, name).map(|inner| WakeListener { inner })
57}
58
59/// One listener. Dropping it removes its endpoint.
60pub struct WakeListener {
61    inner: imp::Inner,
62}
63
64impl WakeListener {
65    /// Wait up to `timeout` for a wake. `true` when woken (every post since the last call counts
66    /// once), `false` at the timeout.
67    pub fn wait(&self, timeout: Duration) -> io::Result<bool> {
68        self.inner.wait(timeout)
69    }
70}
71
72#[cfg(unix)]
73mod imp {
74    use super::*;
75    use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
76    use std::os::unix::net::UnixDatagram;
77    use std::path::{Path, PathBuf};
78    use std::sync::atomic::{AtomicU64, Ordering};
79
80    static NEXT: AtomicU64 = AtomicU64::new(1);
81
82    fn dir_for(suite: &Suite, name: &str) -> PathBuf {
83        let key = format!("{}|{name}", suite.root().display());
84        std::env::temp_dir().join(format!(
85            "rkwake-{}-{}",
86            crate::transport::current_uid(),
87            hash_hex(&key)
88        ))
89    }
90
91    /// The directory, created 0700 when missing, refused unless it is a real directory owned by
92    /// the current user that nobody else can write.
93    fn ensure_dir(dir: &Path) -> io::Result<()> {
94        match std::fs::DirBuilder::new().mode(0o700).create(dir) {
95            Ok(()) => {}
96            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
97            Err(e) => return Err(e),
98        }
99        let md = std::fs::symlink_metadata(dir)?;
100        if !md.is_dir()
101            || md.uid() != crate::transport::current_uid()
102            || md.permissions().mode() & 0o022 != 0
103        {
104            return Err(io::Error::new(
105                io::ErrorKind::PermissionDenied,
106                "the wake directory is not a private directory of this user",
107            ));
108        }
109        Ok(())
110    }
111
112    pub(super) struct Inner {
113        sock: UnixDatagram,
114        path: PathBuf,
115    }
116
117    pub(super) fn listen(suite: &Suite, name: &str) -> io::Result<Inner> {
118        let dir = dir_for(suite, name);
119        ensure_dir(&dir)?;
120        let path = dir.join(format!(
121            "{}-{}.sock",
122            std::process::id(),
123            NEXT.fetch_add(1, Ordering::SeqCst)
124        ));
125        let _ = std::fs::remove_file(&path);
126        let sock = UnixDatagram::bind(&path)?;
127        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
128        Ok(Inner { sock, path })
129    }
130
131    impl Inner {
132        pub(super) fn wait(&self, timeout: Duration) -> io::Result<bool> {
133            self.sock
134                .set_read_timeout(Some(timeout.max(Duration::from_millis(1))))?;
135            let mut buf = [0u8; 16];
136            match self.sock.recv(&mut buf) {
137                Ok(_) => {}
138                Err(e)
139                    if matches!(
140                        e.kind(),
141                        io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
142                    ) =>
143                {
144                    return Ok(false)
145                }
146                Err(e) => return Err(e),
147            }
148            // Coalesce: anything already queued is the same wake.
149            self.sock.set_nonblocking(true)?;
150            while self.sock.recv(&mut buf).is_ok() {}
151            self.sock.set_nonblocking(false)?;
152            Ok(true)
153        }
154    }
155
156    impl Drop for Inner {
157        fn drop(&mut self) {
158            let _ = std::fs::remove_file(&self.path);
159        }
160    }
161
162    pub(super) fn post(suite: &Suite, name: &str) -> io::Result<usize> {
163        let dir = dir_for(suite, name);
164        let entries = match std::fs::read_dir(&dir) {
165            Ok(entries) => entries,
166            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(0),
167            Err(e) => return Err(e),
168        };
169        ensure_dir(&dir)?;
170        let sender = UnixDatagram::unbound()?;
171        sender.set_nonblocking(true)?;
172        let mut woken = 0;
173        for entry in entries.flatten() {
174            let path = entry.path();
175            if path.extension().and_then(|e| e.to_str()) != Some("sock") {
176                continue;
177            }
178            match sender.send_to(&[1], &path) {
179                Ok(_) => woken += 1,
180                // A full queue is a pending wake already: the listener will look.
181                Err(e) if e.kind() == io::ErrorKind::WouldBlock => woken += 1,
182                Err(e)
183                    if matches!(
184                        e.kind(),
185                        io::ErrorKind::ConnectionRefused | io::ErrorKind::NotFound
186                    ) =>
187                {
188                    let _ = std::fs::remove_file(&path);
189                }
190                Err(_) => {}
191            }
192        }
193        Ok(woken)
194    }
195}
196
197#[cfg(windows)]
198mod imp {
199    use super::*;
200    use std::path::PathBuf;
201    use std::sync::atomic::{AtomicU64, Ordering};
202    use windows::core::PCWSTR;
203    use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_OBJECT_0};
204    use windows::Win32::System::Threading::{
205        CreateEventW, OpenEventW, SetEvent, WaitForSingleObject, EVENT_MODIFY_STATE,
206    };
207
208    static NEXT: AtomicU64 = AtomicU64::new(1);
209
210    fn dir_for(suite: &Suite, name: &str) -> PathBuf {
211        suite.service_dir(&format!("wake.{name}"))
212    }
213
214    fn wide(s: &str) -> Vec<u16> {
215        s.encode_utf16().chain(std::iter::once(0)).collect()
216    }
217
218    pub(super) struct Inner {
219        event: HANDLE,
220        registration: PathBuf,
221    }
222
223    // SAFETY: an event handle may be waited on from any thread.
224    unsafe impl Send for Inner {}
225    unsafe impl Sync for Inner {}
226
227    pub(super) fn listen(suite: &Suite, name: &str) -> io::Result<Inner> {
228        let dir = dir_for(suite, name);
229        crate::suite::make_private_dir(&dir).map_err(io::Error::other)?;
230        let event_name = format!(
231            "Local\\rk-wake-{}-{}-{}",
232            hash_hex(&format!("{}|{name}", suite.root().display())),
233            std::process::id(),
234            NEXT.fetch_add(1, Ordering::SeqCst)
235        );
236        let attrs = crate::peer::owner_only_security_attributes()?;
237        let wname = wide(&event_name);
238        // Auto-reset: one wait consumes the signal; posts before it coalesce into one.
239        // SAFETY: `attrs` and `wname` outlive the call; the DACL is copied into the kernel object.
240        let event = unsafe {
241            CreateEventW(
242                Some(attrs.raw() as *const _),
243                false,
244                false,
245                PCWSTR(wname.as_ptr()),
246            )
247        }
248        .map_err(|e| io::Error::from_raw_os_error((e.code().0 as u32 & 0xFFFF) as i32))?;
249        let registration = dir.join(format!(
250            "{}-{}.ev",
251            std::process::id(),
252            NEXT.fetch_add(1, Ordering::SeqCst)
253        ));
254        if let Err(e) = crate::record::write_atomic(&registration, event_name.as_bytes()) {
255            // SAFETY: the handle was just created and is closed once.
256            unsafe {
257                let _ = CloseHandle(event);
258            }
259            return Err(e);
260        }
261        Ok(Inner {
262            event,
263            registration,
264        })
265    }
266
267    impl Inner {
268        pub(super) fn wait(&self, timeout: Duration) -> io::Result<bool> {
269            let ms = timeout.as_millis().min(u128::from(u32::MAX - 1)) as u32;
270            // SAFETY: the handle is open for the life of `self`.
271            let status = unsafe { WaitForSingleObject(self.event, ms) };
272            Ok(status == WAIT_OBJECT_0)
273        }
274    }
275
276    impl Drop for Inner {
277        fn drop(&mut self) {
278            let _ = std::fs::remove_file(&self.registration);
279            // SAFETY: closed once, here.
280            unsafe {
281                let _ = CloseHandle(self.event);
282            }
283        }
284    }
285
286    pub(super) fn post(suite: &Suite, name: &str) -> io::Result<usize> {
287        let dir = dir_for(suite, name);
288        let entries = match std::fs::read_dir(&dir) {
289            Ok(entries) => entries,
290            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(0),
291            Err(e) => return Err(e),
292        };
293        let mut woken = 0;
294        for entry in entries.flatten() {
295            let path = entry.path();
296            if path.extension().and_then(|e| e.to_str()) != Some("ev") {
297                continue;
298            }
299            let Ok(event_name) = std::fs::read_to_string(&path) else {
300                continue;
301            };
302            let wname = wide(event_name.trim());
303            // SAFETY: `wname` is NUL-terminated and outlives the call.
304            match unsafe { OpenEventW(EVENT_MODIFY_STATE, false, PCWSTR(wname.as_ptr())) } {
305                Ok(handle) => {
306                    // SAFETY: the handle was just opened and is closed once.
307                    unsafe {
308                        if SetEvent(handle).is_ok() {
309                            woken += 1;
310                        }
311                        let _ = CloseHandle(handle);
312                    }
313                }
314                // The listener is gone: its registration is stale.
315                Err(_) => {
316                    let _ = std::fs::remove_file(&path);
317                }
318            }
319        }
320        Ok(woken)
321    }
322}
323
324#[cfg(not(any(unix, windows)))]
325mod imp {
326    use super::*;
327
328    pub(super) struct Inner;
329
330    pub(super) fn listen(_suite: &Suite, _name: &str) -> io::Result<Inner> {
331        Err(io::Error::new(
332            io::ErrorKind::Unsupported,
333            "no cross-process wake on this platform",
334        ))
335    }
336
337    impl Inner {
338        pub(super) fn wait(&self, _timeout: Duration) -> io::Result<bool> {
339            Ok(false)
340        }
341    }
342
343    pub(super) fn post(_suite: &Suite, _name: &str) -> io::Result<usize> {
344        Ok(0)
345    }
346}