about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <e@80x24.org>2023-12-28 04:23:00 +0000
committerEric Wong <e@80x24.org>2023-12-29 16:56:08 +0000
commitcf977e706b07e80f394570a393eb2169b9b9a1a7 (patch)
treecaa1a8a4cad1ec89e59658fc088a3b5054dbf58a
parent84874a852c80e3d4eb96af14c017b37424cdf840 (diff)
downloadpublic-inbox-cf977e706b07e80f394570a393eb2169b9b9a1a7.tar.gz
This is a step towards improving the out-of-the-box experience
in achieving notifications without XS, extra downloads, and .so
loading + runtime mmap overhead.

This also fixes loongarch support of all Linux syscalls due to
a bad regexp :x

All the reachable Linux architectures listed at
<https://portal.cfarm.net/machines/list/> should be supported.
At the moment, there appears to be no reachable sparc* Linux
machines available to cfarm users.

Fixes: b0e5093aa3572a86 (syscall: add support for riscv64, 2022-08-11)
-rw-r--r--MANIFEST4
-rwxr-xr-xdevel/sysdefs-list28
-rw-r--r--lib/PublicInbox/DirIdle.pm16
-rw-r--r--lib/PublicInbox/In3Event.pm24
-rw-r--r--lib/PublicInbox/In3Watch.pm20
-rw-r--r--lib/PublicInbox/InboxIdle.pm6
-rw-r--r--lib/PublicInbox/Inotify.pm27
-rw-r--r--lib/PublicInbox/Inotify3.pm115
-rw-r--r--lib/PublicInbox/Syscall.pm37
-rw-r--r--lib/PublicInbox/TailNotify.pm6
-rw-r--r--t/imapd.t2
-rw-r--r--t/inotify3.t17
-rw-r--r--t/lei-auto-watch.t4
-rw-r--r--t/lei-watch.t4
-rw-r--r--t/nntpd.t2
-rw-r--r--t/watch_maildir.t2
16 files changed, 287 insertions, 27 deletions
diff --git a/MANIFEST b/MANIFEST
index e22674b7..109ce88a 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -223,10 +223,13 @@ lib/PublicInbox/IPC.pm
 lib/PublicInbox/IdxStack.pm
 lib/PublicInbox/Import.pm
 lib/PublicInbox/In2Tie.pm
+lib/PublicInbox/In3Event.pm
+lib/PublicInbox/In3Watch.pm
 lib/PublicInbox/Inbox.pm
 lib/PublicInbox/InboxIdle.pm
 lib/PublicInbox/InboxWritable.pm
 lib/PublicInbox/Inotify.pm
+lib/PublicInbox/Inotify3.pm
 lib/PublicInbox/InputPipe.pm
 lib/PublicInbox/Isearch.pm
 lib/PublicInbox/KQNotify.pm
@@ -493,6 +496,7 @@ t/index-git-times.t
 t/indexlevels-mirror-v1.t
 t/indexlevels-mirror.t
 t/init.t
+t/inotify3.t
 t/io.t
 t/ipc.t
 t/iso-2202-jp.eml
diff --git a/devel/sysdefs-list b/devel/sysdefs-list
index d0166461..61532cf2 100755
--- a/devel/sysdefs-list
+++ b/devel/sysdefs-list
@@ -88,9 +88,37 @@ int main(void)
         MAYBE D(SYS_epoll_wait);
         D(SYS_epoll_pwait);
         D(SYS_signalfd4);
+
+        X(IN_CLOEXEC);
+        X(IN_ACCESS);
+        X(IN_ALL_EVENTS);
+        X(IN_ATTRIB);
+        X(IN_CLOSE);
+        X(IN_CLOSE_NOWRITE);
+        X(IN_CLOSE_WRITE);
+        X(IN_CREATE);
+        X(IN_DELETE);
+        X(IN_DELETE_SELF);
+        X(IN_DONT_FOLLOW);
+        X(IN_EXCL_UNLINK);
+        X(IN_IGNORED);
+        X(IN_ISDIR);
+        X(IN_MASK_ADD);
+        X(IN_MODIFY);
+        X(IN_MOVE);
+        X(IN_MOVED_FROM);
+        X(IN_MOVED_TO);
+        X(IN_MOVE_SELF);
+        X(IN_ONESHOT);
+        X(IN_ONLYDIR);
+        X(IN_OPEN);
+        X(IN_Q_OVERFLOW);
+        X(IN_UNMOUNT);
+
         D(SYS_inotify_init1);
         D(SYS_inotify_add_watch);
         D(SYS_inotify_rm_watch);
