Catalyst::Restarter::Forking: clear watcher in child process
[catagits/Catalyst-Devel.git] / lib / Catalyst / Restarter.pm
index 33b3d17..81295f1 100644 (file)
@@ -2,20 +2,34 @@ package Catalyst::Restarter;
 
 use Moose;
 
-use Catalyst::Watcher;
+use Cwd qw( abs_path );
+use File::ChangeNotify;
 use File::Spec;
 use FindBin;
+use Catalyst::Utils;
 use namespace::clean -except => 'meta';
 
-has restart_sub => (
+has start_sub => (
     is       => 'ro',
     isa      => 'CodeRef',
     required => 1,
 );
 
+has argv =>  (
+    is       => 'ro',
+    isa      => 'ArrayRef',
+    required => 1,
+);
+
 has _watcher => (
-    is  => 'rw',
-    isa => 'Catalyst::Watcher',
+    is      => 'rw',
+    isa     => 'File::ChangeNotify::Watcher',
+    clearer => '_clear_watcher',
+);
+
+has _filter => (
+    is      => 'rw',
+    isa     => 'RegexpRef',
 );
 
 has _child => (
@@ -23,15 +37,51 @@ has _child => (
     isa => 'Int',
 );
 
+sub pick_subclass {
+    my $class = shift;
+
+    my $subclass;
+    $subclass =
+        defined $ENV{CATALYST_RESTARTER}
+            ? $ENV{CATALYST_RESTARTER}
+            :  $^O eq 'MSWin32'
+            ? 'Win32'
+            : 'Forking';
+
+    $subclass = 'Catalyst::Restarter::' . $subclass;
+
+    Catalyst::Utils::ensure_class_loaded($subclass);
+
+    return $subclass;
+}
+
 sub BUILD {
     my $self = shift;
     my $p    = shift;
 
-    delete $p->{restart_sub};
+    delete $p->{start_sub};
+
+    $p->{filter} ||= qr/(?:\/|^)(?![.#_]).+(?:\.yml$|\.yaml$|\.conf|\.pm)$/;
+
+    my $app_root = abs_path( File::Spec->catdir( $FindBin::Bin, '..' ) );
+
+    # Monitor application root dir
+    $p->{directories} ||= $app_root;
+
+    # exclude t/, root/ and hidden dirs
+    $p->{exclude} ||= [
+        File::Spec->catdir($app_root, 't'),
+        File::Spec->catdir($app_root, 'root'),
+        qr(/\.[^/]*/?$),    # match hidden dirs
+    ];
+
+    # keep filter regexp to make sure we don't restart on deleted
+    # files or directories where we can't check -d
+    $self->_filter( $p->{filter} );
 
     # 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} ) );
+    $self->_watcher( File::ChangeNotify->instantiate_watcher( %{$p} ) );
 }
 
 sub run_and_watch {
@@ -44,48 +94,61 @@ sub run_and_watch {
     $self->_restart_on_changes;
 }
 
-sub _fork_and_start {
+sub _restart_on_changes {
     my $self = shift;
 
-    if ( my $pid = fork ) {
-        $self->_child($pid);
-    }
-    else {
-        $self->restart_sub->();
+    # We use this loop in order to avoid having _handle_events() call back
+    # into this method. We used to do that, and the end result was that stack
+    # traces became longer and longer with every restart. Using this loop, the
+    # portion of the stack trace that covers this code does not grow.
+    while (1) {
+        my @events = $self->_watcher->wait_for_events();
+        $self->_handle_events(@events);
     }
 }
 
-sub _restart_on_changes {
-    my $self = shift;
+sub _handle_events {
+    my $self   = shift;
+    my @events = @_;
+
+    my @files;
+    # Filter out any events which are the creation / deletion of directories
+    # so that creating an empty directory won't cause a restart
+    for my $event (@events) {
+        my $path = $event->path();
+        my $type = $event->type();
+        if ( (    ( $type ne 'delete' && -f $path )
+               || ( $type eq 'delete' )
+             )
+             && ( $path =~ $self->_filter )
+        ) {
+            push @files, { path => $path, type => $type };
+        }
+    }
 
-    my $watcher = $self->_watcher;
+    if (@files) {
+        print STDERR "\n";
+        print STDERR "Saw changes to the following files:\n";
 
-    while (1) {
-        my @files = $watcher->find_changed_files
-            or next;
+        for my $f (@files) {
+            my $path = $f->{path};
+            my $type = $f->{type};
+            print STDERR " - $path ($type)\n";
+        }
 
-        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->_kill_child;
 
         $self->_fork_and_start;
-
-        return unless $self->_child;
     }
 }
 
 sub DEMOLISH {
     my $self = shift;
 
-    if ( $self->_child ) {
-        kill 2, $self->_child;
-    }
+    $self->_kill_child;
 }
 
 __PACKAGE__->meta->make_immutable;
@@ -96,42 +159,53 @@ __END__
 
 =head1 NAME
 
-Catalyst::Restarter - Uses Catalyst::Watcher to check for changed files and restart the server
+Catalyst::Restarter - Uses File::ChangeNotify 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,
+    my $class = Catalyst::Restarter->pick_subclass;
+
+    my $restarter = $class->new(
+        directories => '/path/to/MyApp',
+        regex       => '\.yml$|\.yaml$|\.conf|\.pm$',
+        start_sub => sub { ... }
     );
 
-    while (1) {
-        my @changed_files = $watcher->watch();
-    }
+    $restarter->run_and_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.
+This is the base class for all restarters, and it also provide
+functionality for picking an appropriate restarter subclass for a
+given platform.
+
+This class uses L<File::ChangeNotify> to watch one or more directories
+of files and restart the Catalyst server when any of those files
+changes.
 
 =head1 METHODS
 
-=head2 new ( directory => $path [, regex => $regex, delay => $delay ] )
+=head2 pick_subclass
+
+Returns the name of an appropriate subclass for the given platform.
+
+=head2 new ( start_sub => sub { ... }, ... )
+
+This method creates a new restarter object, but should be called on a
+subclass, not this class.
 
-Creates a new Watcher object.
+The "start_sub" argument is required. This is a subroutine reference
+that can be used to start the Catalyst server.
 
-=head2 find_changed_files
+=head2 run_and_watch
 
-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".
+This method forks, starts the server in a child process, and then
+watched for changed files in the parent. When files change, it kills
+the child, forks again, and starts a new server.
 
 =head1 SEE ALSO
 
-L<Catalyst>, L<Catalyst::Restarter>, <File::Modified>
+L<Catalyst>, L<File::ChangeNotify>
 
 =head1 AUTHORS