Refactored restarter into a subclass of Engine::HTTP, improved restarter performance
Andy Grundman [Wed, 2 Nov 2005 03:39:05 +0000 (03:39 +0000)]
Build.PL
lib/Catalyst/Engine/HTTP.pm
lib/Catalyst/Engine/HTTP/Restarter.pm [new file with mode: 0644]
lib/Catalyst/Engine/HTTP/Restarter/Watcher.pm [new file with mode: 0644]
lib/Catalyst/Helper.pm

index 08b48ba..bd21d73 100644 (file)
--- a/Build.PL
+++ b/Build.PL
@@ -12,6 +12,7 @@ my $build = Module::Build->new(
         'Class::Data::Inheritable'          => 0,
         'Class::Inspector'                  => 0,
         'CGI::Cookie'                       => 0,
+        'File::Modified'                    => 0,
         'HTML::Entities'                    => 0,
         'HTTP::Body'                        => 0.03,
         'HTTP::Headers'                     => 1.59,
@@ -26,6 +27,7 @@ my $build = Module::Build->new(
         'Template'                          => 0,
         'Text::ASCIITable'                  => 0.17,
         'Test::MockObject'                  => 0,
+        'Time::HiRes'                       => 0,
         'Tree::Simple'                      => 0,
         'Tree::Simple::Visitor::FindByPath' => 0,
         'URI'                               => 1.35,
index 332e771..8f08b8f 100644 (file)
@@ -3,9 +3,6 @@ package Catalyst::Engine::HTTP;
 use strict;
 use base 'Catalyst::Engine::CGI';
 use Errno 'EWOULDBLOCK';
-use FindBin;
-use File::Find;
-use File::Spec;
 use HTTP::Status;
 use NEXT;
 use Socket;
@@ -112,55 +109,6 @@ sub run {
     my ( $self, $class, $port, $host, $options ) = @_;
 
     $options ||= {};
-
-    # Setup restarter
-    my $restarter;
-    if ( $options->{restart} ) {
-        my $parent = $$;
-        unless ( $restarter = fork ) {
-
-            # Prepare
-            close STDIN;
-            close STDOUT;
-
-            # Index parent directory
-            my $dir = File::Spec->catdir( $FindBin::Bin, '..' );
-
-            my $regex = $options->{restart_regex};
-            my $one   = _index( $dir, $regex );
-          RESTART: while (1) {
-                sleep $options->{restart_delay} || 1;
-                
-                # check if our parent has died
-                exit if ( getppid == 1 );
-                
-                my $two     = _index( $dir,         $regex );
-                my $changes = _compare_index( $one, $two );
-                if (@$changes) {
-                    $one = $two;
-
-                    # Test modified pm's
-                    for my $file (@$changes) {
-                        next unless $file =~ /\.pm$/;
-                        if ( my $error = _test($file) ) {
-                            print STDERR
-                              qq/File "$file" modified, not restarting\n\n/;
-                            print STDERR '*' x 80, "\n";
-                            print STDERR $error;
-                            print STDERR '*' x 80, "\n";
-                            next RESTART;
-                        }
-                    }
-
-                    # Restart
-                    my $files = join ', ', @$changes;
-                    print STDERR qq/File(s) "$files" modified, restarting\n\n/;
-                    kill( 1, $parent );
-                    exit;
-                }
-            }
-        }
-    }
     
     our $GOT_HUP;
     local $GOT_HUP = 0;
@@ -282,20 +230,6 @@ sub run {
     }
 }
 
-sub _compare_index {
-    my ( $one, $two ) = @_;
-    my %clone = %$two;
-    my @changes;
-    while ( my ( $key, $val ) = each %$one ) {
-        if ( !$clone{$key} || ( $clone{$key} ne $val ) ) {
-            push @changes, $key;
-        }
-        delete $clone{$key};
-    }
-    for my $key ( keys %clone ) { push @changes, $key }
-    return \@changes;
-}
-
 sub _get_line {
     my ( $self, $handle ) = @_;
 
@@ -311,37 +245,6 @@ sub _get_line {
     return $line;
 }
 
-sub _index {
-    my ( $dir, $regex ) = @_;
-    my %index;
-    finddepth(
-        {
-            wanted => sub {
-                my $file = File::Spec->rel2abs($File::Find::name);
-                return unless $file =~ /$regex/;
-                return unless -f $file;
-                my $time = ( stat $file )[9];
-                $index{$file} = $time;
-            },
-            no_chdir => 1
-        },
-        $dir
-    );
-    return \%index;
-}
-
-sub _test {
-    my $file = shift;
-    delete $INC{$file};
-    local $SIG{__WARN__} = sub { };
-    open my $olderr, '>&STDERR';
-    open STDERR, '>', File::Spec->devnull;
-    eval "require '$file'";
-    open STDERR, '>&', $olderr;
-    return $@ if $@;
-    return 0;
-}
-
 =back
 
 =head1 SEE ALSO
diff --git a/lib/Catalyst/Engine/HTTP/Restarter.pm b/lib/Catalyst/Engine/HTTP/Restarter.pm
new file mode 100644 (file)
index 0000000..56290b8
--- /dev/null
@@ -0,0 +1,96 @@
+package Catalyst::Engine::HTTP::Restarter;
+
+use strict;
+use warnings;
+use base 'Catalyst::Engine::HTTP';
+use Catalyst::Engine::HTTP::Restarter::Watcher;
+use NEXT;
+
+sub run {
+    my ( $self, $class, $port, $host, $options ) = @_;
+
+    $options ||= {};
+
+    # Setup restarter
+    my $restarter;
+    my $parent = $$;
+    
+    unless ( $restarter = fork ) {
+
+        # Prepare
+        close STDIN;
+        close STDOUT;
+        
+        my $watcher = Catalyst::Engine::HTTP::Restarter::Watcher->new(
+            directory => File::Spec->catdir( $FindBin::Bin, '..' ),
+            regex     => $options->{restart_regex},
+            delay     => $options->{restart_delay},
+        );
+
+        while (1) {
+            # poll for changed files
+            my @changed_files = $watcher->watch();
+            
+            # check if our parent process has died
+            exit if ( getppid == 1 );            
+            
+            # Restart if any files have changed
+            if ( @changed_files ) {
+                my $files = join ', ', @changed_files;
+                print STDERR qq/File(s) "$files" modified, restarting\n\n/;
+                kill( 1, $parent );
+                exit;
+            }
+        }
+    }
+
+    return $self->NEXT::run( $class, $port, $host, $options );
+}
+
+1;
+__END__
+
+=head1 NAME
+
+Catalyst::Engine::HTTP::Restarter - Catalyst Auto-Restarting HTTP Engine
+
+=head1 SYNOPSIS
+
+    script/myapp_server.pl -restart
+
+=head1 DESCRIPTION
+
+The Restarter engine will monitor files in your application for changes
+and restart the server when any changes are detected.
+
+=head1 METHODS
+
+=over 4
+
+=item run
+
+=back
+
+=head1 SEE ALSO
+
+L<Catalyst>, L<Catalyst::Engine::HTTP>, L<Catalyst::Engine::CGI>,
+L<Catalyst::Engine>.
+
+=head1 AUTHORS
+
+Sebastian Riedel, <sri@cpan.org>
+
+Dan Kubb, <dan.kubb-cpan@onautopilot.com>
+
+Andy Grundman, <andy@hybridized.org>
+
+=head1 THANKS
+
+Many parts are ripped out of C<HTTP::Server::Simple> by Jesse Vincent.
+
+=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/Engine/HTTP/Restarter/Watcher.pm b/lib/Catalyst/Engine/HTTP/Restarter/Watcher.pm
new file mode 100644 (file)
index 0000000..7a6bd10
--- /dev/null
@@ -0,0 +1,194 @@
+package Catalyst::Engine::HTTP::Restarter::Watcher;
+
+use strict;
+use warnings;
+use base 'Class::Accessor::Fast';
+use File::Find;
+use File::Modified;
+use File::Spec;
+use Time::HiRes qw/sleep/;
+
+__PACKAGE__->mk_accessors( qw/delay
+                              directory 
+                              modified
+                              regex
+                              watch_list/ );
+
+sub new {
+    my ( $class, %args ) = @_;
+    
+    my $self = { %args };
+    
+    bless $self, $class;
+    
+    $self->_init;
+    
+    return $self;
+}
+
+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 watch {
+    my $self = shift;
+    
+    my @changes;
+    my @changed_files;
+    
+    sleep $self->delay || 1;
+    
+    eval { @changes = $self->modified->changed };
+    if ( $@ ) {
+        # File::Modified will die if a file is deleted.
+        my ($deleted_file) = $@ =~ /stat '(.+)'/;
+        push @changed_files, $deleted_file || 'unknown file';
+    }
+    
+    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 ) {
+            # 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 };
+                             
+            return unless @changed_files;
+        }
+
+        # Test modified pm's
+        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 '*' x 80, "\n";
+                print STDERR $error;
+                print STDERR '*' x 80, "\n";
+                return;
+            }
+        }
+    }
+    
+    return @changed_files;
+}
+
+sub _index_directory {
+    my $self = shift;
+    
+    my $dir   = $self->directory   || die "No directory specified";
+    my $regex = $self->regex       || '\.pm$';
+    my %list;
+    
+    finddepth(
+        {
+            wanted => sub {
+                my $file = File::Spec->rel2abs($File::Find::name);
+                return unless $file =~ /$regex/;
+                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/..}{};                
+                $list{$cur_dir} = 1;
+            },
+            no_chdir => 1
+        },
+        $dir
+    );
+    return \%list;
+}
+
+sub _test {
+    my ( $self, $file ) = @_;
+    
+    delete $INC{$file};
+    local $SIG{__WARN__} = sub { };
+    
+    open my $olderr, '>&STDERR';
+    open STDERR, '>', File::Spec->devnull;
+    eval "require '$file'";
+    open STDERR, '>&', $olderr;
+    
+    return ($@) ? $@ : 0;
+}    
+
+1;
+__END__
+
+=head1 NAME
+
+Catalyst::Engine::HTTP::Restarter::Watcher - Watch for changed application
+files
+
+=head1 SYNOPSIS
+
+    my $watcher = Catalyst::Engine::HTTP::Restarter::Watcher->new(
+        directory => '/path/to/MyApp',
+        regex     => '\.yml$|\.yaml$|\.pm$',
+        delay     => 1,
+    );
+    
+    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 watch
+
+Returns a list of files that have been added, deleted, or changed since the
+last time watch was called.
+
+=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>
+
+=head1 THANKS
+
+Many parts are ripped out of C<HTTP::Server::Simple> by Jesse Vincent.
+
+=head1 COPYRIGHT
+
+This program is free software, you can redistribute it and/or modify it under
+the same terms as Perl itself.
+
+=cut
index 3dabe5b..d63ab87 100644 (file)
@@ -734,7 +734,6 @@ use Getopt::Long;
 use Pod::Usage;
 use FindBin;
 use lib "$FindBin::Bin/../lib";
-use [% name %];
 
 my $fork          = 0;
 my $help          = 0;
@@ -758,6 +757,12 @@ GetOptions(
 
 pod2usage(1) if $help;
 
+if ( $restart ) {
+    $ENV{CATALYST_ENGINE} = 'HTTP::Restarter';
+}
+
+require [% name %];
+
 [% name %]->run( $port, $host, {
     argv   => \@argv,
     'fork' => $fork,