]> Sergey Matveev's repositories - public-inbox.git/commitdiff
lei: drop $SIG{__DIE__}, add oneshot fallbacks
authorEric Wong <e@80x24.org>
Thu, 17 Dec 2020 23:54:04 +0000 (23:54 +0000)
committerEric Wong <e@80x24.org>
Sat, 19 Dec 2020 09:32:08 +0000 (09:32 +0000)
We'll force stdout+stderr to be a pipe the spawning client
controls, thus there's no need to lose error reporting by
prematurely redirecting stdout+stderr to /dev/null.

We can now rely exclusively on OnDestroy to write to syslog() on
uncaught die failures.

Also support falling back to oneshot mode on socket and cwd
failures, since some commands may still be useful if the current
working directory goes missing :P

lib/PublicInbox/LEI.pm
script/lei
t/lei.t

index 95b48095ef6af108d67aad827d5acb6583f2482f..fd412324be3b9d3eefbc73f983a0e11938fcd79f 100644 (file)
@@ -12,7 +12,7 @@ use parent qw(PublicInbox::DS);
 use Getopt::Long ();
 use Socket qw(AF_UNIX SOCK_STREAM pack_sockaddr_un);
 use Errno qw(EAGAIN ECONNREFUSED ENOENT);
-use POSIX qw(setsid);
+use POSIX ();
 use IO::Handle ();
 use Sys::Syslog qw(syslog openlog);
 use PublicInbox::Config;
@@ -584,60 +584,44 @@ sub noop {}
 
 # lei(1) calls this when it can't connect
 sub lazy_start {
-       my ($path, $err) = @_;
-       require IO::FDPass; # require this early so caller sees it
-       if ($err == ECONNREFUSED) {
+       my ($path, $errno) = @_;
+       if ($errno == ECONNREFUSED) {
                unlink($path) or die "unlink($path): $!";
-       } elsif ($err != ENOENT) {
-               $! = $err; # allow interpolation to stringify in die
+       } elsif ($errno != ENOENT) {
+               $! = $errno; # allow interpolation to stringify in die
                die "connect($path): $!";
        }
        umask(077) // die("umask(077): $!");
        socket(my $l, AF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
        bind($l, pack_sockaddr_un($path)) or die "bind($path): $!";
-       listen($l, 1024) or die "listen $!";
+       listen($l, 1024) or die "listen: $!";
        my @st = stat($path) or die "stat($path): $!";
        my $dev_ino_expect = pack('dd', $st[0], $st[1]); # dev+ino
        pipe(my ($eof_r, $eof_w)) or die "pipe: $!";
        my $oldset = PublicInbox::Sigfd::block_signals();
-       my $pid = fork // die "fork: $!";
-       return if $pid;
+       require IO::FDPass;
        require PublicInbox::Listener;
        require PublicInbox::EOFpipe;
-       openlog($path, 'pid', 'user');
-       local $SIG{__DIE__} = sub {
-               syslog('crit', "@_");
-               die; # calls the default __DIE__ handler
-       };
-       local $SIG{__WARN__} = sub { syslog('warning', "@_") };
-       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";
-       setsid();
-       $pid = fork // die "fork: $!";
+       (-p STDOUT && -p STDERR) or die "E: stdout+stderr must be pipes\n";
+       open(STDIN, '+<', '/dev/null') or die "redirect stdin failed: $!";
+       POSIX::setsid() > 0 or die "setsid: $!";
+       my $pid = fork // die "fork: $!";
        return if $pid;
-       $SIG{__DIE__} = 'DEFAULT';
-       my $on_destroy = PublicInbox::OnDestroy->new(sub {
-               my ($owner_pid) = @_;
-               syslog('crit', "$@") if $@ && $$ == $owner_pid;
-       }, $$);
        $0 = "lei-daemon $path";
        local %PATH2CFG;
-       $l->blocking(0);
-       $eof_w->blocking(0);
-       $eof_r->blocking(0);
-       my $listener = PublicInbox::Listener->new($l, \&accept_dispatch, $l);
+       $_->blocking(0) for ($l, $eof_r, $eof_w);
+       $l = PublicInbox::Listener->new($l, \&accept_dispatch, $l);
        my $exit_code;
        local $quit = sub {
                $exit_code //= shift;
-               my $tmp = $listener or exit($exit_code);
+               my $listener = $l or exit($exit_code);
                unlink($path) if defined($path);
-               syswrite($eof_w, '.');
-               $l = $listener = $path = undef;
-               $tmp->close if $tmp; # DS::close
+               # closing eof_w triggers \&noop wakeup
+               $eof_w = $l = $path = undef;
+               $listener->close; # DS::close
                PublicInbox::DS->SetLoopTimeout(1000);
        };
-       PublicInbox::EOFpipe->new($eof_r, sub {}, undef);
+       PublicInbox::EOFpipe->new($eof_r, \&noop, undef);
        my $sig = {
                CHLD => \&PublicInbox::DS::enqueue_reap,
                QUIT => $quit,
@@ -682,8 +666,21 @@ sub lazy_start {
                }
                $n; # true: continue, false: stop
        });
+
+       # STDIN was redirected to /dev/null above, closing STDOUT and
+       # STDERR will cause the calling `lei' client process to finish
+       # reading <$daemon> pipe.
+       open STDOUT, '>&STDIN' or die "redirect stdout failed: $!";
+       openlog($path, 'pid', 'user');
+       local $SIG{__WARN__} = sub { syslog('warning', "@_") };
+       my $owner_pid = $$;
+       my $on_destroy = PublicInbox::OnDestroy->new(sub {
+               syslog('crit', "$@") if $@ && $$ == $owner_pid;
+       });
+       open STDERR, '>&STDIN' or die "redirect stderr failed: $!";
+       # $daemon pipe to `lei' closed, main loop begins:
        PublicInbox::DS->EventLoop;
-       $@ = undef if $on_destroy; # quiet OnDestroy if we got here
+       @$on_destroy = (); # cancel on_destroy if we get here
        exit($exit_code // 0);
 }
 
index 2b041fb4636624e4b81af3aa3c5ef4576ac00834..ceaf1e004666474308c37c34d59e2719941e3c0c 100755 (executable)
@@ -4,8 +4,8 @@
 use strict;
 use v5.10.1;
 use Socket qw(AF_UNIX SOCK_STREAM pack_sockaddr_un);
-
-if (eval { require IO::FDPass; 1 }) { # use daemon to reduce load time
+if (my ($sock, $pwd) = eval {
+       require IO::FDPass; # will try to use a daemon to reduce load time
        my $path = do {
                my $runtime_dir = ($ENV{XDG_RUNTIME_DIR} // '') . '/lei';
                if ($runtime_dir eq '/lei') {
@@ -21,32 +21,41 @@ if (eval { require IO::FDPass; 1 }) { # use daemon to reduce load time
        my $addr = pack_sockaddr_un($path);
        socket(my $sock, AF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
        unless (connect($sock, $addr)) { # start the daemon if not started
-               my $err = $! + 0;
-               my $env = { PERL5LIB => join(':', @INC) };
                my $cmd = [ $^X, qw[-MPublicInbox::LEI
                        -E PublicInbox::LEI::lazy_start(@ARGV)],
-                       $path, $err ];
+                       $path, $! + 0 ];
+               my $env = { PERL5LIB => join(':', @INC) };
+               pipe(my ($daemon, $w)) or die "pipe: $!";
+               my $opt = { 1 => $w, 2 => $w };
                require PublicInbox::Spawn;
-               waitpid(PublicInbox::Spawn::spawn($cmd, $env), 0);
-               warn "lei-daemon exited with \$?=$?\n" if $?;
+               my $pid = PublicInbox::Spawn::spawn($cmd, $env, $opt);
+               $opt = $w = undef;
+               while (<$daemon>) { warn $_ } # EOF when STDERR is redirected
+               waitpid($pid, 0) or warn <<"";
+lei-daemon could not start, PID:$pid exited with \$?=$?
 
                # try connecting again anyways, unlink+bind may be racy
-               connect($sock, $addr) or die
-                       "connect($path): $! (after attempted daemon start)";
+               unless (connect($sock, $addr)) {
+                       die <<"";
+connect($path): $! (after attempted daemon start)
+Falling back to (slow) one-shot mode
+
+               }
        }
        require Cwd;
-       my $cwd = Cwd::fastcwd() // die "fastcwd: $!";
+       my $cwd = Cwd::fastcwd() // die "fastcwd(PWD=".($ENV{PWD}//'').": $!";
        my $pwd = $ENV{PWD} // '';
-       if ($pwd eq $cwd) { # likely, all good
-       } elsif ($pwd) { # prefer ENV{PWD} if it's a symlink to real cwd
-               my @st_cwd = stat($cwd) or die "stat(cwd=$cwd): $!\n";
-               my @st_pwd = stat($pwd);
+       if ($pwd ne $cwd) { # prefer ENV{PWD} if it's a symlink to real cwd
+               my @st_cwd = stat($cwd) or die "stat(cwd=$cwd): $!";
+               my @st_pwd = stat($pwd); # PWD invalid, use cwd
                # make sure st_dev/st_ino match for {PWD} to be valid
                $pwd = $cwd if (!@st_pwd || $st_pwd[1] != $st_cwd[1] ||
                                        $st_pwd[0] != $st_cwd[0]);
        } else {
                $pwd = $cwd;
        }
+       ($sock, $pwd);
+}) { # IO::FDPass, $sock, $pwd are all available:
        local $ENV{PWD} = $pwd;
        my $buf = "$$\0\0>" . join("]\0[", @ARGV) . "\0\0>";
        while (my ($k, $v) = each %ENV) { $buf .= "$k=$v\0" }
@@ -60,6 +69,8 @@ if (eval { require IO::FDPass; 1 }) { # use daemon to reduce load time
                die $buf;
        }
 } else { # for systems lacking IO::FDPass
+       # don't warn about IO::FDPass since it's not commonly installed
+       warn $@ if $@ && index($@, 'IO::FDPass') < 0;
        require PublicInbox::LEI;
        PublicInbox::LEI::oneshot(__PACKAGE__);
 }
diff --git a/t/lei.t b/t/lei.t
index bdf6cc1c73731f784f877474250ce10041901cde..cce90fff89c62c5bbaf5d30d03db24a65844de21 100644 (file)
--- a/t/lei.t
+++ b/t/lei.t
@@ -127,7 +127,7 @@ my $test_lei_common = sub {
 my $test_lei_oneshot = $ENV{TEST_LEI_ONESHOT};
 SKIP: {
        last SKIP if $test_lei_oneshot;
-       require_mods('IO::FDPass', 16);
+       require_mods(qw(IO::FDPass Cwd), 41);
        my $sock = "$ENV{XDG_RUNTIME_DIR}/lei/sock";
 
        ok(run_script([qw(lei daemon-pid)], undef, $opt), 'daemon-pid');
@@ -188,7 +188,34 @@ SKIP: {
        chomp(my $new_pid = $out);
        ok(kill(0, $new_pid), 'new pid is running');
        ok(-S $sock, 'sock exists again');
-       unlink $sock or BAIL_OUT "unlink $!";
+
+       if ('socket inaccessible') {
+               chmod 0000, $sock or BAIL_OUT "chmod 0000: $!";
+               $out = $err = '';
+               ok(run_script([qw(lei help)], undef, $opt),
+                       'connect fail, one-shot fallback works');
+               like($err, qr/\bconnect\(/, 'connect error noted');
+               like($out, qr/^usage: /, 'help output works');
+               chmod 0700, $sock or BAIL_OUT "chmod 0700: $!";
+       }
+       if ('oneshot on cwd gone') {
+               my $cwd = Cwd::fastcwd() or BAIL_OUT "fastcwd: $!";
+               my $d = "$home/to-be-removed";
+               mkdir $d or BAIL_OUT "mkdir($d) $!";
+               chdir $d or BAIL_OUT "chdir($d) $!";
+               if (rmdir($d)) {
+                       $out = $err = '';
+                       ok(run_script([qw(lei help)], undef, $opt),
+                               'cwd fail, one-shot fallback works');
+               } else {
+                       $err = "rmdir=$!";
+               }
+               chdir $cwd or BAIL_OUT "chdir($cwd) $!";
+               like($err, qr/cwd\(/, 'cwd error noted');
+               like($out, qr/^usage: /, 'help output still works');
+       }
+
+       unlink $sock or BAIL_OUT "unlink($sock) $!";
        for (0..100) {
                kill('CHLD', $new_pid) or last;
                tick();