File Coverage

blib/lib/AnyEvent/Filesys/Watcher.pm
Criterion Covered Total %
statement 151 172 87.7
branch 60 84 71.4
condition 14 30 46.6
subroutine 31 32 96.8
pod 5 5 100.0
total 261 323 80.8


line stmt bran cond sub pod time code
1             #! /bin/false
2              
3             # This next lines is here to make Dist::Zilla happy.
4             # ABSTRACT: Watch file system for changes
5              
6             package AnyEvent::Filesys::Watcher;
7              
8 17     17   2697135 use strict;
  17         45  
  17         1138  
9              
10             our $VERSION = 'v0.1.1'; # VERSION
11              
12 17     17   11594 use Locale::TextDomain ('AnyEvent-Filesys-Watcher');
  17         511247  
  17         140  
13 17     17   680621 use Scalar::Util qw(reftype);
  17         51  
  17         959  
14 17     17   12188 use Path::Iterator::Rule;
  17         300287  
  17         877  
15 17     17   158 use File::Spec;
  17         40  
  17         525  
16 17     17   92 use Cwd;
  17         31  
  17         1470  
17 17     17   109 use Scalar::Util qw(reftype);
  17         32  
  17         896  
18              
19 17     17   10664 use AnyEvent::Filesys::Watcher::Event;
  17         52  
  17         53817  
