merge from trunk
Dave Rolsky [Fri, 24 Apr 2009 02:08:29 +0000 (02:08 +0000)]
lib/Catalyst/Helper.pm
lib/Catalyst/Restarter.pm [new file with mode: 0644]
lib/Catalyst/Watcher.pm [new file with mode: 0644]

index 6db579f..2e7b94b 100644 (file)
@@ -969,11 +969,11 @@ my $host              = undef;
 my $port              = $ENV{[% appenv %]_PORT} || $ENV{CATALYST_PORT} || 3000;
 my $keepalive         = 0;
 my $restart           = $ENV{[% appenv %]_RELOAD} || $ENV{CATALYST_RELOAD} || 0;
-my $restart_delay     = 1;
-my $restart_regex     = '(?:/|^)(?!\.#).+(?:\.yml$|\.yaml$|\.conf|\.pm)$';
-my $restart_directory = undef;
-my $follow_symlinks   = 0;
-my $background        = 0;
+
+my $check_interval;
+my $file_regex;
+my $watch_directory;
+my $follow_symlinks;
 
 my @argv = @ARGV;
 
@@ -985,37 +985,58 @@ GetOptions(
     'port=s'              => \$port,
     'keepalive|k'         => \$keepalive,
     'restart|r'           => \$restart,
-    'restartdelay|rd=s'   => \$restart_delay,
-    'restartregex|rr=s'   => \$restart_regex,
-    'restartdirectory=s@' => \$restart_directory,
+    'restartdelay|rd=s'   => \$check_interval,
+    'restartregex|rr=s'   => \$file_regex,
+    'restartdirectory=s@' => \$watch_directory,
     'followsymlinks'      => \$follow_symlinks,
-    'background'          => \$background,
 );
 
 pod2usage(1) if $help;
 
-if ( $restart && $ENV{CATALYST_ENGINE} eq 'HTTP' ) {
-    $ENV{CATALYST_ENGINE} = 'HTTP::Restarter';
-}
 if ( $debug ) {
     $ENV{CATALYST_DEBUG} = 1;
 }
 
-# This is require instead of use so that the above environment
-# variables can be set at runtime.
-require [% name %];
-
-[% name %]->run( $port, $host, {
-    argv              => \@argv,
-    'fork'            => $fork,
-    keepalive         => $keepalive,
-    restart           => $restart,
-    restart_delay     => $restart_delay,
-    restart_regex     => qr/$restart_regex/,
-    restart_directory => $restart_directory,
-    follow_symlinks   => $follow_symlinks,
-    background        => $background,
-} );
+# If we load this here, then in the case of a restarter, it does not
+# need to be reloaded for each restart.
+require Catalyst;
+
+my $runner = sub {
+    # This is require instead of use so that the above environment
+    # variables can be set at runtime.
+    require [% name %];
+
+    [% name %]->run(
+        $port, $host,
+        {
+            argv      => \@argv,
+            'fork'    => $fork,
+            keepalive => $keepalive,
+        }
+    );
+};
+
+if ( $restart ) {
+    require Catalyst::Restarter;
+
+    my %args;
+    $args{watch_directory} = $watch_directory
+        if defined $watch_directory;
+    $args{check_interval} = $check_interval
+        if defined $check_interval;
+    $args{file_regex} = qr/$file_regex/
+        if defined $file_regex;
+
+    my $restarter = Catalyst::Restarter->new(
+        %args,
+        restart_sub => $runner,
+    );
+
+    $restarter->run_and_watch;
+}
+else {
+    $runner->();
+}
 
 1;
 
