# Copyright (C) all contributors # License: AGPL-3.0+ # 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(<{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;