Commit | Line | Data |
8462f41e |
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 | |
714e3652 |
159 | my $watcher = Catalyst::Watcher->new( |
8462f41e |
160 | directory => '/path/to/MyApp', |
161 | regex => '\.yml$|\.yaml$|\.conf|\.pm$', |
714e3652 |
162 | interval => 3, |
8462f41e |
163 | ); |
714e3652 |
164 | |
8462f41e |
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 |
714e3652 |
172 | matching a regular expression. It correctly handles new files added to the |
8462f41e |
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 | |
714e3652 |
181 | =head2 find_changed_files |
8462f41e |
182 | |
714e3652 |
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". |
8462f41e |
187 | |
188 | =head1 SEE ALSO |
189 | |
714e3652 |
190 | L<Catalyst>, L<Catalyst::Restarter>, <File::Modified> |
8462f41e |
191 | |
192 | =head1 AUTHORS |
193 | |
194 | Catalyst Contributors, see Catalyst.pm |
195 | |
8462f41e |
196 | =head1 COPYRIGHT |
197 | |
714e3652 |
198 | This program is free software, you can redistribute it and/or modify |
199 | it under the same terms as Perl itself. |
8462f41e |
200 | |
201 | =cut |