about summary refs log tree commit homepage
path: root/lib/PublicInbox/Daemon.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/PublicInbox/Daemon.pm')
-rw-r--r--lib/PublicInbox/Daemon.pm395
1 files changed, 198 insertions, 197 deletions
diff --git a/lib/PublicInbox/Daemon.pm b/lib/PublicInbox/Daemon.pm
index 16bae231..28458b19 100644
--- a/lib/PublicInbox/Daemon.pm
+++ b/lib/PublicInbox/Daemon.pm
@@ -5,31 +5,30 @@
 # and designed for handling thousands of untrusted clients over slow
 # and/or lossy connections.
 package PublicInbox::Daemon;
-use strict;
-use v5.10.1;
+use v5.12;
 use Getopt::Long qw(:config gnu_getopt no_ignore_case auto_abbrev);
 use IO::Handle; # ->autoflush
 use IO::Socket;
 use File::Spec;
-use POSIX qw(WNOHANG :signal_h);
+use POSIX qw(WNOHANG :signal_h F_SETFD);
 use Socket qw(IPPROTO_TCP SOL_SOCKET);
 STDOUT->autoflush(1);
 STDERR->autoflush(1);
-use PublicInbox::DS qw(now);
+use PublicInbox::DS qw(now awaitpid);
 use PublicInbox::Listener;
 use PublicInbox::EOFpipe;
-use PublicInbox::Sigfd;
 use PublicInbox::Git;
 use PublicInbox::GitAsyncCat;
 use PublicInbox::Eml;
 use PublicInbox::Config;
+use PublicInbox::OnDestroy;
+use PublicInbox::Search;
+use PublicInbox::XapClient;
 our $SO_ACCEPTFILTER = 0x1000;
 my @CMD;
-my ($set_user, $oldset);
+my ($set_user, $oldset, $xh_workers);
 my (@cfg_listen, $stdout, $stderr, $group, $user, $pid_file, $daemonize);
-my $worker_processes = 1;
-my @listeners;
-my (%pids, %logs);
+my ($nworker, @listeners, %WORKERS, %logs);
 my %tls_opt; # scheme://sockname => args for IO::Socket::SSL::SSL_Context->new
 my $reexec_pid;
 my ($uid, $gid);
@@ -40,6 +39,19 @@ my %SCHEME2PORT = map { $KNOWN_TLS{$_} => $_ + 0 } keys %KNOWN_TLS;
 for (keys %KNOWN_STARTTLS) { $SCHEME2PORT{$KNOWN_STARTTLS{$_}} = $_ + 0 }
 $SCHEME2PORT{http} = 80;
 
+our ($parent_pipe, %POST_ACCEPT, %XNETD);
+our %WORKER_SIG = (
+        INT => \&worker_quit,
+        QUIT => \&worker_quit,
+        TERM => \&worker_quit,
+        TTIN => 'IGNORE',
+        TTOU => 'IGNORE',
+        USR1 => \&reopen_logs,
+        USR2 => 'IGNORE',
+        WINCH => 'IGNORE',
+        CHLD => \&PublicInbox::DS::enqueue_reap,
+);
+
 sub listener_opt ($) {
         my ($str) = @_; # opt1=val1,opt2=val2 (opt may repeat for multi-value)
         my $o = {};
@@ -134,13 +146,18 @@ sub load_mod ($;$$) {
                 $tlsd->{$f} = $logs{$p} //= open_log_path(my $fh, $p);
                 warn "# $scheme://$addr $f=$p\n";
         }
+        # for per-listener $SIG{__WARN__}:
         my $err = $tlsd->{err};
-        $tlsd->{warn_cb} = sub { print $err @_ }; # for local $SIG{__WARN__}
+        $tlsd->{warn_cb} = sub {
+                print $err @_ unless PublicInbox::Eml::warn_ignore(@_)
+        };
+        $opt->{'multi-accept'} and
+                $xn{'multi-accept'} = $opt->{'multi-accept'}->[-1];
         \%xn;
 }
 
-sub daemon_prepare ($$) {
-        my ($default_listen, $xnetd) = @_;
+sub daemon_prepare ($) {
+        my ($default_listen) = @_;
         my $listener_names = {}; # sockname => IO::Handle
         $oldset = PublicInbox::DS::block_signals();
         @CMD = ($0, @ARGV);
@@ -153,8 +170,9 @@ options:
 
   -l ADDRESS    address to listen on$dh
   --cert=FILE   default SSL/TLS certificate
-  --key=FILE    default SSL/TLS certificate
+  --key=FILE    default SSL/TLS certificate key
   -W WORKERS    number of worker processes to spawn (default: 1)
+  -X XWORKERS   number of Xapian helper processes (default: undefined)
 
 See public-inbox-daemon(8) and $prog(1) man pages for more.
 EOF
@@ -162,13 +180,15 @@ EOF
                 'l|listen=s' => \@cfg_listen,
                 '1|stdout=s' => \$stdout,
                 '2|stderr=s' => \$stderr,
-                'W|worker-processes=i' => \$worker_processes,
+                'W|worker-processes=i' => \$nworker,
                 'P|pid-file=s' => \$pid_file,
                 'u|user=s' => \$user,
                 'g|group=s' => \$group,
                 'D|daemonize' => \$daemonize,
+                'multi-accept=i' => \$PublicInbox::Listener::MULTI_ACCEPT,
                 'cert=s' => \$default_cert,
                 'key=s' => \$default_key,
+                'X|xapian-helpers=i' => \$xh_workers,
                 'help|h' => \(my $show_help),
         );
         GetOptions(%opt) or die $help;
@@ -215,7 +235,7 @@ EOF
                         die "$orig specified w/o cert=\n";
                 }
                 if ($listener_names->{$l}) { # already inherited
-                        $xnetd->{$l} = load_mod($scheme, $opt, $l);
+                        $XNETD{$l} = load_mod($scheme, $opt, $l);
                         next;
                 }
                 my (%o, $sock_pkg);
@@ -251,7 +271,7 @@ EOF
                 $s->blocking(0);
                 my $sockname = sockname($s);
                 warn "# bound $scheme://$sockname\n";
-                $xnetd->{$sockname} //= load_mod($scheme);
+                $XNETD{$sockname} //= load_mod($scheme, $opt);
                 $listener_names->{$sockname} = $s;
                 push @listeners, $s;
         }
@@ -265,10 +285,10 @@ EOF
                 for my $x (@inherited_names) {
                         $x =~ /:([0-9]+)\z/ or next; # no TLS for AF_UNIX
                         if (my $scheme = $KNOWN_TLS{$1}) {
-                                $xnetd->{$x} //= load_mod($scheme);
+                                $XNETD{$x} //= load_mod($scheme);
                                 $tls_opt{"$scheme://$x"} ||= accept_tls_opt('');
                         } elsif (($scheme = $KNOWN_STARTTLS{$1})) {
-                                $xnetd->{$x} //= load_mod($scheme);
+                                $XNETD{$x} //= load_mod($scheme);
                                 $tls_opt{"$scheme://$x"} ||= accept_tls_opt('');
                         } elsif (defined $stls) {
                                 $tls_opt{"$stls://$x"} ||= accept_tls_opt('');
@@ -277,7 +297,7 @@ EOF
         }
         if (defined $default_scheme) {
                 for my $x (@inherited_names) {
-                        $xnetd->{$x} //= load_mod($default_scheme);
+                        $XNETD{$x} //= load_mod($default_scheme);
                 }
         }
         die "No listeners bound\n" unless @listeners;
@@ -323,22 +343,38 @@ EOF
         };
 
         if ($daemonize) {
-                my $pid = fork // die "fork: $!";
+                my $pid = PublicInbox::OnDestroy::fork_tmp;
                 exit if $pid;
-
                 open(STDIN, '+<', '/dev/null') or
                                         die "redirect stdin failed: $!\n";
                 open STDOUT, '>&STDIN' or die "redirect stdout failed: $!\n";
                 open STDERR, '>&STDIN' or die "redirect stderr failed: $!\n";
                 POSIX::setsid();
-                $pid = fork // die "fork: $!";
+                $pid = PublicInbox::OnDestroy::fork_tmp;
                 exit if $pid;
         }
         return unless defined $pid_file;
 
         write_pid($pid_file);
-        # for ->DESTROY:
-        bless { pid => $$, pid_file => \$pid_file }, __PACKAGE__;
+        on_destroy \&unlink_pid_file_safe_ish, \$pid_file;
+}
+
+sub has_busy_clients { # post_loop_do CB
+        my ($state) = @_;
+        my $now = now();
+        my $n = PublicInbox::DS::close_non_busy();
+        if ($n) {
+                if ($state->{-w} < now()) {
+                        warn "$$ quitting, $n client(s) left\n";
+                        $state->{-w} = now() + 5;
+                }
+                unless (defined $state->{0}) {
+                        $state->{0} = (split(/\s+/, $0))[0];
+                        $state->{0} =~ s!\A.*?([^/]+)\z!$1!;
+                }
+                $0 = "$state->{0} quitting, $n client(s) left";
+        }
+        $n; # true: loop continues, false: loop breaks
 }
 
 sub worker_quit { # $_[0] = signal name or number (unused)
@@ -347,40 +383,35 @@ sub worker_quit { # $_[0] = signal name or number (unused)
 
         $_->close foreach @listeners; # call PublicInbox::DS::close
         @listeners = ();
-        my $proc_name;
-        my $warn = 0;
+
         # drop idle connections and try to quit gracefully
-        PublicInbox::DS->SetPostLoopCallback(sub {
-                my ($dmap, undef) = @_;
-                my $n = 0;
-                my $now = now();
-                for my $s (values %$dmap) {
-                        $s->can('busy') or next;
-                        if ($s->busy) {
-                                ++$n;
-                        } else { # close as much as possible, early as possible
-                                $s->close;
-                        }
-                }
-                if ($n) {
-                        if (($warn + 5) < now()) {
-                                warn "$$ quitting, $n client(s) left\n";
-                                $warn = now();
-                        }
-                        unless (defined $proc_name) {
-                                $proc_name = (split(/\s+/, $0))[0];
-                                $proc_name =~ s!\A.*?([^/]+)\z!$1!;
-                        }
-                        $0 = "$proc_name quitting, $n client(s) left";
-                }
-                $n; # true: loop continues, false: loop breaks
-        });
+        @PublicInbox::DS::post_loop_do = (\&has_busy_clients, { -w => 0 })
+}
+
+sub spawn_xh () {
+        $xh_workers // return;
+        require PublicInbox::XhcMset;
+        local $) = $gid if defined $gid;
+        local $( = $gid if defined $gid;
+        local $> = $uid if defined $uid;
+        local $< = $uid if defined $uid;
+        $PublicInbox::Search::XHC = eval {
+                local $ENV{STDERR_PATH} = $stderr;
+                local $ENV{STDOUT_PATH} = $stdout;
+                PublicInbox::XapClient::start_helper('-j', $xh_workers)
+        };
+        warn "E: $@" if $@;
+        awaitpid($PublicInbox::Search::XHC->{io}->attached_pid, \&respawn_xh)
+                if $PublicInbox::Search::XHC;
 }
 
 sub reopen_logs {
+        my ($sig) = @_;
         $logs{$stdout} //= \*STDOUT if defined $stdout;
         $logs{$stderr} //= \*STDERR if defined $stderr;
         while (my ($p, $fh) = each %logs) { open_log_path($fh, $p) }
+        ($sig && defined($xh_workers) && $PublicInbox::Search::XHC) and
+                kill('USR1', $PublicInbox::Search::XHC->{io}->attached_pid);
 }
 
 sub sockname ($) {
@@ -468,168 +499,139 @@ sub upgrade { # $_[0] = signal name or number (unused)
                         warn "BUG: .oldbin suffix exists: $pid_file\n";
                         return;
                 }
-                unlink_pid_file_safe_ish($$, $pid_file);
+                unlink_pid_file_safe_ish(\$pid_file);
                 $pid_file .= '.oldbin';
                 write_pid($pid_file);
         }
-        my $pid = fork;
-        unless (defined $pid) {
-                warn "fork failed: $!\n";
-                return;
-        }
-        if ($pid == 0) {
-                use Fcntl qw(FD_CLOEXEC F_SETFD F_GETFD);
+        my $pid = eval { PublicInbox::OnDestroy::fork_tmp };
+        if (!defined($pid)) {
+                warn "fork failed: $! $@\n";
+        } elsif ($pid == 0) {
                 $ENV{LISTEN_FDS} = scalar @listeners;
                 $ENV{LISTEN_PID} = $$;
                 foreach my $s (@listeners) {
                         # @listeners are globs with workers, PI::L w/o workers
                         $s = $s->{sock} if ref($s) eq 'PublicInbox::Listener';
-
-                        my $fl = fcntl($s, F_GETFD, 0);
-                        fcntl($s, F_SETFD, $fl &= ~FD_CLOEXEC);
+                        fcntl($s, F_SETFD, 0) // die "F_SETFD: $!";
                 }
                 exec @CMD;
                 die "Failed to exec: $!\n";
+        } else {
+                awaitpid($pid, \&upgrade_aborted);
+                $reexec_pid = $pid;
         }
-        $reexec_pid = $pid;
 }
 
-sub kill_workers ($) {
-        my ($sig) = @_;
-        kill $sig, keys(%pids);
-}
+sub kill_workers ($) { kill $_[0], values(%WORKERS) }
 
-sub upgrade_aborted ($) {
-        my ($p) = @_;
-        warn "reexec PID($p) died with: $?\n";
+sub upgrade_aborted {
+        my ($pid) = @_;
+        warn "reexec PID($pid) died with: $?\n";
         $reexec_pid = undef;
         return unless $pid_file;
 
         my $file = $pid_file;
         $file =~ s/\.oldbin\z// or die "BUG: no '.oldbin' suffix in $file";
-        unlink_pid_file_safe_ish($$, $pid_file);
+        unlink_pid_file_safe_ish(\$pid_file);
         $pid_file = $file;
         eval { write_pid($pid_file) };
         warn $@, "\n" if $@;
 }
 
-sub reap_children { # $_[0] = 'CHLD' or POSIX::SIGCHLD()
-        while (1) {
-                my $p = waitpid(-1, WNOHANG) or return;
-                if (defined $reexec_pid && $p == $reexec_pid) {
-                        upgrade_aborted($p);
-                } elsif (defined(my $id = delete $pids{$p})) {
-                        warn "worker[$id] PID($p) died with: $?\n";
-                } elsif ($p > 0) {
-                        warn "unknown PID($p) reaped: $?\n";
-                } else {
-                        return;
-                }
-        }
-}
-
-sub unlink_pid_file_safe_ish ($$) {
-        my ($unlink_pid, $file) = @_;
-        return unless defined $unlink_pid && $unlink_pid == $$;
+sub unlink_pid_file_safe_ish ($) {
+        my ($fref) = @_;
 
-        open my $fh, '<', $file or return;
+        open my $fh, '<', $$fref or return;
         local $/ = "\n";
         defined(my $read_pid = <$fh>) or return;
         chomp $read_pid;
-        if ($read_pid == $unlink_pid) {
-                Net::Server::Daemonize::unlink_pid_file($file);
-        }
+        Net::Server::Daemonize::unlink_pid_file($$fref) if $read_pid == $$;
 }
 
 sub master_quit ($) {
         exit unless @listeners;
         @listeners = ();
-        kill_workers($_[0]);
+        exit unless kill_workers($_[0]);
+}
+
+sub reap_worker { # awaitpid CB
+        my ($pid, $nr) = @_;
+        warn "worker[$nr] died \$?=$?\n" if $?;
+        delete $WORKERS{$nr};
+        exit if !@listeners && !keys(%WORKERS);
+        PublicInbox::DS::requeue(\&start_workers);
+}
+
+sub start_worker ($) {
+        my ($nr) = @_;
+        return unless @listeners;
+        my $pid = PublicInbox::DS::fork_persist;
+        if ($pid == 0) {
+                undef %WORKERS;
+                undef $xh_workers;
+                local $PublicInbox::DS::Poller; # allow epoll/kqueue
+                $set_user->() if $set_user;
+                PublicInbox::EOFpipe->new($parent_pipe, \&worker_quit);
+                worker_loop();
+                exit 0;
+        } else {
+                $WORKERS{$nr} = $pid;
+                awaitpid($pid, \&reap_worker, $nr);
+        }
+}
+
+sub start_workers {
+        my @idx = grep { !defined($WORKERS{$_}) } (0..($nworker - 1)) or return;
+        eval { start_worker($_) for @idx };
+        warn "E: $@\n" if $@;
+}
+
+sub trim_workers {
+        my @nr = grep { $_ >= $nworker } keys %WORKERS;
+        kill('TERM', @WORKERS{@nr});
 }
 
 sub master_loop {
-        pipe(my ($p0, $p1)) or die "failed to create parent-pipe: $!";
-        my $set_workers = $worker_processes;
+        local $parent_pipe;
+        pipe($parent_pipe, my $p1) or die "failed to create parent-pipe: $!";
+        my $set_workers = $nworker; # for SIGWINCH
         reopen_logs();
-        my $ignore_winch;
-        my $sig = {
-                USR1 => sub { reopen_logs(); kill_workers($_[0]); },
+        spawn_xh;
+        my $msig = {
+                USR1 => sub { reopen_logs($_[0]); kill_workers($_[0]); },
                 USR2 => \&upgrade,
                 QUIT => \&master_quit,
                 INT => \&master_quit,
                 TERM => \&master_quit,
                 WINCH => sub {
-                        return if $ignore_winch || !@listeners;
-                        if (-t STDIN || -t STDOUT || -t STDERR) {
-                                $ignore_winch = 1;
-                                warn <<EOF;
-ignoring SIGWINCH since we are not daemonized
-EOF
-                        } else {
-                                $worker_processes = 0;
-                        }
+                        $nworker = 0;
+                        trim_workers();
                 },
                 HUP => sub {
-                        return unless @listeners;
-                        $worker_processes = $set_workers;
+                        $nworker = $set_workers; # undo WINCH
                         kill_workers($_[0]);
+                        PublicInbox::DS::requeue(\&start_workers)
                 },
                 TTIN => sub {
-                        return unless @listeners;
-                        if ($set_workers > $worker_processes) {
-                                ++$worker_processes;
+                        if ($set_workers > $nworker) {
+                                ++$nworker;
                         } else {
-                                $worker_processes = ++$set_workers;
+                                $nworker = ++$set_workers;
                         }
+                        PublicInbox::DS::requeue(\&start_workers);
                 },
                 TTOU => sub {
-                        $worker_processes = --$set_workers if $set_workers > 0;
+                        return if $nworker <= 0;
+                        --$nworker;
+                        trim_workers();
                 },
-                CHLD => \&reap_children,
+                CHLD => \&PublicInbox::DS::enqueue_reap,
         };
-        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
-                my $n = scalar keys %pids;
-                unless (@listeners) {
-                        exit if $n == 0;
-                        $set_workers = $worker_processes = $n = 0;
-                }
-
-                if ($n > $worker_processes) {
-                        while (my ($k, $v) = each %pids) {
-                                kill('TERM', $k) if $v >= $worker_processes;
-                        }
-                        $n = $worker_processes;
-                }
-                my $want = $worker_processes - 1;
-                if ($n <= $want) {
-                        PublicInbox::DS::block_signals() if !$sigfd;
-                        for my $i ($n..$want) {
-                                my $seed = rand(0xffffffff);
-                                my $pid = fork;
-                                if (!defined $pid) {
-                                        warn "failed to fork worker[$i]: $!\n";
-                                } elsif ($pid == 0) {
-                                        srand($seed);
-                                        eval { Net::SSLeay::randomize() };
-                                        $set_user->() if $set_user;
-                                        return $p0; # run normal work code
-                                } else {
-                                        warn "PID=$pid is worker[$i]\n";
-                                        $pids{$pid} = $i;
-                                }
-                        }
-                        PublicInbox::DS::sig_setmask($oldset) if !$sigfd;
-                }
-
-                if ($sigfd) { # Linux and IO::KQueue users:
-                        $sigfd->wait_once;
-                } else { # wake up every second
-                        sleep(1);
-                }
-        }
+        $msig->{WINCH} = sub {
+                warn "ignoring SIGWINCH since we are not daemonized\n";
+        } if -t STDIN || -t STDOUT || -t STDERR;
+        start_workers();
+        PublicInbox::DS::event_loop($msig, $oldset);
         exit # never gets here, just for documentation
 }
 
@@ -651,88 +653,91 @@ sub defer_accept ($$) {
                 my $sec = unpack('i', $x);
                 return if $sec > 0; # systemd users may set a higher value
                 setsockopt($s, IPPROTO_TCP, $TCP_DEFER_ACCEPT, 1);
-        } elsif ($^O eq 'freebsd') {
+        } elsif ($^O =~ /\A(?:freebsd|netbsd|dragonfly)\z/) {
                 my $x = getsockopt($s, SOL_SOCKET, $SO_ACCEPTFILTER);
-                return if defined $x; # don't change if set
+                return if ($x // "\0") =~ /[^\0]/s; # don't change if set
                 my $accf_arg = pack('a16a240', $af_name, '');
                 setsockopt($s, SOL_SOCKET, $SO_ACCEPTFILTER, $accf_arg);
         }
 }
 
-sub daemon_loop ($) {
-        my ($xnetd) = @_;
+sub daemon_loop () {
         local $PublicInbox::Config::DEDUPE = {}; # enable dedupe cache
-        my $refresh = sub {
+        my $refresh = $WORKER_SIG{HUP} = sub {
                 my ($sig) = @_;
                 %$PublicInbox::Config::DEDUPE = (); # clear cache
-                for my $xn (values %$xnetd) {
+                for my $xn (values %XNETD) {
                         delete $xn->{tlsd}->{ssl_ctx}; # PublicInbox::TLS::start
                         eval { $xn->{refresh}->($sig) };
                         warn "refresh $@\n" if $@;
                 }
         };
-        my %post_accept;
         while (my ($k, $ctx_opt) = each %tls_opt) {
                 $ctx_opt // next;
                 my ($scheme, $l) = split(m!://!, $k, 2);
-                my $xn = $xnetd->{$l} // die "BUG: no xnetd for $k";
+                my $xn = $XNETD{$l} // die "BUG: no xnetd for $k";
                 $xn->{tlsd}->{ssl_ctx_opt} //= $ctx_opt;
                 $scheme =~ m!\A(?:https|imaps|nntps|pop3s)! and
-                        $post_accept{$l} = tls_cb(@$xn{qw(post_accept tlsd)});
+                        $POST_ACCEPT{$l} = tls_cb(@$xn{qw(post_accept tlsd)});
         }
         undef %tls_opt;
-        my $sig = {
-                HUP => $refresh,
-                INT => \&worker_quit,
-                QUIT => \&worker_quit,
-                TERM => \&worker_quit,
-                TTIN => 'IGNORE',
-                TTOU => 'IGNORE',
-                USR1 => \&reopen_logs,
-                USR2 => 'IGNORE',
-                WINCH => 'IGNORE',
-                CHLD => \&PublicInbox::DS::enqueue_reap,
-        };
-        if ($worker_processes > 0) {
+        if ($nworker > 0) {
                 $refresh->(); # preload by default
-                my $fh = master_loop(); # returns if in child process
-                PublicInbox::EOFpipe->new($fh, \&worker_quit, undef);
+                return master_loop();
         } else {
                 reopen_logs();
                 $set_user->() if $set_user;
-                $sig->{USR2} = sub { worker_quit() if upgrade() };
+                $WORKER_SIG{USR2} = sub { worker_quit() if upgrade() };
                 $refresh->();
         }
+        local $PublicInbox::DS::Poller; # allow epoll/kqueue
+        worker_loop();
+}
+
+sub worker_loop {
         $uid = $gid = undef;
         reopen_logs();
+        spawn_xh; # only for -W0
         @listeners = map {;
                 my $l = sockname($_);
-                my $tls_cb = $post_accept{$l};
-                my $xn = $xnetd->{$l} // die "BUG: no xnetd for $l";
+                my $tls_cb = $POST_ACCEPT{$l};
+                my $xn = $XNETD{$l} // die "BUG: no xnetd for $l";
 
                 # NNTPS, HTTPS, HTTP, IMAPS and POP3S are client-first traffic
                 # IMAP, NNTP and POP3 are server-first
                 defer_accept($_, $tls_cb ? 'dataready' : $xn->{af_default});
 
                 # this calls epoll_create:
-                PublicInbox::Listener->new($_, $tls_cb || $xn->{post_accept})
+                PublicInbox::Listener->new($_, $tls_cb || $xn->{post_accept},
+                                                $xn->{'multi-accept'})
         } @listeners;
-        PublicInbox::DS::event_loop($sig, $oldset);
+        PublicInbox::DS::event_loop(\%WORKER_SIG, $oldset);
+}
+
+sub respawn_xh { # awaitpid cb
+        my ($pid) = @_;
+        return unless @listeners;
+        warn "W: xap_helper PID:$pid died: \$?=$?, respawning...\n";
+        spawn_xh;
 }
 
 sub run {
         my ($default_listen) = @_;
-        daemon_prepare($default_listen, my $xnetd = {});
-        my $for_destroy = daemonize();
+        $nworker = 1;
+        local (%XNETD, %POST_ACCEPT);
+        daemon_prepare($default_listen);
+        my $unlink_on_leave = daemonize();
 
         # localize GCF2C for tests:
         local $PublicInbox::GitAsyncCat::GCF2C;
         local $PublicInbox::Git::async_warn = 1;
         local $SIG{__WARN__} = PublicInbox::Eml::warn_ignore_cb();
+        local %WORKER_SIG = %WORKER_SIG;
+        local $PublicInbox::XapClient::tries = 0;
+        local $PublicInbox::Search::XHC if defined($xh_workers);
 
-        daemon_loop($xnetd);
-        PublicInbox::DS->Reset;
-        # ->DESTROY runs when $for_destroy goes out-of-scope
+        daemon_loop();
+        # $unlink_on_leave runs
 }
 
 sub write_pid ($) {
@@ -741,8 +746,4 @@ sub write_pid ($) {
         do_chown($path);
 }
 
-sub DESTROY {
-        unlink_pid_file_safe_ish($_[0]->{pid}, ${$_[0]->{pid_file}});
-}
-
 1;