File Coverage

blib/lib/File/ChangeNotify/Watcher/KQueue.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package File::ChangeNotify::Watcher::KQueue;
2              
3 2     2   873 use strict;
  2         5  
  2         56  
4 2     2   10 use warnings;
  2         4  
  2         44  
5 2     2   10 use namespace::autoclean;
  2         4  
  2         11  
6              
7             our $VERSION = '0.29';
8              
9 2     2   186 use File::Find ();
  2         3  
  2         32  
10 2     2   177 use IO::KQueue;
  0            
  0            
11             use Types::Standard qw( HashRef Int );
12             use Type::Utils qw( class_type );
13              
14             use Moo;
15              
16             has absorb_delay => (
17             is => 'ro',
18             isa => Int,
19             default => 100,
20             );
21              
22             has _kqueue => (
23             is => 'ro',
24             isa => class_type('IO::KQueue'),
25             default => sub { IO::KQueue->new },
26             init_arg => undef,
27             );
28              
29             # We need to keep hold of filehandles for all the directories *and* files in the
30             # tree. KQueue events will be automatically deleted when the filehandles go out
31             # of scope.
32             has _files => (
33             is => 'ro',
34             isa => HashRef,
35             default => sub { {} },
36             init_arg => undef,
37             );
38              
39             with 'File::ChangeNotify::Watcher';
40              
41             sub sees_all_events {0}
42              
43             sub BUILD {
44             my ($self) = @_;
45             $self->_watch_dir($_) for @{ $self->directories };
46             }
47              
48             sub wait_for_events {
49             my ($self) = @_;
50              
51             while (1) {
52             my @events = $self->_get_events;
53             return @events if @events;
54             }
55             }
56              
57             sub new_events {
58             my ($self) = @_;
59             my @events = $self->_get_events(0);
60             }
61              
62             sub _get_events {
63             my ( $self, $timeout ) = @_;
64              
65             my @kevents = $self->_kqueue->kevent( defined $timeout ? $timeout : () );
66              
67             # Events come in groups, wait for a short period to absorb any extra ones
68             # that might happen immediately after the ones we've detected.
69             push @kevents, $self->_kqueue->kevent( $self->absorb_delay )
70             if $self->absorb_delay;
71              
72             my @events;
73             foreach my $kevent (@kevents) {
74             my $path = $kevent->[KQ_UDATA];
75             next if $self->_path_is_excluded($path);
76              
77             my $flags = $kevent->[KQ_FFLAGS];
78              
79             # Delete - this works reasonably well with KQueue
80             if ( $flags & NOTE_DELETE ) {
81             delete $self->_files->{$path};
82             push @events, $self->_event( $path, 'delete' );
83             }
84              
85             # Rename - represented as deletes and creates
86             elsif ( $flags & NOTE_RENAME ) {
87              
88             # Renamed dirs
89             # Use the stored filehandle (it survives renaming) to identify a dir
90             # and remove any filehandles we're storing to its contents
91             my $fh = $self->_files->{$path};
92             if ( -d $fh ) {
93             foreach my $stored_path ( keys %{ $self->_files } ) {
94             next unless index( $stored_path, $path ) == 0;
95             delete $self->_files->{$stored_path};
96             push @events, $self->_event( $stored_path, 'delete' );
97             }
98             }
99              
100             # Renamed files
101             else {
102             delete $self->_files->{$path};
103             push @events, $self->_event( $path, 'delete' );
104             }
105             }
106              
107             # Modify/Create - writes to files indicate modification, but we get
108             # writes to dirs too, which indicates a file (or dir) was created or
109             # removed from the dir. Deletes are picked up by delete events, but to
110             # find created files we have to scan the dir again.
111             elsif ( $flags & NOTE_WRITE ) {
112              
113             if ( -f $path ) {
114             push @events, $self->_event( $path, 'modify' );
115             }
116             elsif ( -d $path ) {
117             push @events,
118             map { $self->_event( $_, 'create' ) }
119             $self->_watch_dir($path);
120             }
121             }
122             }
123              
124             return @events;
125             }
126              
127             sub _event {
128             my ( $self, $path, $type ) = @_;
129             return $self->event_class->new( path => $path, type => $type );
130             }
131              
132             sub _watch_dir {
133             my ( $self, $dir ) = @_;
134              
135             my @new_files;
136              
137             # use find(), finddepth() doesn't support pruning
138             $self->_find(
139             $dir,
140             sub {
141             my $path = $File::Find::name;
142              
143             # Don't monitor anything below excluded dirs
144             return $File::Find::prune = 1
145             if $self->_path_is_excluded($path);
146              
147             # Skip file names that don't match the filter
148             return unless $self->_is_included_file($path);
149              
150             # Skip if we're watching it already
151             return if $self->_files->{$path};
152              
153             $self->_watch_file($path);
154             push @new_files, $path;
155             }
156             );
157              
158             return @new_files;
159             }
160              
161             sub _is_included_file {
162             my ( $self, $path ) = @_;
163              
164             return 1 if -d $path;
165              
166             my $filter = $self->filter;
167             my $filename = ( File::Spec->splitpath($path) )[2];
168             return 1 if $filename =~ m{$filter};
169             }
170              
171             sub _find {
172             my ( $self, $dir, $wanted ) = @_;
173             File::Find::find(
174             {
175             wanted => $wanted,
176             no_chdir => 1,
177             follow_fast => ( $self->follow_symlinks ? 1 : 0 ),,
178             follow_skip => 2,
179             },
180             $dir,
181             );
182             }
183              
184             sub _watch_file {
185             my ( $self, $file ) = @_;
186              
187             ## no critic (InputOutput::RequireBriefOpen)
188              
189             # Don't panic if we can't open a file
190             open my $fh, '<', $file or warn "Can't open '$file': $!";
191             return unless $fh && defined fileno $fh;
192              
193             # Store this filehandle (this will automatically nuke any existing events
194             # assigned to the file)
195             $self->_files->{$file} = $fh;
196              
197             # Watch it for changes
198             $self->_kqueue->EV_SET(
199             fileno($fh),
200             EVFILT_VNODE,
201             EV_ADD | EV_CLEAR,
202             NOTE_DELETE | NOTE_WRITE | NOTE_RENAME | NOTE_REVOKE,
203             0,
204             $file,
205             );
206             }
207              
208             __PACKAGE__->meta->make_immutable;
209              
210             1;
211              
212             __END__