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(
-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$')
use Moose;
use Catalyst::Watcher;
-use File::Spec;
-use FindBin;
use namespace::clean -except => 'meta';
has 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} ) );
+ $self->_watcher( Catalyst::Watcher->instantiate_subclass( %{$p} ) );
}
sub run_and_watch {
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 {
-package Catalyst::Watcher;
+package Catalyst::Watcher::FileModified;
use Moose;
-use Moose::Util::TypeConstraints;
use File::Find;
use File::Modified;
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]',
is => 'rw',
isa => 'File::Modified',
lazy_build => 1,
+ clearer => '_clear_modified',
);
+
sub _build__watched_files {
my $self = shift;
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);
follow_fast => $self->follow_symlinks ? 1 : 0,
no_chdir => 1
},
- @{ $self->directory }
+ @{ $self->directories }
);
return \%list;
);
}
-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;
=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
=head1 SEE ALSO
-L<Catalyst>, L<Catalyst::Restarter>, <File::Modified>
+L<Catalyst>, L<Catalyst::Watcher>, L<Catalyst::Restarter>,
+<File::Modified>
=head1 AUTHORS
--- /dev/null
+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