Duh, I prefer putting my actual name in changes
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Engine / HTTP / Restarter / Watcher.pm
CommitLineData
65586a18 1package Catalyst::Engine::HTTP::Restarter::Watcher;
2
7fa2c9c1 3use Moose;
88f165ea 4with 'MooseX::Emulate::Class::Accessor::Fast' => { excludes => [ 'BUILD' ] };
531f1ab6 5
65586a18 6use File::Find;
7use File::Modified;
8use File::Spec;
9use Time::HiRes qw/sleep/;
65b708a6 10use Moose::Util qw/find_meta/;
11use namespace::clean -except => 'meta';
12
13BEGIN {
14 # If we can detect stash changes, then we do magic
15 # to make their metaclass mutable (if they have one)
16 # so that restarting works as expected.
17 eval { require B::Hooks::OP::Check::StashChange; };
18 *DETECT_PACKAGE_COMPILATION = $@
19 ? sub () { 0 }
20 : sub () { 1 }
21}
65586a18 22
7fa2c9c1 23has delay => (is => 'rw');
24has regex => (is => 'rw');
25has modified => (is => 'rw');
26has directory => (is => 'rw');
27has watch_list => (is => 'rw');
f7174b10 28has follow_symlinks => (is => 'rw');
65586a18 29
0fc2d522 30sub BUILD {
65b708a6 31 shift->_init;
65586a18 32}
33
34sub _init {
35 my $self = shift;
b5ecfcf0 36
65586a18 37 my $watch_list = $self->_index_directory;
b5ecfcf0 38 $self->watch_list($watch_list);
39
65586a18 40 $self->modified(
41 File::Modified->new(
42 method => 'mtime',
43 files => [ keys %{$watch_list} ],
44 )
45 );
46}
47
48sub watch {
49 my $self = shift;
b5ecfcf0 50
65586a18 51 my @changes;
52 my @changed_files;
ac5c933b 53
951572c0 54 my $delay = ( defined $self->delay ) ? $self->delay : 1;
b5ecfcf0 55
951572c0 56 sleep $delay if $delay > 0;
b5ecfcf0 57
65586a18 58 eval { @changes = $self->modified->changed };
b5ecfcf0 59 if ($@) {
60
65586a18 61 # File::Modified will die if a file is deleted.
62 my ($deleted_file) = $@ =~ /stat '(.+)'/;
63 push @changed_files, $deleted_file || 'unknown file';
64 }
b5ecfcf0 65
66 if (@changes) {
67
65586a18 68 # update all mtime information
69 $self->modified->update;
b5ecfcf0 70
65586a18 71 # check if any files were changed
72 @changed_files = grep { -f $_ } @changes;
b5ecfcf0 73
65586a18 74 # Check if only directories were changed. This means
75 # a new file was created.
b5ecfcf0 76 unless (@changed_files) {
77
65586a18 78 # re-index to find new files
79 my $new_watch = $self->_index_directory;
b5ecfcf0 80
65586a18 81 # look through the new list for new files
82 my $old_watch = $self->watch_list;
b5ecfcf0 83 @changed_files = grep { !defined $old_watch->{$_} }
84 keys %{$new_watch};
85
65586a18 86 return unless @changed_files;
87 }
88
89 # Test modified pm's
b5ecfcf0 90 for my $file (@changed_files) {
65586a18 91 next unless $file =~ /\.pm$/;
92 if ( my $error = $self->_test($file) ) {
b5ecfcf0 93 print STDERR qq/File "$file" modified, not restarting\n\n/;
65586a18 94 print STDERR '*' x 80, "\n";
95 print STDERR $error;
96 print STDERR '*' x 80, "\n";
97 return;
98 }
99 }
100 }
b5ecfcf0 101
65586a18 102 return @changed_files;
103}
104
105sub _index_directory {
106 my $self = shift;
b5ecfcf0 107
9c71d51d 108 my $dir = $self->directory;
109 die "No directory specified" if !$dir or ref($dir) && !@{$dir};
110
b5ecfcf0 111 my $regex = $self->regex || '\.pm$';
65586a18 112 my %list;
b5ecfcf0 113
65586a18 114 finddepth(
115 {
116 wanted => sub {
117 my $file = File::Spec->rel2abs($File::Find::name);
118 return unless $file =~ /$regex/;
119 return unless -f $file;
120 $file =~ s{/script/..}{};
121 $list{$file} = 1;
b5ecfcf0 122
65586a18 123 # also watch the directory for changes
124 my $cur_dir = File::Spec->rel2abs($File::Find::dir);
b5ecfcf0 125 $cur_dir =~ s{/script/..}{};
65586a18 126 $list{$cur_dir} = 1;
127 },
9c71d51d 128 follow_fast => $self->follow_symlinks ? 1 : 0,
65586a18 129 no_chdir => 1
130 },
9c71d51d 131 ref $dir eq 'ARRAY' ? @{$dir} : $dir
65586a18 132 );
133 return \%list;
134}
135
136sub _test {
137 my ( $self, $file ) = @_;
b5ecfcf0 138
65b708a6 139 my $id;
140 if (DETECT_PACKAGE_COMPILATION) {
141 $id = B::Hooks::OP::Check::StashChange::register(sub {
142 my ($new, $old) = @_;
143 my $meta = find_meta($new);
2bab21f6 144 if ($meta) { # A little paranoia here - Moose::Meta::Role has neither of these methods.
145 my $is_immutable = $meta->can('is_immutable');
146 my $make_mutable = $meta->can('make_mutable');
147 $meta->$make_mutable() if $is_immutable && $make_mutable && $meta->$is_immutable();
16b7c476 148 eval { # Do not explode the watcher process if this fails.
149 my $superclasses = $meta->can('superclasses');
150 $meta->$superclasses('Moose::Object') if $superclasses;
151 };
65b708a6 152 }
153 });
154 }
155
16b7c476 156 local $Catalyst::__AM_RESTARTING = 1; # Hack to avoid C3 fail
65b708a6 157 delete $INC{$file}; # Remove from %INC so it will reload
65586a18 158 local $SIG{__WARN__} = sub { };
b5ecfcf0 159
65586a18 160 open my $olderr, '>&STDERR';
161 open STDERR, '>', File::Spec->devnull;
162 eval "require '$file'";
163 open STDERR, '>&', $olderr;
b5ecfcf0 164
65b708a6 165 B::Hooks::OP::Check::StashChange::unregister($id) if $id;
166
65586a18 167 return ($@) ? $@ : 0;
b5ecfcf0 168}
65586a18 169
1701;
171__END__
172
173=head1 NAME
174
175Catalyst::Engine::HTTP::Restarter::Watcher - Watch for changed application
176files
177
178=head1 SYNOPSIS
179
180 my $watcher = Catalyst::Engine::HTTP::Restarter::Watcher->new(
181 directory => '/path/to/MyApp',
2f381252 182 regex => '\.yml$|\.yaml$|\.conf|\.pm$',
65586a18 183 delay => 1,
184 );
ac5c933b 185
65586a18 186 while (1) {
187 my @changed_files = $watcher->watch();
188 }
189
190=head1 DESCRIPTION
191
192This class monitors a directory of files for changes made to any file
193matching a regular expression. It correctly handles new files added to the
194application as well as files that are deleted.
195
196=head1 METHODS
197
198=head2 new ( directory => $path [, regex => $regex, delay => $delay ] )
199
200Creates a new Watcher object.
201
202=head2 watch
203
204Returns a list of files that have been added, deleted, or changed since the
205last time watch was called.
206
c03163b8 207=head2 DETECT_PACKAGE_COMPILATION
208
209Returns true if L<B::Hooks::OP::Check::StashChange> is installed and
210can be used to detect when files are compiled. This is used internally
211to make the L<Moose> metaclass of any class being reloaded immutable.
212
213If L<B::Hooks::OP::Check::StashChange> is not installed, then the
214restarter makes all application components immutable. This covers the
215simple case, but is less useful if you're using Moose in components
216outside Catalyst's namespaces, but inside your application directory.
217
65586a18 218=head1 SEE ALSO
219
220L<Catalyst>, L<Catalyst::Engine::HTTP::Restarter>, L<File::Modified>
221
222=head1 AUTHORS
223
2f381252 224Catalyst Contributors, see Catalyst.pm
65586a18 225
226=head1 THANKS
227
228Many parts are ripped out of C<HTTP::Server::Simple> by Jesse Vincent.
229
230=head1 COPYRIGHT
231
232This program is free software, you can redistribute it and/or modify it under
233the same terms as Perl itself.
234
235=cut