X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=t%2Fhttpd-unix.t;h=fe4a21616373a682d71020a6be26e7b10b6f76fe;hb=refs%2Fheads%2Fmaster;hp=4b0f116e8678595ac2a1012e7307d983b32cb610;hpb=279955959d2242dfcb1081656806aa1426085bcd;p=public-inbox.git diff --git a/t/httpd-unix.t b/t/httpd-unix.t index 4b0f116e..414ca0c8 100644 --- a/t/httpd-unix.t +++ b/t/httpd-unix.t @@ -1,38 +1,37 @@ -# Copyright (C) 2016 all contributors +#!perl -w +# Copyright (C) all contributors # License: AGPL-3.0+ # Tests for binding Unix domain sockets use strict; -use warnings; use Test::More; - -foreach my $mod (qw(Plack::Util Plack::Builder Danga::Socket - HTTP::Date HTTP::Status)) { - eval "require $mod"; - plan skip_all => "$mod missing for httpd-unix.t" if $@; -} - -use File::Temp qw/tempdir/; +use PublicInbox::TestCommon; +use Errno qw(EADDRINUSE); +use Cwd qw(abs_path); +use Carp qw(croak); +use Fcntl qw(FD_CLOEXEC F_SETFD); +require_mods(qw(Plack::Util Plack::Builder HTTP::Date HTTP::Status)); use IO::Socket::UNIX; -use Cwd qw/getcwd/; -use Fcntl qw(FD_CLOEXEC F_SETFD F_GETFD :seek); -my $tmpdir = tempdir('httpd-unix-XXXXXX', TMPDIR => 1, CLEANUP => 1); +use POSIX qw(mkfifo); +my ($tmpdir, $for_destroy) = tmpdir(); my $unix = "$tmpdir/unix.sock"; -my $httpd = 'blib/script/public-inbox-httpd'; -my $psgi = getcwd() . '/t/httpd-corner.psgi'; +my $psgi = './t/httpd-corner.psgi'; my $out = "$tmpdir/out.log"; my $err = "$tmpdir/err.log"; +my $td; -my $pid; -END { kill 'TERM', $pid if defined $pid }; +my $register_exit_fifo = sub { + my ($s, $f) = @_; + my $sock = new_sock($s); + ok($sock->write("GET /exit-fifo$f HTTP/1.0\r\n\r\n"), + 'request exit-fifo'); + ok($sock->read(my $buf, 4096), 'read exit-fifo response'); + like($buf, qr!\r\n\r\nfifo \Q$f\E registered\z!, 'set exit fifo'); +}; my $spawn_httpd = sub { my (@args) = @_; - $pid = fork; - if ($pid == 0) { - exec $httpd, @args, "--stdout=$out", "--stderr=$err", $psgi; - die "FAIL: $!\n"; - } - ok(defined $pid, 'forked httpd process successfully'); + my $cmd = [ '-httpd', @args, "--stdout=$out", "--stderr=$err", $psgi ]; + $td = start_script($cmd); }; { @@ -44,37 +43,46 @@ my $spawn_httpd = sub { } ok(!-S $unix, 'UNIX socket does not exist, yet'); -$spawn_httpd->("-l$unix"); -for (1..1000) { - last if -S $unix; - select undef, undef, undef, 0.02 +my $f1 = "$tmpdir/f1"; +mkfifo($f1, 0600); +{ + local $ENV{TEST_OPEN_FIFO} = $f1; + $spawn_httpd->("-l$unix", '-W0'); + open my $fh, '<', $f1 or xbail "open($f1): $!"; + is(my $hi = <$fh>, "hi\n", 'got FIFO greeting'); } - ok(-S $unix, 'UNIX socket was bound by -httpd'); + +sub new_sock ($) { + IO::Socket::UNIX->new(Peer => $_[0], Type => SOCK_STREAM) + // xbail "E: $! connecting to $_[0]"; +} + sub check_sock ($) { my ($unix) = @_; - my $sock = IO::Socket::UNIX->new(Peer => $unix, Type => SOCK_STREAM); - warn "E: $! connecting to $unix\n" unless defined $sock; - ok($sock, 'client UNIX socket connected'); + my $sock = new_sock($unix); ok($sock->write("GET /host-port HTTP/1.0\r\n\r\n"), 'wrote req to server'); ok($sock->read(my $buf, 4096), 'read response'); - like($buf, qr!\r\n\r\n127\.0\.0\.1:0\z!, + like($buf, qr!\r\n\r\n127\.0\.0\.1 0\z!, 'set REMOTE_ADDR and REMOTE_PORT for Unix socket'); } check_sock($unix); { # do not clobber existing socket - my $fpid = fork; - if ($fpid == 0) { - open STDOUT, '>>', "$tmpdir/1" or die "redirect failed: $!"; - open STDERR, '>>', "$tmpdir/2" or die "redirect failed: $!"; - exec $httpd, '-l', $unix, '-W0', $psgi; - die "FAIL: $!\n"; - } - is($fpid, waitpid($fpid, 0), 'second httpd exits'); - isnt($?, 0, 'httpd failed with failure to bind'); + my %err = ( 'linux' => EADDRINUSE, 'freebsd' => EADDRINUSE ); + open my $out, '>>', "$tmpdir/1" or die "redirect failed: $!"; + open my $err, '>>', "$tmpdir/2" or die "redirect failed: $!"; + my $cmd = ['-httpd', '-l', $unix, '-W0', $psgi]; + my $ftd = start_script($cmd, undef, { 1 => $out, 2 => $err }); + $ftd->join; + isnt($?, 0, 'httpd failure set $?'); + SKIP: { + my $ec = $err{$^O} or + skip("not sure if $^O fails with EADDRINUSE", 1); + is($? >> 8, $ec, 'httpd failed with EADDRINUSE'); + }; open my $fh, "$tmpdir/2" or die "failed to open $tmpdir/2: $!"; local $/; my $e = <$fh>; @@ -83,37 +91,150 @@ check_sock($unix); } { - my $kpid = $pid; - $pid = undef; - is(kill('TERM', $kpid), 1, 'terminate existing process'); - is(waitpid($kpid, 0), $kpid, 'existing httpd terminated'); + is($td->kill, 1, 'terminate existing process'); + $td->join; is($?, 0, 'existing httpd exited successfully'); ok(-S $unix, 'unix socket still exists'); } +# portable Perl can delay or miss signal dispatches due to races, +# so disable some tests on systems lacking signalfd(2) or EVFILT_SIGNAL +my $has_sigfd = PublicInbox::Sigfd->new({}, 0) ? 1 : $ENV{TEST_UNRELIABLE}; + +sub delay_until { + my $cond = shift; + my $end = time + 30; + do { + return if $cond->(); + tick(0.012); + } until (time > $end); + Carp::confess('condition failed'); +} + SKIP: { - eval 'require Net::Server::Daemonize'; - skip('Net::Server missing for pid-file/daemonization test', 10) if $@; - - # wait for daemonization - $spawn_httpd->("-l$unix", '-D', '-P', "$tmpdir/pid"); - my $kpid = $pid; - $pid = undef; - is(waitpid($kpid, 0), $kpid, 'existing httpd terminated'); - check_sock($unix); - - ok(-f "$tmpdir/pid", 'pid file written'); - open my $fh, '<', "$tmpdir/pid" or die "open failed: $!"; - local $/ = "\n"; - my $rpid = <$fh>; - chomp $rpid; - like($rpid, qr/\A\d+\z/s, 'pid file looks like a pid'); - is(kill('TERM', $rpid), 1, 'signalled daemonized process'); - for (1..100) { - kill(0, $rpid) or last; - select undef, undef, undef, 0.02; + require_mods('Net::Server::Daemonize', 52); + $has_sigfd or skip('signalfd / EVFILT_SIGNAL not available', 52); + my $pid_file = "$tmpdir/pid"; + my $read_pid = sub { + my $f = shift; + open my $fh, '<', $f or die "open $f failed: $!"; + my $pid = do { local $/; <$fh> }; + chomp($pid) or die("pid file not ready $!"); + $pid; + }; + + for my $w (qw(-W0 -W1)) { + pipe(my ($p0, $p1)) or xbail "pipe: $!"; + fcntl($p1, F_SETFD, 0) or xbail "fcntl: $!"; # clear FD_CLOEXEC + # wait for daemonization + $spawn_httpd->("-l$unix", '-D', '-P', $pid_file, $w); + close $p1 or xbail "close: $!"; + $td->join; + is($?, 0, "daemonized $w process"); + check_sock($unix); + ok(-s $pid_file, "$w pid file written"); + my $pid = $read_pid->($pid_file); + is(kill('TERM', $pid), 1, "signaled daemonized $w process"); + vec(my $rvec = '', fileno($p0), 1) = 1; + delete $td->{-extra}; # drop tail(1) process + is(select($rvec, undef, undef, 1), 1, 'timeout for pipe HUP'); + is(my $undef = <$p0>, undef, 'process closed pipe writer at exit'); + ok(!-e $pid_file, "$w pid file unlinked at exit"); + } + + my $httpd = abs_path('blib/script/public-inbox-httpd'); + $psgi = abs_path($psgi); + my $opt = { run_mode => 0 }; + my @args = ("-l$unix", '-D', '-P', $pid_file, -1, $out, -2, $err); + + if ('USR2 upgrades with workers') { + pipe(my ($p0, $p1)) or xbail "pipe: $!"; + fcntl($p1, F_SETFD, 0) or xbail "fcntl: $!"; # clear FD_CLOEXEC + + $td = start_script([$httpd, @args, $psgi], undef, $opt); + close($p1) or xbail "close: $!"; + $td->join; + is($?, 0, "daemonized process again"); + check_sock($unix); + ok(-s $pid_file, 'pid file written'); + my $pid = $read_pid->($pid_file); + + # stop worker to ensure check_sock below hits $new_pid + kill('TTOU', $pid) or die "TTOU failed: $!"; + + kill('USR2', $pid) or die "USR2 failed: $!"; + delay_until(sub { + $pid != (eval { $read_pid->($pid_file) } // $pid) + }); + my $new_pid = $read_pid->($pid_file); + isnt($new_pid, $pid, 'new child started'); + ok($new_pid > 0, '$new_pid valid'); + delay_until(sub { -s "$pid_file.oldbin" }); + my $old_pid = $read_pid->("$pid_file.oldbin"); + is($old_pid, $pid, '.oldbin pid file written'); + ok($old_pid > 0, '$old_pid valid'); + + check_sock($unix); # ensures $new_pid is ready to receive signals + + # first, back out of the upgrade + kill('QUIT', $new_pid) or die "kill new PID failed: $!"; + delay_until(sub { + $pid == (eval { $read_pid->($pid_file) } // 0) + }); + is($read_pid->($pid_file), $pid, 'old PID file restored'); + ok(!-f "$pid_file.oldbin", '.oldbin PID file gone'); + + # retry USR2 upgrade + kill('USR2', $pid) or die "USR2 failed: $!"; + delay_until(sub { + $pid != (eval { $read_pid->($pid_file) } // $pid) + }); + $new_pid = $read_pid->($pid_file); + isnt($new_pid, $pid, 'new child started again'); + $old_pid = $read_pid->("$pid_file.oldbin"); + is($old_pid, $pid, '.oldbin pid file written'); + + # drop the old parent + kill('QUIT', $old_pid) or die "QUIT failed: $!"; + delay_until(sub { !kill(0, $old_pid) }); # UGH + + ok(!-f "$pid_file.oldbin", '.oldbin PID file gone'); + + # drop the new child + check_sock($unix); + kill('QUIT', $new_pid) or die "QUIT failed: $!"; + + vec(my $rvec = '', fileno($p0), 1) = 1; + is(select($rvec, undef, undef, 1), 1, 'timeout for pipe HUP'); + is(my $u = <$p0>, undef, 'process closed pipe writer at exit'); + + ok(!-f $pid_file, 'PID file is gone'); + } + + if ('try USR2 without workers (-W0)') { + pipe(my ($p0, $p1)) or xbail "pipe: $!"; + fcntl($p1, F_SETFD, 0) or xbail "fcntl: $!"; # clear FD_CLOEXEC + $td = start_script([$httpd, @args, '-W0', $psgi], undef, $opt); + close $p1 or xbail "close: $!"; + $td->join; + is($?, 0, 'daemonized w/o workers'); + $register_exit_fifo->($unix, $f1); + my $pid = $read_pid->($pid_file); + + # replace running process + kill('USR2', $pid) or xbail "USR2 failed: $!"; + open my $fh, '<', $f1 or xbail "open($f1): $!"; + is(my $bye = <$fh>, "bye from $pid\n", 'got FIFO bye'); + + check_sock($unix); + $pid = $read_pid->($pid_file); + kill('QUIT', $pid) or xbail "USR2 failed: $!"; + + vec(my $rvec = '', fileno($p0), 1) = 1; + is(select($rvec, undef, undef, 1), 1, 'timeout for pipe HUP'); + is(my $u = <$p0>, undef, 'process closed pipe writer at exit'); + ok(!-f $pid_file, 'PID file is gone'); } - is(kill(0, $rpid), 0, 'daemonized process exited') } done_testing();