# Copyright (C) 2020-2021 all contributors <meta@public-inbox.org>
 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
 
-# Intended for PublicInbox::DS->EventLoop in read-only daemons
+# Intended for PublicInbox::DS::event_loop in read-only daemons
 # to avoid each_inbox() monopolizing the event loop when hundreds/thousands
 # of inboxes are in play.
 package PublicInbox::ConfigIter;
 
     }
 }
 
-=head2 C<< CLASS->EventLoop() >>
-
-Start processing IO events. In most daemon programs this never exits. See
-C<PostLoopCallback> below for how to exit the loop.
-
-=cut
-
 sub now () { clock_gettime(CLOCK_MONOTONIC) }
 
 sub next_tick () {
        $PostLoopCallback ? $PostLoopCallback->(\%DescriptorMap) : 1;
 }
 
-sub EventLoop {
-    $Epoll //= _InitPoller();
-    local $in_loop = 1;
-    my @events;
-    do {
-        my $timeout = RunTimers();
-
-        # get up to 1000 events
-        epoll_wait($Epoll, 1000, $timeout, \@events);
-        for my $fd (@events) {
-            # it's possible epoll_wait returned many events, including some at the end
-            # that ones in the front triggered unregister-interest actions.  if we
-            # can't find the %sock entry, it's because we're no longer interested
-            # in that event.
-
-           # guard stack-not-refcounted w/ Carp + @DB::args
-            my $obj = $DescriptorMap{$fd};
-            $obj->event_step;
-        }
-    } while (PostEventLoop());
+# Start processing IO events. In most daemon programs this never exits. See
+# C<PostLoopCallback> for how to exit the loop.
+sub event_loop (;$$) {
+       my ($sig, $oldset) = @_;
+       $Epoll //= _InitPoller();
+       require PublicInbox::Sigfd if $sig;
+       my $sigfd = PublicInbox::Sigfd->new($sig, 1) if $sig;
+       local @SIG{keys %$sig} = values(%$sig) if $sig && !$sigfd;
+       local $SIG{PIPE} = 'IGNORE';
+       if (!$sigfd && $sig) {
+               # wake up every second to accept signals if we don't
+               # have signalfd or IO::KQueue:
+               sig_setmask($oldset);
+               PublicInbox::DS->SetLoopTimeout(1000);
+       }
+       $_[0] = $sigfd = $sig = undef; # $_[0] == sig
+       local $in_loop = 1;
+       my @events;
+       do {
+               my $timeout = RunTimers();
+
+               # get up to 1000 events
+               epoll_wait($Epoll, 1000, $timeout, \@events);
+               for my $fd (@events) {
+                       # it's possible epoll_wait returned many events,
+                       # including some at the end that ones in the front
+                       # triggered unregister-interest actions.  if we can't
+                       # find the %sock entry, it's because we're no longer
+                       # interested in that event.
+
+                       # guard stack-not-refcounted w/ Carp + @DB::args
+                       my $obj = $DescriptorMap{$fd};
+                       $obj->event_step;
+               }
+       } while (PostEventLoop());
 }
 
 =head2 C<< CLASS->SetPostLoopCallback( CODEREF ) >>
 =head2 C<< CLASS->new( $socket ) >>
 
 Create a new PublicInbox::DS subclass object for the given I<socket> which will
-react to events on it during the C<EventLoop>.
+react to events on it during the C<event_loop>.
 
 This is normally (always?) called from your subclass via:
 
 
 use IO::KQueue;
 use Errno qw(EAGAIN);
 use PublicInbox::Syscall qw(EPOLLONESHOT EPOLLIN EPOLLOUT EPOLLET
-       EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL SFD_NONBLOCK);
+       EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL);
 our @EXPORT_OK = qw(epoll_ctl epoll_wait);
 
 sub EV_DISPATCH () { 0x0080 }
 # It's wasteful in that it uses another FD, but it simplifies
 # our epoll-oriented code.
 sub signalfd {
-       my ($class, $signo, $flags) = @_;
+       my ($class, $signo, $nonblock) = @_;
        my $sym = gensym;
-       tie *$sym, $class, $signo, $flags; # calls TIEHANDLE
+       tie *$sym, $class, $signo, $nonblock; # calls TIEHANDLE
        $sym
 }
 
 sub TIEHANDLE { # similar to signalfd()
-       my ($class, $signo, $flags) = @_;
+       my ($class, $signo, $nonblock) = @_;
        my $self = $class->new;
-       $self->{timeout} = ($flags & SFD_NONBLOCK) ? 0 : -1;
+       $self->{timeout} = $nonblock ? 0 : -1;
        my $kq = $self->{kq};
        $kq->EV_SET($_, EVFILT_SIGNAL, EV_ADD) for @$signo;
        $self;
 
 STDOUT->autoflush(1);
 STDERR->autoflush(1);
 use PublicInbox::DS qw(now);
-use PublicInbox::Syscall qw(SFD_NONBLOCK);
 require PublicInbox::Listener;
 use PublicInbox::EOFpipe;
 use PublicInbox::Sigfd;
                },
                CHLD => \&reap_children,
        };
-       my $sigfd = PublicInbox::Sigfd->new($sig, 0);
+       my $sigfd = PublicInbox::Sigfd->new($sig);
        local @SIG{keys %$sig} = values(%$sig) unless $sigfd;
        PublicInbox::DS::sig_setmask($oldset) if !$sigfd;
        while (1) { # main loop
                # this calls epoll_create:
                PublicInbox::Listener->new($_, $tls_cb || $post_accept)
        } @listeners;
-       my $sigfd = PublicInbox::Sigfd->new($sig, SFD_NONBLOCK);
-       local @SIG{keys %$sig} = values(%$sig) unless $sigfd;
-       if (!$sigfd) {
-               # wake up every second to accept signals if we don't
-               # have signalfd or IO::KQueue:
-               PublicInbox::DS::sig_setmask($oldset);
-               PublicInbox::DS->SetLoopTimeout(1000);
-       }
-       PublicInbox::DS->EventLoop;
+       PublicInbox::DS::event_loop($sig, $oldset);
 }
 
 sub run ($$$;$) {
        my ($default, $refresh, $post_accept, $tlsd) = @_;
-       local $SIG{PIPE} = 'IGNORE';
        daemon_prepare($default);
        my $af_default = $default =~ /:8080\z/ ? 'httpready' : undef;
        my $for_destroy = daemonize();
 
        };
 }
 
-# called via PublicInbox::DS->EventLoop
+# called via PublicInbox::DS::event_loop
 sub event_step {
        my ($ctx, $sync) = @_;
        # can't find a partial match in current inbox, try the others:
 
        };
        my $quit = PublicInbox::SearchIdx::quit_cb($sync);
        $sig->{QUIT} = $sig->{INT} = $sig->{TERM} = $quit;
-       my $sigfd = PublicInbox::Sigfd->new($sig,
-                                       $PublicInbox::Syscall::SFD_NONBLOCK);
-       @SIG{keys %$sig} = values(%$sig) if !$sigfd;
        local $self->{-watch_sync} = $sync; # for ->on_inbox_unlock
-       if (!$sigfd) {
-               # wake up every second to accept signals if we don't
-               # have signalfd or IO::KQueue:
-               PublicInbox::DS::sig_setmask($oldset);
-               PublicInbox::DS->SetLoopTimeout(1000);
-       }
        PublicInbox::DS->SetPostLoopCallback(sub { !$sync->{quit} });
        $pr->("initial scan complete, entering event loop\n") if $pr;
-       PublicInbox::DS->EventLoop; # calls InboxIdle->event_step
+       # calls InboxIdle->event_step:
+       PublicInbox::DS::event_loop($sig, $oldset);
        done($self);
 }
 
 
 # ensure PublicInbox::Git::cat_async_step never calls cat_async_retry
 sub alternates_changed {}
 
-# DS->EventLoop will call this
+# DS::event_loop will call this
 sub event_step {
        my ($self) = @_;
        $self->flush_write;
 
 sub DESTROY {
        my ($self) = @_;
-       delete $self->{sock}; # if outside EventLoop
+       delete $self->{sock}; # if outside event_loop
        PublicInbox::Git::DESTROY($self);
 }
 
 
        my $wqw = PublicInbox::WQWorker->new($self, $self->{-wq_s2});
        PublicInbox::WQWorker->new($self, $bcast2) if $bcast2;
        PublicInbox::DS->SetPostLoopCallback(sub { $wqw->{sock} });
-       PublicInbox::DS->EventLoop;
+       PublicInbox::DS::event_loop();
        PublicInbox::DS->Reset;
 }
 
                delete @$self{qw(-wq_s1 -wq_ppid)};
                $self->{-wq_worker_nr} =
                                keys %{delete($self->{-wq_workers}) // {}};
-               $SIG{$_} = 'IGNORE' for (qw(PIPE));
                $SIG{$_} = 'DEFAULT' for (qw(TTOU TTIN TERM QUIT INT CHLD));
                local $0 = $one ? $self->{-wq_ident} :
                        "$self->{-wq_ident} $self->{-wq_worker_nr}";
 
 use IO::Handle ();
 use Fcntl qw(SEEK_SET);
 use PublicInbox::Config;
-use PublicInbox::Syscall qw(SFD_NONBLOCK EPOLLIN EPOLLET);
-use PublicInbox::Sigfd;
+use PublicInbox::Syscall qw(EPOLLIN EPOLLET);
 use PublicInbox::DS qw(now dwaitpid);
 use PublicInbox::Spawn qw(spawn popen_rd);
 use PublicInbox::Lock;
                USR1 => \&noop,
                USR2 => \&noop,
        };
-       my $sigfd = PublicInbox::Sigfd->new($sig, SFD_NONBLOCK);
-       local @SIG{keys %$sig} = values(%$sig) unless $sigfd;
-       undef $sig;
-       local $SIG{PIPE} = 'IGNORE';
        require PublicInbox::DirIdle;
        local $dir_idle = PublicInbox::DirIdle->new([$sock_dir], sub {
                # just rely on wakeup to hit PostLoopCallback set below
                dir_idle_handler($_[0]) if $_[0]->fullname ne $path;
        }, 1);
-       if ($sigfd) {
-               undef $sigfd; # unref, already in DS::DescriptorMap
-       } else {
-               # wake up every second to accept signals if we don't
-               # have signalfd or IO::KQueue:
-               PublicInbox::DS::sig_setmask($oldset);
-               PublicInbox::DS->SetLoopTimeout(1000);
-       }
        PublicInbox::DS->SetPostLoopCallback(sub {
                my ($dmap, undef) = @_;
                if (@st = defined($path) ? stat($path) : ()) {
        open STDERR, '>&STDIN' or die "redirect stderr failed: $!";
        open STDOUT, '>&STDIN' or die "redirect stdout failed: $!";
        # $daemon pipe to `lei' closed, main loop begins:
-       eval { PublicInbox::DS->EventLoop };
+       eval { PublicInbox::DS::event_loop($sig, $oldset) };
        warn "event loop error: $@\n" if $@;
        # exit() may trigger waitpid via various DESTROY, ensure interruptible
        PublicInbox::DS::sig_setmask($oldset);
 
 # operate in.  This can be useful to ensure smaller inboxes can
 # be cloned while cloning of large inboxes is maxed out.
 #
-# This does not depend on the PublicInbox::DS->EventLoop or any
+# This does not depend on the PublicInbox::DS::event_loop or any
 # other external scheduling mechanism, you just need to call
 # start() and finish() appropriately. However, public-inbox-httpd
 # (which uses PublicInbox::DS)  will be able to schedule this
 
 package PublicInbox::Sigfd;
 use strict;
 use parent qw(PublicInbox::DS);
-use PublicInbox::Syscall qw(signalfd EPOLLIN EPOLLET SFD_NONBLOCK);
+use PublicInbox::Syscall qw(signalfd EPOLLIN EPOLLET);
 use POSIX ();
 
 # returns a coderef to unblock signals if neither signalfd or kqueue
 # are available.
 sub new {
-       my ($class, $sig, $flags) = @_;
+       my ($class, $sig, $nonblock) = @_;
        my %signo = map {;
                my $cb = $sig->{$_};
                # SIGWINCH is 28 on FreeBSD, NetBSD, OpenBSD
        } keys %$sig;
        my $self = bless { sig => \%signo }, $class;
        my $io;
-       my $fd = signalfd(-1, [keys %signo], $flags);
+       my $fd = signalfd([keys %signo], $nonblock);
        if (defined $fd && $fd >= 0) {
                open($io, '+<&=', $fd) or die "open: $!";
        } elsif (eval { require PublicInbox::DSKQXS }) {
-               $io = PublicInbox::DSKQXS->signalfd([keys %signo], $flags);
+               $io = PublicInbox::DSKQXS->signalfd([keys %signo], $nonblock);
        } else {
                return; # wake up every second to check for signals
        }
-       if ($flags & SFD_NONBLOCK) { # it can go into the event loop
+       if ($nonblock) { # it can go into the event loop
                $self->SUPER::new($io, EPOLLIN | EPOLLET);
        } else { # master main loop
                $self->{sock} = $io;
 
                   EPOLLIN EPOLLOUT EPOLLET
                   EPOLL_CTL_ADD EPOLL_CTL_DEL EPOLL_CTL_MOD
                   EPOLLONESHOT EPOLLEXCLUSIVE
-                  signalfd SFD_NONBLOCK);
+                  signalfd);
 our %EXPORT_TAGS = (epoll => [qw(epoll_ctl epoll_create epoll_wait
                              EPOLLIN EPOLLOUT
                              EPOLL_CTL_ADD EPOLL_CTL_DEL EPOLL_CTL_MOD
      );
 
 my $SFD_CLOEXEC = 02000000; # Perl does not expose O_CLOEXEC
-sub SFD_NONBLOCK () { O_NONBLOCK }
 our $no_deprecated = 0;
 
 if ($^O eq "linux") {
        }
 }
 
-sub signalfd ($$$) {
-       my ($fd, $signos, $flags) = @_;
+sub signalfd ($$) {
+       my ($signos, $nonblock) = @_;
        if ($SYS_signalfd4) {
                my $set = POSIX::SigSet->new(@$signos);
-               syscall($SYS_signalfd4, $fd, "$$set",
+               syscall($SYS_signalfd4, -1, "$$set",
                        # $Config{sig_count} is NSIG, so this is NSIG/8:
                        int($Config{sig_count}/8),
-                       $flags|$SFD_CLOEXEC);
+                       # SFD_NONBLOCK == O_NONBLOCK for every architecture
+                       ($nonblock ? O_NONBLOCK : 0) |$SFD_CLOEXEC);
        } else {
                $! = ENOSYS;
                undef;
 
 use PublicInbox::NetReader;
 use PublicInbox::Filter::Base qw(REJECT);
 use PublicInbox::Spamcheck;
-use PublicInbox::Sigfd;
 use PublicInbox::DS qw(now add_timer);
 use PublicInbox::MID qw(mids);
 use PublicInbox::ContentHash qw(content_hash);
        }
        watch_fs_init($self) if $self->{mdre};
        PublicInbox::DS->SetPostLoopCallback(sub { !$self->quit_done });
-       PublicInbox::DS->EventLoop; # calls ->event_step
+       PublicInbox::DS::event_loop($sig, $oldset); # calls ->event_step
        _done_for_now($self);
 }
 
 
 use PublicInbox::Watch;
 use PublicInbox::Config;
 use PublicInbox::DS;
-use PublicInbox::Sigfd;
-use PublicInbox::Syscall qw(SFD_NONBLOCK);
 my $do_scan = 1;
 GetOptions('scan!' => \$do_scan, # undocumented, testing only
        'help|h' => \(my $show_help)) or do { print STDERR $help; exit 1 };
 
        # --no-scan is only intended for testing atm, undocumented.
        PublicInbox::DS::requeue($scan) if $do_scan;
-
-       my $sigfd = PublicInbox::Sigfd->new($sig, SFD_NONBLOCK);
-       local @SIG{keys %$sig} = values(%$sig) unless $sigfd;
-       if (!$sigfd) {
-               PublicInbox::DS::sig_setmask($oldset);
-               PublicInbox::DS->SetLoopTimeout(1000);
-       }
        $watch->watch($sig, $oldset) while ($watch);
 }
 
 PublicInbox::DS->SetPostLoopCallback(sub { scalar(@x) == 0 && now < $end });
 tick(0.011);
 rmdir("$tmpdir/a/b") or xbail "rmdir $!";
-PublicInbox::DS->EventLoop;
+PublicInbox::DS::event_loop();
 is(scalar(@x), 1, 'got an event') and
        is($x[0]->[0]->fullname, "$tmpdir/a/b", 'got expected fullname') and
        ok($x[0]->[0]->IN_DELETE, 'IN_DELETE set');
 rmdir("$tmpdir/a") or xbail "rmdir $!";
 @x = ();
 $end = 3 + now;
-PublicInbox::DS->EventLoop;
+PublicInbox::DS::event_loop();
 is(scalar(@x), 1, 'got an event') and
        is($x[0]->[0]->fullname, "$tmpdir/a", 'got expected fullname') and
        ok($x[0]->[0]->IN_DELETE_SELF, 'IN_DELETE_SELF set');
 rename("$tmpdir/c", "$tmpdir/j") or xbail "rmdir $!";
 @x = ();
 $end = 3 + now;
-PublicInbox::DS->EventLoop;
+PublicInbox::DS::event_loop();
 is(scalar(@x), 1, 'got an event') and
        is($x[0]->[0]->fullname, "$tmpdir/c", 'got expected fullname') and
        ok($x[0]->[0]->IN_DELETE_SELF || $x[0]->[0]->IN_MOVE_SELF,
 
        pipe($r, $w) or die "pipe: $!";
 
        PublicInbox::DS::add_timer(0, sub { $pid = spawn([qw(sleep 10)]) });
-       PublicInbox::DS->EventLoop;
+       PublicInbox::DS::event_loop();
        ok($pid, 'subprocess spawned');
 
        # wait for execve, we need to ensure lsof sees sleep(1)
        for my $i (0..$n) {
                PublicInbox::DS->SetLoopTimeout(0);
                PublicInbox::DS->SetPostLoopCallback($cb);
-               PublicInbox::DS->EventLoop;
+               PublicInbox::DS::event_loop();
                PublicInbox::DS->Reset;
        }
        ok(1, "Reset works and doesn't hit RLIMIT_NOFILE ($n)");
 
        my $w = start_script(['-watch'], undef, { 2 => $err_wr });
 
        diag 'waiting for initial fetch...';
-       PublicInbox::DS->EventLoop;
+       PublicInbox::DS::event_loop();
        diag 'inbox unlocked on initial fetch, waiting for IDLE';
 
        tick until (grep(/I: \S+ idling/, <$err>));
                diag "mda error \$?=$?";
        diag 'waiting for IMAP IDLE wakeup';
        PublicInbox::DS->SetPostLoopCallback(undef);
-       PublicInbox::DS->EventLoop;
+       PublicInbox::DS::event_loop();
        diag 'inbox unlocked on IDLE wakeup';
 
        # try again with polling
 
        diag 'waiting for PollInterval wakeup';
        PublicInbox::DS->SetPostLoopCallback(undef);
-       PublicInbox::DS->EventLoop;
+       PublicInbox::DS::event_loop();
        diag 'inbox unlocked (poll)';
        $w->kill;
        $w->join;
 
        my $w = start_script(['-watch'], undef, { 2 => $err_wr });
 
        diag 'waiting for initial fetch...';
-       PublicInbox::DS->EventLoop;
+       PublicInbox::DS::event_loop();
        diag 'inbox unlocked on initial fetch';
        $w->kill;
        $w->join;
 
 use IO::Handle;
 use POSIX qw(:signal_h);
 use Errno qw(ENOSYS);
-use PublicInbox::Syscall qw(SFD_NONBLOCK);
 require_ok 'PublicInbox::Sigfd';
 use PublicInbox::DS;
 
                }
                $sigfd = undef;
 
-               my $nbsig = PublicInbox::Sigfd->new($sig, SFD_NONBLOCK);
+               my $nbsig = PublicInbox::Sigfd->new($sig, 1);
                ok($nbsig, 'Sigfd->new SFD_NONBLOCK works');
                is($nbsig->wait_once, undef, 'nonblocking ->wait_once');
                ok($! == Errno::EAGAIN, 'got EAGAIN');
                kill('HUP', $$) or die "kill $!";
                PublicInbox::DS->SetPostLoopCallback(sub {}); # loop once
-               PublicInbox::DS->EventLoop;
+               PublicInbox::DS::event_loop();
                is($hit->{HUP}->{sigfd}, 2, 'HUP sigfd fired in event loop') or
                        diag explain($hit); # sometimes fails on FreeBSD 11.x
                kill('TERM', $$) or die "kill $!";
                kill('HUP', $$) or die "kill $!";
-               PublicInbox::DS->EventLoop;
+               PublicInbox::DS::event_loop();
                PublicInbox::DS->Reset;
                is($hit->{TERM}->{sigfd}, 1, 'TERM sigfd fired in event loop');
                is($hit->{HUP}->{sigfd}, 3, 'HUP sigfd fired in event loop');
 
 
        $em->commit; # wake -watch up
        diag 'waiting for -watch to import new message';
-       PublicInbox::DS->EventLoop;
+       PublicInbox::DS::event_loop();
        $wm->kill;
        $wm->join;
        $ii->close;
 
 
        # one step through the event loop
        # do a little work as we connect:
-       PublicInbox::DS->EventLoop;
+       PublicInbox::DS::event_loop();
 
        # try not to overflow the listen() backlog:
        if (!($n % 128) && $DONE != $n) {
                PublicInbox::DS->SetPostLoopCallback(sub { $DONE != $n });
 
                # clear the backlog:
-               PublicInbox::DS->EventLoop;
+               PublicInbox::DS::event_loop();
 
                # resume looping
                PublicInbox::DS->SetLoopTimeout(0);
 if ($DONE != $nfd) {
        PublicInbox::DS->SetLoopTimeout(-1);
        PublicInbox::DS->SetPostLoopCallback(sub { $DONE != $nfd });
-       PublicInbox::DS->EventLoop;
+       PublicInbox::DS::event_loop();
 }
 is($nfd, $DONE, "$nfd/$DONE done");
 if ($^O eq 'linux' && open(my $f, '<', "/proc/$pid/status")) {
 
        $pub_cfg->each_inbox(sub { $_[0]->subscribe_unlock('ident', $obj) });
        my $w = start_script(['-watch'], undef, { 2 => $err_wr });
        diag 'waiting for initial fetch...';
-       PublicInbox::DS->EventLoop;
+       PublicInbox::DS::event_loop();
        my $ibx = $pub_cfg->lookup_name('wtest');
        my $mm = $ibx->mm;
        ok(defined($mm->num_for('Seen@test.example.com')),