File Coverage

blib/lib/File/ChangeNotify/Watcher/Inotify.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::Inotify;
2              
3 2     2   909 use strict;
  2         4  
  2         59  
4 2     2   10 use warnings;
  2         4  
  2         51  
5 2     2   11 use namespace::autoclean;
  2         4  
  2         15  
6              
7             our $VERSION = '0.30';
8              
9 2     2   179 use File::Find qw( find );
  2         4  
  2         116  
10 2     2   160 use Linux::Inotify2 1.2;
  0            
  0            
11             use Types::Standard qw( Bool Int );
12             use Type::Utils qw( class_type );
13              
14             use Moo;
15              
16             has is_blocking => (
17             is => 'ro',
18             isa => Bool,
19             default => 1,
20             );
21              
22             has _inotify => (
23             is => 'ro',
24             isa => class_type('Linux::Inotify2'),
25             default => sub {
26             Linux::Inotify2->new
27             or die "Cannot construct a Linux::Inotify2 object: $!";
28             },
29             init_arg => undef,
30             );
31              
32             has _mask => (
33             is => 'ro',
34             isa => Int,
35             lazy => 1,
36             builder => '_build_mask',
37             );
38              
39             with 'File::ChangeNotify::Watcher';
40              
41             sub sees_all_events {1}
42              
43             sub BUILD {
44             my $self = shift;
45              
46             $self->_inotify->blocking( $self->is_blocking );
47              
48             # If this is done via a lazy_build then the call to
49             # ->_watch_directory ends up causing endless recursion when it
50             # calls ->_inotify itself.
51             $self->_watch_directory($_) for @{ $self->directories };
52              
53             $self->_set_map( $self->_current_map )
54             if $self->modify_includes_file_attributes
55             || $self->modify_includes_content;
56              
57             return;
58             }
59              
60             sub wait_for_events {
61             my $self = shift;
62              
63             $self->_inotify->blocking(1);
64              
65             while (1) {
66             my @events = $self->_interesting_events;
67             return @events if @events;
68             }
69             }
70              
71             around new_events => sub {
72             my $orig = shift;
73             my $self = shift;
74              
75             $self->_inotify->blocking(0);
76              
77             return $self->$orig(@_);
78             };
79              
80             sub _interesting_events {
81             my $self = shift;
82              
83             # This may be a blocking read, in which case it will not return until
84             # something happens. For Catalyst, the restarter will end up calling
85             # ->wait_for_events again after handling the changes.
86             my @events = $self->_inotify->read;
87              
88             my ( $old_map, $new_map );
89             if ( $self->modify_includes_file_attributes
90             || $self->modify_includes_content ) {
91             $old_map = $self->_map;
92             $new_map = $self->_current_map;
93             }
94              
95             my $filter = $self->filter;
96              
97             my @interesting;
98             for my $event (@events) {
99              
100             # An excluded path will show up here if ...
101             #
102             # Something created a new directory and that directory needs to be
103             # excluded or when the exclusion excludes a file, not a dir.
104             next if $self->_path_is_excluded( $event->fullname );
105              
106             ## no critic (ControlStructures::ProhibitCascadingIfElse)
107             if ( $event->IN_CREATE && $event->IN_ISDIR ) {
108             $self->_watch_directory( $event->fullname );
109             push @interesting, $event;
110             push @interesting,
111             $self->_fake_events_for_new_dir( $event->fullname );
112             }
113             elsif ( $event->IN_DELETE_SELF ) {
114             $self->_remove_directory( $event->fullname );
115             }
116             elsif ( $event->IN_ATTRIB ) {
117             next
118             unless $self->_path_matches(
119             $self->modify_includes_file_attributes,
120             $event->fullname
121             );
122             push @interesting, $event;
123             }
124              
125             # We just want to check the _file_ name
126             elsif ( $event->name =~ /$filter/ ) {
127             push @interesting, $event;
128             }
129             }
130              
131             $self->_set_map($new_map)
132             if $self->_has_map;
133              
134             return map {
135             $_->can('path')
136             ? $_
137             : $self->_convert_event( $_, $old_map, $new_map )
138             } @interesting;
139             }
140              
141             sub _build_mask {
142             my $self = shift;
143              
144             my $mask
145             = IN_MODIFY | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF
146             | IN_MOVED_TO;
147             $mask |= IN_DONT_FOLLOW unless $self->follow_symlinks;
148             $mask |= IN_ATTRIB if $self->modify_includes_file_attributes;
149              
150             return $mask;
151             }
152              
153             sub _watch_directory {
154             my $self = shift;
155             my $dir = shift;
156              
157             # A directory could be created & then deleted before we get a
158             # chance to act on it.
159             return unless -d $dir;
160              
161             find(
162             {
163             wanted => sub {
164             my $path = $File::Find::name;
165              
166             if ( $self->_path_is_excluded($path) ) {
167             $File::Find::prune = 1;
168             return;
169             }
170             $self->_add_watch_if_dir($path);
171             },
172             follow_fast => ( $self->follow_symlinks ? 1 : 0 ),
173             no_chdir => 1,
174             follow_skip => 2,
175             },
176             $dir
177             );
178             }
179              
180             sub _add_watch_if_dir {
181             my $self = shift;
182             my $path = shift;
183              
184             return if -l $path && !$self->follow_symlinks;
185              
186             return unless -d $path;
187             return if $self->_path_is_excluded($path);
188              
189             $self->_inotify->watch( $path, $self->_mask );
190             }
191              
192             sub _fake_events_for_new_dir {
193             my $self = shift;
194             my $dir = shift;
195              
196             return unless -d $dir;
197              
198             my @events;
199             File::Find::find(
200             {
201             wanted => sub {
202             my $path = $File::Find::name;
203              
204             return if $path eq $dir;
205             if ( $self->_path_is_excluded($path) ) {
206             $File::Find::prune = 1;
207             return;
208             }
209              
210             push @events,
211             $self->event_class->new(
212             path => $path,
213             type => 'create',
214             );
215             },
216             follow_fast => ( $self->follow_symlinks ? 1 : 0 ),
217             no_chdir => 1
218             },
219             $dir
220             );
221              
222             return @events;
223             }
224              
225             sub _convert_event {
226             my $self = shift;
227             my $event = shift;
228             my $old_map = shift;
229             my $new_map = shift;
230              
231             my $path = $event->fullname;
232             my $type
233             = $event->IN_CREATE || $event->IN_MOVED_TO ? 'create'
234             : $event->IN_MODIFY || $event->IN_ATTRIB ? 'modify'
235             : $event->IN_DELETE ? 'delete'
236             : 'unknown';
237              
238             my @extra;
239             if (
240             $type eq 'modify'
241             && ( $self->modify_includes_file_attributes
242             || $self->modify_includes_content )
243             ) {
244              
245             @extra = (
246             $self->_modify_event_maybe_file_attribute_changes(
247             $path, $old_map, $new_map
248             ),
249             $self->_modify_event_maybe_content_changes(
250             $path, $old_map, $new_map
251             ),
252             );
253             }
254              
255             return $self->event_class->new(
256             path => $path,
257             type => $type,
258             @extra,
259             );
260             }
261              
262             __PACKAGE__->meta->make_immutable;
263              
264             1;
265              
266             # ABSTRACT: Inotify-based watcher subclass
267              
268             __END__