568b5100bfe6af591d92a2300259689cfdf19081
[catagits/Catalyst-Devel.git] / lib / Catalyst / Watcher / Inotify.pm
1 package Catalyst::Watcher::Inotify;
2
3 use Moose;
4
5 use File::Find;
6 use Linux::Inotify2;
7 use namespace::clean -except => 'meta';
8
9 extends 'Catalyst::Watcher';
10
11 has _inotify => (
12     is       => 'rw',
13     isa      => 'Linux::Inotify2',
14     default  => sub { Linux::Inotify2->new },
15     init_arg => undef,
16
17 );
18
19 has _mask => (
20     is         => 'rw',
21     isa        => 'Int',
22     lazy_build => 1,
23 );
24
25 sub BUILD {
26     my $self = shift;
27
28     # If this is done via a lazy_build then the call to
29     # ->_add_directory ends up causing endless recursion when it calls
30     # ->_inotify itself.
31     $self->_add_directory($_) for @{ $self->directories };
32
33     return $self;
34 }
35
36 sub watch {
37     my $self      = shift;
38     my $restarter = shift;
39
40     my @events = $self->_wait_for_events;
41
42     $restarter->handle_changes( map { $self->_event_to_change($_) } @events );
43
44     return;
45 }
46
47 sub _wait_for_events {
48     my $self = shift;
49
50     my $regex = $self->regex;
51
52     while (1) {
53         # This is a blocking read, so it will not return until
54         # something happens. The restarter will end up calling ->watch
55         # again after handling the changes.
56         my @events = $self->_inotify->read;
57
58         my @interesting;
59         for my $event (@events) {
60             if ( ( $event->IN_CREATE && $event->IN_ISDIR ) ) {
61                 $self->_add_directory( $event->fullname );
62                 push @interesting, $event;
63             }
64             elsif ( $event->IN_DELETE_SELF ) {
65                 $event->w->cancel;
66                 push @interesting, $event;
67             }
68             elsif ( $event->fullname =~ /$regex/ ) {
69                 push @interesting, $event;
70             }
71         }
72
73         return @interesting if @interesting;
74     }
75 }
76
77 sub _build__mask {
78     my $self = shift;
79
80     my $mask = IN_MODIFY | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF;
81     $mask |= IN_DONT_FOLLOW unless $self->follow_symlinks;
82
83     return $mask;
84 }
85
86 sub _add_directory {
87     my $self = shift;
88     my $dir  = shift;
89
90     finddepth(
91         {
92             wanted => sub {
93                 my $path = File::Spec->rel2abs($File::Find::name);
94                 return unless -d $path;
95
96                 $self->_inotify->watch( $path, $self->_mask );
97             },
98             follow_fast => $self->follow_symlinks ? 1 : 0,
99             no_chdir    => 1
100         },
101         $dir
102     );
103 }
104
105 sub _event_to_change {
106     my $self  = shift;
107     my $event = shift;
108
109     my %change = ( file => $event->fullname );
110     if ( $event->IN_CREATE ) {
111         $change{status} = 'added';
112     }
113     elsif ( $event->IN_MODIFY ) {
114         $change{status} = 'modified';
115     }
116     elsif ( $event->IN_DELETE ) {
117         $change{status} = 'deleted';
118     }
119     else {
120         $change{status} = 'containing directory modified';
121     }
122
123     return \%change;
124 }
125
126 __PACKAGE__->meta->make_immutable;
127
128 1;
129
130 __END__
131
132 =head1 NAME
133
134 Catalyst::Watcher - Watch for changed application files
135
136 =head1 SYNOPSIS
137
138     my $watcher = Catalyst::Watcher->new(
139         directory => '/path/to/MyApp',
140         regex     => '\.yml$|\.yaml$|\.conf|\.pm$',
141         interval  => 3,
142     );
143
144     while (1) {
145         my @changed_files = $watcher->watch();
146     }
147
148 =head1 DESCRIPTION
149
150 This class monitors a directory of files for changes made to any file
151 matching a regular expression. It correctly handles new files added to the
152 application as well as files that are deleted.
153
154 =head1 METHODS
155
156 =head2 new ( directory => $path [, regex => $regex, delay => $delay ] )
157
158 Creates a new Watcher object.
159
160 =head2 find_changed_files
161
162 Returns a list of files that have been added, deleted, or changed
163 since the last time watch was called. Each element returned is a hash
164 reference with two keys. The C<file> key contains the filename, and
165 the C<status> key contains one of "modified", "added", or "deleted".
166
167 =head1 SEE ALSO
168
169 L<Catalyst>, L<Catalyst::Restarter>, <File::Modified>
170
171 =head1 AUTHORS
172
173 Catalyst Contributors, see Catalyst.pm
174
175 =head1 COPYRIGHT
176
177 This program is free software, you can redistribute it and/or modify
178 it under the same terms as Perl itself.
179
180 =cut