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   887 use strict;
  2         5  
  2         61  
4 2     2   45 use warnings;
  2         5  
  2         59  
5 2     2   10 use namespace::autoclean;
  2         5  
  2         11  
6              
7             our $VERSION = '0.29';
8              
9 2     2   174 use File::Find ();
  2         4  
  2         41  
10 2     2   180 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             return $self;
54             }
55              
56             sub wait_for_events {
57             my $self = shift;
58              
59             $self->_inotify()->blocking(1);
60              
61             while (1) {
62             my @events = $self->_interesting_events();
63             return @events if @events;
64             }
65             }
66              
67             around new_events => sub {
68             my $orig = shift;
69             my $self = shift;
70              
71             $self->_inotify()->blocking(0);
72              
73             return $self->$orig(@_);
74             };
75              
76             sub _interesting_events {
77             my $self = shift;
78              
79             my $filter = $self->filter();
80              
81             my @interesting;
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             # ->watch again after handling the changes.
86             for my $event ( $self->_inotify()->read() ) {
87              
88             # An excluded path will show up here if ...
89             #
90             # Something created a new directory and that directory needs to be
91             # excluded or when the exclusion excludes a file, not a dir.
92             next if $self->_path_is_excluded( $event->fullname() );
93              
94             if ( $event->IN_CREATE() && $event->IN_ISDIR() ) {
95             $self->_watch_directory( $event->fullname() );
96             push @interesting, $event;
97             push @interesting,
98             $self->_fake_events_for_new_dir( $event->fullname() );
99             }
100             elsif ( $event->IN_DELETE_SELF() ) {
101             $self->_remove_directory( $event->fullname() );
102             }
103              
104             # We just want to check the _file_ name
105             elsif ( $event->name() =~ /$filter/ ) {
106             push @interesting, $event;
107             }
108             }
109              
110             return
111             map { $_->can('path') ? $_ : $self->_convert_event($_) } @interesting;
112             }
113              
114             sub _build_mask {
115             my $self = shift;
116              
117             my $mask
118             = IN_MODIFY | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF
119             | IN_MOVED_TO;
120             $mask |= IN_DONT_FOLLOW unless $self->follow_symlinks();
121              
122             return $mask;
123             }
124              
125             sub _watch_directory {
126             my $self = shift;
127             my $dir = shift;
128              
129             # A directory could be created & then deleted before we get a
130             # chance to act on it.
131             return unless -d $dir;
132              
133             File::Find::find(
134             {
135             wanted => sub {
136             my $path = $File::Find::name;
137              
138             if ( $self->_path_is_excluded($path) ) {
139             $File::Find::prune = 1;
140             return;
141             }
142              
143             $self->_add_watch_if_dir($path);
144             },
145             follow_fast => ( $self->follow_symlinks() ? 1 : 0 ),
146             no_chdir => 1,
147             follow_skip => 2,
148             },
149             $dir
150             );
151             }
152              
153             sub _add_watch_if_dir {
154             my $self = shift;
155             my $path = shift;
156              
157             return if -l $path && !$self->follow_symlinks();
158              
159             return unless -d $path;
160             return if $self->_path_is_excluded($path);
161              
162             $self->_inotify()->watch( $path, $self->_mask() );
163             }
164              
165             sub _fake_events_for_new_dir {
166             my $self = shift;
167             my $dir = shift;
168              
169             return unless -d $dir;
170              
171             my @events;
172             File::Find::find(
173             {
174             wanted => sub {
175             my $path = $File::Find::name;
176              
177             return if $path eq $dir;
178             if ( $self->_path_is_excluded($path) ) {
179             $File::Find::prune = 1;
180             return;
181             }
182              
183             push @events,
184             $self->event_class()->new(
185             path => $path,
186             type => 'create',
187             );
188             },
189             follow_fast => ( $self->follow_symlinks() ? 1 : 0 ),
190             no_chdir => 1
191             },
192             $dir
193             );
194              
195             return @events;
196             }
197              
198             sub _convert_event {
199             my $self = shift;
200             my $event = shift;
201              
202             return $self->event_class()->new(
203             path => $event->fullname(),
204             type => (
205             $event->IN_CREATE() || $event->IN_MOVED_TO() ? 'create'
206             : $event->IN_MODIFY() ? 'modify'
207             : $event->IN_DELETE() ? 'delete'
208             : 'unknown'
209             ),
210             );
211             }
212              
213             __PACKAGE__->meta()->make_immutable();
214              
215             1;
216              
217             # ABSTRACT: Inotify-based watcher subclass
218              
219             __END__