Lots of changes to how the restarter & watcher work.
Dave Rolsky [Sat, 2 May 2009 01:55:00 +0000 (01:55 +0000)]
First, the "wait on events" loop is no longer in the restarter. The
restarter lets the watcher handle this. When an event happens, the
watcher calls

   $restarter->handle_changes(@events);

This made it _much_ easier to implement the Inotify-based watcher I've
added. Catalyst::Watcher is now a parent & factory. The old watcher
code has been split between ::Watcher and ::Watcher::FileModified.

The FileModified version has been fixed to handle file deletions a
little better, but it's still kind of broken.

The Inotify version handles everything correctly, though sometimes
it's reports of what changed can be off (but it restarts as needed,
and adds new directories to the watch list properly).

Finally, I fixed some bugs in the Helper module where the generated
script did not use the right names for arguments passed when creating
the restarter.

lib/Catalyst/Helper.pm
lib/Catalyst/Restarter.pm
lib/Catalyst/Watcher/FileModified.pm [moved from lib/Catalyst/Watcher.pm with 51% similarity]
lib/Catalyst/Watcher/Inotify.pm [new file with mode: 0644]

index 76cd37b..27accf4 100644 (file)
@@ -1030,11 +1030,13 @@ if ( $restart ) {
         if $background;
 
     my %args;
-    $args{watch_directory} = $watch_directory
+    $args{follow_symlinks} = 1
+        if $follow_symlinks;
+    $args{directories} = $watch_directory
         if defined $watch_directory;
-    $args{check_interval} = $check_interval
+    $args{interval} = $check_interval
         if defined $check_interval;
-    $args{file_regex} = qr/$file_regex/
+    $args{regex} = qr/$file_regex/
         if defined $file_regex;
 
     my $restarter = Catalyst::Restarter->new(
@@ -1069,6 +1071,7 @@ else {
    -r -restart        restart when files get modified
                       (defaults to false)
    -rd -restartdelay  delay between file checks
+                      (ignored if you have Linux::Inotify2 installed)
    -rr -restartregex  regex match files that trigger
                       a restart when modified
                       (defaults to '\.yml$|\.yaml$|\.conf|\.pm$')
index 3a1f6b1..e35731b 100644 (file)
@@ -3,8 +3,6 @@ package Catalyst::Restarter;
 use Moose;
 
 use Catalyst::Watcher;
-use File::Spec;
-use FindBin;
 use namespace::clean -except => 'meta';
 
 has restart_sub => (
@@ -31,7 +29,7 @@ sub BUILD {
 
     # 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( Catalyst::Watcher->instantiate_subclass( %{$p} ) );
 }
 
 sub run_and_watch {
@@ -58,24 +56,24 @@ sub _fork_and_start {
 sub _restart_on_changes {
     my $self = shift;
 
-    my $watcher = $self->_watcher;
+    $self->_watcher->watch($self);
+}
 
-    while (1) {
-        my @files = $watcher->find_changed_files
-            or next;
+sub handle_changes {
+    my $self  = shift;
+    my @files = @_;
 
-        print STDERR "\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";
+    print STDERR "\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";
 
-        $self->_kill_child;
+    $self->_kill_child;
 
-        $self->_fork_and_start;
+    $self->_fork_and_start;
 
-        return unless $self->_child;
-    }
+    $self->_restart_on_changes;
 }
 
 sub _kill_child {
similarity index 51%
rename from lib/Catalyst/Watcher.pm
rename to lib/Catalyst/Watcher/FileModified.pm
index 29adf3b..4a25874 100644 (file)
@@ -1,7 +1,6 @@
-package Catalyst::Watcher;
+package Catalyst::Watcher::FileModified;
 
 use Moose;
-use Moose::Util::TypeConstraints;
 
 use File::Find;
 use File::Modified;
@@ -9,45 +8,14 @@ use File::Spec;
 use Time::HiRes qw/sleep/;
 use namespace::clean -except => 'meta';
 
+extends 'Catalyst::Watcher';
+
 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]',
@@ -59,8 +27,10 @@ has _modified => (
     is         => 'rw',
     isa        => 'File::Modified',
     lazy_build => 1,
+    clearer    => '_clear_modified',
 );
 
+
 sub _build__watched_files {
     my $self = shift;
 
@@ -70,11 +40,11 @@ sub _build__watched_files {
     finddepth(
         {
             wanted => sub {
-                my $file = File::Spec->rel2abs($File::Find::name);
-                return unless $file =~ /$regex/;
-                return unless -f $file;
+                my $path = File::Spec->rel2abs($File::Find::name);
+                return unless $path =~ /$regex/;
+                return unless -f $path;
 
-                $list{$file} = 1;
+                $list{$path} = 1;
 
                 # also watch the directory for changes
                 my $cur_dir = File::Spec->rel2abs($File::Find::dir);
@@ -84,7 +54,7 @@ sub _build__watched_files {
             follow_fast => $self->follow_symlinks ? 1 : 0,
             no_chdir    => 1
         },
-        @{ $self->directory }
+        @{ $self->directories }
     );
 
     return \%list;
@@ -99,49 +69,65 @@ sub _build__modified {
     );
 }
 
-sub find_changed_files {
+sub watch {
+    my $self      = shift;
+    my $restarter = shift;
+
+    while (1) {
+        sleep $self->interval if $self->interval > 0;
+
+        my @changes = $self->_changed_files;
+
+        next unless @changes;
+
+        $restarter->handle_changes(@changes);
+
+        last;
+    }
+}
+
+sub _changed_files {
     my $self = shift;
 
     my @changes;
-    my @changed_files;
 
-    sleep $self->interval if $self->interval > 0;
+    eval {
+        @changes = map { { file => $_, status => 'modified' } }
+            grep { -f $_ } $self->_modified->changed;
+    };
 
-    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',
+        die unless $@ =~ /stat '(.+)'/;
+
+        push @changes, {
+            file   => $1 || 'unknown file',
             status => 'deleted',
-            };
-    }
+        };
 
-    if (@changes) {
+        $self->_clear_watched_files;
+        $self->_clear_modified;
+    }
+    else {
         $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;
+        my $old_watch = $self->_watched_files;
 
-            $self->_clear_watched_files;
+        $self->_clear_watched_files;
 
-            my $new_watch = $self->_watched_files;
+        my $new_watch = $self->_watched_files;
 
-            @changed_files
-                = map { { file => $_, status => 'added' } }
-                grep { !defined $old_watch->{$_} }
-                keys %{$new_watch};
+        my @new_files = grep { !defined $old_watch->{$_} }
+            grep {-f}
+            keys %{$new_watch};
 
-            return unless @changed_files;
+        if (@new_files) {
+            $self->_clear_modified;
+            push @changes, map { { file => $_, status => 'added' } } @new_files;
         }
     }
 
-    return @changed_files;
+    return @changes;
 }
 
 __PACKAGE__->meta->make_immutable;
@@ -152,18 +138,18 @@ __END__
 
 =head1 NAME
 
-Catalyst::Watcher - Watch for changed application files
+Catalyst::Watcher::FileModified - Watch for changed application files using File::Modified
 
 =head1 SYNOPSIS
 
-    my $watcher = Catalyst::Watcher->new(
-        directory => '/path/to/MyApp',
-        regex     => '\.yml$|\.yaml$|\.conf|\.pm$',
-        interval  => 3,
+    my $watcher = Catalyst::Watcher::FileModified->new(
+        directories => '/path/to/MyApp',
+        regex       => '\.yml$|\.yaml$|\.conf|\.pm$',
     );
 
     while (1) {
         my @changed_files = $watcher->watch();
+        ...
     }
 
 =head1 DESCRIPTION
@@ -187,7 +173,8 @@ the C<status> key contains one of "modified", "added", or "deleted".
 
 =head1 SEE ALSO
 
-L<Catalyst>, L<Catalyst::Restarter>, <File::Modified>
+L<Catalyst>, L<Catalyst::Watcher>, L<Catalyst::Restarter>,
+<File::Modified>
 
 =head1 AUTHORS
 
diff --git a/lib/Catalyst/Watcher/Inotify.pm b/lib/Catalyst/Watcher/Inotify.pm
new file mode 100644 (file)
index 0000000..71393ba
--- /dev/null
@@ -0,0 +1,174 @@
+package Catalyst::Watcher::Inotify;
+
+use Moose;
+
+use Linux::Inotify2;
+use namespace::clean -except => 'meta';
+
+extends 'Catalyst::Watcher';
+
+has _inotify => (
+    is         => 'rw',
+    isa        => 'Linux::Inotify2',
+    lazy_build => 1,
+);
+
+has _mask => (
+    is         => 'rw',
+    isa        => 'Int',
+    lazy_build => 1,
+);
+
+sub watch {
+    my $self      = shift;
+    my $restarter = shift;
+
+    my @events = $self->_wait_for_events;
+
+    $restarter->handle_changes( map { $self->_event_to_change($_) } @events );
+
+    return;
+}
+
+sub _wait_for_events {
+    my $self = shift;
+
+    while (1) {
+        # This is a blocking read, so it will not return until
+        # something happens. The restarter will end up calling ->watch
+        # again after handling the changes.
+        my @events = $self->_inotify->read;
+
+        my @interesting;
+        for my $event ( grep { $_->mask | IN_ISDIR } @events ) {
+            if ( $event->mask | IM_CREATE ) {
+                $self->_add_directory( $event->fullname );
+                push @interesting, $event;
+            }
+            elsif ( $event->mask | IM_DELETE_SELF ) {
+                $event->w->cancel;
+                push @interesting, $event;
+            }
+            elsif ( $event->name =~ /$regex/ ) {
+                push @interesting, $event;
+            }
+        }
+
+        return @interesting if @interesting;
+    }
+}
+
+sub _build__inotify {
+    my $self = shift;
+
+    my $inotify = Linux::Inotify2->new();
+
+    $self->_add_directory($_) for @{ $self->directories };
+
+    return $inotify;
+}
+
+sub _build__mask {
+    my $self = shift;
+
+    my $mask = IN_MODIFY | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF;
+    $mask |= IN_DONT_FOLLOW unless $self->follow_symlinks;
+
+    return $mask;
+}
+
+sub _add_directory {
+    my $self = shift;
+    my $dir  = shift;
+
+    finddepth(
+        {
+            wanted => sub {
+                my $path = File::Spec->rel2abs($File::Find::name);
+                return unless -d $path;
+
+                $self->_inotify->watch( $path, $self->_mask );
+            },
+            follow_fast => $self->follow_symlinks ? 1 : 0,
+            no_chdir    => 1
+        },
+        $dir;
+    );
+}
+
+sub _event_to_change {
+    my $self  = shift;
+    my $event = shift;
+
+    my %change = { file => $event->fullname };
+    if ( $event->mask() | IN_CREATE || $event->mask() ) {
+        $change{status} = 'added';
+    }
+    elsif ( $event->mask() | IN_MODIFY ) {
+        $change{status} = 'modified';
+    }
+    elsif ( $event->mask() | IN_DELETE || $event->mask() ) {
+        $change{status} = 'deleted';
+    }
+    else {
+        $change{status} = 'containing directory modified';
+    }
+
+    return \%change;
+}
+
+__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