about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--lib/PublicInbox/CodeSearchIdx.pm29
-rw-r--r--lib/PublicInbox/DS.pm9
-rw-r--r--lib/PublicInbox/Daemon.pm12
-rw-r--r--lib/PublicInbox/IPC.pm12
-rw-r--r--lib/PublicInbox/LEI.pm8
-rw-r--r--lib/PublicInbox/LeiMirror.pm25
-rw-r--r--lib/PublicInbox/LeiP2q.pm2
-rw-r--r--lib/PublicInbox/LeiStore.pm3
-rw-r--r--lib/PublicInbox/LeiTag.pm8
-rw-r--r--lib/PublicInbox/Lock.pm4
-rw-r--r--lib/PublicInbox/MHreader.pm4
-rw-r--r--lib/PublicInbox/MboxLock.pm6
-rw-r--r--lib/PublicInbox/OnDestroy.pm27
-rw-r--r--lib/PublicInbox/SearchIdxShard.pm2
-rw-r--r--lib/PublicInbox/SpawnPP.pm5
-rw-r--r--lib/PublicInbox/TestCommon.pm4
-rw-r--r--lib/PublicInbox/Umask.pm2
-rw-r--r--lib/PublicInbox/ViewVCS.pm5
-rw-r--r--lib/PublicInbox/Watch.pm4
-rw-r--r--lib/PublicInbox/WwwCoderepo.pm2
-rw-r--r--lib/PublicInbox/XapClient.pm2
-rw-r--r--lib/PublicInbox/XapHelper.pm2
-rw-r--r--lib/PublicInbox/Xapcmd.pm2
-rwxr-xr-xscript/public-inbox-init2
-rw-r--r--t/lei-sigpipe.t4
-rw-r--r--t/mbox_lock.t7
-rw-r--r--t/mh_reader.t1
-rw-r--r--t/on_destroy.t25
-rw-r--r--xt/net_writer-imap.t4
29 files changed, 117 insertions, 105 deletions
diff --git a/lib/PublicInbox/CodeSearchIdx.pm b/lib/PublicInbox/CodeSearchIdx.pm
index 570ff64f..6d777bf6 100644
--- a/lib/PublicInbox/CodeSearchIdx.pm
+++ b/lib/PublicInbox/CodeSearchIdx.pm
@@ -368,7 +368,7 @@ sub repo_stored {
         $did > 0 or die "BUG: $repo_ctx->{repo}->{git_dir}: docid=$did";
         my ($c, $p) = PublicInbox::PktOp->pair;
         $c->{ops}->{shard_done} = [ $self, $repo_ctx,
-                PublicInbox::OnDestroy->new(\&next_repos, $repo_ctx, $drs)];
+                                on_destroy(\&next_repos, $repo_ctx, $drs)];
         # shard_done fires when all shards are committed
         my @active = keys %{$repo_ctx->{active}};
         $IDX_SHARDS[$_]->wq_io_do('shard_commit', [ $p->{op_p} ]) for @active;
