1use kqueue_sys::{kevent, kqueue};
2use libc::{close, pid_t, uintptr_t};
3use std::fmt::Debug;
4use std::fs::File;
5use std::io::{self, Error, Result};
6use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd};
7use std::path::Path;
8use std::ptr;
9use std::time::Duration;
10
11pub use kqueue_sys::constants::*;
12
13mod os;
14use crate::os::vnode;
15
16mod time;
17use crate::time::duration_to_timespec;
18
19#[derive(Debug, Eq, Clone)]
21pub enum Ident {
22 Filename(RawFd, String),
23 Fd(RawFd),
24 Pid(pid_t),
25 Signal(i32),
26 Timer(i32),
27}
28
29#[doc(hidden)]
30#[derive(Debug, PartialEq, Clone)]
31pub struct Watched {
32 filter: EventFilter,
33 flags: FilterFlag,
34 ident: Ident,
35}
36
37#[derive(Debug)]
52pub struct Watcher {
53 watched: Vec<Watched>,
54 queue: RawFd,
55 started: bool,
56 opts: KqueueOpts,
57}
58
59#[derive(Debug)]
64#[non_exhaustive]
65pub enum Vnode {
66 Delete,
68
69 Write,
71
72 Extend,
74
75 Truncate,
77
78 Attrib,
80
81 Link,
83
84 Rename,
86
87 Revoke,
89
90 Open,
92
93 CloseWrite,
95
96 Close,
98}
99
100#[derive(Debug)]
105pub enum Proc {
106 Exit(usize),
108
109 Fork,
111
112 Exec,
114
115 Track(libc::pid_t),
117
118 Trackerr,
120
121 Child(libc::pid_t),
124}
125
126#[derive(Debug)]
131pub enum EventData {
132 Vnode(Vnode),
134
135 Proc(Proc),
137
138 ReadReady(usize),
141
142 WriteReady(usize),
145
146 Signal(usize),
149
150 Timer(usize),
153
154 Error(Error),
156}
157
158#[derive(Debug)]
164pub struct Event {
165 pub ident: Ident,
167
168 pub data: EventData,
170}
171
172pub struct EventIter<'a> {
173 watcher: &'a Watcher,
174}
175
176#[derive(Debug)]
178pub struct KqueueOpts {
179 clear: bool,
181}
182
183impl Default for KqueueOpts {
184 fn default() -> KqueueOpts {
188 KqueueOpts { clear: true }
189 }
190}
191
192#[allow(clippy::from_over_into)]
195impl Into<usize> for Ident {
196 fn into(self) -> usize {
197 match self {
198 Ident::Filename(fd, _) => fd as usize,
199 Ident::Fd(fd) => fd as usize,
200 Ident::Pid(pid) => pid as usize,
201 Ident::Signal(sig) => sig as usize,
202 Ident::Timer(timer) => timer as usize,
203 }
204 }
205}
206
207impl PartialEq<Ident> for Ident {
208 fn eq(&self, other: &Ident) -> bool {
209 match *self {
210 Ident::Filename(_, ref name) => {
211 if let Ident::Filename(_, ref othername) = *other {
212 name == othername
213 } else {
214 false
215 }
216 }
217 _ => self.as_usize() == other.as_usize(),
218 }
219 }
220}
221
222impl Ident {
223 fn as_usize(&self) -> usize {
224 match *self {
225 Ident::Filename(fd, _) => fd as usize,
226 Ident::Fd(fd) => fd as usize,
227 Ident::Pid(pid) => pid as usize,
228 Ident::Signal(sig) => sig as usize,
229 Ident::Timer(timer) => timer as usize,
230 }
231 }
232}
233
234impl Watcher {
235 pub fn new() -> Result<Watcher> {
240 let queue = unsafe { kqueue() };
241
242 if queue == -1 {
243 Err(Error::last_os_error())
244 } else {
245 Ok(Watcher {
246 watched: Vec::new(),
247 queue,
248 started: false,
249 opts: Default::default(),
250 })
251 }
252 }
253
254 pub fn disable_clears(&mut self) -> &mut Self {
257 self.opts.clear = false;
258 self
259 }
260
261 pub fn add_pid(
263 &mut self,
264 pid: libc::pid_t,
265 filter: EventFilter,
266 flags: FilterFlag,
267 ) -> Result<()> {
268 let watch = Watched {
269 filter,
270 flags,
271 ident: Ident::Pid(pid),
272 };
273
274 if !self.watched.contains(&watch) {
275 self.watched.push(watch);
276 }
277
278 Ok(())
279 }
280
281 pub fn add_filename<P: AsRef<Path>>(
291 &mut self,
292 filename: P,
293 filter: EventFilter,
294 flags: FilterFlag,
295 ) -> Result<()> {
296 let file = File::open(filename.as_ref())?;
297 let watch = Watched {
298 filter,
299 flags,
300 ident: Ident::Filename(
301 file.into_raw_fd(),
302 filename.as_ref().to_string_lossy().into_owned(),
303 ),
304 };
305
306 if !self.watched.contains(&watch) {
307 self.watched.push(watch);
308 }
309
310 Ok(())
311 }
312
313 pub fn add_fd(&mut self, fd: RawFd, filter: EventFilter, flags: FilterFlag) -> Result<()> {
318 let watch = Watched {
319 filter,
320 flags,
321 ident: Ident::Fd(fd),
322 };
323
324 if !self.watched.contains(&watch) {
325 self.watched.push(watch);
326 }
327
328 Ok(())
329 }
330
331 pub fn add_file(&mut self, file: &File, filter: EventFilter, flags: FilterFlag) -> Result<()> {
336 self.add_fd(file.as_raw_fd(), filter, flags)
337 }
338
339 fn delete_kevents(&self, ident: Ident, filter: EventFilter) -> Result<()> {
340 let kev = &[kevent::new(
341 ident.as_usize(),
342 filter,
343 EventFlag::EV_DELETE,
344 FilterFlag::empty(),
345 )];
346
347 let ret = unsafe {
348 kevent(
349 self.queue,
350 kev.as_ptr(),
351 #[allow(clippy::useless_conversion)]
353 i32::try_from(kev.len()).unwrap().try_into().unwrap(),
354 ptr::null_mut(),
355 0,
356 ptr::null(),
357 )
358 };
359
360 match ret {
361 -1 => Err(Error::last_os_error()),
362 _ => Ok(()),
363 }
364 }
365
366 pub fn remove_pid(&mut self, pid: libc::pid_t, filter: EventFilter) -> Result<()> {
368 let new_watched = self
369 .watched
370 .drain(..)
371 .filter(|x| {
372 if let Ident::Pid(iterpid) = x.ident {
373 iterpid != pid
374 } else {
375 true
376 }
377 })
378 .collect();
379
380 self.watched = new_watched;
381 self.delete_kevents(Ident::Pid(pid), filter)
382 }
383
384 pub fn remove_filename<P: AsRef<Path> + Debug>(
389 &mut self,
390 filename: P,
391 filter: EventFilter,
392 ) -> Result<()> {
393 let mut fd: RawFd = 0;
394 let new_watched = self
395 .watched
396 .drain(..)
397 .filter(|x| {
398 if let Ident::Filename(iterfd, ref iterfile) = x.ident {
399 if iterfile == filename.as_ref().to_str().unwrap() {
400 fd = iterfd;
401 false
402 } else {
403 true
404 }
405 } else {
406 true
407 }
408 })
409 .collect();
410
411 if fd == 0 {
412 return Err(Error::new(
413 io::ErrorKind::NotFound,
414 format!("{filename:?} was not being watched"),
415 ));
416 }
417
418 self.watched = new_watched;
419 let ret = self.delete_kevents(Ident::Fd(fd), filter);
420 let close_err = unsafe { close(fd) };
421 if close_err != 0 {
422 Err(Error::from_raw_os_error(close_err))
423 } else {
424 ret
425 }
426 }
427
428 pub fn remove_fd(&mut self, fd: RawFd, filter: EventFilter) -> Result<()> {
430 let new_watched = self
431 .watched
432 .drain(..)
433 .filter(|x| {
434 if let Ident::Fd(iterfd) = x.ident {
435 iterfd != fd
436 } else {
437 true
438 }
439 })
440 .collect();
441
442 self.watched = new_watched;
443 let ret = self.delete_kevents(Ident::Fd(fd), filter);
444 let close_err = unsafe { close(fd) };
445 if close_err != 0 {
446 Err(Error::from_raw_os_error(close_err))
447 } else {
448 ret
449 }
450 }
451
452 pub fn remove_file(&mut self, file: &File, filter: EventFilter) -> Result<()> {
454 self.remove_fd(file.as_raw_fd(), filter)
455 }
456
457 pub fn watch(&mut self) -> Result<()> {
461 let kevs: Vec<kevent> = self
462 .watched
463 .iter()
464 .map(|watched| {
465 let raw_ident = match watched.ident {
466 Ident::Fd(fd) => fd as uintptr_t,
467 Ident::Filename(fd, _) => fd as uintptr_t,
468 Ident::Pid(pid) => pid as uintptr_t,
469 Ident::Signal(sig) => sig as uintptr_t,
470 Ident::Timer(ident) => ident as uintptr_t,
471 };
472
473 kevent::new(
474 raw_ident,
475 watched.filter,
476 if self.opts.clear {
477 EventFlag::EV_ADD | EventFlag::EV_CLEAR
478 } else {
479 EventFlag::EV_ADD
480 },
481 watched.flags,
482 )
483 })
484 .collect();
485
486 let ret = unsafe {
487 kevent(
488 self.queue,
489 kevs.as_ptr(),
490 #[allow(clippy::useless_conversion)]
492 i32::try_from(kevs.len()).unwrap().try_into().unwrap(),
493 ptr::null_mut(),
494 0,
495 ptr::null(),
496 )
497 };
498
499 self.started = true;
500 match ret {
501 -1 => Err(Error::last_os_error()),
502 _ => Ok(()),
503 }
504 }
505
506 pub fn poll(&self, timeout: Option<Duration>) -> Option<Event> {
509 match timeout {
512 Some(timeout) => get_event(self, Some(timeout)),
513 None => get_event(self, Some(Duration::new(0, 0))),
514 }
515 }
516
517 pub fn poll_forever(&self, timeout: Option<Duration>) -> Option<Event> {
520 if timeout.is_some() {
521 self.poll(timeout)
522 } else {
523 get_event(self, None)
524 }
525 }
526
527 pub fn iter(&self) -> EventIter<'_> {
530 EventIter { watcher: self }
531 }
532}
533
534impl AsRawFd for Watcher {
535 fn as_raw_fd(&self) -> RawFd {
536 self.queue
537 }
538}
539
540impl Drop for Watcher {
541 fn drop(&mut self) {
542 unsafe { libc::close(self.queue) };
543 for watched in &self.watched {
544 match watched.ident {
545 Ident::Fd(fd) => unsafe { libc::close(fd) },
546 Ident::Filename(fd, _) => unsafe { libc::close(fd) },
547 _ => continue,
548 };
549 }
550 }
551}
552
553fn find_file_ident(watcher: &Watcher, fd: RawFd) -> Option<Ident> {
554 for watched in &watcher.watched {
555 match watched.ident.clone() {
556 Ident::Fd(ident_fd) => {
557 if fd == ident_fd {
558 return Some(Ident::Fd(fd));
559 } else {
560 continue;
561 }
562 }
563 Ident::Filename(ident_fd, ident_str) => {
564 if fd == ident_fd {
565 return Some(Ident::Filename(ident_fd, ident_str));
566 } else {
567 continue;
568 }
569 }
570 _ => continue,
571 }
572 }
573
574 None
575}
576
577fn get_event(watcher: &Watcher, timeout: Option<Duration>) -> Option<Event> {
578 let mut kev = kevent::new(
579 0,
580 EventFilter::EVFILT_SYSCOUNT,
581 EventFlag::empty(),
582 FilterFlag::empty(),
583 );
584 let ret = if let Some(ts) = timeout {
585 unsafe {
586 kevent(
587 watcher.queue,
588 ptr::null(),
589 0,
590 &mut kev,
591 1,
592 &duration_to_timespec(ts),
593 )
594 }
595 } else {
596 unsafe { kevent(watcher.queue, ptr::null(), 0, &mut kev, 1, ptr::null()) }
597 };
598
599 match ret {
600 -1 => Some(Event::from_error(kev, watcher)),
601 0 => None, _ => Some(Event::new(kev, watcher)),
603 }
604}
605
606impl Event {
609 #[doc(hidden)]
610 pub fn new(ev: kevent, watcher: &Watcher) -> Event {
611 let data = match ev.filter {
612 EventFilter::EVFILT_READ => EventData::ReadReady(ev.data as usize),
613 EventFilter::EVFILT_WRITE => EventData::WriteReady(ev.data as usize),
614 EventFilter::EVFILT_SIGNAL => EventData::Signal(ev.data as usize),
615 EventFilter::EVFILT_TIMER => EventData::Timer(ev.data as usize),
616 EventFilter::EVFILT_PROC => {
617 let inner = if ev.fflags.contains(FilterFlag::NOTE_EXIT) {
618 Proc::Exit(ev.data as usize)
619 } else if ev.fflags.contains(FilterFlag::NOTE_FORK) {
620 Proc::Fork
621 } else if ev.fflags.contains(FilterFlag::NOTE_EXEC) {
622 Proc::Exec
623 } else if ev.fflags.contains(FilterFlag::NOTE_TRACK) {
624 Proc::Track(ev.data as libc::pid_t)
625 } else if ev.fflags.contains(FilterFlag::NOTE_CHILD) {
626 Proc::Child(ev.data as libc::pid_t)
627 } else {
628 panic!("proc filterflag not supported: {0:?}", ev.fflags)
629 };
630
631 EventData::Proc(inner)
632 }
633 EventFilter::EVFILT_VNODE => {
634 let inner = if ev.fflags.contains(FilterFlag::NOTE_DELETE) {
635 Vnode::Delete
636 } else if ev.fflags.contains(FilterFlag::NOTE_WRITE) {
637 Vnode::Write
638 } else if ev.fflags.contains(FilterFlag::NOTE_EXTEND) {
639 Vnode::Extend
640 } else if ev.fflags.contains(FilterFlag::NOTE_ATTRIB) {
641 Vnode::Attrib
642 } else if ev.fflags.contains(FilterFlag::NOTE_LINK) {
643 Vnode::Link
644 } else if ev.fflags.contains(FilterFlag::NOTE_RENAME) {
645 Vnode::Rename
646 } else if ev.fflags.contains(FilterFlag::NOTE_REVOKE) {
647 Vnode::Revoke
648 } else {
649 vnode::handle_vnode_extras(ev.fflags)
651 };
652
653 EventData::Vnode(inner)
654 }
655 _ => panic!("eventfilter not supported: {0:?}", ev.filter),
656 };
657
658 let ident = match ev.filter {
659 EventFilter::EVFILT_READ => find_file_ident(watcher, ev.ident as RawFd).unwrap(),
660 EventFilter::EVFILT_WRITE => find_file_ident(watcher, ev.ident as RawFd).unwrap(),
661 EventFilter::EVFILT_VNODE => find_file_ident(watcher, ev.ident as RawFd).unwrap(),
662 EventFilter::EVFILT_SIGNAL => Ident::Signal(ev.ident as i32),
663 EventFilter::EVFILT_TIMER => Ident::Timer(ev.ident as i32),
664 EventFilter::EVFILT_PROC => Ident::Pid(ev.ident as pid_t),
665 _ => panic!("not supported"),
666 };
667
668 Event { ident, data }
669 }
670
671 #[doc(hidden)]
672 pub fn from_error(ev: kevent, watcher: &Watcher) -> Event {
673 let ident = match ev.filter {
674 EventFilter::EVFILT_READ => find_file_ident(watcher, ev.ident as RawFd).unwrap(),
675 EventFilter::EVFILT_WRITE => find_file_ident(watcher, ev.ident as RawFd).unwrap(),
676 EventFilter::EVFILT_VNODE => find_file_ident(watcher, ev.ident as RawFd).unwrap(),
677 EventFilter::EVFILT_SIGNAL => Ident::Signal(ev.ident as i32),
678 EventFilter::EVFILT_TIMER => Ident::Timer(ev.ident as i32),
679 EventFilter::EVFILT_PROC => Ident::Pid(ev.ident as pid_t),
680 _ => panic!("not supported"),
681 };
682
683 Event {
684 data: EventData::Error(io::Error::last_os_error()),
685 ident,
686 }
687 }
688
689 #[doc(hidden)]
690 pub fn is_err(&self) -> bool {
691 matches!(self.data, EventData::Error(_))
692 }
693}
694
695impl Iterator for EventIter<'_> {
696 type Item = Event;
697
698 fn next(&mut self) -> Option<Self::Item> {
701 if !self.watcher.started {
702 return None;
703 }
704
705 get_event(self.watcher, None)
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::{EventData, EventFilter, FilterFlag, Ident, Vnode, Watcher};
712 use std::fs;
713 use std::io::{ErrorKind, Write};
714 use std::os::unix::io::{AsRawFd, FromRawFd};
715 use std::path::Path;
716 use std::thread;
717 use std::time;
718
719 #[cfg(target_os = "freebsd")]
720 use std::process;
721
722 #[test]
723 fn test_new_watcher() {
724 let mut watcher = Watcher::new().expect("new failed");
725 let file = tempfile::tempfile().expect("Couldn't create tempfile");
726
727 watcher
728 .add_file(&file, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
729 .expect("add failed");
730 watcher.watch().expect("watch failed");
731 }
732
733 #[test]
734 fn test_filename() {
735 let mut watcher = Watcher::new().expect("new failed");
736 let file = tempfile::NamedTempFile::new().expect("Couldn't create tempfile");
737
738 watcher
739 .add_filename(
740 file.path(),
741 EventFilter::EVFILT_VNODE,
742 FilterFlag::NOTE_WRITE,
743 )
744 .expect("add failed");
745 watcher.watch().expect("watch failed");
746
747 let mut new_file = fs::OpenOptions::new()
748 .write(true)
749 .open(file.path())
750 .expect("open failed");
751
752 new_file.write_all(b"foo").expect("write failed");
753
754 thread::sleep(time::Duration::from_secs(1));
755
756 let ev = watcher.iter().next().expect("Could not get a watch");
757 assert!(matches!(ev.data, EventData::Vnode(Vnode::Write)));
758
759 match ev.ident {
760 Ident::Filename(_, name) => assert!(Path::new(&name) == file.path()),
761 _ => panic!(),
762 };
763 }
764
765 #[test]
766 fn test_file() {
767 let mut watcher = Watcher::new().expect("new failed");
768 let mut file = tempfile::tempfile().expect("Could not create tempfile");
769
770 watcher
771 .add_file(&file, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
772 .expect("add failed");
773 watcher.watch().expect("watch failed");
774 file.write_all(b"foo").expect("write failed");
775
776 thread::sleep(time::Duration::from_secs(1));
777
778 let ev = watcher.iter().next().expect("Didn't get an event");
779
780 assert!(matches!(ev.data, EventData::Vnode(Vnode::Write)));
781 assert!(matches!(ev.ident, Ident::Fd(_)));
782 }
783
784 #[test]
785 fn test_delete_filename() {
786 let mut watcher = Watcher::new().expect("new failed");
787
788 let file = tempfile::NamedTempFile::new().expect("Could not create tempfile");
789 let filename = file.path();
790
791 watcher
792 .add_filename(filename, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
793 .expect("add failed");
794 watcher.watch().expect("watch failed");
795 watcher
796 .remove_filename(filename, EventFilter::EVFILT_VNODE)
797 .expect("delete failed");
798 }
799
800 #[test]
801 fn test_dupe() {
802 let mut watcher = Watcher::new().expect("new failed");
803 let file = tempfile::NamedTempFile::new().expect("Couldn't create tempfile");
804 let filename = file.path();
805
806 watcher
807 .add_filename(filename, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
808 .expect("add failed");
809 watcher
810 .add_filename(filename, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
811 .expect("second add failed");
812
813 assert_eq!(
814 watcher.watched.len(),
815 1,
816 "Did not get an expected number of events"
817 );
818 }
819
820 #[test]
821 fn test_two_files() {
822 let mut watcher = Watcher::new().expect("new failed");
823
824 let mut first_file = tempfile::tempfile().expect("Unable to create first temporary file");
825 let mut second_file = tempfile::tempfile().expect("Unable to create second temporary file");
826
827 watcher
828 .add_file(
829 &first_file,
830 EventFilter::EVFILT_VNODE,
831 FilterFlag::NOTE_WRITE,
832 )
833 .expect("add failed");
834
835 watcher
836 .add_file(
837 &second_file,
838 EventFilter::EVFILT_VNODE,
839 FilterFlag::NOTE_WRITE,
840 )
841 .expect("add failed");
842
843 watcher.watch().expect("watch failed");
844 first_file.write_all(b"foo").expect("first write failed");
845 second_file.write_all(b"foo").expect("second write failed");
846
847 thread::sleep(time::Duration::from_secs(1));
848
849 watcher.iter().next().expect("didn't get any events");
850 watcher.iter().next().expect("didn't get any events");
851 }
852
853 #[test]
854 fn test_nested_kqueue() {
855 let mut watcher = Watcher::new().expect("Failed to create main watcher");
856 let mut nested_watcher = Watcher::new().expect("Failed to create nested watcher");
857
858 let kqueue_file = unsafe { fs::File::from_raw_fd(nested_watcher.as_raw_fd()) };
859 watcher
860 .add_file(&kqueue_file, EventFilter::EVFILT_READ, FilterFlag::empty())
861 .expect("add_file failed for main watcher");
862
863 let mut file = tempfile::tempfile().expect("Couldn't create tempfile");
864 nested_watcher
865 .add_file(&file, EventFilter::EVFILT_VNODE, FilterFlag::NOTE_WRITE)
866 .expect("add_file failed for nested watcher");
867
868 watcher.watch().expect("watch failed on main watcher");
869 nested_watcher
870 .watch()
871 .expect("watch failed on nested watcher");
872
873 file.write_all(b"foo").expect("write failed");
874
875 thread::sleep(time::Duration::from_secs(1));
876
877 watcher.iter().next().expect("didn't get any events");
878 nested_watcher.iter().next().expect("didn't get any events");
879 }
880
881 #[test]
882 #[cfg(target_os = "freebsd")]
883 fn test_close_read() {
884 let mut watcher = Watcher::new().expect("new failed");
885
886 {
887 let file = tempfile::NamedTempFile::new().expect("temporary file failed to create");
888 watcher
889 .add_filename(
890 file.path(),
891 EventFilter::EVFILT_VNODE,
892 FilterFlag::NOTE_CLOSE,
893 )
894 .expect("add failed");
895 watcher.watch().expect("watch failed");
896
897 process::Command::new("cat")
900 .arg(file.path())
901 .spawn()
902 .expect("should spawn a file");
903 thread::sleep(time::Duration::from_secs(1));
904 }
905 let ev = watcher.iter().next().expect("did not receive event");
906 assert!(matches!(ev.data, EventData::Vnode(Vnode::Close)));
907 }
908
909 #[test]
910 #[cfg(target_os = "freebsd")]
911 fn test_close_write() {
912 let mut watcher = match Watcher::new() {
913 Ok(wat) => wat,
914 Err(_) => panic!("new failed"),
915 };
916
917 {
918 let file = tempfile::NamedTempFile::new().expect("couldn't create tempfile");
919 watcher
920 .add_filename(
921 file.path(),
922 EventFilter::EVFILT_VNODE,
923 FilterFlag::NOTE_CLOSE_WRITE,
924 )
925 .expect("add failed");
926 watcher.watch().expect("watch failed");
927
928 process::Command::new("cat")
930 .arg(file.path())
931 .spawn()
932 .expect("should spawn a file");
933 thread::sleep(time::Duration::from_secs(1));
934 }
935 let ev = watcher.iter().next().expect("didn't get an event");
936 assert!(matches!(ev.data, EventData::Vnode(Vnode::CloseWrite)));
937 }
938
939 #[test]
940 fn test_not_found_remove_watch() {
941 let mut watcher = Watcher::new().unwrap();
942
943 let ret = watcher.remove_filename("foo", EventFilter::EVFILT_VNODE);
944 assert!(ret.is_err());
945
946 let err = ret.unwrap_err();
947 assert_eq!(err.kind(), ErrorKind::NotFound);
948 assert_eq!(err.to_string(), "\"foo\" was not being watched");
949 }
950}