dumping ground for random patches and texts
 help / color / mirror / Atom feed
From: Eric Wong <e@80x24.org>
To: spew@80x24.org
Subject: [PATCH] notify rewrite;
Date: Thu,  7 Sep 2023 08:04:02 +0000	[thread overview]
Message-ID: <20230907080402.2143325-1-e@80x24.org> (raw)

---
 lib/PublicInbox/FakeInotify.pm | 160 +++++++++++++++++----------------
 lib/PublicInbox/KQNotify.pm    |  73 ++-------------
 t/fake_inotify.t               |  22 +++--
 3 files changed, 103 insertions(+), 152 deletions(-)

diff --git a/lib/PublicInbox/FakeInotify.pm b/lib/PublicInbox/FakeInotify.pm
index 45b80f50..ae9b7dea 100644
--- a/lib/PublicInbox/FakeInotify.pm
+++ b/lib/PublicInbox/FakeInotify.pm
@@ -17,97 +17,109 @@ sub IN_DELETE () { 0x200 }
 sub IN_DELETE_SELF () { 0x400 }
 sub IN_MOVE_SELF () { 0x800 }
 
-our @EXPORT_OK = qw(fill_dirlist on_dir_change);
-
 my $poll_intvl = 2; # same as Filesys::Notify::Simple
 
-sub new { bless { watch => {}, dirlist => {} }, __PACKAGE__ }
+sub new { bless { watch => {} }, __PACKAGE__ }
 
-sub fill_dirlist ($$$) {
-	my ($self, $path, $dh) = @_;
-	my $dirlist = $self->{dirlist}->{$path} = {};
-	while (defined(my $n = readdir($dh))) {
-		$dirlist->{$n} = undef if $n !~ /\A\.\.?\z/;
-	}
+sub watch_open ($$$) { # used by KQNotify subclass
+	my ($self, $path, $mask) = @_;
+	my ($fh, @st, @st0, $tries);
+	do {
+		@st0 = stat($path) or return;
+		(-d _ ? opendir($fh, $path) : open($fh, '<', $path)) or return;
+		@st = stat($fh) or die "fstat($path): $!";
+	} while ("@st[0,1]" ne "@st0[0,1]" &&
+		((++$tries < 10) || return warn(<<EOM)));
+E: $path switching inodes too frequently to watch
+EOM
+	($mask & IN_DELETE && -d _) and
+		@{$self->{dirlist}->{$path}} = grep(!/\A\.\.?\z/, readdir($fh));
+
+	bless [ @st[0, 1, 10], $path, $fh ], 'PublicInbox::FakeInotify::Watch'
 }
 
 # behaves like Linux::Inotify2->watch
 sub watch {
-	my ($self, $path, $mask) = @_;
-	my @st = stat($path) or return;
-	my $k = "$path\0$mask";
-	$self->{watch}->{$k} = $st[10]; # 10 - ctime
-	if ($mask & IN_DELETE) {
-		opendir(my $dh, $path) or return;
-		fill_dirlist($self, $path, $dh);
-	}
-	bless [ $self->{watch}, $k ], 'PublicInbox::FakeInotify::Watch';
+	my ($self, $path, $mask) = @_; # mask is ignored
+	my $w = watch_open($self, $path, $mask) or return;
+	pop @$w; # no need to keep $fh open for non-kqueue
+	$self->{watch}->{$w->[3]} = $w;
 }
 
-# also used by KQNotify since it kevent requires readdir on st_nlink
-# count changes.
 sub on_dir_change ($$$$$) {
-	my ($events, $dh, $path, $old_ctime, $dirlist) = @_;
-	my $oldlist = $dirlist->{$path};
-	my $newlist = $oldlist ? {} : undef;
+	my ($self, $events, $dh, $path, $old_ctime) = @_;
+	my $old = $self->{dirlist}->{$path}; # only set if watching IN_DELETE
+	my @cur;
+	my $latest = $old_ctime;
 	while (defined(my $base = readdir($dh))) {
 		next if $base =~ /\A\.\.?\z/;
-		my $full = "$path/$base";
-		my @st = stat($full);
-		if (@st && $st[10] > $old_ctime) {
-			push @$events,
-				bless(\$full, 'PublicInbox::FakeInotify::Event')
+		my $f = "$path/$base";
+		if (my @st = stat($f)) {
+			push @cur, $base if $old;
+			if ($st[10] > $old_ctime) {
+				$latest = $st[10] if $st[10] > $latest;
+				push(@$events, bless(\$f,
+					'PublicInbox::FakeInotify::Event'));
+			}
+		} else { # ignore ENOENT due to race
+			warn "W: stat($f): $!\n" if !$!{ENOENT};
 		}
-		if (!@st) {
-			# ignore ENOENT due to race
-			warn "unhandled stat($full) error: $!\n" if !$!{ENOENT};
-		} elsif ($newlist) {
-			$newlist->{$base} = undef;
+	}
+	# -watch only cares for new files, lei wants deletes
+	if ($old) {
+		$self->{dirlist}->{$path} = \@cur;
+		my %gone = map { $_ => undef } @$old;
+		delete @gone{@cur};
+		push(@$events, map {
+			bless \"$path/$_", 'PublicInbox::FakeInotify::GoneEvent'
+		} keys %gone);
+	}
+	$latest;
+}
+
+sub gone_event ($$$) {
+	my ($self, $ident, $path) = @_;
+	delete $self->{watch}->{$ident};
+	delete $self->{dirlist}->{$path};
+	bless(\$path, 'PublicInbox::FakeInotify::SelfGoneEvent');
+}
+
+sub ckwatch_i ($$$) { # used by KQNotify subclass
+	my ($self, $events, $ident) = @_;
+	my $w = $self->{watch}->{$ident} or return;
+	return delete($self->{watch}->{$ident}) if !@$w; # cancelled
+	# $fh is only defined for KQNotify subclass
+	my ($old_dev, $old_ino, $old_ctime, $path, $fh) = @$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 @$events, gone_event($self, $ident, $path);
+	} elsif ($fh || $new_st[10] > $old_ctime) {
+		$w->[2] = $new_st[10];
+		if (-d _) {
+			if ($fh) { # kevent requires FDs stay open
+				rewinddir($fh);
+			} elsif (!opendir($fh, $path)) {
+				push @$events, gone_event($self, $ident, $path);
+				return;
+			}
+			my $ct = on_dir_change($self, $events, $fh, $path,
+						$old_ctime);
+			$w->[2] = $ct if $ct > $w->[2];
+		} else {
+			push @$events, bless(\$path,
+					'PublicInbox::FakeInotify::Event');
 		}
 	}
-	return if !$newlist;
-	delete @$oldlist{keys %$newlist};
-	$dirlist->{$path} = $newlist;
-	push(@$events, map {
-		bless \"$path/$_", 'PublicInbox::FakeInotify::GoneEvent'
-	} keys %$oldlist);
 }
 
 # behaves like non-blocking Linux::Inotify2->read
 sub read {
 	my ($self) = @_;
-	my $watch = $self->{watch} or return ();
 	my $events = [];
-	my @watch_gone;
-	for my $x (keys %$watch) {
-		my ($path, $mask) = split(/\0/, $x, 2);
-		my @now = stat($path);
-		if (!@now && $!{ENOENT} && ($mask & IN_DELETE_SELF)) {
-			push @$events, bless(\$path,
-				'PublicInbox::FakeInotify::SelfGoneEvent');
-			push @watch_gone, $x;
-			delete $self->{dirlist}->{$path};
-		}
-		next if !@now;
-		my $old_ctime = $watch->{$x};
-		$watch->{$x} = $now[10];
-		next if $old_ctime == $now[10];
-		if ($mask & IN_MODIFY) {
-			push @$events,
-				bless(\$path, 'PublicInbox::FakeInotify::Event')
-		} elsif ($mask & (MOVED_TO_OR_CREATE | IN_DELETE)) {
-			if (opendir(my $dh, $path)) {
-				on_dir_change($events, $dh, $path, $old_ctime,
-						$self->{dirlist});
-			} elsif ($!{ENOENT}) {
-				push @watch_gone, $x;
-				delete $self->{dirlist}->{$path};
-			} else {
-				warn "W: opendir $path: $!\n";
-			}
-		}
+	for my $ident (keys %{$self->{watch}}) {
+		ckwatch_i($self, $events, $ident);
 	}
-	delete @$watch{@watch_gone};
 	@$events;
 }
 
@@ -120,15 +132,9 @@ sub poll_once {
 package PublicInbox::FakeInotify::Watch;
 use v5.12;
 
-sub cancel {
-	my ($self) = @_;
-	delete $self->[0]->{$self->[1]};
-}
+sub cancel { @{$_[0]} = () }
 
-sub name {
-	my ($self) = @_;
-	(split(/\0/, $self->[1], 2))[0];
-}
+sub name { $_[0]->[3] }
 
 package PublicInbox::FakeInotify::Event;
 use v5.12;
diff --git a/lib/PublicInbox/KQNotify.pm b/lib/PublicInbox/KQNotify.pm
index 381711fa..cec68bb6 100644
--- a/lib/PublicInbox/KQNotify.pm
+++ b/lib/PublicInbox/KQNotify.pm
@@ -5,10 +5,9 @@
 # using IO::KQueue on *BSD systems.
 package PublicInbox::KQNotify;
 use v5.12;
+use parent qw(PublicInbox::FakeInotify);   
 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 }
@@ -20,31 +19,14 @@ sub new {
 
 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);
+	my $w = $self->watch_open($path, $mask);
+	my $ident = fileno($w->[4]);
 	$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;
+	$self->{watch}->{$ident} = $w;
 }
 
 # emulate Linux::Inotify::fileno
@@ -61,54 +43,11 @@ 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});
-		}
+	for my $kev ($self->{dskq}->{kq}->kevent(0)) {
+		$self->ckwatch_i($self, $events, $kev->[KQ_IDENT]);
 	}
-	delete @$watch{@gone};
 	@$events;
 }
 
-package PublicInbox::KQNotify::Watch;
-use v5.12;
-
-sub name { $_[0]->[1] }
-
-sub cancel { close $_[0]->[0] or die "close: $!" }
-
-package PublicInbox::KQNotify::Watchdir;
-use v5.12;
-
-sub name { $_[0]->[1] }
-
-sub cancel { closedir $_[0]->[0] or die "closedir: $!" }
-
 1;
diff --git a/t/fake_inotify.t b/t/fake_inotify.t
index 734ddbfb..9f42c592 100644
--- a/t/fake_inotify.t
+++ b/t/fake_inotify.t
@@ -1,10 +1,10 @@
 #!perl -w
-# Copyright (C) 2020-2021 all contributors <meta@public-inbox.org>
+# Copyright (C) all contributors <meta@public-inbox.org>
 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
 #
 # Ensure FakeInotify can pick up rename(2) and link(2) operations
 # used by Maildir writing tools
-use strict;
+use v5.12;
 use PublicInbox::TestCommon;
 use_ok 'PublicInbox::FakeInotify';
 my $MIN_FS_TICK = 0.011; # for low-res CONFIG_HZ=100 systems
@@ -21,32 +21,38 @@ my $w = $fi->watch("$tmpdir/new", $mask);
 tick $MIN_FS_TICK;
 rename("$tmpdir/tst", "$tmpdir/new/tst") or BAIL_OUT "rename: $!";
 my @events = map { $_->fullname } $fi->read;
-is_deeply(\@events, ["$tmpdir/new/tst"], 'rename(2) detected');
+is_deeply(\@events, ["$tmpdir/new/tst"], 'rename(2) detected') or
+	diag explain(\@events);
 
 tick $MIN_FS_TICK;
 open $fh, '>', "$tmpdir/tst" or BAIL_OUT "open: $!";
 close $fh or BAIL_OUT "close: $!";
 link("$tmpdir/tst", "$tmpdir/new/link") or BAIL_OUT "link: $!";
 @events = map { $_->fullname } $fi->read;
-is_deeply(\@events, ["$tmpdir/new/link"], 'link(2) detected');
+is_deeply(\@events, ["$tmpdir/new/link"], 'link(2) detected') or
+	diag explain(\@events);
 
 $w->cancel;
 tick $MIN_FS_TICK;
 link("$tmpdir/new/tst", "$tmpdir/new/link2") or BAIL_OUT "link: $!";
 @events = map { $_->fullname } $fi->read;
-is_deeply(\@events, [], 'link(2) not detected after cancel');
+is_deeply(\@events, [], 'link(2) not detected after cancel') or
+	diag explain(\@events);
 $fi->watch("$tmpdir/new", PublicInbox::FakeInotify::IN_DELETE());
 
 tick $MIN_FS_TICK;
 rmdir("$tmpdir/new/rmd") or xbail "rmdir: $!";
+tick $MIN_FS_TICK;
 @events = $fi->read;
-is_deeply([map{ $_->fullname }@events], ["$tmpdir/new/rmd"], 'rmdir detected');
-ok($events[0]->IN_DELETE, 'IN_DELETE set on rmdir');
+is_deeply([map{ $_->fullname }@events], ["$tmpdir/new/rmd"], 'rmdir detected') or
+	diag explain(\@events);
+ok($events[-1]->IN_DELETE, 'IN_DELETE set on rmdir');
 
 tick $MIN_FS_TICK;
 unlink("$tmpdir/new/tst") or xbail "unlink: $!";
 @events = grep { ref =~ /Gone/ } $fi->read;
-is_deeply([map{ $_->fullname }@events], ["$tmpdir/new/tst"], 'unlink detected');
+is_deeply([map{ $_->fullname }@events], ["$tmpdir/new/tst"], 'unlink detected') or
+	diag explain(\@events);
 ok($events[0]->IN_DELETE, 'IN_DELETE set on unlink');
 
 PublicInbox::DS->Reset;

                 reply	other threads:[~2023-09-07  8:04 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20230907080402.2143325-1-e@80x24.org \
    --to=e@80x24.org \
    --cc=spew@80x24.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).