1package AnyEvent::Filesys::Notify; 2 3# ABSTRACT: An AnyEvent compatible module to monitor files/directories for changes 4 5use Moo; 6use Moo::Role (); 7use MooX::late; 8use namespace::autoclean; 9use AnyEvent; 10use Path::Iterator::Rule; 11use Cwd qw/abs_path/; 12use AnyEvent::Filesys::Notify::Event; 13use Carp; 14use Try::Tiny; 15 16our $VERSION = '1.23'; 17my $AEFN = 'AnyEvent::Filesys::Notify'; 18 19has dirs => ( is => 'ro', isa => 'ArrayRef[Str]', required => 1 ); 20has cb => ( is => 'rw', isa => 'CodeRef', required => 1 ); 21has interval => ( is => 'ro', isa => 'Num', default => 2 ); 22has no_external => ( is => 'ro', isa => 'Bool', default => 0 ); 23has backend => ( is => 'ro', isa => 'Str', default => '' ); 24has filter => ( is => 'rw', isa => 'RegexpRef|CodeRef' ); 25has parse_events => ( is => 'rw', isa => 'Bool', default => 0 ); 26has skip_subdirs => ( is => 'ro', isa => 'Bool', default => 0 ); 27has _fs_monitor => ( is => 'rw', ); 28has _old_fs => ( is => 'rw', isa => 'HashRef' ); 29has _watcher => ( is => 'rw', ); 30 31sub BUILD { 32 my $self = shift; 33 34 $self->_old_fs( $self->_scan_fs( $self->dirs ) ); 35 36 $self->_load_backend; 37 return $self->_init; # initialize the backend 38} 39 40sub _process_events { 41 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 my @events; 49 50 if ( $self->parse_events and $self->can('_parse_events') ) { 51 @events = 52 $self->_parse_events( sub { $self->_apply_filter(@_) }, @raw_events ); 53 } else { 54 my $new_fs = $self->_scan_fs( $self->dirs ); 55 @events = 56 $self->_apply_filter( $self->_diff_fs( $self->_old_fs, $new_fs ) ); 57 $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 $self->_post_process_events(@events) 63 if $self->can('_post_process_events'); 64 } 65 66 $self->cb->(@events) if @events; 67 68 return \@events; 69} 70 71sub _apply_filter { 72 my ( $self, @events ) = @_; 73 74 if ( ref $self->filter eq 'CODE' ) { 75 my $cb = $self->filter; 76 @events = grep { $cb->( $_->path ) } @events; 77 } elsif ( ref $self->filter eq 'Regexp' ) { 78 my $re = $self->filter; 79 @events = grep { $_->path =~ $re } @events; 80 } 81 82 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 88sub _scan_fs { 89 my ( $self, @args ) = @_; 90 91 # Accept either an array of dirs or a array ref of dirs 92 my @paths = ref $args[0] eq 'ARRAY' ? @{ $args[0] } : @args; 93 94 my $fs_stats = {}; 95 96 my $rule = Path::Iterator::Rule->new; 97 $rule->skip_subdirs(qr/./) 98 if (ref $self) =~ /^AnyEvent::Filesys::Notify/ 99 && $self->skip_subdirs; 100 my $next = $rule->iter(@paths); 101 while ( my $file = $next->() ) { 102 my $stat = $self->_stat($file) 103 or next; # Skip files that we can't stat (ie, broken symlinks on ext4) 104 $fs_stats->{ abs_path($file) } = $stat; 105 } 106 107 return $fs_stats; 108} 109 110sub _diff_fs { 111 my ( $self, $old_fs, $new_fs ) = @_; 112 my @events = (); 113 114 for my $path ( keys %$old_fs ) { 115 if ( not exists $new_fs->{$path} ) { 116 push @events, 117 AnyEvent::Filesys::Notify::Event->new( 118 path => $path, 119 type => 'deleted', 120 is_dir => $old_fs->{$path}->{is_dir}, 121 ); 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 ); 131 } 132 } 133 134 for my $path ( keys %$new_fs ) { 135 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 ); 142 } 143 } 144 145 return @events; 146} 147 148sub _is_path_modified { 149 my ( $self, $old_path, $new_path ) = @_; 150 151 return 1 if $new_path->{mode} != $old_path->{mode}; 152 return if $new_path->{is_dir}; 153 return 1 if $new_path->{mtime} != $old_path->{mtime}; 154 return 1 if $new_path->{size} != $old_path->{size}; 155 return; 156} 157 158# Originally taken from Filesys::Notify::Simple --Thanks Miyagawa 159sub _stat { 160 my ( $self, $path ) = @_; 161 162 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 return unless @stat; 167 168 return { 169 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. 185sub _load_backend { 186 my $self = shift; 187 188 if ( $self->backend ) { 189 190 # Use the AEFN::Role prefix unless the backend starts with a + 191 my $prefix = "${AEFN}::Role::"; 192 my $backend = $self->backend; 193 $backend = $prefix . $backend unless $backend =~ s{^\+}{}; 194 195 try { Moo::Role->apply_roles_to_object( $self, $backend ); } 196 catch { 197 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 } elsif ( $self->no_external ) { 202 Moo::Role->apply_roles_to_object( $self, "${AEFN}::Role::Fallback" ); 203 } elsif ( $^O eq 'linux' ) { 204 try { 205 Moo::Role->apply_roles_to_object( $self, 206 "${AEFN}::Role::Inotify2" ); 207 } 208 catch { 209 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 } elsif ( $^O eq 'darwin' ) { 214 try { 215 Moo::Role->apply_roles_to_object( $self, 216 "${AEFN}::Role::FSEvents" ); 217 } 218 catch { 219 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 } elsif ( $^O =~ /bsd/ ) { 224 try { 225 Moo::Role->apply_roles_to_object( $self, "${AEFN}::Role::KQueue" ); 226 } 227 catch { 228 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 } else { 233 Moo::Role->apply_roles_to_object( $self, "${AEFN}::Role::Fallback" ); 234 } 235 236 return 1; 237} 238 2391; 240 241__END__ 242 243=pod 244 245=head1 NAME 246 247AnyEvent::Filesys::Notify - An AnyEvent compatible module to monitor files/directories for changes 248 249=head1 VERSION 250 251version 1.23 252 253=head1 STATUS 254 255=for html <img src="https://travis-ci.org/mvgrimes/AnyEvent-Filesys-Notify.svg?branch=master" alt="Build Status"> 256<a href="https://metacpan.org/pod/AnyEvent::Filesys::Notify"><img alt="CPAN version" src="https://badge.fury.io/pl/AnyEvent-Filesys-Notify.svg" /></a> 257 258=head1 SYNOPSIS 259 260 use AnyEvent::Filesys::Notify; 261 262 my $notifier = AnyEvent::Filesys::Notify->new( 263 dirs => [ qw( this_dir that_dir ) ], 264 interval => 2.0, # Optional depending on underlying watcher 265 filter => sub { shift !~ /\.(swp|tmp)$/ }, 266 cb => sub { 267 my (@events) = @_; 268 # ... process @events ... 269 }, 270 parse_events => 1, # Improves efficiency on certain platforms 271 ); 272 273 # enter an event loop, see AnyEvent documentation 274 Event::loop(); 275 276=head1 DESCRIPTION 277 278This module provides a cross platform interface to monitor files and 279directories within an L<AnyEvent> event loop. The heavy lifting is done by 280L<Linux::INotify2> or L<Mac::FSEvents> on their respective O/S. A fallback 281which scans the directories at regular intervals is include for other systems. 282See L</WATCHER IMPLEMENTATIONS> for more on the backends. 283 284Events are passed to the callback (specified as a CodeRef to C<cb> in the 285constructor) in the form of L<AnyEvent::Filesys::Notify::Event>s. 286 287=head1 METHODS 288 289=head2 new() 290 291A constructor for a new AnyEvent watcher that will monitor the files in the 292given directories and execute a callback when a modification is detected. 293No action is take until a event loop is entered. 294 295Arguments for new are: 296 297=over 4 298 299=item dirs 300 301 dirs => [ '/var/log', '/etc' ], 302 303An ArrayRef of directories to watch. Required. 304 305=item interval 306 307 interval => 1.5, # seconds 308 309Specifies the time in fractional seconds between file system checks for 310the L<AnyEvent::Filesys::Notify::Role::Fallback> implementation. 311 312Specifies the latency for L<Mac::FSEvents> for the 313C<AnyEvent::Filesys::Notify::Role::FSEvents> implementation. 314 315Ignored for the C<AnyEvent::Filesys::Notify::Role::Inotify2> implementation. 316 317=item filter 318 319 filter => qr/\.(ya?ml|co?nf|jso?n)$/, 320 filter => sub { shift !~ /\.(swp|tmp)$/, 321 322A CodeRef or Regexp which is used to filter wanted/unwanted events. If this 323is a Regexp, we attempt to match the absolute path name and filter out any 324that do not match. If a CodeRef, the absolute path name is passed as the 325only argument and the event is fired only if there sub returns a true value. 326 327=item cb 328 329 cb => sub { my @events = @_; ... }, 330 331A CodeRef that is called when a modification to the monitored directory(ies) is 332detected. The callback is passed a list of 333L<AnyEvent::Filesys::Notify::Event>s. Required. 334 335=item backend 336 337 backend => 'Fallback', 338 backend => 'KQueue', 339 backend => '+My::Filesys::Notify::Role::Backend', 340 341Force the use of the specified backend. The backend is assumed to have the 342C<AnyEvent::Filesys::Notify::Role> prefix, but you can force a fully qualified 343name by prefixing it with a plus. Optional. 344 345=item no_external 346 347 no_external => 1, 348 349This is retained for backward compatibility. Using C<backend => 'Fallback'> 350is preferred. Force the use of the L</Fallback> watcher implementation. This is 351not encouraged as the L</Fallback> implement is very inefficient, but it does 352not require either L<Linux::INotify2> nor L<Mac::FSEvents>. Optional. 353 354=item parse_events 355 356 parse_events => 1, 357 358In backends that support it (currently INotify2), parse the events instead of 359rescanning file system for changed C<stat()> information. Note, that this might 360cause slight changes in behavior. In particular, the Inotify2 backend will 361generate an additional 'modified' event when a file changes (once when opened 362for write, and once when modified). 363 364=item skip_subdirs 365 366 skip_subdirs => 1, 367 368Skips subdirectories and anything in them while building a list of files/dirs 369to watch. Optional. 370 371=back 372 373=head1 WATCHER IMPLEMENTATIONS 374 375=head2 INotify2 (Linux) 376 377Uses L<Linux::INotify2> to monitor directories. Sets up an C<AnyEvent-E<gt>io> 378watcher to monitor the C<$inotify-E<gt>fileno> filehandle. 379 380=head2 FSEvents (Mac) 381 382Uses L<Mac::FSEvents> to monitor directories. Sets up an C<AnyEvent-E<gt>io> 383watcher to monitor the C<$fsevent-E<gt>watch> filehandle. 384 385=head2 KQueue (BSD/Mac) 386 387Uses L<IO::KQueue> to monitor directories. Sets up an C<AnyEvent-E<gt>io> 388watcher to monitor the C<IO::KQueue> object. 389 390B<WARNING> - L<IO::KQueue> and the C<kqueue()> system call require an open 391filehandle for every directory and file that is being watched. This makes 392it impossible to watch large directory structures (and inefficient to watch 393moderately sized directories). The use of the KQueue backend is discouraged. 394 395=head2 Fallback 396 397A simple scan of the watched directories at regular intervals. Sets up an 398C<AnyEvent-E<gt>timer> watcher which is executed every C<interval> seconds 399(or fractions thereof). C<interval> can be specified in the constructor to 400L<AnyEvent::Filesys::Notify> and defaults to 2.0 seconds. 401 402This is a very inefficient implementation. Use one of the others if possible. 403 404=head1 Why Another Module For File System Notifications 405 406At the time of writing there were several very nice modules that accomplish 407the task of watching files or directories and providing notifications about 408changes. Two of which offer a unified interface that work on any system: 409L<Filesys::Notify::Simple> and L<File::ChangeNotify>. 410 411L<AnyEvent::Filesys::Notify> exists because I need a way to simply tie the 412functionality those modules provide into an event framework. Neither of the 413existing modules seem to work with well with an event loop. 414L<Filesys::Notify::Simple> does not supply a non-blocking interface and 415L<File::ChangeNotify> requires you to poll an method for new events. You could 416fork off a process to run L<Filesys::Notify::Simple> and use an event handler 417to watch for notices from that child, or setup a timer to check 418L<File::ChangeNotify> at regular intervals, but both of those approaches seem 419inefficient or overly complex. Particularly, since the underlying watcher 420implementations (L<Mac::FSEvents> and L<Linux::INotify2>) provide a filehandle 421that you can use and IO event to watch. 422 423This is not slight against the authors of those modules. Both are well 424respected, are certainly finer coders than I am, and built modules which 425are perfect for many situations. If one of their modules will work for you 426by all means use it, but if you are already using an event loop, this 427module may fit the bill. 428 429=head1 SEE ALSO 430 431Modules used to implement this module L<AnyEvent>, L<Mac::FSEvents>, 432L<Linux::INotify2>, L<Moose>. 433 434Alternatives to this module L<Filesys::Notify::Simple>, L<File::ChangeNotify>. 435 436=head1 AUTHOR 437 438Mark Grimes, E<lt>mgrimes@cpan.orgE<gt> 439 440=head1 CONTRIBUTORS 441 442=over 4 443 444=item * 445 446Gasol Wu E<lt>gasol.wu@gmail.comE<gt> who contributed the BSD support for IO::KQueue 447 448=item * 449 450Dave Hayes E<lt>dave@jetcafe.orgE<gt> 451 452=item * 453 454Carsten Wolff E<lt>carsten@wolffcarsten.deE<gt> 455 456=item * 457 458Ettore Di Giacinto (@mudler) 459 460=item * 461 462Martin Barth (@ufobat) 463 464=back 465 466=head1 SOURCE 467 468Source repository is at L<https://github.com/mvgrimes/AnyEvent-Filesys-Notify>. 469 470=head1 BUGS 471 472Please report any bugs or feature requests on the bugtracker website L<http://github.com/mvgrimes/AnyEvent-Filesys-Notify/issues> 473 474When submitting a bug or request, please include a test-file or a 475patch to an existing test-file that illustrates the bug or desired 476feature. 477 478=head1 COPYRIGHT AND LICENSE 479 480This software is copyright (c) 2017 by Mark Grimes, E<lt>mgrimes@cpan.orgE<gt>. 481 482This is free software; you can redistribute it and/or modify it under 483the same terms as the Perl 5 programming language system itself. 484 485=cut 486