@@ -425,7 +425,7 @@ sub fp_start ($$) {
         open my $refs, '+>', undef;
         $git->{-repo}->{refs} = $refs;
         my ($c, $p) = PublicInbox::PktOp->pair;
-        my $next_on_err = PublicInbox::OnDestroy->new(\&index_next, $self);
+        my $next_on_err = on_destroy \&index_next, $self;
         $c->{ops}->{fp_done} = [ $self, $git, $next_on_err ];
         $IDX_SHARDS[++$ANY_SHARD % scalar(@IDX_SHARDS)]->wq_io_do('fp_async',
                                         [ $p->{op_p}, $refs ], $git->{git_dir})
@@ -664,8 +664,7 @@ sub index_repo {
         my $repo_ctx = $REPO_CTX = { self => $self, repo => $repo };
         delete $git->{-cidx_gits_fini}; # may fire gits_fini
         my $drs = delete $git->{-cidx_dump_roots_start};
-        my $index_done = PublicInbox::OnDestroy->new(\&index_done,
-                                                        $repo_ctx, $drs);
+        my $index_done = on_destroy \&index_done, $repo_ctx, $drs;
         my ($c, $p) = PublicInbox::PktOp->pair;
         $c->{ops}->{shard_done} = [ $self, $repo_ctx, $index_done ];
         for my $n (0..$#shard_in) {
@@ -690,7 +689,7 @@ sub ct_fini { # run_git cb
 sub prep_repo ($$) {
         my ($self, $git) = @_;
         return if $DO_QUIT;
-        my $index_repo = PublicInbox::OnDestroy->new(\&index_repo, $self, $git);
+        my $index_repo = on_destroy \&index_repo, $self, $git;
         my $refs = $git->{-repo}->{refs} // die 'BUG: no {-repo}->{refs}';
         sysseek($refs, 0, SEEK_SET);
         open my $roots_fh, '+>', undef;
@@ -787,7 +786,7 @@ sub scan_git_dirs ($) {
         my ($self) = @_;
         @$SCANQ = () unless $self->{-opt}->{scan};
         $GITS_NR = @$SCANQ or return;
-        my $gits_fini = PublicInbox::OnDestroy->new(\&gits_fini);
+        my $gits_fini = on_destroy \&gits_fini;
         $_->{-cidx_gits_fini} = $gits_fini for @$SCANQ;
         if (my $drs = $TODO{dump_roots_start}) {
                 $_->{-cidx_dump_roots_start} = $drs for @$SCANQ;
@@ -859,7 +858,7 @@ sub prep_umask ($) {
                 umask == $um or progress($self, 'using umask from ',
                                                 $self->{cidx_dir}, ': ',
                                                 sprintf('0%03o', $um));
-                PublicInbox::OnDestroy->new(\&CORE::umask, umask($um));
+                on_destroy \&CORE::umask, umask($um);
         } else {
                 $self->{umask} = umask; # for SearchIdx->with_umask
                 undef;
@@ -1083,12 +1082,12 @@ EOM
         ($JOIN_DT[1]) = ($QRY_STR =~ /\.\.([0-9]{14})\z/); # YYYYmmddHHMMSS
         ($JOIN_DT[0]) = ($QRY_STR =~ /\Adt:([0-9]{14})/); # YYYYmmddHHMMSS
         $JOIN_DT[0] //= '19700101'.'000000'; # git uses unsigned times
-        $TODO{do_join} = PublicInbox::OnDestroy->new(\&do_join, $self);
+        $TODO{do_join} = on_destroy \&do_join, $self;
         $TODO{joining} = 1; # keep shards_active() happy
-        $TODO{dump_ibx_start} = PublicInbox::OnDestroy->new(\&dump_ibx_start,
-                                                        $self, $TODO{do_join});
-        $TODO{dump_roots_start} = PublicInbox::OnDestroy->new(
-                                \&dump_roots_start, $self, $TODO{do_join});
+        $TODO{dump_ibx_start} = on_destroy \&dump_ibx_start,
+                                        $self, $TODO{do_join};
+        $TODO{dump_roots_start} = on_destroy \&dump_roots_start,
+                                        $self, $TODO{do_join};
         progress($self, "will join in $QRY_STR date range...");
         my $id = -1;
         @IBXQ = map { ++$id } @IBX;
@@ -1110,8 +1109,7 @@ sub init_prune ($) {
         require_progs('prune', 'xapian-delve' => \@delve, sed => \@sed,
                         comm => \@COMM, awk => \@AWK);
         for (0..$#IDX_SHARDS) { push @delve, "$self->{xpfx}/$_" }
-        my $run_prune = PublicInbox::OnDestroy->new(\&run_prune, $self,
-                                                $TODO{dump_roots_start});
+        my $run_prune = on_destroy \&run_prune, $self, $TODO{dump_roots_start};
         my ($sort_opt, $sed_opt, $delve_opt);
         pipe(local $sed_opt->{0}, local $delve_opt->{1});
         pipe(local $sort_opt->{0}, local $sed_opt->{1});
@@ -1279,8 +1277,7 @@ sub cidx_run { # main entry point
         my $restore_umask = prep_umask($self);
         local $SIGSET = PublicInbox::DS::block_signals(
                                         POSIX::SIGTSTP, POSIX::SIGCONT);
-        my $restore = PublicInbox::OnDestroy->new($$,
-                \&PublicInbox::DS::sig_setmask, $SIGSET);
+        my $restore = on_destroy \&PublicInbox::DS::sig_setmask, $SIGSET;
         local $PRUNE_DONE = [];
         local $IDXQ = [];
         local $SCANQ = [];
diff --git a/lib/PublicInbox/DS.pm b/lib/PublicInbox/DS.pm
index 8bc8cfb7..a6fec954 100644
--- a/lib/PublicInbox/DS.pm
+++ b/lib/PublicInbox/DS.pm
@@ -32,9 +32,9 @@ use PublicInbox::Syscall qw(%SIGNUM
         EPOLLIN EPOLLOUT EPOLLONESHOT EPOLLEXCLUSIVE);
 use PublicInbox::Tmpfile;
 use PublicInbox::Select;
+use PublicInbox::OnDestroy;
 use Errno qw(EAGAIN EINVAL ECHILD);
 use Carp qw(carp croak);
-use autodie qw(fork);
 our @EXPORT_OK = qw(now msg_more awaitpid add_timer add_uniq_timer);
 
 my $nextq; # queue for next_tick
@@ -679,12 +679,13 @@ sub awaitpid {
         }
 }
 
-sub do_fork () {
+# for persistent child process
+sub fork_persist () {
         my $seed = rand(0xffffffff);
-        my $pid = fork;
+        my $pid = PublicInbox::OnDestroy::fork_tmp;
         if ($pid == 0) {
                 srand($seed);
-                eval { Net::SSLeay::randomize() };
+                eval { Net::SSLeay::randomize() }; # may not be loaded
                 Reset();
         }
         $pid;
diff --git a/lib/PublicInbox/Daemon.pm b/lib/PublicInbox/Daemon.pm
index e578f2e8..1cc0c9e6 100644
--- a/lib/PublicInbox/Daemon.pm
+++ b/lib/PublicInbox/Daemon.pm
@@ -21,6 +21,7 @@ use PublicInbox::Git;
 use PublicInbox::GitAsyncCat;
 use PublicInbox::Eml;
 use PublicInbox::Config;
+use PublicInbox::OnDestroy;
 our $SO_ACCEPTFILTER = 0x1000;
 my @CMD;
 my ($set_user, $oldset);
@@ -338,15 +339,14 @@ 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;
@@ -480,9 +480,9 @@ sub upgrade { # $_[0] = signal name or number (unused)
                 $pid_file .= '.oldbin';
                 write_pid($pid_file);
         }
-        my $pid = fork;
+        my $pid = eval { PublicInbox::OnDestroy::fork_tmp };
         if (!defined($pid)) {
-                warn "fork failed: $!\n";
+                warn "fork failed: $! $@\n";
         } elsif ($pid == 0) {
                 $ENV{LISTEN_FDS} = scalar @listeners;
                 $ENV{LISTEN_PID} = $$;
@@ -545,7 +545,7 @@ sub reap_worker { # awaitpid CB
 sub start_worker ($) {
         my ($nr) = @_;
         return unless @listeners;
-        my $pid = PublicInbox::DS::do_fork;
+        my $pid = PublicInbox::DS::fork_persist;
         if ($pid == 0) {
                 undef %WORKERS;
                 local $PublicInbox::DS::Poller; # allow epoll/kqueue
diff --git a/lib/PublicInbox/IPC.pm b/lib/PublicInbox/IPC.pm
index a5cae6f2..ed6d27fd 100644
--- a/lib/PublicInbox/IPC.pm
+++ b/lib/PublicInbox/IPC.pm
@@ -10,7 +10,7 @@
 package PublicInbox::IPC;
 use v5.12;
 use parent qw(Exporter);
-use autodie qw(close fork pipe read socketpair sysread);
+use autodie qw(close pipe read socketpair sysread);
 use Carp qw(croak);
 use PublicInbox::DS qw(awaitpid);
 use PublicInbox::Spawn;
@@ -93,6 +93,8 @@ sub ipc_worker_loop ($$$) {
         }
 }
 
+sub exit_exception { exit(!!$@) }
+
 # starts a worker if Sereal or Storable is installed
 sub ipc_worker_spawn {
         my ($self, $ident, $oldset, $fields, @cb_args) = @_;
@@ -102,7 +104,7 @@ sub ipc_worker_spawn {
         pipe(my $r_res, my $w_res);
         my $sigset = $oldset // PublicInbox::DS::block_signals();
         $self->ipc_atfork_prepare;
-        my $pid = PublicInbox::DS::do_fork;
+        my $pid = PublicInbox::DS::fork_persist;
         if ($pid == 0) {
                 delete @$self{qw(-wq_s1 -wq_s2 -wq_workers -wq_ppid)};
                 $w_req = $r_res = undef;
@@ -110,7 +112,7 @@ sub ipc_worker_spawn {
                 $SIG{$_} = 'IGNORE' for (qw(TERM INT QUIT));
                 local $0 = $ident;
                 # ensure we properly exit even if warn() dies:
-                my $end = PublicInbox::OnDestroy->new($$, sub { exit(!!$@) });
+                my $end = on_destroy \&exit_exception;
                 eval {
                         $fields //= {};
                         local @$self{keys %$fields} = values(%$fields);
@@ -330,7 +332,7 @@ sub _wq_worker_start {
         my ($self, $oldset, $fields, $one, @cb_args) = @_;
         my ($bcast1, $bcast2);
         $one or socketpair($bcast1, $bcast2, AF_UNIX, SOCK_SEQPACKET, 0);
-        my $pid = PublicInbox::DS::do_fork;
+        my $pid = PublicInbox::DS::fork_persist;
         if ($pid == 0) {
                 undef $bcast1;
                 delete @$self{qw(-wq_s1 -wq_ppid)};
@@ -340,7 +342,7 @@ sub _wq_worker_start {
                 local $0 = $one ? $self->{-wq_ident} :
                         "$self->{-wq_ident} $self->{-wq_worker_nr}";
                 # ensure we properly exit even if warn() dies:
-                my $end = PublicInbox::OnDestroy->new($$, sub { exit(!!$@) });
+                my $end = on_destroy \&exit_exception;
                 eval {
                         $fields //= {};
                         local @$self{keys %$fields} = values(%$fields);
diff --git a/lib/PublicInbox/LEI.pm b/lib/PublicInbox/LEI.pm
index 81f940fe..7c31ab43 100644
--- a/lib/PublicInbox/LEI.pm
+++ b/lib/PublicInbox/LEI.pm
@@ -9,7 +9,7 @@ package PublicInbox::LEI;
 use v5.12;
 use parent qw(PublicInbox::DS PublicInbox::LeiExternal
         PublicInbox::LeiQuery);
-use autodie qw(bind chdir fork open pipe socket socketpair syswrite unlink);
+use autodie qw(bind chdir open pipe socket socketpair syswrite unlink);
 use Getopt::Long ();
 use Socket qw(AF_UNIX SOCK_SEQPACKET pack_sockaddr_un);
 use Errno qw(EPIPE EAGAIN ECONNREFUSED ENOENT ECONNRESET);
@@ -24,6 +24,7 @@ use PublicInbox::Lock;
 use PublicInbox::Eml;
 use PublicInbox::Import;
 use PublicInbox::ContentHash qw(git_sha);
+use PublicInbox::OnDestroy;
 use PublicInbox::IPC;
 use Time::HiRes qw(stat); # ctime comparisons for config cache
 use File::Path ();
@@ -631,9 +632,8 @@ sub _delete_pkt_op { # OnDestroy callback to prevent leaks on die
 
 sub pkt_op_pair {
         my ($self) = @_;
-        require PublicInbox::OnDestroy;
         require PublicInbox::PktOp;
-        my $end = PublicInbox::OnDestroy->new($$, \&_delete_pkt_op, $self);
+        my $end = on_destroy \&_delete_pkt_op, $self;
         @$self{qw(pkt_op_c pkt_op_p)} = PublicInbox::PktOp->pair;
         $end;
 }
@@ -1357,7 +1357,7 @@ sub lazy_start {
         STDIN->autoflush(1);
         dump_and_clear_log();
         POSIX::setsid() > 0 or die "setsid: $!";
-        my $pid = fork;
+        my $pid = PublicInbox::OnDestroy::fork_tmp;
         return if $pid;
         $0 = "lei-daemon $path";
         local (%PATH2CFG, $MDIR2CFGPATH);
diff --git a/lib/PublicInbox/LeiMirror.pm b/lib/PublicInbox/LeiMirror.pm
index 4e3e1d1c..08e61e4b 100644
--- a/lib/PublicInbox/LeiMirror.pm
+++ b/lib/PublicInbox/LeiMirror.pm
@@ -293,7 +293,7 @@ sub start_update_ref {
         pipe(my $r, my $w);
         my $cmd = [ 'git', "--git-dir=$fgrp->{cur_dst}",
                 qw(update-ref --stdin -z) ];
-        my $pack = PublicInbox::OnDestroy->new($$, \&satellite_done, $fgrp);
+        my $pack = on_destroy \&satellite_done, $fgrp;
         start_cmd($fgrp, $cmd, { 0 => $r, 2 => $fgrp->{lei}->{2} }, $pack);
         close $r;
         $fgrp->{dry_run} ? undef : $w;
@@ -373,10 +373,7 @@ sub fgrpv_done {
         for my $fgrp (@$fgrpv) {
                 my $rn = $fgrp->{-remote};
                 my %opt = ( 2 => $fgrp->{lei}->{2} );
-
-                my $update_ref = PublicInbox::OnDestroy->new($$,
-                                                        \&fgrp_update, $fgrp);
-
+                my $update_ref = on_destroy \&fgrp_update, $fgrp;
                 my $src = [ 'git', "--git-dir=$fgrp->{-osdir}", 'for-each-ref',
                         "--format=refs/%(refname:lstrip=3)%00%(objectname)",
                         "refs/remotes/$rn/" ];
@@ -467,7 +464,7 @@ sub fgrp_fetch_all {
                 }
                 $cmd  = [ @git, "--git-dir=$osdir", @fetch, $grp ];
                 push @$old, @$new;
-                my $end = PublicInbox::OnDestroy->new($$, \&fgrpv_done, $old);
+                my $end = on_destroy \&fgrpv_done, $old;
                 start_cmd($self, $cmd, $opt, $end);
         }
 }
@@ -567,7 +564,7 @@ sub resume_fetch {
         my $cmd = [ @{$self->{-torsocks}}, @git,
                         fetch_args($self->{lei}, $opt), $rn ];
         push @$cmd, '-P' if $self->{lei}->{prune}; # --prune-tags implied
-        my $run_puh = PublicInbox::OnDestroy->new($$, \&run_puh, $self, $fini);
+        my $run_puh = on_destroy \&run_puh, $self, $fini;
         ++$self->{chg}->{nr_chg};
         start_cmd($self, $cmd, $opt, $run_puh);
 }
@@ -599,7 +596,7 @@ sub clone_v1 {
                         return;
                 }
         }
-        my $fini = PublicInbox::OnDestroy->new($$, \&v1_done, $self);
+        my $fini = on_destroy \&v1_done, $self;
         if (my $fgrp = forkgroup_prep($self, $uri)) {
                 $fgrp->{-fini} = $fini;
                 if ($resume) {
@@ -621,8 +618,8 @@ sub clone_v1 {
                         }
                 }
                 ++$self->{chg}->{nr_chg};
-                start_cmd($self, $cmd, $opt, PublicInbox::OnDestroy->new($$,
-                                                \&run_puh, $self, $fini));
+                start_cmd($self, $cmd, $opt,
+                        on_destroy(\&run_puh, $self, $fini));
         }
         if (!$self->{-is_epoch} && $lei->{opt}->{'inbox-config'} =~
                                 /\A(?:always|v1)\z/s &&
@@ -737,7 +734,7 @@ sub atomic_write ($$$) {
 sub run_next_puh {
         my ($self) = @_;
         my $puh = shift @{$self->{-puh_todo}} // return delete($self->{-fini});
-        my $fini = PublicInbox::OnDestroy->new($$, \&run_next_puh, $self);
+        my $fini = on_destroy \&run_next_puh, $self;
         my $cmd = [ @$puh, ($self->{cur_dst} // $self->{dst}) ];
         my $opt = +{ map { $_ => $self->{lei}->{$_} } (0..2) };
         start_cmd($self, $cmd, undef, $opt, $fini);
@@ -762,7 +759,7 @@ sub update_ent {
                 my $opt = { 2 => $self->{lei}->{2} };
                 open($opt->{1}, '+>', undef);
                 $self->{-show_ref_up} = $opt->{1};
-                my $done = PublicInbox::OnDestroy->new($$, \&up_fp_done, $self);
+                my $done = on_destroy \&up_fp_done, $self;
                 start_cmd($self, $cmd, $opt, $done);
         }
         $new = $self->{-ent}->{head};
@@ -883,7 +880,7 @@ sub clone_v2_prep ($$;$) {
         my $want = parse_epochs($lei->{opt}->{epoch}, $v2_epochs);
         my $task = $m ? bless { %$self }, __PACKAGE__ : $self;
         my (@skip, $desc);
-        my $fini = PublicInbox::OnDestroy->new($$, \&v2_done, $task);
+        my $fini = on_destroy \&v2_done, $task;
         for my $nr (sort { $a <=> $b } keys %$v2_epochs) {
                 my ($uri, $key) = @{$v2_epochs->{$nr}};
                 my $src = $uri->as_string;
@@ -1018,7 +1015,7 @@ sub clone_all {
         my ($self, $m) = @_;
         my $todo = $TODO;
         $TODO = \'BUG on further use';
-        my $end = PublicInbox::OnDestroy->new($$, \&fgrp_fetch_all, $self);
+        my $end = on_destroy \&fgrp_fetch_all, $self;
         {
                 my $nodep = delete $todo->{''};
 
diff --git a/lib/PublicInbox/LeiP2q.pm b/lib/PublicInbox/LeiP2q.pm
index 610adb78..68faa016 100644
--- a/lib/PublicInbox/LeiP2q.pm
+++ b/lib/PublicInbox/LeiP2q.pm
@@ -189,7 +189,7 @@ sub lei_p2q { # the "lei patch-to-query" entry point
 sub ipc_atfork_child {
         my ($self) = @_;
         PublicInbox::LeiInput::input_only_atfork_child($self);
-        PublicInbox::OnDestroy->new($$, \&emit_query, $self);
+        on_destroy \&emit_query, $self;
 }
 
 no warnings 'once';
diff --git a/lib/PublicInbox/LeiStore.pm b/lib/PublicInbox/LeiStore.pm
index a752174d..2eb09eca 100644
--- a/lib/PublicInbox/LeiStore.pm
+++ b/lib/PublicInbox/LeiStore.pm
@@ -28,6 +28,7 @@ use PublicInbox::Spawn qw(spawn);
 use PublicInbox::MdirReader;
 use PublicInbox::LeiToMail;
 use PublicInbox::Compat qw(uniqstr);
+use PublicInbox::OnDestroy;
 use File::Temp qw(tmpnam);
 use POSIX ();
 use IO::Handle (); # ->autoflush
@@ -135,7 +136,7 @@ sub eidx_init {
         my ($self) = @_;
         my $eidx = $self->{priv_eidx};
         my $tl = wantarray && $self->{-err_wr} ?
-                        PublicInbox::OnDestroy->new($$, \&_tail_err, $self) :
+                        on_destroy(\&_tail_err, $self) :
                         undef;
         $eidx->idx_init({-private => 1}); # acquires lock
         wantarray ? ($eidx, $tl) : $eidx;
diff --git a/lib/PublicInbox/LeiTag.pm b/lib/PublicInbox/LeiTag.pm
index 320b0355..da8caeb7 100644
--- a/lib/PublicInbox/LeiTag.pm
+++ b/lib/PublicInbox/LeiTag.pm
@@ -1,12 +1,12 @@
-# Copyright (C) 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>
 
 # handles "lei tag" command
 package PublicInbox::LeiTag;
-use strict;
-use v5.10.1;
+use v5.12;
 use parent qw(PublicInbox::IPC PublicInbox::LeiInput);
 use PublicInbox::InboxWritable qw(eml_from_path);
+use PublicInbox::OnDestroy;
 
 sub input_eml_cb { # used by PublicInbox::LeiInput::input_fh
         my ($self, $eml) = @_;
@@ -49,7 +49,7 @@ sub ipc_atfork_child {
         PublicInbox::LeiInput::input_only_atfork_child($self);
         $self->{lse} = $self->{lei}->{sto}->search;
         # this goes out-of-scope at worker process exit:
-        PublicInbox::OnDestroy->new($$, \&note_unimported, $self);
+        on_destroy \&note_unimported, $self;
 }
 
 # Workaround bash word-splitting s to ['kw', ':', 'keyword' ...]
diff --git a/lib/PublicInbox/Lock.pm b/lib/PublicInbox/Lock.pm
index 2a5a0f30..7162d80e 100644
--- a/lib/PublicInbox/Lock.pm
+++ b/lib/PublicInbox/Lock.pm
@@ -43,7 +43,7 @@ sub lock_release {
 sub lock_for_scope {
         my ($self) = @_;
         lock_acquire($self) or return; # lock_path not set
-        PublicInbox::OnDestroy->new(\&lock_release, $self);
+        on_destroy \&lock_release, $self;
 }
 
 sub lock_acquire_fast {
@@ -60,7 +60,7 @@ sub lock_release_fast {
 sub lock_for_scope_fast {
         my ($self) = @_;
         lock_acquire_fast($self) or return; # lock_path not set
-        PublicInbox::OnDestroy->new(\&lock_release_fast, $self);
+        on_destroy \&lock_release_fast, $self;
 }
 
 1;
diff --git a/lib/PublicInbox/MHreader.pm b/lib/PublicInbox/MHreader.pm
index 3e7bbd5c..16e505a2 100644
--- a/lib/PublicInbox/MHreader.pm
+++ b/lib/PublicInbox/MHreader.pm
@@ -52,7 +52,7 @@ EOM
 sub mh_each_file {
         my ($self, $efcb, @arg) = @_;
         opendir(my $dh, my $dir = $self->{dir});
-        my $restore = PublicInbox::OnDestroy->new($$, \&chdir, $self->{cwdfh});
+        my $restore = on_destroy \&chdir, $self->{cwdfh};
         chdir($dh);
         my $sort = $self->{sort};
         if (defined $sort && "@$sort" ne 'none') {
@@ -96,7 +96,7 @@ sub mh_each_eml {
 
 sub mh_read_one {
         my ($self, $n, $ucb, @arg) = @_;
-        my $restore = PublicInbox::OnDestroy->new($$, \&chdir, $self->{cwdfh});
+        my $restore = on_destroy \&chdir, $self->{cwdfh};
         chdir(my $dir = $self->{dir});
         _file2eml($dir, $n, $self, $ucb, @arg);
 }
diff --git a/lib/PublicInbox/MboxLock.pm b/lib/PublicInbox/MboxLock.pm
index 9d7d4a32..5e373873 100644
--- a/lib/PublicInbox/MboxLock.pm
+++ b/lib/PublicInbox/MboxLock.pm
@@ -4,7 +4,7 @@
 # Various mbox locking methods
 package PublicInbox::MboxLock;
 use v5.12;
-use PublicInbox::OnDestroy;
+use PublicInbox::OnDestroy ();
 use Fcntl qw(:flock F_SETLK F_SETLKW F_RDLCK F_WRLCK
                         O_CREAT O_EXCL O_WRONLY SEEK_SET);
 use Carp qw(croak);
@@ -122,10 +122,10 @@ sub acq {
 sub DESTROY {
         my ($self) = @_;
         my $f = $self->{".lock$$"} or return;
-        my $x;
+        my $od;
         if (my $dh = delete $self->{dh}) {
                 opendir my $c, '.';
-                $x = PublicInbox::OnDestroy->new(\&chdir, $c);
+                $od = PublicInbox::OnDestroy::all \&chdir, $c;
                 chdir($dh);
         }
         CORE::unlink($f) or die "unlink($f): $! (lock stolen?)";
diff --git a/lib/PublicInbox/OnDestroy.pm b/lib/PublicInbox/OnDestroy.pm
index d9a6cd24..4301edff 100644
--- a/lib/PublicInbox/OnDestroy.pm
+++ b/lib/PublicInbox/OnDestroy.pm
@@ -3,22 +3,29 @@
 
 package PublicInbox::OnDestroy;
 use v5.12;
+use parent qw(Exporter);
+use autodie qw(fork);
+our @EXPORT = qw(on_destroy);
+our $fork_gen = 0;
 
-sub new {
-        shift; # ($class, $cb, @args)
-        bless [ @_ ], __PACKAGE__;
+# either parent or child is expected to exit or exec shortly after this:
+sub fork_tmp () {
+        my $pid = fork;
+        ++$fork_gen if $pid == 0;
+        $pid;
 }
 
+# all children
+sub all (@) { bless [ undef, @_ ], __PACKAGE__ }
+
+# same process
+sub on_destroy (@) { bless [ $fork_gen, @_ ], __PACKAGE__ }
+
 sub cancel { @{$_[0]} = () }
 
 sub DESTROY {
-        my ($cb, @args) = @{$_[0]};
-        if (!ref($cb) && $cb) {
-                my $pid = $cb;
-                return if $pid != $$;
-                $cb = shift @args;
-        }
-        $cb->(@args) if $cb;
+        my ($fgen, $cb, @args) = @{$_[0]};
+        $cb->(@args) if ($cb && ($fgen // $fork_gen) == $fork_gen);
 }
 
 1;
diff --git a/lib/PublicInbox/SearchIdxShard.pm b/lib/PublicInbox/SearchIdxShard.pm
index 1630eb4a..ea261bda 100644
--- a/lib/PublicInbox/SearchIdxShard.pm
+++ b/lib/PublicInbox/SearchIdxShard.pm
@@ -45,7 +45,7 @@ sub ipc_atfork_child { # called automatically before ipc_worker_loop
         $v2w->{current_info} = "[$self->{shard}]"; # for $SIG{__WARN__}
         $self->begin_txn_lazy;
         # caller (ipc_worker_spawn) must capture this:
-        PublicInbox::OnDestroy->new($$, \&_worker_done, $self);
+        on_destroy \&_worker_done, $self;
 }
 
 sub index_eml {
diff --git a/lib/PublicInbox/SpawnPP.pm b/lib/PublicInbox/SpawnPP.pm
index f89d37d4..9ad4d0a1 100644
--- a/lib/PublicInbox/SpawnPP.pm
+++ b/lib/PublicInbox/SpawnPP.pm
@@ -7,7 +7,8 @@
 package PublicInbox::SpawnPP;
 use v5.12;
 use POSIX qw(dup2 _exit setpgid :signal_h);
-use autodie qw(chdir close fork pipe);
+use autodie qw(chdir close pipe);
+use PublicInbox::OnDestroy;
 # this is loaded by PublicInbox::Spawn, so we can't use/require it, here
 
 # Pure Perl implementation for folks that do not use Inline::C
@@ -22,7 +23,7 @@ sub pi_fork_exec ($$$$$$$) {
         }
         sigprocmask(SIG_SETMASK, $set, $old) or die "SIG_SETMASK(set): $!";
         pipe(my $r, my $w);
-        my $pid = fork;
+        my $pid = PublicInbox::OnDestroy::fork_tmp;
         if ($pid == 0) {
                 close $r;
                 $SIG{__DIE__} = sub {
diff --git a/lib/PublicInbox/TestCommon.pm b/lib/PublicInbox/TestCommon.pm
index 5f159683..a7ec9b5b 100644
--- a/lib/PublicInbox/TestCommon.pm
+++ b/lib/PublicInbox/TestCommon.pm
@@ -588,9 +588,9 @@ sub start_script {
         require PublicInbox::DS;
         my $oset = PublicInbox::DS::block_signals();
         require PublicInbox::OnDestroy;
-        my $tmp_mask = PublicInbox::OnDestroy->new(
+        my $tmp_mask = PublicInbox::OnDestroy::all(
                                         \&PublicInbox::DS::sig_setmask, $oset);
-        my $pid = PublicInbox::DS::do_fork();
+        my $pid = PublicInbox::DS::fork_persist();
         if ($pid == 0) {
                 close($_) for (@{delete($opt->{-CLOFORK}) // []});
                 # pretend to be systemd (cf. sd_listen_fds(3))
diff --git a/lib/PublicInbox/Umask.pm b/lib/PublicInbox/Umask.pm
index 00772ce5..2c859e65 100644
--- a/lib/PublicInbox/Umask.pm
+++ b/lib/PublicInbox/Umask.pm
@@ -58,7 +58,7 @@ sub _umask_for {
 sub with_umask {
         my ($self, $cb, @arg) = @_;
         my $old = umask($self->{umask} //= umask_prepare($self));
-        my $restore = PublicInbox::OnDestroy->new($$, \&CORE::umask, $old);
+        my $restore = on_destroy \&CORE::umask, $old;
         $cb ? $cb->(@arg) : $restore;
 }
 
diff --git a/lib/PublicInbox/ViewVCS.pm b/lib/PublicInbox/ViewVCS.pm
index 790b9a2c..f47c2703 100644
--- a/lib/PublicInbox/ViewVCS.pm
+++ b/lib/PublicInbox/ViewVCS.pm
@@ -25,6 +25,7 @@ use PublicInbox::Tmpfile;
 use PublicInbox::ViewDiff qw(flush_diff uri_escape_path);
 use PublicInbox::View;
 use PublicInbox::Eml;
+use PublicInbox::OnDestroy;
 use Text::Wrap qw(wrap);
 use PublicInbox::Hval qw(ascii_html to_filename prurl utf8_maybe);
 use POSIX qw(strftime);
@@ -388,7 +389,7 @@ sub show_commit ($$) {
                         qw(--encoding=UTF-8 -z --no-notes --no-patch), $oid),
                         undef, { 1 => $ctx->{patch_fh} });
         $qsp_h->{qsp_err} = \($ctx->{-qsp_err_h} = '');
-        my $cmt_fin = PublicInbox::OnDestroy->new($$, \&cmt_fin, $ctx);
+        my $cmt_fin = on_destroy \&cmt_fin, $ctx;
         $ctx->{git} = $git;
         $ctx->{oid} = $oid;
         $qsp_h->psgi_qx($ctx->{env}, undef, \&cmt_hdr_prep, $ctx, $cmt_fin);
@@ -624,7 +625,7 @@ sub start_solver ($) {
                 my $v = $ctx->{qp}->{$from} // next;
                 $ctx->{hints}->{$to} = $v if $v ne '';
         }
-        $ctx->{-next_solver} = PublicInbox::OnDestroy->new($$, \&next_solver);
+        $ctx->{-next_solver} = on_destroy \&next_solver;
         ++$solver_nr;
         $ctx->{-tmp} = File::Temp->newdir("solver.$ctx->{oid_b}-XXXX",
                                                 TMPDIR => 1);
diff --git a/lib/PublicInbox/Watch.pm b/lib/PublicInbox/Watch.pm
index 1ec574ea..eb90d353 100644
--- a/lib/PublicInbox/Watch.pm
+++ b/lib/PublicInbox/Watch.pm
@@ -445,7 +445,7 @@ sub imap_idle_reap { # awaitpid callback
 sub imap_idle_fork {
         my ($self, $uri, $intvl) = @_;
         return if $self->{quit};
-        my $pid = PublicInbox::DS::do_fork;
+        my $pid = PublicInbox::DS::fork_persist;
         if ($pid == 0) {
                 watch_atfork_child($self);
                 watch_imap_idle_1($self, $uri, $intvl);
@@ -506,7 +506,7 @@ sub poll_fetch_fork { # DS::add_timer callback
         my @imap = grep { # push() always returns > 0
                 $_->scheme =~ m!\Aimaps?!i ? 1 : (push(@nntp, $_) < 0)
         } @$uris;
-        my $pid = PublicInbox::DS::do_fork;
+        my $pid = PublicInbox::DS::fork_persist;
         if ($pid == 0) {
                 watch_atfork_child($self);
                 watch_imap_fetch_all($self, \@imap) if @imap;
diff --git a/lib/PublicInbox/WwwCoderepo.pm b/lib/PublicInbox/WwwCoderepo.pm
index 61aa7862..a5e2dc4a 100644
--- a/lib/PublicInbox/WwwCoderepo.pm
+++ b/lib/PublicInbox/WwwCoderepo.pm
@@ -253,7 +253,7 @@ sub summary ($$) {
         push(@log, $tip) if defined $tip;
 
         # limit scope for MockHTTP test (t/solver_git.t)
-        my $END = PublicInbox::OnDestroy->new($$, \&summary_END, $ctx);
+        my $END = on_destroy \&summary_END, $ctx;
         for (['log', \@log],
                  [ 'heads', [@EACH_REF, "--count=$nb", 'refs/heads'] ],
                  [ 'tags', [@EACH_REF, "--count=$nt", 'refs/tags'] ]) {
diff --git a/lib/PublicInbox/XapClient.pm b/lib/PublicInbox/XapClient.pm
index 4dcbbe5d..98034130 100644
--- a/lib/PublicInbox/XapClient.pm
+++ b/lib/PublicInbox/XapClient.pm
@@ -11,7 +11,7 @@ use v5.12;
 use PublicInbox::Spawn qw(spawn);
 use Socket qw(AF_UNIX SOCK_SEQPACKET);
 use PublicInbox::IPC;
-use autodie qw(fork pipe socketpair);
+use autodie qw(pipe socketpair);
 
 sub mkreq {
         my ($self, $ios, @arg) = @_;
diff --git a/lib/PublicInbox/XapHelper.pm b/lib/PublicInbox/XapHelper.pm
index ed11a2f8..8c7732f5 100644
--- a/lib/PublicInbox/XapHelper.pm
+++ b/lib/PublicInbox/XapHelper.pm
@@ -244,7 +244,7 @@ sub reap_worker { # awaitpid CB
 
 sub start_worker ($) {
         my ($nr) = @_;
-        my $pid = eval { PublicInbox::DS::do_fork } // return(warn($@));
+        my $pid = eval { PublicInbox::DS::fork_persist } // return(warn($@));
         if ($pid == 0) {
                 undef %WORKERS;
                 $SIG{TTIN} = $SIG{TTOU} = 'IGNORE';
diff --git a/lib/PublicInbox/Xapcmd.pm b/lib/PublicInbox/Xapcmd.pm
index 69f0af43..9a148ae4 100644
--- a/lib/PublicInbox/Xapcmd.pm
+++ b/lib/PublicInbox/Xapcmd.pm
@@ -103,7 +103,7 @@ sub commit_changes ($$$$) {
 
 sub cb_spawn {
         my ($cb, $args, $opt) = @_; # $cb = cpdb() or compact()
-        my $pid = PublicInbox::DS::do_fork;
+        my $pid = PublicInbox::DS::fork_persist;
         return $pid if $pid > 0;
         $SIG{__DIE__} = sub { warn @_; _exit(1) }; # don't jump up stack
         $cb->($args, $opt);
diff --git a/script/public-inbox-init b/script/public-inbox-init
index 8915cf31..cf6443f7 100755
--- a/script/public-inbox-init
+++ b/script/public-inbox-init
@@ -122,7 +122,7 @@ sysopen($lockfh, $lockfile, O_RDWR|O_CREAT|O_EXCL) or do {
         exit(255);
 };
 require PublicInbox::OnDestroy;
-my $auto_unlink = PublicInbox::OnDestroy->new($$, sub { unlink $lockfile });
+my $auto_unlink = PublicInbox::OnDestroy::on_destroy(sub { unlink $lockfile });
 my $perm = 0644 & ~umask;
 my %seen;
 if (-e $pi_config) {
diff --git a/t/lei-sigpipe.t b/t/lei-sigpipe.t
index 72bc6c7d..b9fd88a6 100644
--- a/t/lei-sigpipe.t
+++ b/t/lei-sigpipe.t
@@ -26,9 +26,7 @@ SKIP: {
 # https://public-inbox.org/meta/20220227080422.gyqowrxomzu6gyin@sourcephile.fr/
 my $oldSIGPIPE = $SIG{PIPE};
 $SIG{PIPE} = 'DEFAULT';
-my $cleanup = PublicInbox::OnDestroy->new($$, sub {
-        $SIG{PIPE} = $oldSIGPIPE;
-});
+my $cleanup = on_destroy(sub { $SIG{PIPE} = $oldSIGPIPE });
 
 test_lei(sub {
         my $f = "$ENV{HOME}/big.eml";
diff --git a/t/mbox_lock.t b/t/mbox_lock.t
index c2fee0d4..1fc828aa 100644
--- a/t/mbox_lock.t
+++ b/t/mbox_lock.t
@@ -2,6 +2,7 @@
 # Copyright (C) 2021 all contributors <meta@public-inbox.org>
 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
 use strict; use v5.10.1; use PublicInbox::TestCommon;
+use autodie qw(chdir);
 use POSIX qw(_exit);
 use PublicInbox::DS qw(now);
 use Errno qw(EAGAIN);
@@ -18,11 +19,11 @@ ok(!-f "$f.lock", 'no dotlock with none');
 undef $mbl;
 {
         opendir my $cur, '.' or BAIL_OUT $!;
-        my $od = PublicInbox::OnDestroy->new(sub { chdir $cur });
-        chdir $tmpdir or BAIL_OUT;
+        my $od = on_destroy \&chdir, $cur;
+        chdir $tmpdir;
         my $abs = "$tmpdir/rel.lock";
         my $rel = PublicInbox::MboxLock->acq('rel', 1, ['dotlock']);
-        chdir '/' or BAIL_OUT;
+        chdir '/';
         ok(-f $abs, 'lock with abs path created');
         undef $rel;
         ok(!-f $abs, 'lock gone despite being in the wrong dir');
diff --git a/t/mh_reader.t b/t/mh_reader.t
index c81df32e..95a7be4a 100644
--- a/t/mh_reader.t
+++ b/t/mh_reader.t
@@ -5,7 +5,6 @@ use PublicInbox::TestCommon;
 require_ok 'PublicInbox::MHreader';
 use PublicInbox::IO qw(write_file);
 use PublicInbox::Lock;
-use PublicInbox::OnDestroy;
 use PublicInbox::Eml;
 use File::Path qw(remove_tree);
 use autodie;
diff --git a/t/on_destroy.t b/t/on_destroy.t
index e7945100..e8fdf35e 100644
--- a/t/on_destroy.t
+++ b/t/on_destroy.t
@@ -1,37 +1,44 @@
 #!perl -w
 use v5.12;
 use Test::More;
-require_ok 'PublicInbox::OnDestroy';
+use PublicInbox::OnDestroy;
+use POSIX qw(_exit);
 my @x;
-my $od = PublicInbox::OnDestroy->new(sub { push @x, 'hi' });
+my $od = on_destroy sub { push @x, 'hi' };
 is_deeply(\@x, [], 'not called, yet');
 undef $od;
 is_deeply(\@x, [ 'hi' ], 'no args works');
-$od = PublicInbox::OnDestroy->new(sub { $x[0] = $_[0] }, 'bye');
+$od = on_destroy sub { $x[0] = $_[0] }, 'bye';
 is_deeply(\@x, [ 'hi' ], 'nothing changed while alive');
 undef $od;
 is_deeply(\@x, [ 'bye' ], 'arg passed');
-$od = PublicInbox::OnDestroy->new(sub { @x = @_ }, qw(x y));
+$od = on_destroy sub { @x = @_ }, qw(x y);
 undef $od;
 is_deeply(\@x, [ 'x', 'y' ], '2 args passed');
 
 open my $tmp, '+>>', undef or BAIL_OUT $!;
 $tmp->autoflush(1);
-$od = PublicInbox::OnDestroy->new(1, sub { print $tmp "$$ DESTROY\n" });
-undef $od;
+$od = on_destroy sub { print $tmp "$$ DESTROY\n" };
+my $pid = PublicInbox::OnDestroy::fork_tmp;
+if ($pid == 0) { undef $od; _exit 0; };
+waitpid($pid, 0);
+is $?, 0, 'test process exited';
 is(-s $tmp, 0, '$tmp is empty on pid mismatch');
-$od = PublicInbox::OnDestroy->new($$, sub { $tmp = $$ });
+$od->cancel;
+undef $od;
+is(-s $tmp, 0, '$tmp is empty after ->cancel');
+$od = on_destroy sub { $tmp = $$ };
 undef $od;
 is($tmp, $$, '$tmp set to $$ by callback');
 
-$od = PublicInbox::OnDestroy->new($$, sub { $tmp = 'foo' });
+$od = on_destroy sub { $tmp = 'foo' };
 $od->cancel;
 $od = undef;
 isnt($tmp, 'foo', '->cancel');
 
 if (my $nr = $ENV{TEST_LEAK_NR}) {
         for (0..$nr) {
-                $od = PublicInbox::OnDestroy->new(sub { @x = @_ }, qw(x y));
+                $od = on_destroy sub { @x = @_ }, qw(x y);
         }
 }
 
diff --git a/xt/net_writer-imap.t b/xt/net_writer-imap.t
index f7796e8e..176502ba 100644
--- a/xt/net_writer-imap.t
+++ b/xt/net_writer-imap.t
@@ -82,7 +82,7 @@ my $mics = do {
         $nwr->imap_common_init;
 };
 my $mic = (values %$mics)[0];
-my $cleanup = PublicInbox::OnDestroy->new($$, sub {
+my $cleanup = on_destroy sub {
         if (defined($folder)) {
                 my $mic = $nwr->mic_get($uri);
                 $mic->delete($folder) or
@@ -92,7 +92,7 @@ my $cleanup = PublicInbox::OnDestroy->new($$, sub {
                 local $ENV{HOME} = $tmpdir;
                 system(qw(git credential-cache exit));
         }
-});
+};
 my $imap_append = $nwr->can('imap_append');
 my $smsg = bless { kw => [ 'seen' ] }, 'PublicInbox::Smsg';
 $imap_append->($mic, $folder, undef, $smsg, eml_load('t/plack-qp.eml'));