#!/usr/bin/perl =head1 NAME rrr-server - watch a tree and continuously update indexfiles =head1 SYNOPSIS rrr-server [options] principalfile =head1 OPTIONS =over 8 =cut my @opt = <<'=back' =~ /B<--(\S+)>/g; =item B<--help|h> Prints a brief message and exists. =item B<--verbose|v+> More feedback. =back =head1 DESCRIPTION After you have setup a tree watch it with inotify and keep it uptodate. Depends on inotify which probably only exists on linux. =head1 PREREQUISITE Linux::Inotify2. It is not declared as prerequisites of the F:R:M:Recent package because server side handling is optional. XXX Todo: make server side handling a separate package so we can declare Inotify2 as prereq. =cut use strict; use warnings; use File::Find qw(find); use lib "/home/k/sources/rersyncrecent/lib"; use File::Rsync::Mirror::Recent; use File::Spec; use Getopt::Long; use Pod::Usage qw(pod2usage); use Time::HiRes qw(time); our %Opt; GetOptions(\%Opt, @opt, ) or pod2usage(1); if ($Opt{help}) { pod2usage(0); } if (@ARGV != 1) { pod2usage(1); } sub my_inotify { my($inotify, $directory) = @_; unless ($inotify->watch ( $directory, IN_CLOSE_WRITE() |IN_MOVED_FROM() |IN_MOVED_TO() |IN_CREATE() |IN_DELETE() |IN_DELETE_SELF() |IN_MOVE_SELF() )) { # or die "watch creation failed: $!"; if ($!{ENOSPC}) { die "Alert: ENOSPC reached, probably your system needs to increase the amount of inotify watches allowed per user via '/proc/sys/fs/inotify/max_user_watches'\n"; } else { for my $err (qw(ENOENT EBADF EEXIST)) { if ($!{$err}) { warn "$err encountered on '$directory'. Giving up on this watch, trying to continue.\n" if $Opt{verbose}; return; } } die "Alert: $!"; } } } sub handle_file { my($rf,$fullname,$type,$batch) = @_; push @$batch, {path => $fullname, type => $type}; } sub newdir { my($inotify,$rf,$fullname,$batch) = @_; return if -l $fullname; my_inotify($inotify, $fullname); # immediately inspect it, we certainly have missed the first # events in this directory opendir my $dh, $fullname or return; for my $dirent (readdir $dh) { next if $dirent eq "." || $dirent eq ".."; my $abs = File::Spec->catfile($fullname,$dirent); if (-l $abs || -f _) { warn "[..:..:..] Readdir_F $abs\n" if $Opt{verbose}; handle_file($rf,$abs,"new",$batch); } elsif (-d $abs) { warn "[..:..:..] Readdir_D $abs\n" if $Opt{verbose}; newdir($inotify,$rf,$abs,$batch); } } } sub handle_event { my($inotify,$rf,$ev,$batch) = @_; my @stringifiedmask; for my $watch ( "IN_CREATE", "IN_CLOSE_WRITE", "IN_MOVED_TO", # new "IN_DELETE", "IN_MOVED_FROM", # delete "IN_DELETE_SELF", "IN_MOVE_SELF", # self ) { if ($ev->$watch()){ push @stringifiedmask, $watch; # new directories must be added to the watches, deleted # directories deleted; moved directories both } } my $rootdir = $rf->localroot; # warn sprintf "rootdir[$rootdir]time[%s]ev.w.name[%s]ev.name[%s]ev.fullname[%s]mask[%s]\n", time, $ev->w->name, $ev->name, $ev->fullname, join("|",@stringifiedmask); my $ignore = 0; if ($ev->w->name eq $rootdir) { my $meta = $rf->meta_data; my $ignore_rx = qr((?x: ^ \Q$meta->{filenameroot}\E - [0-9]*[smhdWMQYZ] \Q$meta->{serializer_suffix}\E )); if ($ev->name =~ $ignore_rx) { # warn sprintf "==> Ignoring object in rootdir looking like internal file: %s", $ev->name; $ignore++; } } unless ($ignore) { my $fullname = $ev->fullname; my($reportname) = $fullname =~ m{^\Q$rootdir\E/(.*)}; my $time = sprintf "%02d:%02d:%02d", (localtime)[2,1,0]; if (0) { } elsif ($ev->IN_Q_OVERFLOW) { $rf->_requires_fsck(1); } elsif ($ev->IN_DELETE || $ev->IN_MOVED_FROM) { # we don't know whether it was a directory, we simply pass # it to $rf. $rf must be robust enough to swallow bogus # deletes. Alternatively we could look into $rf whether # this object is known, couldn't we? handle_file($rf,$fullname,"delete",$batch); warn "[$time] Deleteobj $reportname (@stringifiedmask)\n" if $Opt{verbose}; } elsif ($ev->IN_DELETE_SELF || $ev->IN_MOVE_SELF) { $ev->w->cancel; warn "[$time] Delwatcher $reportname (@stringifiedmask)\n" if $Opt{verbose}; } elsif (-l $fullname) { handle_file($rf,$fullname,"new",$batch); warn "[$time] Updatelink $reportname (@stringifiedmask)\n" if $Opt{verbose}; } elsif ($ev->IN_ISDIR) { newdir($inotify,$rf,$fullname,$batch); warn "[$time] Newwatcher $reportname (@stringifiedmask)\n" if $Opt{verbose}; } elsif (-f _) { if ($ev->IN_CLOSE_WRITE || $ev->IN_MOVED_TO) { handle_file($rf,$fullname,"new",$batch); warn "[$time] Updatefile $reportname (@stringifiedmask)\n" if $Opt{verbose}; } } else { warn "[$time] Ignore $reportname (@stringifiedmask)\n" if $Opt{verbose}; } } } sub init { my($inotify, $rootdir) = @_; foreach my $directory ( File::Find::Rule->new->directory->not( File::Find::Rule->new->symlink )->in($rootdir) ) { my_inotify($inotify, $directory); } } # Need also to verify that we watch all directories we encounter. sub fsck { my($rf) = @_; 0 == system $^X, "-Ilib", "bin/rrr-fsck", $rf->rfile, "--verbose" or die; } MAIN: { my($principal) = @ARGV; $principal = File::Spec->rel2abs($principal); my $recc = File::Rsync::Mirror::Recent->new (local => $principal); my($rf) = $recc->principal_recentfile; my $rootdir = $rf->localroot; for my $req (qw(Linux::Inotify2 File::Find::Rule)) { eval qq{ require $req; 1 }; if ($@) { die "Failing on 'require $req': $@" } else { $req->import; } } my $inotify = new Linux::Inotify2 or die "Unable to create new inotify object: $!"; init($inotify, $rootdir); fsck($rf); my $last_aggregate_call = 0; while () { my @events = $inotify->read; unless ( @events > 0 ) { print "Alert: inotify read error: $!"; last; } my @batch; foreach my $event (@events) { handle_event($inotify,$rf,$event,\@batch); } $rf->batch_update(\@batch) if @batch; if (time > $last_aggregate_call + 60) { # arbitrary $rf->aggregate; $last_aggregate_call = time; } if ($rf->_requires_fsck) { fsck($rf); $rf->_requires_fsck(0); } } } __END__ # Local Variables: # mode: cperl # coding: utf-8 # cperl-indent-level: 4 # End: