| # 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 we use. |
| package PublicInbox::FakeInotify; |
| 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 } |
| sub IN_DELETE () { 0x200 } |
| sub IN_DELETE_SELF () { 0x400 } |
| sub IN_MOVE_SELF () { 0x800 } |
| |
| my $poll_intvl = 2; # same as Filesys::Notify::Simple |
| |
| sub new { bless {}, __PACKAGE__ } |
| |
| 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; |
| |
| # 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); |
| } |
| } |
| |
| 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 (!(-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; |
| } |
| 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 $ret = []; |
| while (my ($ident, $w) = each(%{$self->{watch}})) { |
| if (!@$w) { # cancelled |
| delete($self->{watch}->{$ident}); |
| next; |
| } |
| 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'); |
| } |
| } |
| @$ret; |
| } |
| |
| sub poll_once { |
| my ($obj) = @_; |
| $obj->event_step; # PublicInbox::InboxIdle::event_step |
| add_timer($poll_intvl, \&poll_once, $obj); |
| } |
| |
| package PublicInbox::FakeInotify::Watch; |
| use v5.12; |
| |
| sub cancel { @{$_[0]} = () } |
| |
| sub name { $_[0]->[3] } |
| |
| package PublicInbox::FakeInotify::Event; |
| 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 v5.12; |
| our @ISA = qw(PublicInbox::FakeInotify::Event); |
| |
| sub IN_DELETE { 1 } |
| sub IN_MOVED_FROM { 0 } |
| |
| package PublicInbox::FakeInotify::SelfGoneEvent; |
| use v5.12; |
| our @ISA = qw(PublicInbox::FakeInotify::GoneEvent); |
| |
| sub IN_DELETE_SELF { 1 } |
| |
| 1; |