# Copyright (C) 2020-2021 all contributors # License: AGPL-3.0+ # implements the small subset of Linux::Inotify2 functionality we use # using IO::KQueue on *BSD systems. package PublicInbox::KQNotify; use strict; use v5.10.1; use IO::KQueue; use PublicInbox::DSKQXS; # wraps IO::KQueue for fork-safe DESTROY use PublicInbox::FakeInotify qw(fill_dirlist on_dir_change); use Time::HiRes qw(stat); # NOTE_EXTEND detects rename(2), NOTE_WRITE detects link(2) sub MOVED_TO_OR_CREATE () { NOTE_EXTEND|NOTE_WRITE } sub new { my ($class) = @_; bless { dskq => PublicInbox::DSKQXS->new, watch => {} }, $class; } sub watch { my ($self, $path, $mask) = @_; my ($fh, $watch); if (-d $path) { opendir($fh, $path) or return; my @st = stat($fh); $watch = bless [ $fh, $path, $st[10] ], 'PublicInbox::KQNotify::Watchdir'; } else { open($fh, '<', $path) or return; $watch = bless [ $fh, $path ], 'PublicInbox::KQNotify::Watch'; } my $ident = fileno($fh); $self->{dskq}->{kq}->EV_SET($ident, # ident (fd) EVFILT_VNODE, # filter EV_ADD | EV_CLEAR, # flags $mask, # fflags 0, 0); # data, udata if ($mask & (MOVED_TO_OR_CREATE|NOTE_DELETE|NOTE_LINK|NOTE_REVOKE)) { $self->{watch}->{$ident} = $watch; if ($mask & (NOTE_DELETE|NOTE_LINK|NOTE_REVOKE)) { fill_dirlist($self, $path, $fh) } } else { die "TODO Not implemented: $mask"; } $watch; } # emulate Linux::Inotify::fileno sub fileno { ${$_[0]->{dskq}->{kq}} } # noop for Linux::Inotify2 compatibility. Unlike inotify, # kqueue doesn't seem to overflow since it's limited by the number of # open FDs the process has sub on_overflow {} # noop for Linux::Inotify2 compatibility, we use `0' timeout for ->kevent sub blocking {} # behave like Linux::Inotify2->read sub read { my ($self) = @_; my @kevents = $self->{dskq}->{kq}->kevent(0); my $events = []; my @gone; my $watch = $self->{watch}; for my $kev (@kevents) { my $ident = $kev->[KQ_IDENT]; my $mask = $kev->[KQ_FFLAGS]; my ($dh, $path, $old_ctime) = @{$watch->{$ident}}; if (!defined($old_ctime)) { push @$events, bless(\$path, 'PublicInbox::FakeInotify::Event') } elsif ($mask & (MOVED_TO_OR_CREATE|NOTE_DELETE|NOTE_LINK| NOTE_REVOKE|NOTE_RENAME)) { my @new_st = stat($path); if (!@new_st && $!{ENOENT}) { push @$events, bless(\$path, 'PublicInbox::FakeInotify::'. 'SelfGoneEvent'); push @gone, $ident; delete $self->{dirlist}->{$path}; next; } if (!@new_st) { warn "unhandled stat($path) error: $!\n"; next; } $watch->{$ident}->[3] = $new_st[10]; # ctime rewinddir($dh); on_dir_change($events, $dh, $path, $old_ctime, $self->{dirlist}); } } delete @$watch{@gone}; @$events; } package PublicInbox::KQNotify::Watch; use strict; sub name { $_[0]->[1] } sub cancel { close $_[0]->[0] or die "close: $!" } package PublicInbox::KQNotify::Watchdir; use strict; sub name { $_[0]->[1] } sub cancel { closedir $_[0]->[0] or die "closedir: $!" } 1;