windows_namespace_request_sys/watch.rs
1// Copyright (c) Mike Grier.
2
3//! The `FindFirstChangeNotificationW` entry.
4//!
5//! Entry 3 of the audited catalogue. It watches a directory for a class of
6//! changes and produces a handle that becomes signalled when one occurs.
7//!
8//! # The handle it produces is not an ordinary handle
9//!
10//! A change-notification handle is closed with `FindCloseChangeNotification`,
11//! **not** `CloseHandle`. Passing it to `CloseHandle` is a resource leak that
12//! nothing reports: the call may even appear to succeed. That is why this entry
13//! returns [`ChangeNotification`] rather than a bare `OwnedHandle` -- the type
14//! is what remembers which routine closes it, so a caller cannot forget.
15//!
16//! # What it does and does not tell you
17//!
18//! The handle signals that *something* in the watched set changed. It carries
19//! no record of what, and a burst of changes may signal once. A consumer
20//! needing the individual changes wants `ReadDirectoryChangesW`, which is a
21//! different call and therefore a different entry. This one is the cheap
22//! "something happened, go look" primitive, and the crate does not blur them.
23
24use std::ffi::c_void;
25use std::fmt;
26
27use windows_sys::Win32::Foundation::HANDLE;
28use windows_sys::Win32::Storage::FileSystem::{
29 FILE_NOTIFY_CHANGE, FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_CREATION,
30 FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_LAST_ACCESS,
31 FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_SECURITY, FILE_NOTIFY_CHANGE_SIZE,
32 FindCloseChangeNotification, FindFirstChangeNotificationW, FindNextChangeNotification,
33};
34
35use wtf_string::{Wtf16Str, Wtf16String};
36
37use crate::anchored::{AnchoredPath, PathCaptureError};
38use crate::outcome::{Outcome, perform_bool, perform_handle};
39
40/// An owned change-notification handle.
41///
42/// Closed with `FindCloseChangeNotification` on drop. The type exists so that
43/// the close routine travels with the handle rather than being something each
44/// call site has to remember, which is the same shape
45/// [windows-threadpool-sys](https://docs.rs/windows-threadpool-sys) already
46/// needed for wait targets.
47#[derive(Debug)]
48#[must_use = "dropping the notification stops the watch"]
49pub struct ChangeNotification {
50 /// Always a live handle produced by `FindFirstChangeNotificationW`, until
51 /// `Drop` closes it.
52 handle: HANDLE,
53}
54
55impl ChangeNotification {
56 /// The raw handle, for passing to a wait.
57 ///
58 /// Borrowed, not transferred: it stays owned by this value. Do **not** pass
59 /// it to `CloseHandle`.
60 #[must_use]
61 pub fn as_raw(&self) -> HANDLE {
62 self.handle
63 }
64
65 /// Rearms the watch after it has signalled.
66 ///
67 /// A notification handle signals once and then stays signalled until it is
68 /// rearmed, so a consumer that waits in a loop must call this between
69 /// waits.
70 ///
71 /// # Errors
72 ///
73 /// Returns the raw Win32 code, unaltered.
74 ///
75 /// # Example
76 ///
77 /// Without the rearm, the second wait returns immediately on the *first*
78 /// change forever, and a consumer's loop spins:
79 ///
80 /// ```
81 /// use std::fs;
82 ///
83 /// use windows_namespace_request_sys::watch::{NotifyFilter, WatchDirectory};
84 /// use windows_sys::Win32::Foundation::WAIT_OBJECT_0;
85 /// use windows_sys::Win32::System::Threading::WaitForSingleObject;
86 /// use wtf_string::Wtf16String;
87 ///
88 /// let directory = std::env::temp_dir().join(format!("wnrs-rearm-{}", std::process::id()));
89 /// let _ = fs::remove_dir_all(&directory);
90 /// fs::create_dir_all(&directory)?;
91 ///
92 /// let text = directory.to_str().expect("a temporary path is valid UTF-8");
93 /// let notification = WatchDirectory::new(Wtf16String::from(text))?
94 /// .with_filter(NotifyFilter::FILE_NAME)
95 /// .perform()?;
96 ///
97 /// fs::write(directory.join("first.t"), b"x")?;
98 /// // SAFETY: the handle is live for the notification's lifetime.
99 /// assert_eq!(unsafe { WaitForSingleObject(notification.as_raw(), 5_000) }, WAIT_OBJECT_0);
100 ///
101 /// // The handle stays signalled until this runs.
102 /// notification.rearm()?;
103 ///
104 /// fs::write(directory.join("second.t"), b"x")?;
105 /// // SAFETY: as above.
106 /// assert_eq!(unsafe { WaitForSingleObject(notification.as_raw(), 5_000) }, WAIT_OBJECT_0);
107 /// # drop(notification);
108 /// # let _ = fs::remove_dir_all(&directory);
109 /// # Ok::<(), Box<dyn std::error::Error>>(())
110 /// ```
111 pub fn rearm(&self) -> Outcome<()> {
112 // SAFETY: the handle is live for this value's lifetime.
113 perform_bool(|| unsafe { FindNextChangeNotification(self.handle) })
114 }
115}
116
117impl Drop for ChangeNotification {
118 fn drop(&mut self) {
119 // SAFETY: the handle came from FindFirstChangeNotificationW and has not
120 // been closed, since only this Drop closes it. The result is
121 // deliberately ignored: a close failure during drop has nowhere to go,
122 // and this crate does not panic on it.
123 unsafe {
124 FindCloseChangeNotification(self.handle);
125 }
126 }
127}
128
129// SAFETY: the value owns its handle exclusively and has no interior
130// mutability. A Windows handle is process-wide rather than thread-affine, so
131// moving it between threads and sharing a shared reference are both sound; the
132// raw pointer is what blocks the automatic derivation.
133unsafe impl Send for ChangeNotification {}
134// SAFETY: as above. `rearm` takes `&self` and is a single Win32 call that
135// Windows serialises internally.
136unsafe impl Sync for ChangeNotification {}
137
138/// Which changes a watch reports.
139///
140/// A thin newtype over `FILE_NOTIFY_CHANGE` rather than an enum, because the
141/// value is a bitmask and Windows may define bits this crate has not heard of.
142/// The named constants are provided for convenience; an unknown bit still
143/// reaches Windows unaltered.
144///
145/// # Example
146///
147/// ```
148/// use windows_namespace_request_sys::watch::NotifyFilter;
149///
150/// let combined = NotifyFilter::FILE_NAME | NotifyFilter::DIR_NAME;
151///
152/// assert!(combined.contains(NotifyFilter::FILE_NAME));
153/// assert!(!combined.contains(NotifyFilter::SIZE));
154///
155/// // A bitmask, not an enum: a bit Windows defines and this crate has never
156/// // heard of still reaches it.
157/// let unknown = NotifyFilter::from_bits(0x8000_0000);
158/// assert_eq!(unknown.bits(), 0x8000_0000);
159///
160/// // The empty filter watches nothing, and Windows -- not this crate --
161/// // rejects it at the call.
162/// assert_eq!(NotifyFilter::NONE.bits(), 0);
163/// ```
164#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
165pub struct NotifyFilter(FILE_NOTIFY_CHANGE);
166
167impl NotifyFilter {
168 /// An empty filter, which watches for nothing.
169 ///
170 /// Windows rejects this at the call, which is left to happen rather than
171 /// pre-empted here.
172 pub const NONE: Self = Self(0);
173 /// File name creation, deletion, or rename.
174 pub const FILE_NAME: Self = Self(FILE_NOTIFY_CHANGE_FILE_NAME);
175 /// Directory creation or deletion.
176 pub const DIR_NAME: Self = Self(FILE_NOTIFY_CHANGE_DIR_NAME);
177 /// Attribute changes.
178 pub const ATTRIBUTES: Self = Self(FILE_NOTIFY_CHANGE_ATTRIBUTES);
179 /// Size changes, reported when the file is flushed rather than on write.
180 pub const SIZE: Self = Self(FILE_NOTIFY_CHANGE_SIZE);
181 /// Last-write-time changes, likewise reported on flush.
182 pub const LAST_WRITE: Self = Self(FILE_NOTIFY_CHANGE_LAST_WRITE);
183 /// Last-access-time changes.
184 pub const LAST_ACCESS: Self = Self(FILE_NOTIFY_CHANGE_LAST_ACCESS);
185 /// Creation-time changes.
186 pub const CREATION: Self = Self(FILE_NOTIFY_CHANGE_CREATION);
187 /// Security-descriptor changes.
188 pub const SECURITY: Self = Self(FILE_NOTIFY_CHANGE_SECURITY);
189
190 /// Wraps a raw `FILE_NOTIFY_CHANGE` mask.
191 ///
192 /// Any bit pattern is accepted: the crate does not decide which bits
193 /// Windows understands.
194 #[must_use]
195 pub const fn from_bits(bits: FILE_NOTIFY_CHANGE) -> Self {
196 Self(bits)
197 }
198
199 /// The raw mask.
200 #[must_use]
201 pub const fn bits(self) -> FILE_NOTIFY_CHANGE {
202 self.0
203 }
204
205 /// The union of two filters.
206 #[must_use]
207 pub const fn union(self, other: Self) -> Self {
208 Self(self.0 | other.0)
209 }
210
211 /// Whether every bit of `other` is set in this filter.
212 #[must_use]
213 pub const fn contains(self, other: Self) -> bool {
214 self.0 & other.0 == other.0
215 }
216}
217
218impl std::ops::BitOr for NotifyFilter {
219 type Output = Self;
220
221 fn bitor(self, other: Self) -> Self {
222 self.union(other)
223 }
224}
225
226/// An owned, marshalable parameter set for `FindFirstChangeNotificationW`.
227///
228/// # Example
229///
230/// ```
231/// use windows_namespace_request_sys::watch::{NotifyFilter, WatchDirectory};
232/// use wtf_string::Wtf16String;
233///
234/// let directory = std::env::temp_dir();
235/// let text = directory.to_str().expect("the temporary directory is valid UTF-8");
236///
237/// let request = WatchDirectory::new(Wtf16String::from(text))?
238/// .with_subtree(true)
239/// .with_filter(NotifyFilter::FILE_NAME | NotifyFilter::DIR_NAME);
240///
241/// let notification = request.perform()?;
242/// // The handle is closed with FindCloseChangeNotification, which the type
243/// // remembers on the caller's behalf.
244/// drop(notification);
245/// # Ok::<(), Box<dyn std::error::Error>>(())
246/// ```
247#[derive(Clone, Debug)]
248#[must_use = "an unperformed request watches nothing"]
249pub struct WatchDirectory {
250 path: AnchoredPath,
251 subtree: bool,
252 filter: NotifyFilter,
253}
254
255impl WatchDirectory {
256 /// Begins a request to watch `path`.
257 ///
258 /// The path reaches the call as it was given, except that one Win32 would resolve against the
259 /// current directory is fixed to the current directory **as it is now**, so that it still names
260 /// the same place when the request is performed on another thread after the directory has
261 /// changed; see [`OpenFile::new`](crate::open::OpenFile::new) and `D-19` in the crate's
262 /// `DESIGN-NOTES.md`. [`path`](Self::path) is the path as given and
263 /// [`win32_path`](Self::win32_path) is the one handed to the call.
264 ///
265 /// The watch starts non-recursive and watching for nothing; both are set
266 /// explicitly, as everywhere else in this crate.
267 ///
268 /// # Errors
269 ///
270 /// [`PathCaptureError::InteriorNul`] if the path contains a NUL, which Win32 would read as its
271 /// end, and the failure of `GetCurrentDirectoryW` if it had to be read and could not be.
272 pub fn new(path: Wtf16String) -> Result<Self, PathCaptureError> {
273 Ok(Self::from_anchored(AnchoredPath::new(path)?))
274 }
275
276 /// Begins a request to watch a path the caller has already anchored.
277 ///
278 /// As [`OpenFile::from_anchored`](crate::open::OpenFile::from_anchored): the path is not anchored
279 /// again, and the request hands the call the anchored path's [`win32`](AnchoredPath::win32) form.
280 /// The watch starts as [`new`](Self::new) starts it.
281 ///
282 /// # Example
283 ///
284 /// ```
285 /// use windows_namespace_request_sys::{AnchoredPath, WatchDirectory};
286 /// use wtf_string::Wtf16String;
287 ///
288 /// let anchored = AnchoredPath::new(Wtf16String::from("a-child"))?;
289 ///
290 /// let request = WatchDirectory::from_anchored(anchored.clone());
291 /// assert_eq!(request.path(), anchored.given());
292 /// assert_eq!(request.win32_path(), anchored.win32());
293 /// # Ok::<(), Box<dyn std::error::Error>>(())
294 /// ```
295 pub fn from_anchored(path: AnchoredPath) -> Self {
296 Self {
297 path,
298 subtree: false,
299 filter: NotifyFilter::NONE,
300 }
301 }
302
303 /// Sets `bWatchSubtree`.
304 pub fn with_subtree(mut self, subtree: bool) -> Self {
305 self.subtree = subtree;
306 self
307 }
308
309 /// Sets `dwNotifyFilter`.
310 pub fn with_filter(mut self, filter: NotifyFilter) -> Self {
311 self.filter = filter;
312 self
313 }
314
315 /// The path as it was given.
316 #[must_use]
317 pub fn path(&self) -> &Wtf16Str {
318 self.path.given()
319 }
320
321 /// The path that is handed to `FindFirstChangeNotificationW`: the one given, or that one fixed
322 /// to the current directory of the moment the request was built.
323 #[must_use]
324 pub fn win32_path(&self) -> &Wtf16Str {
325 self.path.win32()
326 }
327
328 /// Whether the watch covers the whole subtree.
329 #[must_use]
330 pub fn subtree(&self) -> bool {
331 self.subtree
332 }
333
334 /// The change classes the watch reports.
335 #[must_use]
336 pub fn filter(&self) -> NotifyFilter {
337 self.filter
338 }
339
340 /// Performs the call on the calling thread.
341 ///
342 /// # Errors
343 ///
344 /// Returns the raw Win32 code, unaltered.
345 pub fn perform(&self) -> Outcome<ChangeNotification> {
346 let raw = perform_handle(|| {
347 // SAFETY: the path has no interior NUL, since the request refused one when it was
348 // built, is NUL-terminated and outlives the call, and both remaining arguments are
349 // plain values.
350 unsafe {
351 FindFirstChangeNotificationW(
352 self.path.win32_terminated(),
353 i32::from(self.subtree),
354 self.filter.bits(),
355 )
356 }
357 })?;
358
359 Ok(ChangeNotification {
360 handle: raw.cast::<c_void>(),
361 })
362 }
363}
364
365impl fmt::Display for NotifyFilter {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 write!(f, "FILE_NOTIFY_CHANGE({:#x})", self.0)
368 }
369}
370
371impl crate::request::Request for WatchDirectory {
372 type Error = crate::Win32Error;
373 type Output = ChangeNotification;
374
375 fn perform(&self) -> Outcome<ChangeNotification> {
376 Self::perform(self)
377 }
378}
379
380#[cfg(test)]
381mod tests;