Update docs
[catagits/Catalyst-Devel.git] / lib / Catalyst / Watcher.pm
1 package Catalyst::Watcher;
2
3 use Moose;
4 use Moose::Util::TypeConstraints;
5
6 use File::Find;
7 use File::Modified;
8 use File::Spec;
9 use Time::HiRes qw/sleep/;
10 use namespace::clean -except => 'meta';
11
12 has interval => (
13     is      => 'ro',
14     isa     => 'Int',
15     default => 1,
16 );
17
18 has regex => (
19     is      => 'ro',
20     isa     => 'RegexpRef',
21     default => sub { qr/(?:\/|^)(?!\.\#).+(?:\.yml$|\.yaml$|\.conf|\.pm)$/ },
22 );
23
24 my $dir = subtype
25        as 'Str'
26     => where { -d $_ }
27     => message { "$_ is not a valid directory" };
28
29 my $array_of_dirs = subtype
30        as 'ArrayRef[Str]',
31     => where { map { -d } @{$_} }
32     => message { "@{$_} is not a list of valid directories" };
33
34 coerce $array_of_dirs
35     => from $dir
36     => via { [ $_ ] };
37
38 has directory => (
39     is      => 'ro',
40     isa     => $array_of_dirs,
41     default => sub { [ File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..' ) ) ] },
42     coerce  => 1,
43 );
44
45 has follow_symlinks => (
46     is      => 'ro',
47     isa     => 'Bool',
48     default => 0,
49 );
50
51 has _watched_files => (
52     is         => 'ro',
53     isa        => 'HashRef[Str]',
54     lazy_build => 1,
55     clearer    => '_clear_watched_files',
56 );
57
58 has _modified => (
59     is         => 'rw',
60     isa        => 'File::Modified',
61     lazy_build => 1,
62 );
63
64 sub _build__watched_files {
65     my $self = shift;
66
67     my $regex = $self->regex;
68
69     my %list;
70     finddepth(
71         {
72             wanted => sub {
73                 my $file = File::Spec->rel2abs($File::Find::name);
74                 return unless $file =~ /$regex/;
75                 return unless -f $file;
76
77                 $list{$file} = 1;
78
79                 # also watch the directory for changes
80                 my $cur_dir = File::Spec->rel2abs($File::Find::dir);
81                 $cur_dir =~ s{/script/..}{};
82                 $list{$cur_dir} = 1;
83             },
84             follow_fast => $self->follow_symlinks ? 1 : 0,
85             no_chdir    => 1
86         },
87         @{ $self->directory }
88     );
89
90     return \%list;
91 }
92
93 sub _build__modified {
94     my $self = shift;
95
96     return File::Modified->new(
97         method => 'mtime',
98         files  => [ keys %{ $self->_watched_files } ],
99     );
100 }
101
102 sub find_changed_files {
103     my $self = shift;
104
105     my @changes;
106     my @changed_files;
107
108     sleep $self->interval if $self->interval > 0;
109
110     eval { @changes = $self->_modified->changed };
111     if ($@) {
112         # File::Modified will die if a file is deleted.
113         my ($deleted_file) = $@ =~ /stat '(.+)'/;
114         push @changed_files,
115             {
116             file => $deleted_file || 'unknown file',
117             status => 'deleted',
118             };
119     }
120
121     if (@changes) {
122         $self->_modified->update;
123
124         @changed_files = map { { file => $_, status => 'modified' } }
125             grep { -f $_ } @changes;
126
127         # We also need to check to see if a new directory was created
128         unless (@changed_files) {
129             my $old_watch = $self->_watched_files;
130
131             $self->_clear_watched_files;
132
133             my $new_watch = $self->_watched_files;
134
135             @changed_files
136                 = map { { file => $_, status => 'added' } }
137                 grep { !defined $old_watch->{$_} }
138                 keys %{$new_watch};
139
140             return unless @changed_files;
141         }
142     }
143
144     return @changed_files;
145 }
146
147 __PACKAGE__->meta->make_immutable;
148
149 1;
150
151 __END__
152
153 =head1 NAME
154
155 Catalyst::Watcher - Watch for changed application files
156
157 =head1 SYNOPSIS
158
159     my $watcher = Catalyst::Watcher->new(
160         directory => '/path/to/MyApp',
161         regex     => '\.yml$|\.yaml$|\.conf|\.pm$',
162         interval  => 3,
163     );
164
165     while (1) {
166         my @changed_files = $watcher->watch();
167     }
168
169 =head1 DESCRIPTION
170
171 This class monitors a directory of files for changes made to any file
172 matching a regular expression. It correctly handles new files added to the
173 application as well as files that are deleted.
174
175 =head1 METHODS
176
177 =head2 new ( directory => $path [, regex => $regex, delay => $delay ] )
178
179 Creates a new Watcher object.
180
181 =head2 find_changed_files
182
183 Returns a list of files that have been added, deleted, or changed
184 since the last time watch was called. Each element returned is a hash
185 reference with two keys. The C<file> key contains the filename, and
186 the C<status> key contains one of "modified", "added", or "deleted".
187
188 =head1 SEE ALSO
189
190 L<Catalyst>, L<Catalyst::Restarter>, <File::Modified>
191
192 =head1 AUTHORS
193
194 Catalyst Contributors, see Catalyst.pm
195
196 =head1 COPYRIGHT
197
198 This program is free software, you can redistribute it and/or modify
199 it under the same terms as Perl itself.
200
201 =cut