File Coverage

blib/lib/AnyEvent/Filesys/Notify.pm
Criterion Covered Total %
statement 97 105 92.3
branch 38 44 86.3
condition 5 6 83.3
subroutine 22 27 81.4
pod 0 1 0.0
total 162 183 88.5


line stmt bran cond sub pod time code
1             package AnyEvent::Filesys::Notify;
2              
3             # ABSTRACT: An AnyEvent compatible module to monitor files/directories for changes
4              
5 13     13   1143741 use Moo;
  13         117958  
  13         52  
6 13     13   19730 use Moo::Role ();
  13         87135  
  13         262  
7 13     13   4700 use MooX::late;
  13         252375  
  13         74  
8 13     13   6945 use namespace::autoclean;
  13         144317  
  13         45  
9 13     13   10890 use AnyEvent;
  13         58905  
  13         398  
10 13     13   6467 use Path::Iterator::Rule;
  13         129691  
  13         428  
11 13     13   84 use Cwd qw/abs_path/;
  13         24  
  13         594  
12 13     13   5347 use AnyEvent::Filesys::Notify::Event;
  13         54  
  13         435  
13 13     13   88 use Carp;
  13         26  
  13         717  
14 13     13   76 use Try::Tiny;
  13         26  
  13         17753  
15              
16             our $VERSION = '1.23';
17             my $AEFN = 'AnyEvent::Filesys::Notify';
18              
19             has dirs => ( is => 'ro', isa => 'ArrayRef[Str]', required => 1 );
20             has cb => ( is => 'rw', isa => 'CodeRef', required => 1 );
21             has interval => ( is => 'ro', isa => 'Num', default => 2 );
22             has no_external => ( is => 'ro', isa => 'Bool', default => 0 );
23             has backend => ( is => 'ro', isa => 'Str', default => '' );
24             has filter => ( is => 'rw', isa => 'RegexpRef|CodeRef' );
25             has parse_events => ( is => 'rw', isa => 'Bool', default => 0 );
26             has skip_subdirs => ( is => 'ro', isa => 'Bool', default => 0 );
27             has _fs_monitor => ( is => 'rw', );
28             has _old_fs => ( is => 'rw', isa => 'HashRef' );
29             has _watcher => ( is => 'rw', );
30              
31             sub BUILD {
32 13     13 0 91971 my $self = shift;
33              
34 13         80 $self->_old_fs( $self->_scan_fs( $self->dirs ) );
35              
36 13         375 $self->_load_backend;
37 12         57 return $self->_init; # initialize the backend
38             }
39              
40             sub _process_events {
41 54     54   170 my ( $self, @raw_events ) = @_;
42              
43             # Some implementations provided enough information to parse the raw events,
44             # other require rescanning the file system (ie, Mac::FSEvents).
45             # The original behavior was to rescan in all implementations, so we
46             # have added a flag to avoid breaking old code.
47              
48 54         107 my @events;
49              
50 54 100 66     1540 if ( $self->parse_events and $self->can('_parse_events') ) {
51             @events =
52 18     23   316 $self->_parse_events( sub { $self->_apply_filter(@_) }, @raw_events );
  23         83  
53             } else {
54 36         549 my $new_fs = $self->_scan_fs( $self->dirs );
55 36         918 @events =
56             $self->_apply_filter( $self->_diff_fs( $self->_old_fs, $new_fs ) );
57 36         582 $self->_old_fs($new_fs);
58              
59             # Some backends (when not using parse_events) need to add files
60             # (KQueue) or directories (Inotify2) to the watch list after they are
61             # created. Give them a chance to do that here.
62 36 100       1189 $self->_post_process_events(@events)
63             if $self->can('_post_process_events');
64             }
65              
66 54 100       1164 $self->cb->(@events) if @events;
67              
68 54         2354 return \@events;
69             }
70              
71             sub _apply_filter {
72 59     59   149 my ( $self, @events ) = @_;
73              
74 59 100       1207 if ( ref $self->filter eq 'CODE' ) {
    100          
75 47         987 my $cb = $self->filter;
76 47         268 @events = grep { $cb->( $_->path ) } @events;
  57         336  
77             } elsif ( ref $self->filter eq 'Regexp' ) {
78 6         327 my $re = $self->filter;
79 6         49 @events = grep { $_->path =~ $re } @events;
  8         65  
80             }
81              
82 59         597 return @events;
83             }
84              
85             # Return a hash ref representing all the files and stats in @path.
86             # Keys are absolute path and values are path/mtime/size/is_dir
87             # Takes either array or arrayref
88             sub _scan_fs {
89 58     58   105355 my ( $self, @args ) = @_;
90              
91             # Accept either an array of dirs or a array ref of dirs
92 58 100       234 my @paths = ref $args[0] eq 'ARRAY' ? @{ $args[0] } : @args;
  50         153  
93              
94 58         118 my $fs_stats = {};
95              
96 58         463 my $rule = Path::Iterator::Rule->new;
97 58 100 100     928 $rule->skip_subdirs(qr/./)
98             if (ref $self) =~ /^AnyEvent::Filesys::Notify/
99             && $self->skip_subdirs;
100 58         576 my $next = $rule->iter(@paths);
101 58         6748 while ( my $file = $next->() ) {
102 528 100       43328 my $stat = $self->_stat($file)
103             or next; # Skip files that we can't stat (ie, broken symlinks on ext4)
104 526         14076 $fs_stats->{ abs_path($file) } = $stat;
105             }
106              
107 58         2024 return $fs_stats;
108             }
109              
110             sub _diff_fs {
111 43     43   852 my ( $self, $old_fs, $new_fs ) = @_;
112 43         114 my @events = ();
113              
114 43         179 for my $path ( keys %$old_fs ) {
115 361 100       2807 if ( not exists $new_fs->{$path} ) {
    100          
116             push @events,
117             AnyEvent::Filesys::Notify::Event->new(
118             path => $path,
119             type => 'deleted',
120             is_dir => $old_fs->{$path}->{is_dir},
121 14         306 );
122             } elsif (
123             $self->_is_path_modified( $old_fs->{$path}, $new_fs->{$path} ) )
124             {
125             push @events,
126             AnyEvent::Filesys::Notify::Event->new(
127             path => $path,
128             type => 'modified',
129             is_dir => $old_fs->{$path}->{is_dir},
130 16         359 );
131             }
132             }
133              
134 43         405 for my $path ( keys %$new_fs ) {
135 383 100       14304 if ( not exists $old_fs->{$path} ) {
136             push @events,
137             AnyEvent::Filesys::Notify::Event->new(
138             path => $path,
139             type => 'created',
140             is_dir => $new_fs->{$path}->{is_dir},
141 36         784 );
142             }
143             }
144              
145 43         2346 return @events;
146             }
147              
148             sub _is_path_modified {
149 347     347   547 my ( $self, $old_path, $new_path ) = @_;
150              
151 347 100       725 return 1 if $new_path->{mode} != $old_path->{mode};
152 338 100       672 return if $new_path->{is_dir};
153 197 50       359 return 1 if $new_path->{mtime} != $old_path->{mtime};
154 197 100       360 return 1 if $new_path->{size} != $old_path->{size};
155 190         391 return;
156             }
157              
158             # Originally taken from Filesys::Notify::Simple --Thanks Miyagawa
159             sub _stat {
160 528     528   1004 my ( $self, $path ) = @_;
161              
162 528         5310 my @stat = stat $path;
163              
164             # Return undefined if no stats can be retrieved, as it happens with broken
165             # symlinks (at least under ext4).
166 528 100       1429 return unless @stat;
167              
168             return {
169 526         3189 path => $path,
170             mtime => $stat[9],
171             size => $stat[7],
172             mode => $stat[2],
173             is_dir => -d _,
174             };
175              
176             }
177              
178             # Figure out which backend to use:
179             # I would prefer this to be done at compile time not object build, but I also
180             # want the user to be able to force the Fallback role. Something like an
181             # import flag would be great, but Moose creates an import sub for us and
182             # I'm not sure how to cleanly do it. Maybe need to use traits, but the
183             # documentation suggests traits are for application of roles by object.
184             # This will work for now.
185             sub _load_backend {
186 13     13   24 my $self = shift;
187              
188 13 100       94 if ( $self->backend ) {
    100          
    50          
    0          
    0          
189              
190             # Use the AEFN::Role prefix unless the backend starts with a +
191 3         9 my $prefix = "${AEFN}::Role::";
192 3         8 my $backend = $self->backend;
193 3 100       14 $backend = $prefix . $backend unless $backend =~ s{^\+}{};
194              
195 3     3   208 try { Moo::Role->apply_roles_to_object( $self, $backend ); }
196             catch {
197 0     0   0 croak "Unable to load the specified backend ($backend). You may "
198             . "need to install Linux::INotify2, Mac::FSEvents or IO::KQueue:"
199             . "\n$_";
200             }
201 3         25 } elsif ( $self->no_external ) {
202 3         29 Moo::Role->apply_roles_to_object( $self, "${AEFN}::Role::Fallback" );
203             } elsif ( $^O eq 'linux' ) {
204             try {
205 7     7   503 Moo::Role->apply_roles_to_object( $self,
206             "${AEFN}::Role::Inotify2" );
207             }
208             catch {
209 1     1   1009 croak "Unable to load the Linux plugin. You may want to install "
210             . "Linux::INotify2 or specify 'no_external' (but that is very "
211             . "inefficient):\n$_";
212             }
213 7         102 } elsif ( $^O eq 'darwin' ) {
214             try {
215 0     0   0 Moo::Role->apply_roles_to_object( $self,
216             "${AEFN}::Role::FSEvents" );
217             }
218             catch {
219 0     0   0 croak "Unable to load the Mac plugin. You may want to install "
220             . "Mac::FSEvents or specify 'no_external' (but that is very "
221             . "inefficient):\n$_";
222             }
223 0         0 } elsif ( $^O =~ /bsd/ ) {
224             try {
225 0     0   0 Moo::Role->apply_roles_to_object( $self, "${AEFN}::Role::KQueue" );
226             }
227             catch {
228 0     0   0 croak "Unable to load the BSD plugin. You may want to install "
229             . "IO::KQueue or specify 'no_external' (but that is very "
230             . "inefficient):\n$_";
231             }
232 0         0 } else {
233 0         0 Moo::Role->apply_roles_to_object( $self, "${AEFN}::Role::Fallback" );
234             }
235              
236 12         7080 return 1;
237             }
238              
239             1;
240              
241             __END__