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 {
274 path: AnchoredPath::new(path)?,
275 subtree: false,
276 filter: NotifyFilter::NONE,
277 })
278 }
279
280 /// Sets `bWatchSubtree`.
281 pub fn with_subtree(mut self, subtree: bool) -> Self {
282 self.subtree = subtree;
283 self
284 }
285
286 /// Sets `dwNotifyFilter`.
287 pub fn with_filter(mut self, filter: NotifyFilter) -> Self {
288 self.filter = filter;
289 self
290 }
291
292 /// The path as it was given.
293 #[must_use]
294 pub fn path(&self) -> &Wtf16Str {
295 self.path.given()
296 }
297
298 /// The path that is handed to `FindFirstChangeNotificationW`: the one given, or that one fixed
299 /// to the current directory of the moment the request was built.
300 #[must_use]
301 pub fn win32_path(&self) -> &Wtf16Str {
302 self.path.win32()
303 }
304
305 /// Whether the watch covers the whole subtree.
306 #[must_use]
307 pub fn subtree(&self) -> bool {
308 self.subtree
309 }
310
311 /// The change classes the watch reports.
312 #[must_use]
313 pub fn filter(&self) -> NotifyFilter {
314 self.filter
315 }
316
317 /// Performs the call on the calling thread.
318 ///
319 /// # Errors
320 ///
321 /// Returns the raw Win32 code, unaltered.
322 pub fn perform(&self) -> Outcome<ChangeNotification> {
323 let raw = perform_handle(|| {
324 // SAFETY: the path has no interior NUL, since the request refused one when it was
325 // built, is NUL-terminated and outlives the call, and both remaining arguments are
326 // plain values.
327 unsafe {
328 FindFirstChangeNotificationW(
329 self.path.win32_terminated(),
330 i32::from(self.subtree),
331 self.filter.bits(),
332 )
333 }
334 })?;
335
336 Ok(ChangeNotification {
337 handle: raw.cast::<c_void>(),
338 })
339 }
340}
341
342impl fmt::Display for NotifyFilter {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 write!(f, "FILE_NOTIFY_CHANGE({:#x})", self.0)
345 }
346}
347
348impl crate::request::Request for WatchDirectory {
349 type Error = crate::Win32Error;
350 type Output = ChangeNotification;
351
352 fn perform(&self) -> Outcome<ChangeNotification> {
353 Self::perform(self)
354 }
355}
356
357#[cfg(test)]
358mod tests;