From cf977e706b07e80f394570a393eb2169b9b9a1a7 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 28 Dec 2023 04:23:00 +0000 Subject: pure Perl inotify support 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 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) --- lib/PublicInbox/DirIdle.pm | 16 +++--- lib/PublicInbox/In3Event.pm | 24 +++++++++ lib/PublicInbox/In3Watch.pm | 20 ++++++++ lib/PublicInbox/InboxIdle.pm | 6 +-- lib/PublicInbox/Inotify.pm | 27 ++++++++-- lib/PublicInbox/Inotify3.pm | 115 ++++++++++++++++++++++++++++++++++++++++++ lib/PublicInbox/Syscall.pm | 37 +++++++++++++- lib/PublicInbox/TailNotify.pm | 6 +-- 8 files changed, 231 insertions(+), 20 deletions(-) create mode 100644 lib/PublicInbox/In3Event.pm create mode 100644 lib/PublicInbox/In3Watch.pm create mode 100644 lib/PublicInbox/Inotify3.pm (limited to 'lib') 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 +# License: AGPL-3.0+ + +# 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 +# License: AGPL-3.0+ + +# 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 < +# License: AGPL-3.0+ + +# 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 <{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() | -- cgit v1.2.3-24-ge0c7