1#![deny(missing_docs)]
8
9use flex_client::{MessageBuf, ProxyHasDomain, channel_to_async};
10use flex_fuchsia_io as fio;
11use futures::stream::{FusedStream, Stream};
12use std::ffi::OsStr;
13use std::os::unix::ffi::OsStrExt;
14use std::path::PathBuf;
15use std::pin::Pin;
16use std::task::{Context, Poll};
17use thiserror::Error;
18
19#[derive(Debug, Error, Clone)]
20#[allow(missing_docs)]
21pub enum WatcherCreateError {
22 #[error("while sending watch request: {0}")]
23 SendWatchRequest(#[source] fidl::Error),
24
25 #[error("watch failed with status: {0}")]
26 WatchError(#[source] zx_status::Status),
27
28 #[error("while converting client end to fasync channel: {0}")]
29 ChannelConversion(#[source] zx_status::Status),
30}
31
32#[derive(Debug, Error)]
33#[cfg_attr(not(feature = "fdomain"), derive(Eq, PartialEq))]
34#[allow(missing_docs)]
35pub enum WatcherStreamError {
36 #[cfg(not(feature = "fdomain"))]
37 #[error("read from watch channel failed with status: {0}")]
38 ChannelRead(#[from] zx_status::Status),
39 #[cfg(feature = "fdomain")]
40 #[error("read from watch channel failed: {0}")]
41 ChannelRead(#[from] flex_client::Error),
42}
43
44impl WatcherStreamError {
45 #[cfg(not(feature = "fdomain"))]
46 fn invalid_data() -> Self {
47 WatcherStreamError::ChannelRead(zx_status::Status::IO_DATA_INTEGRITY)
48 }
49
50 #[cfg(feature = "fdomain")]
51 fn invalid_data() -> Self {
52 WatcherStreamError::ChannelRead(flex_client::Error::StreamingAborted)
53 }
54}
55
56#[repr(C)]
58#[derive(Copy, Clone, Eq, PartialEq)]
59pub struct WatchEvent(fio::WatchEvent);
60
61impl WatchEvent {
62 pub const DELETED: Self = Self(fio::WatchEvent::Deleted);
65 pub const ADD_FILE: Self = Self(fio::WatchEvent::Added);
67 pub const REMOVE_FILE: Self = Self(fio::WatchEvent::Removed);
69 pub const EXISTING: Self = Self(fio::WatchEvent::Existing);
71 pub const IDLE: Self = Self(fio::WatchEvent::Idle);
73
74 const fn assoc_const_name(&self) -> &'static str {
75 match self.0 {
76 fio::WatchEvent::Deleted => "DELETED",
77 fio::WatchEvent::Added => "ADD_FILE",
78 fio::WatchEvent::Removed => "REMOVE_FILE",
79 fio::WatchEvent::Existing => "EXISTING",
80 fio::WatchEvent::Idle => "IDLE",
81 }
82 }
83}
84
85impl std::fmt::Debug for WatchEvent {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 write!(f, "WatchEvent({})", self.assoc_const_name())
88 }
89}
90
91#[derive(Debug, Eq, PartialEq)]
94pub struct WatchMessage {
95 pub event: WatchEvent,
97 pub filename: PathBuf,
99}
100
101#[derive(Debug, Eq, PartialEq)]
102enum WatcherState {
103 Watching,
104 TerminateOnNextPoll,
105 Terminated,
106}
107
108#[derive(Debug)]
112#[must_use = "futures/streams must be polled"]
113pub struct Watcher {
114 ch: flex_client::AsyncChannel,
115 buf: MessageBuf,
117 idx: usize,
118 state: WatcherState,
119}
120
121impl Unpin for Watcher {}
122
123impl Watcher {
124 pub async fn new(dir: &fio::DirectoryProxy) -> Result<Watcher, WatcherCreateError> {
126 Self::new_with_mask(dir, fio::WatchMask::all()).await
127 }
128
129 pub async fn new_with_mask(
132 dir: &fio::DirectoryProxy,
133 mask: fio::WatchMask,
134 ) -> Result<Watcher, WatcherCreateError> {
135 let (client_end, server_end) = dir.domain().create_endpoints();
136 let options = 0u32;
137 let status = dir
138 .watch(mask, options, server_end)
139 .await
140 .map_err(WatcherCreateError::SendWatchRequest)?;
141 zx_status::Status::ok(status).map_err(WatcherCreateError::WatchError)?;
142 let mut buf = MessageBuf::new();
143 buf.ensure_capacity_bytes(fio::MAX_BUF as usize);
144 Ok(Watcher {
145 ch: channel_to_async(client_end.into_channel()),
146 buf,
147 idx: 0,
148 state: WatcherState::Watching,
149 })
150 }
151
152 fn reset_buf(&mut self) {
153 self.idx = 0;
154 self.buf.clear();
155 }
156
157 fn get_next_msg(&mut self) -> Result<WatchMessage, WatcherStreamError> {
158 let next_msg = VfsWatchMsg::from_raw(&self.buf.bytes()[self.idx..])
161 .ok_or_else(|| WatcherStreamError::invalid_data())?;
162 self.idx += next_msg.len();
163
164 let mut pathbuf = PathBuf::new();
165 pathbuf.push(OsStr::from_bytes(next_msg.name()));
166 let event = next_msg.event();
167 Ok(WatchMessage { event, filename: pathbuf })
168 }
169}
170
171impl FusedStream for Watcher {
172 fn is_terminated(&self) -> bool {
173 self.state == WatcherState::Terminated
174 }
175}
176
177impl Stream for Watcher {
178 type Item = Result<WatchMessage, WatcherStreamError>;
179
180 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
181 let this = &mut *self;
182 if this.state == WatcherState::TerminateOnNextPoll {
185 this.state = WatcherState::Terminated;
186 }
187 if this.state == WatcherState::Terminated {
188 return Poll::Ready(None);
189 }
190 if this.idx >= this.buf.bytes().len() {
191 this.reset_buf();
192 }
193 if this.idx == 0 {
194 match this.ch.recv_from(cx, &mut this.buf) {
195 Poll::Ready(Ok(())) => {}
196 Poll::Ready(Err(e)) => {
197 this.state = WatcherState::TerminateOnNextPoll;
198 return Poll::Ready(Some(Err(e.into())));
199 }
200 Poll::Pending => return Poll::Pending,
201 }
202 }
203 match this.get_next_msg() {
204 Ok(msg) => Poll::Ready(Some(Ok(msg))),
205 Err(e) => {
206 this.state = WatcherState::TerminateOnNextPoll;
207 Poll::Ready(Some(Err(e)))
208 }
209 }
210 }
211}
212
213#[derive(Debug)]
214struct VfsWatchMsg<'a> {
215 event: WatchEvent,
216 name: &'a [u8],
217}
218
219impl<'a> VfsWatchMsg<'a> {
220 fn from_raw(buf: &'a [u8]) -> Option<VfsWatchMsg<'a>> {
221 if buf.len() < 2 {
222 return None;
223 }
224 let event = fio::WatchEvent::from_primitive(buf[0])?;
225 let namelen = buf[1] as usize;
226 if buf.len() < 2 + namelen {
227 return None;
228 }
229 Some(VfsWatchMsg { event: WatchEvent(event), name: &buf[2..2 + namelen] })
230 }
231
232 fn len(&self) -> usize {
233 2 + self.name.len()
234 }
235
236 fn event(&self) -> WatchEvent {
237 self.event
238 }
239
240 fn name(&self) -> &'a [u8] {
241 self.name
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use assert_matches::assert_matches;
249 use fuchsia_async::{DurationExt, TimeoutExt};
250
251 use futures::prelude::*;
252 use std::fmt::Debug;
253 use std::fs::File;
254 use std::path::Path;
255 use std::sync::Arc;
256 use tempfile::tempdir;
257 use vfs::ObjectRequestRef;
258 use vfs::directory::dirents_sink;
259 use vfs::directory::entry::{EntryInfo, GetEntryInfo};
260 use vfs::directory::entry_container::{Directory, DirectoryWatcher};
261 use vfs::directory::immutable::connection::ImmutableConnection;
262 use vfs::directory::traversal_position::TraversalPosition;
263 use vfs::execution_scope::ExecutionScope;
264 use vfs::node::Node;
265
266 fn one_step<'a, S, OK, ERR>(s: &'a mut S) -> impl Future<Output = OK> + 'a
267 where
268 S: Stream<Item = Result<OK, ERR>> + Unpin,
269 ERR: Debug,
270 {
271 let f = s.next();
272 let f = f.on_timeout(zx::MonotonicDuration::from_millis(500).after_now(), || {
273 panic!("timeout waiting for watcher")
274 });
275 f.map(|next| {
276 next.expect("the stream yielded no next item")
277 .unwrap_or_else(|e| panic!("Error waiting for watcher: {:?}", e))
278 })
279 }
280
281 #[fuchsia::test]
282 async fn test_existing() {
283 let tmp_dir = tempdir().unwrap();
284 let _ = File::create(tmp_dir.path().join("file1")).unwrap();
285
286 let dir = crate::directory::open_in_namespace(
287 tmp_dir.path().to_str().unwrap(),
288 fio::PERM_READABLE,
289 )
290 .unwrap();
291 let mut w = Watcher::new(&dir).await.unwrap();
292
293 let msg = one_step(&mut w).await;
294 assert_eq!(WatchEvent::EXISTING, msg.event);
295 assert_eq!(Path::new("."), msg.filename);
296
297 let msg = one_step(&mut w).await;
298 assert_eq!(WatchEvent::EXISTING, msg.event);
299 assert_eq!(Path::new("file1"), msg.filename);
300
301 let msg = one_step(&mut w).await;
302 assert_eq!(WatchEvent::IDLE, msg.event);
303 }
304
305 #[fuchsia::test]
306 async fn test_add() {
307 let tmp_dir = tempdir().unwrap();
308
309 let dir = crate::directory::open_in_namespace(
310 tmp_dir.path().to_str().unwrap(),
311 fio::PERM_READABLE,
312 )
313 .unwrap();
314 let mut w = Watcher::new(&dir).await.unwrap();
315
316 loop {
317 let msg = one_step(&mut w).await;
318 match msg.event {
319 WatchEvent::EXISTING => continue,
320 WatchEvent::IDLE => break,
321 _ => panic!("Unexpected watch event!"),
322 }
323 }
324
325 let _ = File::create(tmp_dir.path().join("file1")).unwrap();
326 let msg = one_step(&mut w).await;
327 assert_eq!(WatchEvent::ADD_FILE, msg.event);
328 assert_eq!(Path::new("file1"), msg.filename);
329 }
330
331 #[fuchsia::test]
332 async fn test_remove() {
333 let tmp_dir = tempdir().unwrap();
334
335 let filename = "file1";
336 let filepath = tmp_dir.path().join(filename);
337 let _ = File::create(&filepath).unwrap();
338
339 let dir = crate::directory::open_in_namespace(
340 tmp_dir.path().to_str().unwrap(),
341 fio::PERM_READABLE,
342 )
343 .unwrap();
344 let mut w = Watcher::new(&dir).await.unwrap();
345
346 loop {
347 let msg = one_step(&mut w).await;
348 match msg.event {
349 WatchEvent::EXISTING => continue,
350 WatchEvent::IDLE => break,
351 _ => panic!("Unexpected watch event!"),
352 }
353 }
354
355 ::std::fs::remove_file(&filepath).unwrap();
356 let msg = one_step(&mut w).await;
357 assert_eq!(WatchEvent::REMOVE_FILE, msg.event);
358 assert_eq!(Path::new(filename), msg.filename);
359 }
360
361 struct MockDirectory;
362
363 impl MockDirectory {
364 fn new() -> Arc<Self> {
365 Arc::new(Self)
366 }
367 }
368
369 impl GetEntryInfo for MockDirectory {
370 fn entry_info(&self) -> EntryInfo {
371 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
372 }
373 }
374
375 impl Node for MockDirectory {
376 async fn get_attributes(
377 &self,
378 _query: fio::NodeAttributesQuery,
379 ) -> Result<fio::NodeAttributes2, zx::Status> {
380 unimplemented!();
381 }
382
383 fn close(self: Arc<Self>) {}
384 }
385
386 impl Directory for MockDirectory {
387 fn open(
388 self: Arc<Self>,
389 scope: ExecutionScope,
390 _path: vfs::path::Path,
391 flags: fio::Flags,
392 object_request: ObjectRequestRef<'_>,
393 ) -> Result<(), zx::Status> {
394 object_request.take().create_connection_sync::<ImmutableConnection<_>, _>(
395 scope,
396 self.clone(),
397 flags,
398 );
399 Ok(())
400 }
401
402 async fn read_dirents(
403 &self,
404 _pos: &TraversalPosition,
405 _sink: Box<dyn dirents_sink::Sink>,
406 ) -> Result<(TraversalPosition, Box<dyn dirents_sink::Sealed>), zx::Status> {
407 unimplemented!("Not implemented");
408 }
409
410 fn register_watcher(
411 self: Arc<Self>,
412 _scope: ExecutionScope,
413 _mask: fio::WatchMask,
414 _watcher: DirectoryWatcher,
415 ) -> Result<(), zx::Status> {
416 Ok(())
419 }
420
421 fn unregister_watcher(self: Arc<Self>, _key: usize) {
422 unimplemented!("Not implemented");
423 }
424 }
425
426 #[fuchsia::test]
427 async fn test_error() {
428 let test_dir = MockDirectory::new();
429 let client = vfs::directory::serve_read_only(test_dir, ExecutionScope::new());
430 let mut w = Watcher::new(&client).await.unwrap();
431 let msg = w.next().await.expect("the stream yielded no next item");
432 assert!(!w.is_terminated());
433 assert_matches!(msg, Err(WatcherStreamError::ChannelRead(zx::Status::PEER_CLOSED)));
434 assert!(!w.is_terminated());
435 assert_matches!(w.next().await, None);
436 assert!(w.is_terminated());
437 }
438
439 #[test]
440 fn test_vfs_watch_msg_from_raw() {
441 let buf = [fio::WatchEvent::Added as u8, 4, b't', b'e', b's', b't'];
443 let msg = VfsWatchMsg::from_raw(&buf).unwrap();
444 assert_eq!(msg.event(), WatchEvent::ADD_FILE);
445 assert_eq!(msg.name(), b"test");
446 assert_eq!(msg.len(), 6);
447
448 let buf = [0xff, 4, b't', b'e', b's', b't'];
450 assert_matches!(VfsWatchMsg::from_raw(&buf), None);
451
452 let buf = [fio::WatchEvent::Added as u8];
454 assert_matches!(VfsWatchMsg::from_raw(&buf), None);
455
456 let buf = [fio::WatchEvent::Added as u8, 10, b't', b'e', b's', b't'];
458 assert_matches!(VfsWatchMsg::from_raw(&buf), None);
459 }
460
461 #[fuchsia::test]
462 async fn test_invalid_data() {
463 struct BadDirectory;
464 impl GetEntryInfo for BadDirectory {
465 fn entry_info(&self) -> EntryInfo {
466 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
467 }
468 }
469 impl Node for BadDirectory {
470 async fn get_attributes(
471 &self,
472 _query: fio::NodeAttributesQuery,
473 ) -> Result<fio::NodeAttributes2, zx::Status> {
474 unimplemented!();
475 }
476 fn close(self: Arc<Self>) {}
477 }
478 impl Directory for BadDirectory {
479 fn open(
480 self: Arc<Self>,
481 scope: ExecutionScope,
482 _path: vfs::path::Path,
483 flags: fio::Flags,
484 object_request: ObjectRequestRef<'_>,
485 ) -> Result<(), zx::Status> {
486 object_request.take().create_connection_sync::<ImmutableConnection<_>, _>(
487 scope,
488 self.clone(),
489 flags,
490 );
491 Ok(())
492 }
493 async fn read_dirents(
494 &self,
495 _pos: &TraversalPosition,
496 _sink: Box<dyn dirents_sink::Sink>,
497 ) -> Result<(TraversalPosition, Box<dyn dirents_sink::Sealed>), zx::Status>
498 {
499 unimplemented!("Not implemented");
500 }
501 fn register_watcher(
502 self: Arc<Self>,
503 _scope: ExecutionScope,
504 _mask: fio::WatchMask,
505 watcher: DirectoryWatcher,
506 ) -> Result<(), zx::Status> {
507 #[cfg(not(feature = "fdomain"))]
509 let _ = watcher.channel().write(&[0xff, 0], &mut []);
510 #[cfg(feature = "fdomain")]
511 let _ = watcher.channel().write(&[0xff, 0], std::vec::Vec::new());
512 Ok(())
513 }
514 fn unregister_watcher(self: Arc<Self>, _key: usize) {
515 unimplemented!("Not implemented");
516 }
517 }
518
519 let test_dir = Arc::new(BadDirectory);
520 let client = vfs::directory::serve_read_only(test_dir, ExecutionScope::new());
521 let mut w = Watcher::new(&client).await.unwrap();
522 let msg = w.next().await.expect("the stream yielded no next item");
523 assert_matches!(msg, Err(WatcherStreamError::ChannelRead(zx::Status::IO_DATA_INTEGRITY)));
524 assert!(!w.is_terminated());
525 assert_matches!(w.next().await, None);
526 assert!(w.is_terminated());
527 }
528}