diff --git a/lib/Catalyst/Restarter.pm b/lib/Catalyst/Restarter.pm
new file mode 100644 (file)
index 0000000..33b3d17
--- /dev/null
@@ -0,0 +1,145 @@
+package Catalyst::Restarter;
+
+use Moose;
+
+use Catalyst::Watcher;
+use File::Spec;
+use FindBin;
+use namespace::clean -except => 'meta';
+
+has restart_sub => (
+    is       => 'ro',
+    isa      => 'CodeRef',
+    required => 1,
+);
+
+has _watcher => (
+    is  => 'rw',
+    isa => 'Catalyst::Watcher',
+);
+
+has _child => (
+    is  => 'rw',
+    isa => 'Int',
+);
+
+sub BUILD {
+    my $self = shift;
+    my $p    = shift;
+
+    delete $p->{restart_sub};
+
+    # We could make this lazily, but this lets us check that we
+    # received valid arguments for the watcher up front.
+    $self->_watcher( Catalyst::Watcher->new( %{$p} ) );
+}
+
+sub run_and_watch {
+    my $self = shift;
+
+    $self->_fork_and_start;
+
+    return unless $self->_child;
+
+    $self->_restart_on_changes;
+}
+
+sub _fork_and_start {
+    my $self = shift;
+
+    if ( my $pid = fork ) {
+        $self->_child($pid);
+    }
+    else {
+        $self->restart_sub->();
+    }
+}
+
+sub _restart_on_changes {
+    my $self = shift;
+
+    my $watcher = $self->_watcher;
+
+    while (1) {
+        my @files = $watcher->find_changed_files
+            or next;
+
+        print STDERR "Saw changes to the following files:\n";
+        print STDERR " - $_->{file} ($_->{status})\n" for @files;
+        print STDERR "\n";
+        print STDERR "Attempting to restart the server\n\n";
+
+        if ( $self->_child ) {
+            kill 2, $self->_child
+                or die "Cannot send INT to child (" . $self->_child . "): $!";
+        }
+
+        $self->_fork_and_start;
+
+        return unless $self->_child;
+    }
+}
+
+sub DEMOLISH {
+    my $self = shift;
+
+    if ( $self->_child ) {
+        kill 2, $self->_child;
+    }
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
+
+__END__
+
+=head1 NAME
+
+Catalyst::Restarter - Uses Catalyst::Watcher to check for changed files and restart the server
+
+=head1 SYNOPSIS
+
+    my $watcher = Catalyst::Watcher->new(
+        directory => '/path/to/MyApp',
+        regex     => '\.yml$|\.yaml$|\.conf|\.pm$',
+        interval  => 3,
+    );
+
+    while (1) {
+        my @changed_files = $watcher->watch();
+    }
+
+=head1 DESCRIPTION
+
+This class monitors a directory of files for changes made to any file
+matching a regular expression. It correctly handles new files added to the
+application as well as files that are deleted.
+
+=head1 METHODS
+
+=head2 new ( directory => $path [, regex => $regex, delay => $delay ] )
+
+Creates a new Watcher object.
+
+=head2 find_changed_files
+
+Returns a list of files that have been added, deleted, or changed
+since the last time watch was called. Each element returned is a hash
+reference with two keys. The C<file> key contains the filename, and
+the C<status> key contains one of "modified", "added", or "deleted".
+
+=head1 SEE ALSO
+
+L<Catalyst>, L<Catalyst::Restarter>, <File::Modified>
+
+=head1 AUTHORS
+
+Catalyst Contributors, see Catalyst.pm
+
+=head1 COPYRIGHT
+
+This program is free software, you can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut
diff --git a/lib/Catalyst/Watcher.pm b/lib/Catalyst/Watcher.pm
new file mode 100644 (file)
index 0000000..29adf3b
--- /dev/null
@@ -0,0 +1,201 @@
+package Catalyst::Watcher;
+
+use Moose;
+use Moose::Util::TypeConstraints;
+
+use File::Find;
+use File::Modified;
+use File::Spec;
+use Time::HiRes qw/sleep/;
+use namespace::clean -except => 'meta';
+
+has interval => (
+    is      => 'ro',
+    isa     => 'Int',
+    default => 1,
+);
+
+has regex => (
+    is      => 'ro',
+    isa     => 'RegexpRef',
+    default => sub { qr/(?:\/|^)(?!\.\#).+(?:\.yml$|\.yaml$|\.conf|\.pm)$/ },
+);
+
+my $dir = subtype
+       as 'Str'
+    => where { -d $_ }
+    => message { "$_ is not a valid directory" };
+
+my $array_of_dirs = subtype
+       as 'ArrayRef[Str]',
+    => where { map { -d } @{$_} }
+    => message { "@{$_} is not a list of valid directories" };
+
+coerce $array_of_dirs
+    => from $dir
+    => via { [ $_ ] };
+
+has directory => (
+    is      => 'ro',
+    isa     => $array_of_dirs,
+    default => sub { [ File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..' ) ) ] },
+    coerce  => 1,
+);
+
+has follow_symlinks => (
+    is      => 'ro',
+    isa     => 'Bool',
+    default => 0,
+);
+
+has _watched_files => (
+    is         => 'ro',
+    isa        => 'HashRef[Str]',
+    lazy_build => 1,
+    clearer    => '_clear_watched_files',
+);
+
+has _modified => (
+    is         => 'rw',
+    isa        => 'File::Modified',
+    lazy_build => 1,
+);
+
+sub _build__watched_files {
+    my $self = shift;
+
+    my $regex = $self->regex;
+
+    my %list;
+    finddepth(
+        {
+            wanted => sub {
+                my $file = File::Spec->rel2abs($File::Find::name);
+                return unless $file =~ /$regex/;
+                return unless -f $file;
+
+                $list{$file} = 1;
+
+                # also watch the directory for changes
+                my $cur_dir = File::Spec->rel2abs($File::Find::dir);
+                $cur_dir =~ s{/script/..}{};
+                $list{$cur_dir} = 1;
+            },
+            follow_fast => $self->follow_symlinks ? 1 : 0,
+            no_chdir    => 1
+        },
+        @{ $self->directory }
+    );
+
+    return \%list;
+}
+
+sub _build__modified {
+    my $self = shift;
+
+    return File::Modified->new(
+        method => 'mtime',
+        files  => [ keys %{ $self->_watched_files } ],
+    );
+}
+
+sub find_changed_files {
+    my $self = shift;
+
+    my @changes;
+    my @changed_files;
+
+    sleep $self->interval if $self->interval > 0;
+
+    eval { @changes = $self->_modified->changed };
+    if ($@) {
+        # File::Modified will die if a file is deleted.
+        my ($deleted_file) = $@ =~ /stat '(.+)'/;
+        push @changed_files,
+            {
+            file => $deleted_file || 'unknown file',
+            status => 'deleted',
+            };
+    }
+
+    if (@changes) {
+        $self->_modified->update;
+
+        @changed_files = map { { file => $_, status => 'modified' } }
+            grep { -f $_ } @changes;
+
+        # We also need to check to see if a new directory was created
+        unless (@changed_files) {
+            my $old_watch = $self->_watched_files;
+
+            $self->_clear_watched_files;
+
+            my $new_watch = $self->_watched_files;
+
+            @changed_files
+                = map { { file => $_, status => 'added' } }
+                grep { !defined $old_watch->{$_} }
+                keys %{$new_watch};
+
+            return unless @changed_files;
+        }
+    }
+
+    return @changed_files;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
+
+__END__
+
+=head1 NAME
+
+Catalyst::Watcher - Watch for changed application files
+
+=head1 SYNOPSIS
+
+    my $watcher = Catalyst::Watcher->new(
+        directory => '/path/to/MyApp',
+        regex     => '\.yml$|\.yaml$|\.conf|\.pm$',
+        interval  => 3,
+    );
+
+    while (1) {
+        my @changed_files = $watcher->watch();
+    }
+
+=head1 DESCRIPTION
+
+This class monitors a directory of files for changes made to any file
+matching a regular expression. It correctly handles new files added to the
+application as well as files that are deleted.
+
+=head1 METHODS
+
+=head2 new ( directory => $path [, regex => $regex, delay => $delay ] )
+
+Creates a new Watcher object.
+
+=head2 find_changed_files
+
+Returns a list of files that have been added, deleted, or changed
+since the last time watch was called. Each element returned is a hash
+reference with two keys. The C<file> key contains the filename, and
+the C<status> key contains one of "modified", "added", or "deleted".
+
+=head1 SEE ALSO
+
+L<Catalyst>, L<Catalyst::Restarter>, <File::Modified>
+
+=head1 AUTHORS
+
+Catalyst Contributors, see Catalyst.pm
+
+=head1 COPYRIGHT
+
+This program is free software, you can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut