diff options
Diffstat (limited to 'lib/PublicInbox/FakeInotify.pm')
-rw-r--r-- | lib/PublicInbox/FakeInotify.pm | 203 |
1 files changed, 113 insertions, 90 deletions
diff --git a/lib/PublicInbox/FakeInotify.pm b/lib/PublicInbox/FakeInotify.pm index 641bc5bd..8be07135 100644 --- a/lib/PublicInbox/FakeInotify.pm +++ b/lib/PublicInbox/FakeInotify.pm @@ -1,15 +1,15 @@ -# Copyright (C) 2020-2021 all contributors <meta@public-inbox.org> +# Copyright (C) all contributors <meta@public-inbox.org> # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> # for systems lacking Linux::Inotify2 or IO::KQueue, just emulates -# enough of Linux::Inotify2 +# enough of Linux::Inotify2 we use. package PublicInbox::FakeInotify; -use strict; -use v5.10.1; -use parent qw(Exporter); +use v5.12; use Time::HiRes qw(stat); use PublicInbox::DS qw(add_timer); +use Errno qw(ENOTDIR ENOENT); sub IN_MODIFY () { 0x02 } # match Linux inotify +# my $IN_MOVED_FROM 0x00000040 /* File was moved from X. */ # my $IN_MOVED_TO = 0x80; # my $IN_CREATE = 0x100; sub MOVED_TO_OR_CREATE () { 0x80 | 0x100 } @@ -17,98 +17,125 @@ sub IN_DELETE () { 0x200 } sub IN_DELETE_SELF () { 0x400 } sub IN_MOVE_SELF () { 0x800 } -our @EXPORT_OK = qw(fill_dirlist on_dir_change); - my $poll_intvl = 2; # same as Filesys::Notify::Simple -sub new { bless { watch => {}, dirlist => {} }, __PACKAGE__ } +sub new { bless {}, __PACKAGE__ } -sub fill_dirlist ($$$) { - my ($self, $path, $dh) = @_; - my $dirlist = $self->{dirlist}->{$path} = {}; - while (defined(my $n = readdir($dh))) { - $dirlist->{$n} = undef if $n !~ /\A\.\.?\z/; - } -} +sub on_dir_change ($$$$$) { # used by KQNotify subclass + my ($self, $events, $dh, $path, $dir_delete) = @_; + my $old = $self->{dirlist}->{$path}; + my @cur = grep(!/\A\.\.?\z/, readdir($dh)); + $self->{dirlist}->{$path} = \@cur; -# behaves like Linux::Inotify2->watch -sub watch { - my ($self, $path, $mask) = @_; - my @st = stat($path) or return; - my $k = "$path\0$mask"; - $self->{watch}->{$k} = $st[10]; # 10 - ctime - if ($mask & IN_DELETE) { - opendir(my $dh, $path) or return; - fill_dirlist($self, $path, $dh); + # new files: + my %tmp = map { $_ => undef } @cur; + delete @tmp{@$old}; + push(@$events, map { + bless \"$path/$_", 'PublicInbox::FakeInotify::Event' + } keys %tmp); + + if ($dir_delete) { + %tmp = map { $_ => undef } @$old; + delete @tmp{@cur}; + push(@$events, map { + bless \"$path/$_", 'PublicInbox::FakeInotify::GoneEvent' + } keys %tmp); } - bless [ $self->{watch}, $k ], 'PublicInbox::FakeInotify::Watch'; } -# also used by KQNotify since it kevent requires readdir on st_nlink -# count changes. -sub on_dir_change ($$$$$) { - my ($events, $dh, $path, $old_ctime, $dirlist) = @_; - my $oldlist = $dirlist->{$path}; - my $newlist = $oldlist ? {} : undef; - while (defined(my $base = readdir($dh))) { - next if $base =~ /\A\.\.?\z/; - my $full = "$path/$base"; - my @st = stat($full); - if (@st && $st[10] > $old_ctime) { - push @$events, - bless(\$full, 'PublicInbox::FakeInotify::Event') +sub watch_open ($$$) { # used by KQNotify subclass + my ($self, $path, $dir_delete) = @_; + my ($fh, @st, @st0, $tries); + do { +again: + unless (@st0 = stat($path)) { + warn "W: stat($path): $!" if $! != ENOENT; + return; } - if (!@st) { - # ignore ENOENT due to race - warn "unhandled stat($full) error: $!\n" if !$!{ENOENT}; - } elsif ($newlist) { - $newlist->{$base} = undef; + if (!(-d _ ? opendir($fh, $path) : open($fh, '<', $path))) { + goto again if $! == ENOTDIR && ++$tries < 10; + warn "W: open($path): $!" if $! != ENOENT; + return; } + @st = stat($fh) or die "fstat($path): $!"; + } while ("@st[0,1]" ne "@st0[0,1]" && + ((++$tries < 10) || (warn(<<EOM) && return))); +E: $path switching inodes too frequently to watch +EOM + if (-d _) { + $self->{dirlist}->{$path} = []; + on_dir_change($self, [], $fh, $path, $$dir_delete); + } else { + $$dir_delete = 0; } - return if !$newlist; - delete @$oldlist{keys %$newlist}; - $dirlist->{$path} = $newlist; - push(@$events, map { - bless \"$path/$_", 'PublicInbox::FakeInotify::GoneEvent' - } keys %$oldlist); + bless [ @st[0, 1, 10], $path, $fh ], 'PublicInbox::FakeInotify::Watch' +} + +# behaves like Linux::Inotify2->watch +sub watch { + my ($self, $path, $mask) = @_; # mask is ignored + my $dir_delete = $mask & IN_DELETE ? 1 : 0; + my $w = watch_open($self, $path, \$dir_delete) or return; + pop @$w; # no need to keep $fh open for non-kqueue + $self->{watch}->{"$path\0$dir_delete"} = $w; +} + +sub gone ($$$) { # used by KQNotify subclass + my ($self, $ident, $path) = @_; + delete $self->{watch}->{$ident}; + delete $self->{dirlist}->{$path}; + bless(\$path, 'PublicInbox::FakeInotify::SelfGoneEvent'); +} + +# fuzz the time for freshly modified directories for low-res VFS +sub dir_adj ($) { + my ($old_ctime) = @_; + my $now = Time::HiRes::time; + my $diff = $now - $old_ctime; + my $adj = $poll_intvl + 1; + ($diff > -$adj && $diff < $adj) ? 1 : 0; } # behaves like non-blocking Linux::Inotify2->read sub read { my ($self) = @_; - my $watch = $self->{watch} or return (); - my $events = []; - my @watch_gone; - for my $x (keys %$watch) { - my ($path, $mask) = split(/\0/, $x, 2); - my @now = stat($path); - if (!@now && $!{ENOENT} && ($mask & IN_DELETE_SELF)) { - push @$events, bless(\$path, - 'PublicInbox::FakeInotify::SelfGoneEvent'); - push @watch_gone, $x; - delete $self->{dirlist}->{$path}; + my $ret = []; + while (my ($ident, $w) = each(%{$self->{watch}})) { + if (!@$w) { # cancelled + delete($self->{watch}->{$ident}); + next; } - next if !@now; - my $old_ctime = $watch->{$x}; - $watch->{$x} = $now[10]; - next if $old_ctime == $now[10]; - if ($mask & IN_MODIFY) { - push @$events, - bless(\$path, 'PublicInbox::FakeInotify::Event') - } elsif ($mask & (MOVED_TO_OR_CREATE | IN_DELETE)) { - if (opendir(my $dh, $path)) { - on_dir_change($events, $dh, $path, $old_ctime, - $self->{dirlist}); - } elsif ($!{ENOENT}) { - push @watch_gone, $x; - delete $self->{dirlist}->{$path}; - } else { - warn "W: opendir $path: $!\n"; + my $dir_delete = (split(/\0/, $ident, 2))[1]; + my ($old_dev, $old_ino, $old_ctime, $path) = @$w; + my @new_st = stat($path); + warn "W: stat($path): $!\n" if !@new_st && $! != ENOENT; + if (!@new_st || "$old_dev $old_ino" ne "@new_st[0,1]") { + push @$ret, gone($self, $ident, $path); + next; + } + if (-d _ && $new_st[10] > ($old_ctime - dir_adj($old_ctime))) { + opendir(my $fh, $path) or do { + if ($! == ENOENT || $! == ENOTDIR) { + push @$ret, gone($self, $ident, $path); + } else { + warn "W: opendir($path): $!"; + } + next; + }; + @new_st = stat($fh) or die "fstat($path): $!"; + if ("$old_dev $old_ino" ne "@new_st[0,1]") { + push @$ret, gone($self, $ident, $path); + next; } + $w->[2] = $new_st[10]; + on_dir_change($self, $ret, $fh, $path, $dir_delete); + } elsif ($new_st[10] > $old_ctime) { # regular files, etc + $w->[2] = $new_st[10]; + push @$ret, bless(\$path, + 'PublicInbox::FakeInotify::Event'); } } - delete @$watch{@watch_gone}; - @$events; + @$ret; } sub poll_once { @@ -118,34 +145,30 @@ sub poll_once { } package PublicInbox::FakeInotify::Watch; -use strict; +use v5.12; -sub cancel { - my ($self) = @_; - delete $self->[0]->{$self->[1]}; -} +sub cancel { @{$_[0]} = () } -sub name { - my ($self) = @_; - (split(/\0/, $self->[1], 2))[0]; -} +sub name { $_[0]->[3] } package PublicInbox::FakeInotify::Event; -use strict; +use v5.12; sub fullname { ${$_[0]} } sub IN_DELETE { 0 } +sub IN_MOVED_FROM { 0 } sub IN_DELETE_SELF { 0 } package PublicInbox::FakeInotify::GoneEvent; -use strict; +use v5.12; our @ISA = qw(PublicInbox::FakeInotify::Event); sub IN_DELETE { 1 } +sub IN_MOVED_FROM { 0 } package PublicInbox::FakeInotify::SelfGoneEvent; -use strict; +use v5.12; our @ISA = qw(PublicInbox::FakeInotify::GoneEvent); sub IN_DELETE_SELF { 1 } |