package Catalyst::Engine::HTTP::Restarter::Watcher;
-use strict;
-use warnings;
-use base 'Class::Accessor::Fast';
+use Moose;
+with 'MooseX::Emulate::Class::Accessor::Fast';
+
use File::Find;
use File::Modified;
use File::Spec;
use Time::HiRes qw/sleep/;
+use Moose::Util qw/find_meta/;
+use namespace::clean -except => 'meta';
+
+BEGIN {
+ # If we can detect stash changes, then we do magic
+ # to make their metaclass mutable (if they have one)
+ # so that restarting works as expected.
+ eval { require B::Hooks::OP::Check::StashChange; };
+ *DETECT_PACKAGE_COMPILATION = $@
+ ? sub () { 0 }
+ : sub () { 1 }
+}
-__PACKAGE__->mk_accessors( qw/delay
- directory
- modified
- regex
- watch_list/ );
+has delay => (is => 'rw');
+has regex => (is => 'rw');
+has modified => (is => 'rw', builder => '_build_modified', lazy => 1);
+has directory => (is => 'rw');
+has watch_list => (is => 'rw', builder => '_build_watch_list', lazy => 1);
+has follow_symlinks => (is => 'rw');
-sub new {
- my ( $class, %args ) = @_;
-
- my $self = { %args };
-
- bless $self, $class;
-
- $self->_init;
-
- return $self;
+sub _build_watch_list {
+ my ($self) = @_;
+ return $self->_index_directory;
}
-sub _init {
- my $self = shift;
-
- my $watch_list = $self->_index_directory;
- $self->watch_list( $watch_list );
-
- $self->modified(
- File::Modified->new(
- method => 'mtime',
- files => [ keys %{$watch_list} ],
- )
+sub _build_modified {
+ my ($self) = @_;
+ return File::Modified->new(
+ method => 'mtime',
+ files => [ keys %{ $self->watch_list } ],
);
}
sub watch {
my $self = shift;
-
+
my @changes;
my @changed_files;
- sleep $self->delay || 1;
-
+ my $delay = ( defined $self->delay ) ? $self->delay : 1;
+
+ sleep $delay if $delay > 0;
+
eval { @changes = $self->modified->changed };
- if ( $@ ) {
+ if ($@) {
+
# File::Modified will die if a file is deleted.
my ($deleted_file) = $@ =~ /stat '(.+)'/;
push @changed_files, $deleted_file || 'unknown file';
}
-
- if ( @changes ) {
+
+ if (@changes) {
+
# update all mtime information
$self->modified->update;
-
+
# check if any files were changed
@changed_files = grep { -f $_ } @changes;
-
+
# Check if only directories were changed. This means
# a new file was created.
- unless ( @changed_files ) {
+ unless (@changed_files) {
+
# re-index to find new files
my $new_watch = $self->_index_directory;
-
+
# look through the new list for new files
my $old_watch = $self->watch_list;
- @changed_files = grep { ! defined $old_watch->{$_} }
- keys %{ $new_watch };
-
+ @changed_files = grep { !defined $old_watch->{$_} }
+ keys %{$new_watch};
+
return unless @changed_files;
}
# Test modified pm's
- for my $file ( @changed_files ) {
+ for my $file (@changed_files) {
next unless $file =~ /\.pm$/;
if ( my $error = $self->_test($file) ) {
- print STDERR
- qq/File "$file" modified, not restarting\n\n/;
+ print STDERR qq/File "$file" modified, not restarting\n\n/;
print STDERR '*' x 80, "\n";
print STDERR $error;
print STDERR '*' x 80, "\n";
}
}
}
-
+
return @changed_files;
}
sub _index_directory {
my $self = shift;
-
- my $dir = $self->directory || die "No directory specified";
- my $regex = $self->regex || '\.pm$';
+
+ my $dir = $self->directory;
+ die "No directory specified" if !$dir or ref($dir) && !@{$dir};
+
+ my $regex = $self->regex || '\.pm$';
my %list;
-
+
finddepth(
{
wanted => sub {
return unless -f $file;
$file =~ s{/script/..}{};
$list{$file} = 1;
-
+
# also watch the directory for changes
my $cur_dir = File::Spec->rel2abs($File::Find::dir);
- $cur_dir =~ s{/script/..}{};
+ $cur_dir =~ s{/script/..}{};
$list{$cur_dir} = 1;
},
+ follow_fast => $self->follow_symlinks ? 1 : 0,
no_chdir => 1
},
- $dir
+ ref $dir eq 'ARRAY' ? @{$dir} : $dir
);
return \%list;
}
sub _test {
my ( $self, $file ) = @_;
-
- delete $INC{$file};
+
+ my $id;
+ if (DETECT_PACKAGE_COMPILATION) {
+ $id = B::Hooks::OP::Check::StashChange::register(sub {
+ my ($new, $old) = @_;
+ my $meta = find_meta($new);
+ if ($meta) { # A little paranoia here - Moose::Meta::Role has neither of these methods.
+ my $is_immutable = $meta->can('is_immutable');
+ my $make_mutable = $meta->can('make_mutable');
+ $meta->$make_mutable() if $is_immutable && $make_mutable && $meta->$is_immutable();
+ eval { # Do not explode the watcher process if this fails.
+ my $superclasses = $meta->can('superclasses');
+ $meta->$superclasses('Moose::Object') if $superclasses;
+ };
+ }
+ });
+ }
+
+ local $Catalyst::__AM_RESTARTING = 1; # Hack to avoid C3 fail
+ delete $INC{$file}; # Remove from %INC so it will reload
local $SIG{__WARN__} = sub { };
-
+
open my $olderr, '>&STDERR';
open STDERR, '>', File::Spec->devnull;
eval "require '$file'";
open STDERR, '>&', $olderr;
-
+
+ B::Hooks::OP::Check::StashChange::unregister($id) if $id;
+
return ($@) ? $@ : 0;
-}
+}
1;
__END__
my $watcher = Catalyst::Engine::HTTP::Restarter::Watcher->new(
directory => '/path/to/MyApp',
- regex => '\.yml$|\.yaml$|\.pm$',
+ regex => '\.yml$|\.yaml$|\.conf|\.pm$',
delay => 1,
);
Returns a list of files that have been added, deleted, or changed since the
last time watch was called.
+=head2 DETECT_PACKAGE_COMPILATION
+
+Returns true if L<B::Hooks::OP::Check::StashChange> is installed and
+can be used to detect when files are compiled. This is used internally
+to make the L<Moose> metaclass of any class being reloaded immutable.
+
+If L<B::Hooks::OP::Check::StashChange> is not installed, then the
+restarter makes all application components immutable. This covers the
+simple case, but is less useful if you're using Moose in components
+outside Catalyst's namespaces, but inside your application directory.
+
=head1 SEE ALSO
L<Catalyst>, L<Catalyst::Engine::HTTP::Restarter>, L<File::Modified>
=head1 AUTHORS
-Sebastian Riedel, <sri@cpan.org>
-
-Andy Grundman, <andy@hybridized.org>
+Catalyst Contributors, see Catalyst.pm
=head1 THANKS