t/config_limiter.t
t/content_hash.t
t/convert-compact.t
+t/daemon.t
t/data-gen/.gitignore
t/data/0001.patch
t/data/attached-mbox-with-utf8.eml
t/psgi_attach.eml
t/psgi_attach.t
t/psgi_bad_mids.t
+t/psgi_log.psgi
t/psgi_mount.t
t/psgi_multipart_not.t
t/psgi_scan_all.t
open my $fh, '>', $f or die "open $f $!";
print $fh $str or die "print $f $!";
close $fh or die "close $f $!";
+for (qw(sys/ioctl sys/filio)) {
+ my $cfg_name = $_;
+ my $cpp_name = uc $_;
+ $cfg_name =~ tr!/!!d;
+ $cpp_name =~ tr!/!_!;
+ ($Config{"i_$cfg_name"} // '') eq 'define' and
+ push @cflags, "-DHAVE_${cpp_name}_H";
+}
system($cc, '-o', $x, $f, @cflags) == 0 or die "$cc failed \$?=$?";
print STDERR '# %Config',
(map { " $_=$Config{$_}" } qw(ptrsize sizesize lseeksize)), "\n";
#include <stddef.h>
#include <sys/socket.h>
#include <sys/syscall.h>
-#include <sys/ioctl.h>
+#ifdef HAVE_SYS_IOCTL_H
+# include <sys/ioctl.h>
+#endif
+#ifdef HAVE_SYS_FILIO_H
+# include <sys/filio.h>
+#endif
#ifdef __linux__
# include <linux/fs.h>
# include <sys/epoll.h>
#endif /* Linux, any other OSes with stable syscalls? */
D(SIGWINCH);
+ MAYBE X(FIONREAD);
MAYBE D(SO_ACCEPTFILTER);
MAYBE D(_SC_NPROCESSORS_ONLN);
MAYBE D(_SC_AVPHYS_PAGES);
use IO::Handle; # ->autoflush
use IO::Socket;
use File::Spec;
+use IO::Poll qw(POLLERR POLLIN POLLHUP);
use POSIX qw(WNOHANG :signal_h F_SETFD);
use Socket qw(IPPROTO_TCP SOL_SOCKET);
STDOUT->autoflush(1);
do_chown($path);
}
+sub stream_hup ($) {
+ my $ev = POLLIN;
+ my $n = IO::Poll::_poll(0, fileno($_[0]) // return, $ev) or return;
+ return 1 if $ev & (POLLHUP|POLLERR);
+
+ # n.b. POLLHUP isn't reliably detected, so check FIONREAD on POLLIN
+ if (defined(PublicInbox::Syscall::FIONREAD) && ($ev & POLLIN)) {
+ ioctl($_[0], PublicInbox::Syscall::FIONREAD, $n = "") //
+ return;
+ return (unpack('i', $n) == 0);
+ }
+ undef;
+}
+
+if (PublicInbox::Syscall->can('TCP_ESTABLISHED')) {
+ eval <<'EOM';
+sub tcp_hup ($) {
+ my $buf = getsockopt($_[0], Socket::IPPROTO_TCP, Socket::TCP_INFO)
+ or return;
+ unpack('C', $buf) != PublicInbox::Syscall::TCP_ESTABLISHED
+}
+EOM
+ warn "E: $@" if $@;
+}
+
+no warnings 'once';
+*tcp_hup = \&stream_hup if !__PACKAGE__->can('tcp_hup');
+
1;
}
}
+# we use the non-standard 499 code for client disconnects (matching nginx)
+sub status_msg ($) { status_message($_[0]) // 'error' }
+
sub response_header_write ($$$) {
my ($self, $env, $res) = @_;
my $proto = $env->{SERVER_PROTOCOL} or return; # HTTP/0.9 :P
my $status = $res->[0];
- my $h = "$proto $status " . status_message($status) . "\r\n";
+ my $h = "$proto $status " . status_msg($status) . "\r\n";
my ($len, $chunked);
my $headers = $res->[1];
sub quit {
my ($self, $status) = @_;
- my $h = "HTTP/1.1 $status " . status_message($status) . "\r\n\r\n";
+ my $h = "HTTP/1.1 $status " . status_msg($status) . "\r\n\r\n";
$self->write(\$h);
$self->close;
undef; # input_prepare expects this
'pi-httpd.async' => 1,
'pi-httpd.app' => $self->{app},
'pi-httpd.warn_cb' => $self->{warn_cb},
+ 'pi-httpd.ckhup' => $port ? \&PublicInbox::Daemon::tcp_hup :
+ \&PublicInbox::Daemon::stream_hup,
}
}
if ($^O eq 'linux') {
%CONST = (
MSG_MORE => 0x8000,
+ FIONREAD => 0x541b,
+ TCP_ESTABLISHED => 1,
TMPL_cmsg_len => TMPL_size_t,
# cmsg_len, cmsg_level, cmsg_type
SIZEOF_cmsghdr => SIZEOF_int * 2 + SIZEOF_size_t,
} elsif ($^O =~ /\A(?:freebsd|openbsd|netbsd|dragonfly)\z/) {
%CONST = (
TMPL_cmsg_len => 'L', # socklen_t
+ FIONREAD => 0x4004667f,
SIZEOF_cmsghdr => SIZEOF_int * 3,
CMSG_DATA_off => SIZEOF_ptr == 8 ? '@16' : '',
TMPL_msghdr => 'PL' . # msg_name, msg_namelen
TMPL_size_t. # msg_controllen
'i', # msg_flags
- )
+ );
+ # *BSD uses `TCPS_ESTABLISHED', not `TCP_ESTABLISHED'
+ # dragonfly uses TCPS_ESTABLISHED==5, but it lacks TCP_INFO,
+ # so leave it unset on dfly
+ $CONST{TCP_ESTABLISHED} = 4 if $^O ne 'dragonfly';
}
$CONST{CMSG_ALIGN_size} = SIZEOF_size_t;
$CONST{SIZEOF_cmsghdr} //= 0;
$CONST{CMSG_DATA_off} //= undef;
$CONST{TMPL_msghdr} //= undef;
$CONST{MSG_MORE} //= 0;
+ $CONST{FIONREAD} //= undef;
}
# SFD_CLOEXEC is arch-dependent, so IN_CLOEXEC may be, too
$ENV{GIT_TEST_FSYNC} = 0; # hopefully reduce wear
$_ = File::Spec->rel2abs($_) for (grep(!m!^/!, @INC));
-our $CURRENT_DAEMON;
+our ($CURRENT_DAEMON, $CURRENT_LISTENER);
BEGIN {
@EXPORT = qw(tmpdir tcp_server tcp_connect require_git require_mods
run_script start_script key2sub xsys xsys_e xqx eml_load tick
my $ua = LWP::UserAgent->new;
$ua->max_redirect(0);
local $CURRENT_DAEMON = $td;
+ local $CURRENT_LISTENER = $sock;
Plack::Test::ExternalServer::test_psgi(client => $client,
ua => $ua);
$cb->() if $cb;
sub start_solver ($) {
my ($ctx) = @_;
+ $ctx->{-next_solver} = on_destroy \&next_solver;
+ ++$solver_nr;
+ if (my $ck = $ctx->{env}->{'pi-httpd.ckhup'}) {
+ $ck->($ctx->{env}->{'psgix.io'}->{sock}) and
+ return html_page $ctx, 499, 'client disconnected';
+ }
+
while (my ($from, $to) = each %QP_MAP) {
my $v = $ctx->{qp}->{$from} // next;
$ctx->{hints}->{$to} = $v if $v ne '';
}
- $ctx->{-next_solver} = on_destroy \&next_solver;
- ++$solver_nr;
# ->newdir and open may croak
$ctx->{-tmp} = File::Temp->newdir("solver.$ctx->{oid_b}-XXXX",
TMPDIR => 1);
sub next_solver {
--$solver_nr;
my $ctx = shift(@solver_q) // return;
- # XXX FIXME: client may've disconnected if it waited a long while
eval { start_solver($ctx) };
- warn "W: start_solver: $@" if $@;
+ return unless $@;
+ warn "W: start_solver: $@";
html_page($ctx, 500) if $ctx->{-wcb};
}
--- /dev/null
+# Copyright (C) all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+# unit tests for common (public-facing) daemon code
+use v5.12;
+use autodie;
+use PublicInbox::TestCommon;
+use Socket qw(SOCK_STREAM);
+require_ok 'PublicInbox::Daemon';
+use PublicInbox::IO qw(poll_in);
+
+# $fn is stream_hup or tcp_hup
+my $ck_hup = sub {
+ my ($s, $connect, $fn, $type) = @_;
+ my $c = $connect->();
+ poll_in $s; # needed on *BSD
+ my $addr = accept(my $acc, $s);
+ close $c;
+ my $ck = PublicInbox::Daemon->can($fn);
+ poll_in($acc) if $^O ne 'linux'; # netbsd needs this, at least...
+ ok $ck->($acc), "$fn detected close ($type)";
+ $c = $connect->();
+ syswrite $c, 'hi';
+ poll_in $s; # needed on *BSD
+ $addr = accept($acc, $s);
+ ok !$ck->($acc), "$fn false when still established ($type)";
+};
+
+{
+ my $tmpdir = tmpdir;
+ my $l = "$tmpdir/named.sock";
+ my $s = IO::Socket::UNIX->new(Listen => 5, Local => $l,
+ Type => SOCK_STREAM) or
+ xbail "bind+listen($l): $!";
+ my $connect = sub {
+ IO::Socket::UNIX->new(Peer => $l, Type => SOCK_STREAM) or
+ xbail "connect($l): $!";
+ };
+ $ck_hup->($s, $connect, 'stream_hup', 'UNIX');
+}
+
+{
+ my $s = tcp_server;
+ my $tcp_conn = sub { tcp_connect($s) };
+ $ck_hup->($s, $tcp_conn, 'stream_hup', 'TCP');
+ $ck_hup->($s, $tcp_conn, 'tcp_hup', 'TCP');
+}
+
+SKIP: {
+ $^O =~ /\A(?:linux|freebsd|netbsd|openbsd)\z/ or
+ skip "no TCP_INFO support $^O", 1;
+ isnt \&PublicInbox::Daemon::stream_hup,
+ \&PublicInbox::Daemon::tcp_hup,
+ "stream_hup and tcp_hup are different on \$^O=$^O";
+}
+
+done_testing;
--- /dev/null
+#!/usr/bin/perl -w
+# Copyright (C) all contributors <meta@public-inbox.org>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+# Usage: plackup [OPTIONS] /path/to/this/file
+use v5.12;
+use PublicInbox::WWW;
+use Plack::Builder;
+my $www = PublicInbox::WWW->new;
+$www->preload;
+builder {
+ enable 'AccessLog::Timed',
+ logger => sub { syswrite(STDOUT, $_[0]) },
+ format => '%t "%r" %>s %b %D';
+ enable 'Head';
+ sub { $www->call($_[0]) }
+}
use PublicInbox::ContentHash qw(git_sha);
use PublicInbox::Spawn qw(run_qx which);
use File::Path qw(remove_tree);
-use PublicInbox::IO qw(write_file);
-use autodie qw(close mkdir open rename symlink unlink);
+use PublicInbox::IO qw(write_file try_cat);
+use autodie qw(close kill mkdir open read rename symlink unlink);
require_mods qw(DBD::SQLite Xapian);
require PublicInbox::SolverGit;
my $rdr = { 2 => \(my $null) };
my $env = { PI_CONFIG => $cfgpath, TMPDIR => $tmpdir };
xsys_e $cp, qw(-Rp), $binfoo, $gone_repo; # for test_httpd $client
+ my $has_log;
+ SKIP: { # some distros may split out this middleware
+ require_mods 'Plack::Middleware::AccessLog::Timed', 1;
+ $has_log = $env->{psgi_file} = 't/psgi_log.psgi';
+ }
test_httpd($env, $client, 7, sub {
SKIP: {
+ my $td_pid = $PublicInbox::TestCommon::CURRENT_DAEMON->{pid};
+ my $lis = $PublicInbox::TestCommon::CURRENT_LISTENER;
+ kill 'STOP', $td_pid;
+ my @req = ("GET /$name/69df7d5/s/ HTTP/1.0\r\n", "\r\n");
+ my $c0 = tcp_connect($lis);
+ print $c0 $req[0];
+ my @c = map {
+ my $c = tcp_connect($lis);
+ print $c @req;
+ $c;
+ } (1..30);
+ close $_ for @c[(1..29)]; # premature disconnects
+ kill 'CONT', $td_pid;
+ read $c[0], my $buf, 16384;
+ print $c0 $req[1];
+ read $c0, $buf, 16384;
+
+ if ($has_log) {
+ # kick server to ensure all CBs are handled, first
+ $c0 = tcp_connect($lis);
+ print $c0 <<EOM;
+GUH /$name/69df7d5/s/ HTTP/1.1\r
+Connection: close\r
+\r
+EOM
+ read $c0, $buf, 16384;
+ my %counts;
+ my @n = map {
+ my $code = (split ' ', $_)[5];
+ $counts{$code}++;
+ $code;
+ } grep m! HTTP/1\.0\b!,
+ try_cat("$tmpdir/stdout.log");
+ ok $counts{499}, 'got some 499s from disconnects';
+ ok $counts{200} >= 2, 'got at least two 200 codes' or
+ diag explain(\%counts);
+ is $counts{499} + $counts{200}, 31,
+ 'all 1.0 connections logged for disconnects';
+ }
+
require_cmd('curl', 1) or skip 'no curl', 1;
mkdir "$tmpdir/ext";
my $rurl = "$ENV{PLACK_TEST_EXTERNALSERVER_URI}/$name";