Make restarters immutable handling less naive + tests
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Engine / HTTP / Restarter / Watcher.pm
CommitLineData
65586a18 1package Catalyst::Engine::HTTP::Restarter::Watcher;
2
7fa2c9c1 3use Moose;
531f1ab6 4with 'MooseX::Emulate::Class::Accessor::Fast';
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);
144 if ($meta) {
145 $meta->make_mutable if $meta->is_immutable;
146 }
147 });
148 }
149
150 delete $INC{$file}; # Remove from %INC so it will reload
65586a18 151 local $SIG{__WARN__} = sub { };
b5ecfcf0 152
65586a18 153 open my $olderr, '>&STDERR';
154 open STDERR, '>', File::Spec->devnull;
155 eval "require '$file'";
156 open STDERR, '>&', $olderr;
b5ecfcf0 157
65b708a6 158 B::Hooks::OP::Check::StashChange::unregister($id) if $id;
159
65586a18 160 return ($@) ? $@ : 0;
b5ecfcf0 161}
65586a18 162
1631;
164__END__
165
166=head1 NAME
167
168Catalyst::Engine::HTTP::Restarter::Watcher - Watch for changed application
169files
170
171=head1 SYNOPSIS
172
173 my $watcher = Catalyst::Engine::HTTP::Restarter::Watcher->new(
174 directory => '/path/to/MyApp',
2f381252 175 regex => '\.yml$|\.yaml$|\.conf|\.pm$',
65586a18 176 delay => 1,
177 );
ac5c933b 178
65586a18 179 while (1) {
180 my @changed_files = $watcher->watch();
181 }
182
183=head1 DESCRIPTION
184
185This class monitors a directory of files for changes made to any file
186matching a regular expression. It correctly handles new files added to the
187application as well as files that are deleted.
188
189=head1 METHODS
190
191=head2 new ( directory => $path [, regex => $regex, delay => $delay ] )
192
193Creates a new Watcher object.
194
195=head2 watch
196
197Returns a list of files that have been added, deleted, or changed since the
198last time watch was called.
199
200=head1 SEE ALSO
201
202L<Catalyst>, L<Catalyst::Engine::HTTP::Restarter>, L<File::Modified>
203
204=head1 AUTHORS
205
2f381252 206Catalyst Contributors, see Catalyst.pm
65586a18 207
208=head1 THANKS
209
210Many parts are ripped out of C<HTTP::Server::Simple> by Jesse Vincent.
211
212=head1 COPYRIGHT
213
214This program is free software, you can redistribute it and/or modify it under
215the same terms as Perl itself.
216
217=cut