20              
21             # This constructor is kind of doing reversed inheritance. It first sets up
22             # the module, then selects a backend which is then instantiated. The
23             # backend is expected to invoke the protected constructor _new() below.
24             #
25             # Using the factory pattern would be the cleaner approach but we want to
26             # retain a certain compatibility with the original AnyEvent::Filesys::Notify,
27             # because the module is easier to use that way.
28             sub new {
29 32     32 1 3111437 my ($class, %args) = @_;
30              
31 32         115 my $backend_class = $args{backend};
32              
33 32 100 66     262 if (exists $args{cb} && !exists $args{callback}) {
34 1         3 $args{callback} = delete $args{cb};
35             }
36              
37 32 50 33     216 if (exists $args{dirs} && !exists $args{directories}) {
38 0         0 $args{directories} = delete $args{dirs};
39             }
40              
41 32 100       205 if ($backend_class) {
    50          
    0          
    0          
    0          
42             # Use the AEFW:: prefix unless the backend starts with a plus.
43 16 100       161 unless ($backend_class =~ s/^\+//) {
44 15         72 $backend_class = "AnyEvent::Filesys::Watcher::"
45             . $backend_class;
46             }
47             } elsif ($^O eq 'linux') {
48 16         79 $backend_class = 'AnyEvent::Filesys::Watcher::Inotify2';
49             } elsif ($^O eq 'darwin') {
50 0         0 $backend_class = "AnyEvent::Filesys::Watcher::FSEvents";
51             } elsif ($^O eq 'MSWin32') {
52 0         0 $backend_class = "AnyEvent::Filesys::Watcher::ReadDirectoryChanges";
53             } elsif ($^O =~ /bsd/) {
54 0         0 $backend_class = "AnyEvent::Filesys::Watcher::KQueue";
55             } else {
56 0         0 $backend_class = "AnyEvent::Filesys::Watcher::Fallback";
57             }
58              
59 32         131 my $backend_module = $backend_class . '.pm';
60 32         381 $backend_module =~ s{::}{/}g;
61              
62 32         100 my $self;
63 32         144 eval {
64 32         10585 require $backend_module;
65 31         397 $self = $backend_class->new(%args);
66             };
67 32 100       886 if ($@) {
68 1 50       34 if ($@ =~ /^Can't locate $backend_module in \@INC/) {
69 0         0 warn __x(<<"EOF", class => $backend_class);
70             Missing backend module '{class}'!
71             You either have to install it or specify 'Fallback' as the backend but that is
72             not very efficient.
73              
74             Original error message:
75             EOF
76             }
77              
78 1         14 die $@;
79             }
80              
81 31         5014 return $self;
82             }
83              
84             sub _new {
85 32     32   156 my ($class, %args) = @_;
86              
87 32         105 my $self = bless {}, $class;
88              
89             # Resolve aliases once more. This is necessary so that the backend classes
90             # can be instantiated directly.
91 32 50 33     202 if (exists $args{dirs} && !exists $args{directories}) {
92 0         0 $args{directories} = delete $args{dirs};
93             }
94              
95 32 50 33     148 if (exists $args{cb} && !exists $args{callback}) {
96 0         0 $args{callback} = delete $args{cb};
97             }
98              
99 32 50       132 if (!exists $args{callback}) {
100 0         0 require Carp;
101 0         0 Carp::croak(__"The option 'callback' is mandatory");
102             }
103              
104 32 50       194 if (reftype $args{callback} ne 'CODE') {
105 0         0 require Carp;
106 0         0 Carp::croak(__"The argument to 'callback' must be a code reference");
107             }
108              
109 32 50 66     163 if (exists $args{raw_events} && reftype $args{raw_events} ne 'CODE') {
110 0         0 require Carp;
111 0         0 Carp::croak(__"The argument to 'raw_events' must be a code reference");
112             }
113              
114 32 50       119 if (!exists $args{base_dir}) {
115 32         260536 $args{base_dir} = Cwd::cwd();
116             }
117              
118 32 50       751 if (!exists $args{directories}) {
119 0         0 $args{directories} = $args{base_dir};
120             }
121              
122 32 50       566 $args{interval} = 1 if !exists $args{interval};
123             $args{directories} = [$args{directories}]
124 32 100       430 if !ref $args{directories};
125 32 100 66     646 if (exists $args{filter}
      66        
126             && defined $args{filter}
127             && length $args{filter}) {
128 19         425 $args{filter} = $self->__compileFilter($args{filter});
129             } else {
130 13     5   425 $args{filter} = sub { 1 };
  5         53  
131             }
132              
133 32         657 foreach my $arg (keys %args) {
134 223         1960 $self->{'__' . $arg} = $args{$arg};
135             }
136              
137 32         651 $self->_oldFilesystem($self->_scanFilesystem($self->directories));
138              
139 32         328 return $self;
140             }
141              
142             sub directories {
143 42     42 1 198 my ($self) = @_;
144              
145 42         146 return [@{$self->{__directories}}];
  42         552  
146             }
147              
148             sub interval {
149 28     28 1 384 shift->{__interval};
150             }
151              
152             sub callback {
153 47     47 1 170 my ($self, $cb) = @_;
154              
155 47 50       232 if (@_ > 1) {
156 0         0 $self->{__callback} = $cb;
157             }
158              
159 47         208 return $self->{__callback};
160             }
161              
162             sub filter {
163 52     52 1 120 my ($self, $filter) = @_;
164              
165 52 50       157 if (@_ > 1) {
166 0         0 $self->{__filter} = $self->__compileFilter($filter);
167             }
168              
169 52         131 return $self->{__filter};
170             }
171              
172             # Taken from AnyEvent::Filesys::Notify.
173             sub _scanFilesystem {
174 51     51   18391 my ($self, @args) = @_;
175              
176             # Accept either an array of directories or an array reference of
177             # directories.
178 51 100       363 my @paths = ref $args[0] eq 'ARRAY' ? @{ $args[0] } : @args;
  43         197  
179              
180 51         178 my $fs_stats = {};
181              
182 51         1349 my $rule = Path::Iterator::Rule->new;
183 51         1473 my $next = $rule->iter(@paths);
184 51         12469 while (my $file = $next->()) {
185 407         47693 my $path = $self->_makeAbsolute($file);
186 407 100       1286 my %stat = $self->_stat($path)
187             or next; # Skip files that we cannot stat.
188 404         3535 $fs_stats->{$path} = \%stat;
189             }
190              
191 51         4030 return $fs_stats;
192             }
193              
194             sub _makeAbsolute {
195 407     407   968 my ($self, $path) = @_;
196              
197 407         9874 $path = File::Spec->rel2abs($path, $self->{__base_dir});
198 407 50 33     4197 if ('MSWin32' eq $^O || 'cygwin' eq $^O || 'os2' eq $^O || 'dos' eq $^O) {
      33        
      33        
199             # This is what Cwd does.
200 0         0 $path =~ s{\\}{/}g;
201             }
202              
203 407         1031 return $path;
204             }
205              
206             # Taken from AnyEvent::Filesys::Notify.
207             sub _diffFilesystem {
208 17     17   1109 my ($self, $old_fs, $new_fs) = @_;
209 17         63 my @events = ();
210              
211 17         171 for my $path (keys %$old_fs) {
212 87 100       354 if (not exists $new_fs->{$path}) {
    100          
213             push @events,
214             AnyEvent::Filesys::Watcher::Event->new(
215             path => $path,
216             type => 'deleted',
217             is_directory => $old_fs->{$path}->{is_directory},
218 12         107 );
219             } elsif ($self->__isPathModified($old_fs->{$path}, $new_fs->{$path})) {
220             push @events,
221             AnyEvent::Filesys::Watcher::Event->new(
222             path => $path,
223             type => 'modified',
224             is_directory => $old_fs->{$path}->{is_directory},
225 14         164 );
226             }
227             }
228              
229 17         68 for my $path (keys %$new_fs) {
230 90 100       223 if (not exists $old_fs->{$path}) {
231             push @events,
232             AnyEvent::Filesys::Watcher::Event->new(
233             path => $path,
234             type => 'created',
235             is_directory => $new_fs->{$path}->{is_directory},
236 15         154 );
237             }
238             }
239              
240 17         215 return @events;
241             }
242              
243             sub _filesystemMonitor {
244 28     28   87 my ($self, $value) = @_;
245              
246 28 100       152 if (@_ > 1) {
247 18         137 $self->{__filesystem_monitor} = $value;
248             }
249              
250 28         132 return $self->{__filesystem_monitor};
251             }
252              
253             sub _watcher {
254 32     32   50836 my ($self, $watcher) = @_;
255              
256 32 50       255 if (@_ > 1) {
257 32         158 $self->{__watcher} = $watcher;
258             }
259              
260 32         98 return $self->{__watcher};
261             }
262              
263             sub _processEvents {
264 47     47   154 my ($self, @raw_events) = @_;
265              
266 47 100       174 if ($self->{__raw_events}) {
267 3         18 @raw_events = $self->{__raw_events}->(@raw_events);
268             }
269              
270             my @events = $self->_parseEvents(
271 42     42   214 sub { $self->_applyFilter(@_) },
272             @raw_events
273 47         625 );
274              
275 47 50       263 if (@events) {
276 47         196 $self->_postProcessEvents(@events);
277 47 50       272 $self->callback->(@events) if @events;
278             }
279              
280 47         499 return \@events;
281             }
282              
283             sub _parseEvents {
284 10     10   107 shift->_rescan;
285             }
286              
287             sub _rescan {
288 10     10   30 my ($self) = @_;
289              
290 10         63 my $new_fs = $self->_scanFilesystem($self->directories);
291              
292 10         56 my @events = $self->_applyFilter(
293             $self->_diffFilesystem($self->_oldFilesystem, $new_fs));
294 10         73 $self->_oldFilesystem($new_fs);
295              
296 10         40 return @events;
297             }
298              
299             # Some backends need to add files (KQueue) or directories (Inotify2) to the
300             # watch list after they are
301       10     sub _postProcessEvents {}
302              
303             sub _applyFilter {
304 52     52   150 my ($self, @events) = @_;
305              
306 52         169 my $callback = $self->filter;
307 52         123 return grep { $callback->($_) } @events;
  65         388  
308             }
309              
310             sub _oldFilesystem {
311 70     70   257 my ($self, $fs) = @_;
312              
313 70 100       254 if (@_ > 1) {
314 42         318 $self->{__old_filesystem} = $fs;
315             }
316              
317 70         238 return $self->{__old_filesystem};
318             }
319              
320             sub _directoryWrites {
321 0     0   0 shift->{__directory_writes};
322             }
323              
324             sub __compileFilter {
325 19     19   109 my ($self, $filter) = @_;
326              
327 19 100       82 if (!ref $filter) {
328 1         39 $filter = qr/$filter/;
329             }
330              
331 19         179 my $reftype = reftype $filter;
332 19 100       138 if ('REGEXP' eq $reftype) {
    50          
333 2         6 my $regexp = $filter;
334             $filter = sub {
335 4     4   31 my $event = shift;
336 4         17 my $path = $event->path;
337 4         53 my $result = $path =~ $regexp;
338 4         18 return $result;
339 2         37 };
340             } elsif ($reftype ne 'CODE') {
341 0         0 require Carp;
342 0         0 Carp::confess(__("The filter must either be a regular expression or"
343             . " code reference"));
344             }
345              
346 19         76 return $filter;
347             }
348              
349             # Originally taken from Filesys::Notify::Simple --Thanks Miyagawa
350             sub _stat {
351 407     407   904 my ($self, $path) = @_;
352              
353 407         6316 my @stat = stat $path;
354              
355             # Return undefined if no stats can be retrieved, as it happens with broken
356             # symlinks (at least under ext4).
357 407 100       1201 return unless @stat;
358              
359             return (
360 404         4188 path => $path,
361             mtime => $stat[9],
362             size => $stat[7],
363             mode => $stat[2],
364             is_directory => -d _,
365             );
366             }
367              
368             # Taken from AnyEvent::Filesys::Notify.
369             sub __isPathModified {
370 75     75   167 my ($self, $old_path, $new_path) = @_;
371              
372 75 100       218 return 1 if $new_path->{mode} != $old_path->{mode};
373 70 100       267 return if $new_path->{is_directory};
374 37 100       139 return 1 if $new_path->{mtime} != $old_path->{mtime};
375 31 100       103 return 1 if $new_path->{size} != $old_path->{size};
376 28         106 return;
377             }
378              
379             1;