+
         D(SYS_prctl);
         D(SYS_fstatfs);
 
diff --git a/lib/PublicInbox/DirIdle.pm b/lib/PublicInbox/DirIdle.pm
index e6a326ab..230df166 100644
--- a/lib/PublicInbox/DirIdle.pm
+++ b/lib/PublicInbox/DirIdle.pm
@@ -10,12 +10,12 @@ use PublicInbox::In2Tie;
 
 my ($MAIL_IN, $MAIL_GONE, $ino_cls);
 if ($^O eq 'linux' && eval { require PublicInbox::Inotify; 1 }) {
-        $MAIL_IN = Linux::Inotify2::IN_MOVED_TO() |
-                Linux::Inotify2::IN_CREATE();
-        $MAIL_GONE = Linux::Inotify2::IN_DELETE() |
-                        Linux::Inotify2::IN_DELETE_SELF() |
-                        Linux::Inotify2::IN_MOVE_SELF() |
-                        Linux::Inotify2::IN_MOVED_FROM();
+        $MAIL_IN = PublicInbox::Inotify::IN_MOVED_TO() |
+                PublicInbox::Inotify::IN_CREATE();
+        $MAIL_GONE = PublicInbox::Inotify::IN_DELETE() |
+                        PublicInbox::Inotify::IN_DELETE_SELF() |
+                        PublicInbox::Inotify::IN_MOVE_SELF() |
+                        PublicInbox::Inotify::IN_MOVED_FROM();
         $ino_cls = 'PublicInbox::Inotify';
 # Perl 5.22+ is needed for fileno(DIRHANDLE) support:
 } elsif ($^V ge v5.22 && eval { require PublicInbox::KQNotify }) {
@@ -79,7 +79,7 @@ sub event_step {
         my $cb = $self->{cb} or return;
         local $PublicInbox::DS::in_loop = 0; # waitpid() synchronously (FIXME)
         eval {
-                my @events = $self->{inot}->read; # Linux::Inotify2->read
+                my @events = $self->{inot}->read; # Inotify3->read
                 $cb->($_) for @events;
         };
         warn "$self->{inot}->read err: $@\n" if $@;
@@ -88,7 +88,7 @@ sub event_step {
 sub force_close {
         my ($self) = @_;
         my $inot = delete $self->{inot} // return;
-        if ($inot->can('fh')) { # Linux::Inotify2 2.3+
+        if ($inot->can('fh')) { # Inotify3 or Linux::Inotify2 2.3+
                 $inot->fh->close or warn "CLOSE ERROR: $!";
         } elsif ($inot->isa('Linux::Inotify2')) {
                 require PublicInbox::LI2Wrap;
diff --git a/lib/PublicInbox/In3Event.pm b/lib/PublicInbox/In3Event.pm
new file mode 100644
index 00000000..f93dc0da
--- /dev/null
+++ b/lib/PublicInbox/In3Event.pm
@@ -0,0 +1,24 @@
+# Copyright (C) all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# duck-type compatible with Linux::Inotify2::Event for pure Perl
+# PublicInbox::Inotify3 w/o callback support
+package PublicInbox::In3Event;
+use v5.12;
+
+sub w { $_[0]->[2] } # PublicInbox::In3Watch
+sub mask { $_[0]->[0] }
+sub name { $_[0]->[1] }
+
+sub fullname {
+        my ($name, $wname) = ($_[0]->[1], $_[0]->[2]->name);
+        length($name) ? "$wname/$name" : $wname;
+}
+
+my $buf = '';
+while (my ($sym, $mask) = each %PublicInbox::Inotify3::events) {
+        $buf .= "sub $sym { \$_[0]->[0] & $mask }\n";
+}
+eval $buf;
+
+1;
diff --git a/lib/PublicInbox/In3Watch.pm b/lib/PublicInbox/In3Watch.pm
new file mode 100644
index 00000000..bdb91869
--- /dev/null
+++ b/lib/PublicInbox/In3Watch.pm
@@ -0,0 +1,20 @@
+# Copyright (C) all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# duck-type compatible with Linux::Inotify2::Watch for pure Perl
+# PublicInbox::Inotify3 for our needs, only
+package PublicInbox::In3Watch;
+use v5.12;
+
+sub mask { $_[0]->[1] }
+sub name { $_[0]->[2] }
+
+sub cancel {
+        my ($self) = @_;
+        my ($wd, $in3) = @$self[0, 3];
+        $in3 or return 1; # already canceled
+        pop @$self;
+        $in3->rm_watch($wd);
+}
+
+1;
diff --git a/lib/PublicInbox/InboxIdle.pm b/lib/PublicInbox/InboxIdle.pm
index 4231c0a0..3c4d4a68 100644
--- a/lib/PublicInbox/InboxIdle.pm
+++ b/lib/PublicInbox/InboxIdle.pm
@@ -11,7 +11,7 @@ use PublicInbox::Syscall qw(EPOLLIN);
 my $IN_MODIFY = 0x02; # match Linux inotify
 my $ino_cls;
 if ($^O eq 'linux' && eval { require PublicInbox::Inotify }) {
-        $IN_MODIFY = Linux::Inotify2::IN_MODIFY();
+        $IN_MODIFY = PublicInbox::Inotify::IN_MODIFY();
         $ino_cls = 'PublicInbox::Inotify';
 } elsif (eval { require PublicInbox::KQNotify }) {
         $IN_MODIFY = PublicInbox::KQNotify::NOTE_WRITE();
@@ -34,7 +34,7 @@ sub in2_arm ($$) { # PublicInbox::Config::each_inbox callback
                 $ibx->{unlock_subs} = $old_ibx->{unlock_subs};
                 %{$ibx->{unlock_subs}} = (%$u, %{$ibx->{unlock_subs}}) if $u;
 
-                # Linux::Inotify2::Watch::name matches if watches are the
+                # *::Inotify*::Watch::name matches if watches are the
                 # same, no point in replacing a watch of the same name
                 if ($cur->[1]->name eq $lock) {
                         $self->{on_unlock}->{$lock} = $ibx;
@@ -87,7 +87,7 @@ sub new {
 sub event_step {
         my ($self) = @_;
         eval {
-                my @events = $self->{inot}->read; # Linux::Inotify2::read
+                my @events = $self->{inot}->read; # PublicInbox::Inotify3::read
                 my $on_unlock = $self->{on_unlock};
                 for my $ev (@events) {
                         my $fn = $ev->fullname // next; # cancelled
diff --git a/lib/PublicInbox/Inotify.pm b/lib/PublicInbox/Inotify.pm
index 3ef271c8..c4f1ae84 100644
--- a/lib/PublicInbox/Inotify.pm
+++ b/lib/PublicInbox/Inotify.pm
@@ -5,12 +5,29 @@
 package PublicInbox::Inotify;
 use v5.12;
 our @ISA;
-BEGIN {
-        eval { require Linux::Inotify2 };
-        if ($@) { # TODO: get rid of XS dependency
-                die "W: Linux::Inotify2 missing: $@\n";
+BEGIN { # prefer pure Perl since it works out-of-the-box
+        my $isa;
+        for my $m (qw(PublicInbox::Inotify3 Linux::Inotify2)) {
+                eval "require $m";
+                next if $@;
+                $isa = $m;
+        }
+        if ($isa) {
+                push @ISA, $isa;
+                my $buf = '';
+                for (qw(IN_MOVED_TO IN_CREATE IN_DELETE IN_DELETE_SELF
+                                IN_MOVE_SELF IN_MOVED_FROM IN_MODIFY)) {
+                        $buf .= "*$_ = \\&PublicInbox::Inotify3::$_;\n";
+                }
+                eval $buf;
+                die $@ if $@;
         } else {
-                push @ISA, 'Linux::Inotify2';
+                die <<EOM;
+W: inotify syscall numbers unknown on your platform and
+W: Linux::Inotify2 missing: $@
+W: public-inbox hackers welcome the plain-text output of ./devel/sysdefs-list
+W: at meta\@public-inbox.org
+EOM
         }
 };
 
diff --git a/lib/PublicInbox/Inotify3.pm b/lib/PublicInbox/Inotify3.pm
new file mode 100644
index 00000000..4f337a7a
--- /dev/null
+++ b/lib/PublicInbox/Inotify3.pm
@@ -0,0 +1,115 @@
+# Copyright (C) all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# Implements most Linux::Inotify2 functionality we need in pure Perl
+# Anonymous sub support isn't supported since it's expensive in the
+# best case and likely leaky in older Perls (e.g. 5.16.3)
+package PublicInbox::Inotify3;
+use v5.12;
+use autodie qw(open);
+use PublicInbox::Syscall ();
+use Carp;
+use Scalar::Util ();
+
+# this fails if undefined no unsupported platforms
+use constant $PublicInbox::Syscall::INOTIFY;
+our %events;
+
+# extracted from devel/sysdefs-list output, these should be arch-independent
+BEGIN {
+%events = (
+        IN_ACCESS => 0x1,
+        IN_ALL_EVENTS => 0xfff,
+        IN_ATTRIB => 0x4,
+        IN_CLOSE => 0x18,
+        IN_CLOSE_NOWRITE => 0x10,
+        IN_CLOSE_WRITE => 0x8,
+        IN_CREATE => 0x100,
+        IN_DELETE => 0x200,
+        IN_DELETE_SELF => 0x400,
+        IN_DONT_FOLLOW => 0x2000000,
+        IN_EXCL_UNLINK => 0x4000000,
+        IN_IGNORED => 0x8000,
+        IN_ISDIR => 0x40000000,
+        IN_MASK_ADD => 0x20000000,
+        IN_MODIFY => 0x2,
+        IN_MOVE => 0xc0,
+        IN_MOVED_FROM => 0x40,
+        IN_MOVED_TO => 0x80,
+        IN_MOVE_SELF => 0x800,
+        IN_ONESHOT => 0x80000000,
+        IN_ONLYDIR => 0x1000000,
+        IN_OPEN => 0x20,
+        IN_Q_OVERFLOW => 0x4000,
+        IN_UNMOUNT => 0x2000,
+);
+} # /BEGIN
+use constant \%events;
+require PublicInbox::In3Event; # uses %events
+require PublicInbox::In3Watch; # uses SYS_inotify_rm_watch
+
+use constant autocancel =>
+        (IN_IGNORED|IN_UNMOUNT|IN_ONESHOT|IN_DELETE_SELF);
+
+sub new {
+        open my $fh, '+<&=', syscall(SYS_inotify_init1, IN_CLOEXEC);
+        bless { fh => $fh }, __PACKAGE__;
+}
+
+sub read {
+        my ($self) = @_;
+        my (@ret, $wd, $mask, $len, $name, $size, $buf);
+        my $r = sysread($self->{fh}, my $rbuf, 8192);
+        if ($r) {
+                while ($r) {
+                        ($wd, $mask, undef, $len) = unpack('lLLL', $rbuf);
+                        $size = 16 + $len; # 16: sizeof(struct inotify_event)
+                        substr($rbuf, 0, 16, '');
+                        $name = $len ? unpack('Z*', substr($rbuf, 0, $len, ''))
+                                        : undef;
+                        $r -= $size;
+                        next if $self->{ignore}->{$wd};
+                        my $ev = bless [$mask, $name], 'PublicInbox::In3Event';
+                        push @ret, $ev;
+                        if (my $w = $self->{w}->{$wd}) {
+                                $ev->[2] = $w;
+                                $w->cancel if $ev->mask & autocancel;
+                        } elsif ($mask & IN_Q_OVERFLOW) {
+                                carp 'E: IN_Q_OVERFLOW, too busy? (non-fatal)'
+                        } else {
+                                carp "BUG? wd:$wd unknown (non-fatal)";
+                        }
+                }
+        } elsif (defined($r) || ($!{EAGAIN} || $!{EINTR})) {
+        } else {
+                croak "inotify read: $!";
+        }
+        delete $self->{ignore};
+        @ret;
+}
+
+sub fileno { CORE::fileno($_[0]->{fh}) }
+
+sub fh { $_[0]->{fh} }
+
+sub blocking { shift->{fh}->blocking(@_) }
+
+sub watch {
+        my ($self, $name, $mask, $cb) = @_;
+        croak "E: $cb not supported" if $cb; # too much memory
+        my $wd = syscall(SYS_inotify_add_watch, $self->fileno, $name, $mask);
+        return if $wd < 0;
+        my $w = bless [ $wd, $mask, $name, $self ], 'PublicInbox::In3Watch';
+        $self->{w}->{$wd} = $w;
+        Scalar::Util::weaken($w->[3]); # ugh
+        $w;
+}
+
+sub rm_watch {
+        my ($self, $wd) = @_;
+        delete $self->{w}->{$wd};
+        $self->{ignore}->{$wd} = 1; # is this needed?
+        syscall(SYS_inotify_rm_watch, $self->fileno, $wd) < 0 ? undef : 1;
+}
+
+1;
diff --git a/lib/PublicInbox/Syscall.pm b/lib/PublicInbox/Syscall.pm
index 78181bb6..96af2b22 100644
--- a/lib/PublicInbox/Syscall.pm
+++ b/lib/PublicInbox/Syscall.pm
@@ -22,6 +22,7 @@ use POSIX qw(ENOENT ENOSYS EINVAL O_NONBLOCK);
 use Socket qw(SOL_SOCKET SCM_RIGHTS);
 use Config;
 our %SIGNUM = (WINCH => 28); # most Linux, {Free,Net,Open}BSD, *Darwin
+our $INOTIFY;
 
 # $VERSION = '0.25'; # Sys::Syscall version
 our @EXPORT_OK = qw(epoll_ctl epoll_create epoll_wait
@@ -98,6 +99,11 @@ if ($^O eq "linux") {
         $SYS_fstatfs = 100;
         $SYS_sendmsg = 370;
         $SYS_recvmsg = 372;
+        $INOTIFY = { # usage: `use constant $PublicInbox::Syscall::INOTIFY'
+                SYS_inotify_init1 => 332,
+                SYS_inotify_add_watch => 292,
+                SYS_inotify_rm_watch => 293,
+        };
         $FS_IOC_GETFLAGS = 0x80046601;
         $FS_IOC_SETFLAGS = 0x40046602;
     } elsif ($machine eq "x86_64") {
@@ -109,6 +115,11 @@ if ($^O eq "linux") {
         $SYS_fstatfs = 138;
         $SYS_sendmsg = 46;
         $SYS_recvmsg = 47;
+        $INOTIFY = {
+                SYS_inotify_init1 => 294,
+                SYS_inotify_add_watch => 254,
+                SYS_inotify_rm_watch => 255,
+        };
         $FS_IOC_GETFLAGS = 0x80086601;
         $FS_IOC_SETFLAGS = 0x40086602;
     } elsif ($machine eq 'x32') {
@@ -122,6 +133,11 @@ if ($^O eq "linux") {
         $SYS_recvmsg = 0x40000207;
         $FS_IOC_GETFLAGS = 0x80046601;
         $FS_IOC_SETFLAGS = 0x40046602;
+        $INOTIFY = {
+                SYS_inotify_init1 => 1073742118,
+                SYS_inotify_add_watch => 1073742078,
+                SYS_inotify_rm_watch => 1073742079,
+        };
     } elsif ($machine eq 'sparc64') {
         $SYS_epoll_create = 193;
         $SYS_epoll_ctl = 194;
@@ -154,6 +170,11 @@ if ($^O eq "linux") {
         $SYS_recvmsg = 342;
         $FS_IOC_GETFLAGS = 0x40086601;
         $FS_IOC_SETFLAGS = 0x80086602;
+        $INOTIFY = {
+                SYS_inotify_init1 => 318,
+                SYS_inotify_add_watch => 276,
+                SYS_inotify_rm_watch => 277,
+        };
     } elsif ($machine eq "ppc") {
         $SYS_epoll_create = 236;
         $SYS_epoll_ctl    = 237;
@@ -188,7 +209,7 @@ if ($^O eq "linux") {
         $u64_mod_8        = 1;
         $SYS_signalfd4 = 484;
         $SFD_CLOEXEC = 010000000;
-    } elsif ($machine =~ /\A(?:loong)?aarch64\z/ || $machine eq 'riscv64') {
+    } elsif ($machine =~ /\A(?:loong|a)arch64\z/ || $machine eq 'riscv64') {
         $SYS_epoll_create = 20;  # (sys_epoll_create1)
         $SYS_epoll_ctl    = 21;
         $SYS_epoll_wait   = 22;  # (sys_epoll_pwait)
@@ -199,6 +220,11 @@ if ($^O eq "linux") {
         $SYS_fstatfs = 44;
         $SYS_sendmsg = 211;
         $SYS_recvmsg = 212;
+        $INOTIFY = {
+                SYS_inotify_init1 => 26,
+                SYS_inotify_add_watch => 27,
+                SYS_inotify_rm_watch => 28,
+        };
         $FS_IOC_GETFLAGS = 0x80086601;
         $FS_IOC_SETFLAGS = 0x40086602;
     } elsif ($machine =~ m/arm(v\d+)?.*l/) { # ARM OABI (untested on cfarm)
@@ -236,6 +262,11 @@ if ($^O eq "linux") {
         $FS_IOC_GETFLAGS = 0x40046601;
         $FS_IOC_SETFLAGS = 0x80046602;
         $SIGNUM{WINCH} = 20;
+        $INOTIFY = {
+                SYS_inotify_init1 => 4329,
+                SYS_inotify_add_watch => 4285,
+                SYS_inotify_rm_watch => 4286,
+        };
     } else {
         warn <<EOM;
 machine=$machine ptrsize=$Config{ptrsize} has no syscall definitions
@@ -251,6 +282,10 @@ EOM
         *epoll_ctl = \&epoll_ctl_mod4;
     }
 }
+
+# SFD_CLOEXEC is arch-dependent, so IN_CLOEXEC may be, too
+$INOTIFY->{IN_CLOEXEC} //= 0x80000 if $INOTIFY;
+
 # use Inline::C for *BSD-only or general POSIX stuff.
 # Linux guarantees stable syscall numbering, BSDs only offer a stable libc
 # use devel/sysdefs-list on Linux to detect new syscall numbers and
diff --git a/lib/PublicInbox/TailNotify.pm b/lib/PublicInbox/TailNotify.pm
index bdb92d54..84340a35 100644
--- a/lib/PublicInbox/TailNotify.pm
+++ b/lib/PublicInbox/TailNotify.pm
@@ -9,9 +9,9 @@ use PublicInbox::DS qw(now);
 
 my ($TAIL_MOD, $ino_cls);
 if ($^O eq 'linux' && eval { require PublicInbox::Inotify; 1 }) {
-        $TAIL_MOD = Linux::Inotify2::IN_MOVED_TO() |
-                Linux::Inotify2::IN_CREATE() |
-                Linux::Inotify2::IN_MODIFY();
+        $TAIL_MOD = PublicInbox::Inotify::IN_MOVED_TO() |
+                PublicInbox::Inotify::IN_CREATE() |
+                PublicInbox::Inotify::IN_MODIFY();
         $ino_cls = 'PublicInbox::Inotify';
 } elsif (eval { require PublicInbox::KQNotify }) {
         $TAIL_MOD = PublicInbox::KQNotify::MOVED_TO_OR_CREATE() |
diff --git a/t/imapd.t b/t/imapd.t
index 9606291e..549b8766 100644
--- a/t/imapd.t
+++ b/t/imapd.t
@@ -250,7 +250,7 @@ SKIP: {
 
 ok($mic->logout, 'logout works');
 
-my $have_inotify = eval { require Linux::Inotify2; 1 };
+my $have_inotify = eval { require PublicInbox::Inotify; 1 };
 
 for my $ibx (@ibx) {
         my $name = $ibx->{name};
diff --git a/t/inotify3.t b/t/inotify3.t
new file mode 100644
index 00000000..c25c0f42
--- /dev/null
+++ b/t/inotify3.t
@@ -0,0 +1,17 @@
+#!perl -w
+# Copyright (C) all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+use v5.12; use PublicInbox::TestCommon;
+plan skip_all => 'inotify is Linux-only' if $^O ne 'linux';
+use_ok 'PublicInbox::Inotify3';
+my $in = PublicInbox::Inotify3->new;
+my $tmpdir = tmpdir;
+my $w = $in->watch("$tmpdir", PublicInbox::Inotify3::IN_ALL_EVENTS());
+$in->blocking(0);
+is_xdeeply [ $in->read ], [], 'non-blocking has no events, yet';
+undef $tmpdir;
+my @list = $in->read;
+ok scalar(@list), 'got events';
+ok $w->cancel, 'watch canceled';
+
+done_testing;
diff --git a/t/lei-auto-watch.t b/t/lei-auto-watch.t
index f871188d..1e190316 100644
--- a/t/lei-auto-watch.t
+++ b/t/lei-auto-watch.t
@@ -4,10 +4,10 @@
 use strict; use v5.10.1; use PublicInbox::TestCommon;
 use File::Basename qw(basename);
 plan skip_all => "TEST_FLAKY not enabled for $0" if !$ENV{TEST_FLAKY};
-my $have_fast_inotify = eval { require Linux::Inotify2 } ||
+my $have_fast_inotify = eval { require PublicInbox::Inotify } ||
         eval { require IO::KQueue };
 $have_fast_inotify or
-        diag("$0 IO::KQueue or Linux::Inotify2 missing, test will be slow");
+        diag("$0 IO::KQueue or inotify missing, test will be slow");
 
 test_lei(sub {
         my ($ro_home, $cfg_path) = setup_public_inboxes;
diff --git a/t/lei-watch.t b/t/lei-watch.t
index 24d9f5c8..7b357ee0 100644
--- a/t/lei-watch.t
+++ b/t/lei-watch.t
@@ -5,11 +5,11 @@ use strict; use v5.10.1; use PublicInbox::TestCommon;
 use File::Path qw(make_path remove_tree);
 plan skip_all => "TEST_FLAKY not enabled for $0" if !$ENV{TEST_FLAKY};
 require_mods('lei');
-my $have_fast_inotify = eval { require Linux::Inotify2 } ||
+my $have_fast_inotify = eval { require PublicInbox::Inotify } ||
         eval { require IO::KQueue };
 
 $have_fast_inotify or
-        diag("$0 IO::KQueue or Linux::Inotify2 missing, test will be slow");
+        diag("$0 IO::KQueue or inotify missing, test will be slow");
 
 my ($ro_home, $cfg_path) = setup_public_inboxes;
 test_lei(sub {
diff --git a/t/nntpd.t b/t/nntpd.t
index 0f3ef596..7052cb6a 100644
--- a/t/nntpd.t
+++ b/t/nntpd.t
@@ -14,7 +14,7 @@ use PublicInbox::DS;
 my $version = $ENV{PI_TEST_VERSION} || 1;
 require_git('2.6') if $version == 2;
 use_ok 'PublicInbox::Msgmap';
-my $fast_idle = eval { require Linux::Inotify2; 1 } //
+my $fast_idle = eval { require PublicInbox::Inotify; 1 } //
                 eval { require IO::KQueue; 1 };
 
 my ($tmpdir, $for_destroy) = tmpdir();
diff --git a/t/watch_maildir.t b/t/watch_maildir.t
index 07ebeef6..d7f01b1a 100644
--- a/t/watch_maildir.t
+++ b/t/watch_maildir.t
@@ -182,7 +182,7 @@ EOM
 
         # wait for -watch to setup inotify watches
         my $sleep = 1;
-        if (eval { require Linux::Inotify2 } && -d "/proc/$wm->{pid}/fd") {
+        if (eval { require PublicInbox::Inotify } && -d "/proc/$wm->{pid}/fd") {
                 my $end = time + 2;
                 my (@ino, @ino_info);
                 do {