unicorn-maintained 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.CHANGELOG.old +25 -0
- data/.document +28 -0
- data/.gitattributes +5 -0
- data/.gitignore +25 -0
- data/.mailmap +26 -0
- data/.manifest +149 -0
- data/.olddoc.yml +25 -0
- data/Application_Timeouts +77 -0
- data/CONTRIBUTORS +39 -0
- data/COPYING +674 -0
- data/DESIGN +99 -0
- data/Documentation/.gitignore +3 -0
- data/Documentation/unicorn.1 +222 -0
- data/Documentation/unicorn_rails.1 +207 -0
- data/FAQ +70 -0
- data/GIT-VERSION-FILE +1 -0
- data/GIT-VERSION-GEN +39 -0
- data/GNUmakefile +317 -0
- data/HACKING +112 -0
- data/ISSUES +102 -0
- data/KNOWN_ISSUES +79 -0
- data/LATEST +1 -0
- data/LICENSE +67 -0
- data/Links +58 -0
- data/NEWS +1 -0
- data/PHILOSOPHY +139 -0
- data/README +156 -0
- data/Rakefile +16 -0
- data/SIGNALS +123 -0
- data/Sandbox +104 -0
- data/TODO +3 -0
- data/TUNING +119 -0
- data/archive/.gitignore +3 -0
- data/archive/slrnpull.conf +4 -0
- data/bin/unicorn +128 -0
- data/bin/unicorn_rails +209 -0
- data/examples/big_app_gc.rb +2 -0
- data/examples/echo.ru +26 -0
- data/examples/init.sh +102 -0
- data/examples/logger_mp_safe.rb +25 -0
- data/examples/logrotate.conf +44 -0
- data/examples/nginx.conf +156 -0
- data/examples/unicorn.conf.minimal.rb +13 -0
- data/examples/unicorn.conf.rb +110 -0
- data/examples/unicorn.socket +11 -0
- data/examples/unicorn@.service +40 -0
- data/ext/unicorn_http/CFLAGS +13 -0
- data/ext/unicorn_http/c_util.h +116 -0
- data/ext/unicorn_http/common_field_optimization.h +128 -0
- data/ext/unicorn_http/epollexclusive.h +128 -0
- data/ext/unicorn_http/ext_help.h +38 -0
- data/ext/unicorn_http/extconf.rb +39 -0
- data/ext/unicorn_http/global_variables.h +97 -0
- data/ext/unicorn_http/httpdate.c +91 -0
- data/ext/unicorn_http/unicorn_http.c +4334 -0
- data/ext/unicorn_http/unicorn_http.rl +1040 -0
- data/ext/unicorn_http/unicorn_http_common.rl +76 -0
- data/lib/unicorn/app/old_rails/static.rb +59 -0
- data/lib/unicorn/app/old_rails.rb +35 -0
- data/lib/unicorn/cgi_wrapper.rb +147 -0
- data/lib/unicorn/configurator.rb +748 -0
- data/lib/unicorn/const.rb +21 -0
- data/lib/unicorn/http_request.rb +201 -0
- data/lib/unicorn/http_response.rb +93 -0
- data/lib/unicorn/http_server.rb +859 -0
- data/lib/unicorn/launcher.rb +62 -0
- data/lib/unicorn/oob_gc.rb +81 -0
- data/lib/unicorn/preread_input.rb +33 -0
- data/lib/unicorn/select_waiter.rb +6 -0
- data/lib/unicorn/socket_helper.rb +185 -0
- data/lib/unicorn/stream_input.rb +151 -0
- data/lib/unicorn/tee_input.rb +131 -0
- data/lib/unicorn/tmpio.rb +33 -0
- data/lib/unicorn/util.rb +90 -0
- data/lib/unicorn/version.rb +1 -0
- data/lib/unicorn/worker.rb +165 -0
- data/lib/unicorn.rb +136 -0
- data/man/man1/unicorn.1 +222 -0
- data/man/man1/unicorn_rails.1 +207 -0
- data/setup.rb +1586 -0
- data/t/.gitignore +4 -0
- data/t/GNUmakefile +5 -0
- data/t/README +49 -0
- data/t/active-unix-socket.t +117 -0
- data/t/bin/unused_listen +40 -0
- data/t/broken-app.ru +12 -0
- data/t/client_body_buffer_size.ru +14 -0
- data/t/client_body_buffer_size.t +80 -0
- data/t/detach.ru +11 -0
- data/t/env.ru +3 -0
- data/t/fails-rack-lint.ru +5 -0
- data/t/heartbeat-timeout.ru +12 -0
- data/t/heartbeat-timeout.t +62 -0
- data/t/integration.ru +115 -0
- data/t/integration.t +356 -0
- data/t/lib.perl +258 -0
- data/t/listener_names.ru +4 -0
- data/t/my-tap-lib.sh +201 -0
- data/t/oob_gc.ru +17 -0
- data/t/oob_gc_path.ru +17 -0
- data/t/pid.ru +3 -0
- data/t/preread_input.ru +22 -0
- data/t/reload-bad-config.t +54 -0
- data/t/reopen-logs.ru +13 -0
- data/t/reopen-logs.t +39 -0
- data/t/t0008-back_out_of_upgrade.sh +110 -0
- data/t/t0009-broken-app.sh +56 -0
- data/t/t0010-reap-logging.sh +55 -0
- data/t/t0012-reload-empty-config.sh +86 -0
- data/t/t0013-rewindable-input-false.sh +24 -0
- data/t/t0013.ru +12 -0
- data/t/t0014-rewindable-input-true.sh +24 -0
- data/t/t0014.ru +12 -0
- data/t/t0015-configurator-internals.sh +25 -0
- data/t/t0020-at_exit-handler.sh +49 -0
- data/t/t0021-process_detach.sh +29 -0
- data/t/t0022-listener_names-preload_app.sh +32 -0
- data/t/t0300-no-default-middleware.sh +20 -0
- data/t/t0301-no-default-middleware-ignored-in-config.sh +25 -0
- data/t/t0301.ru +13 -0
- data/t/t9001-oob_gc.sh +47 -0
- data/t/t9002-oob_gc-path.sh +75 -0
- data/t/test-lib.sh +125 -0
- data/t/winch_ttin.t +67 -0
- data/t/working_directory.t +94 -0
- data/test/aggregate.rb +15 -0
- data/test/benchmark/README +60 -0
- data/test/benchmark/dd.ru +18 -0
- data/test/benchmark/ddstream.ru +50 -0
- data/test/benchmark/readinput.ru +40 -0
- data/test/benchmark/stack.ru +8 -0
- data/test/benchmark/uconnect.perl +66 -0
- data/test/exec/README +5 -0
- data/test/exec/test_exec.rb +1029 -0
- data/test/test_helper.rb +306 -0
- data/test/unit/test_ccc.rb +91 -0
- data/test/unit/test_configurator.rb +175 -0
- data/test/unit/test_droplet.rb +28 -0
- data/test/unit/test_http_parser.rb +884 -0
- data/test/unit/test_http_parser_ng.rb +714 -0
- data/test/unit/test_request.rb +169 -0
- data/test/unit/test_server.rb +244 -0
- data/test/unit/test_signals.rb +188 -0
- data/test/unit/test_socket_helper.rb +159 -0
- data/test/unit/test_stream_input.rb +210 -0
- data/test/unit/test_tee_input.rb +303 -0
- data/test/unit/test_util.rb +131 -0
- data/test/unit/test_waiter.rb +34 -0
- data/unicorn.gemspec +48 -0
- metadata +275 -0
data/t/integration.t
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!perl -w
|
|
2
|
+
# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
|
|
3
|
+
# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
|
|
4
|
+
|
|
5
|
+
# This is the main integration test for fast-ish things to minimize
|
|
6
|
+
# Ruby startup time penalties.
|
|
7
|
+
|
|
8
|
+
use v5.14; BEGIN { require './t/lib.perl' };
|
|
9
|
+
use autodie;
|
|
10
|
+
use Socket qw(SOL_SOCKET SO_KEEPALIVE SHUT_WR);
|
|
11
|
+
our $srv = tcp_server();
|
|
12
|
+
our $host_port = tcp_host_port($srv);
|
|
13
|
+
|
|
14
|
+
if ('ensure Perl does not set SO_KEEPALIVE by default') {
|
|
15
|
+
my $val = getsockopt($srv, SOL_SOCKET, SO_KEEPALIVE);
|
|
16
|
+
unpack('i', $val) == 0 or
|
|
17
|
+
setsockopt($srv, SOL_SOCKET, SO_KEEPALIVE, pack('i', 0));
|
|
18
|
+
$val = getsockopt($srv, SOL_SOCKET, SO_KEEPALIVE);
|
|
19
|
+
}
|
|
20
|
+
my $t0 = time;
|
|
21
|
+
open my $conf_fh, '>', $u_conf;
|
|
22
|
+
$conf_fh->autoflush(1);
|
|
23
|
+
my $u1 = "$tmpdir/u1";
|
|
24
|
+
print $conf_fh <<EOM;
|
|
25
|
+
early_hints true
|
|
26
|
+
listen "$u1"
|
|
27
|
+
EOM
|
|
28
|
+
my $ar = unicorn(qw(-E none t/integration.ru -c), $u_conf, { 3 => $srv });
|
|
29
|
+
my $curl = which('curl');
|
|
30
|
+
my $fifo = "$tmpdir/fifo";
|
|
31
|
+
POSIX::mkfifo($fifo, 0600) or die "mkfifo: $!";
|
|
32
|
+
my %PUT = (
|
|
33
|
+
chunked_md5 => sub {
|
|
34
|
+
my ($in, $out, $path, %opt) = @_;
|
|
35
|
+
my $dig = Digest::MD5->new;
|
|
36
|
+
print $out <<EOM;
|
|
37
|
+
PUT $path HTTP/1.1\r
|
|
38
|
+
Transfer-Encoding: chunked\r
|
|
39
|
+
Trailer: Content-MD5\r
|
|
40
|
+
\r
|
|
41
|
+
EOM
|
|
42
|
+
my ($buf, $r);
|
|
43
|
+
while (1) {
|
|
44
|
+
$r = read($in, $buf, 999 + int(rand(0xffff)));
|
|
45
|
+
last if $r == 0;
|
|
46
|
+
printf $out "%x\r\n", length($buf);
|
|
47
|
+
print $out $buf, "\r\n";
|
|
48
|
+
$dig->add($buf);
|
|
49
|
+
}
|
|
50
|
+
print $out "0\r\nContent-MD5: ", $dig->b64digest, "\r\n\r\n";
|
|
51
|
+
},
|
|
52
|
+
identity => sub {
|
|
53
|
+
my ($in, $out, $path, %opt) = @_;
|
|
54
|
+
my $clen = $opt{-s} // -s $in;
|
|
55
|
+
print $out <<EOM;
|
|
56
|
+
PUT $path HTTP/1.0\r
|
|
57
|
+
Content-Length: $clen\r
|
|
58
|
+
\r
|
|
59
|
+
EOM
|
|
60
|
+
my ($buf, $r, $len, $bs);
|
|
61
|
+
while ($clen) {
|
|
62
|
+
$bs = 999 + int(rand(0xffff));
|
|
63
|
+
$len = $clen > $bs ? $bs : $clen;
|
|
64
|
+
$r = read($in, $buf, $len);
|
|
65
|
+
die 'premature EOF' if $r == 0;
|
|
66
|
+
print $out $buf;
|
|
67
|
+
$clen -= $r;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
my ($c, $status, $hdr, $bdy);
|
|
73
|
+
|
|
74
|
+
# response header tests
|
|
75
|
+
($status, $hdr) = do_req($srv, 'GET /rack-2-newline-headers HTTP/1.0');
|
|
76
|
+
like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
|
|
77
|
+
my $orig_200_status = $status;
|
|
78
|
+
is_deeply([ grep(/^X-R2: /, @$hdr) ],
|
|
79
|
+
[ 'X-R2: a', 'X-R2: b', 'X-R2: c' ],
|
|
80
|
+
'rack 2 LF-delimited headers supported') or diag(explain($hdr));
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
my $val = getsockopt($srv, SOL_SOCKET, SO_KEEPALIVE);
|
|
84
|
+
is(unpack('i', $val), 1, 'SO_KEEPALIVE set on inherited socket');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
SKIP: { # Date header check
|
|
88
|
+
my @d = grep(/^Date: /i, @$hdr);
|
|
89
|
+
is(scalar(@d), 1, 'got one date header') or diag(explain(\@d));
|
|
90
|
+
eval { require HTTP::Date } or skip "HTTP::Date missing: $@", 1;
|
|
91
|
+
$d[0] =~ s/^Date: //i or die 'BUG: did not strip date: prefix';
|
|
92
|
+
my $t = HTTP::Date::str2time($d[0]);
|
|
93
|
+
my $now = time;
|
|
94
|
+
ok($t >= ($t0 - 1) && $t > 0 && $t <= ($now + 1), 'valid date') or
|
|
95
|
+
diag(explain(["t=$t t0=$t0 now=$now", $!, \@d]));
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
($status, $hdr) = do_req($srv, 'GET /rack-3-array-headers HTTP/1.0');
|
|
100
|
+
is_deeply([ grep(/^x-r3: /, @$hdr) ],
|
|
101
|
+
[ 'x-r3: a', 'x-r3: b', 'x-r3: c' ],
|
|
102
|
+
'rack 3 array headers supported') or diag(explain($hdr));
|
|
103
|
+
|
|
104
|
+
SKIP: {
|
|
105
|
+
eval { require JSON::PP } or skip "JSON::PP missing: $@", 1;
|
|
106
|
+
($status, $hdr, my $json) = do_req $srv, 'GET /env_dump';
|
|
107
|
+
is($status, undef, 'no status for HTTP/0.9');
|
|
108
|
+
is($hdr, undef, 'no header for HTTP/0.9');
|
|
109
|
+
unlike($json, qr/^Connection: /smi, 'no connection header for 0.9');
|
|
110
|
+
unlike($json, qr!\AHTTP/!s, 'no HTTP/1.x prefix for 0.9');
|
|
111
|
+
my $env = JSON::PP->new->decode($json);
|
|
112
|
+
is(ref($env), 'HASH', 'JSON decoded body to hashref');
|
|
113
|
+
is($env->{SERVER_PROTOCOL}, 'HTTP/0.9', 'SERVER_PROTOCOL is 0.9');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# cf. <CAO47=rJa=zRcLn_Xm4v2cHPr6c0UswaFC_omYFEH+baSxHOWKQ@mail.gmail.com>
|
|
117
|
+
($status, $hdr) = do_req($srv, 'GET /nil-header-value HTTP/1.0');
|
|
118
|
+
is_deeply([grep(/^X-Nil:/, @$hdr)], ['X-Nil: '],
|
|
119
|
+
'nil header value accepted for broken apps') or diag(explain($hdr));
|
|
120
|
+
|
|
121
|
+
check_stderr;
|
|
122
|
+
($status, $hdr, $bdy) = do_req($srv, 'GET /broken_app HTTP/1.0');
|
|
123
|
+
like($status, qr!\AHTTP/1\.[0-1] 500\b!, 'got 500 error on broken endpoint');
|
|
124
|
+
is($bdy, undef, 'no response body after exception');
|
|
125
|
+
truncate($errfh, 0);
|
|
126
|
+
|
|
127
|
+
my $ck_early_hints = sub {
|
|
128
|
+
my ($note) = @_;
|
|
129
|
+
$c = unix_start($u1, 'GET /early_hints_rack2 HTTP/1.0');
|
|
130
|
+
($status, $hdr) = slurp_hdr($c);
|
|
131
|
+
like($status, qr!\AHTTP/1\.[01] 103\b!, 'got 103 for rack 2 value');
|
|
132
|
+
is_deeply(['link: r', 'link: 2'], $hdr, 'rack 2 hints match '.$note);
|
|
133
|
+
($status, $hdr) = slurp_hdr($c);
|
|
134
|
+
like($status, qr!\AHTTP/1\.[01] 200\b!, 'got 200 afterwards');
|
|
135
|
+
is(readline($c), 'String', 'early hints used a String for rack 2');
|
|
136
|
+
|
|
137
|
+
$c = unix_start($u1, 'GET /early_hints_rack3 HTTP/1.0');
|
|
138
|
+
($status, $hdr) = slurp_hdr($c);
|
|
139
|
+
like($status, qr!\AHTTP/1\.[01] 103\b!, 'got 103 for rack 3');
|
|
140
|
+
is_deeply(['link: r', 'link: 3'], $hdr, 'rack 3 hints match '.$note);
|
|
141
|
+
($status, $hdr) = slurp_hdr($c);
|
|
142
|
+
like($status, qr!\AHTTP/1\.[01] 200\b!, 'got 200 afterwards');
|
|
143
|
+
is(readline($c), 'Array', 'early hints used a String for rack 3');
|
|
144
|
+
};
|
|
145
|
+
$ck_early_hints->('ccc off'); # we'll retest later
|
|
146
|
+
|
|
147
|
+
if ('TODO: ensure Rack::Utils::HTTP_STATUS_CODES is available') {
|
|
148
|
+
($status, $hdr) = do_req $srv, 'POST /tweak-status-code HTTP/1.0';
|
|
149
|
+
like($status, qr!\AHTTP/1\.[01] 200 HI\b!, 'status tweaked');
|
|
150
|
+
|
|
151
|
+
($status, $hdr) = do_req $srv, 'POST /restore-status-code HTTP/1.0';
|
|
152
|
+
is($status, $orig_200_status, 'original status restored');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
SKIP: {
|
|
156
|
+
eval { require HTTP::Tiny } or skip "HTTP::Tiny missing: $@", 1;
|
|
157
|
+
my $ht = HTTP::Tiny->new;
|
|
158
|
+
my $res = $ht->get("http://$host_port/write_on_close");
|
|
159
|
+
is($res->{content}, 'Goodbye', 'write-on-close body read');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if ('bad requests') {
|
|
163
|
+
($status, $hdr) = do_req $srv, 'GET /env_dump HTTP/1/1';
|
|
164
|
+
like($status, qr!\AHTTP/1\.[01] 400 \b!, 'got 400 on bad request');
|
|
165
|
+
|
|
166
|
+
$c = tcp_start($srv);
|
|
167
|
+
print $c 'GET /';
|
|
168
|
+
my $buf = join('', (0..9), 'ab');
|
|
169
|
+
for (0..1023) { print $c $buf }
|
|
170
|
+
print $c " HTTP/1.0\r\n\r\n";
|
|
171
|
+
($status, $hdr) = slurp_hdr($c);
|
|
172
|
+
like($status, qr!\AHTTP/1\.[01] 414 \b!,
|
|
173
|
+
'414 on REQUEST_PATH > (12 * 1024)');
|
|
174
|
+
|
|
175
|
+
$c = tcp_start($srv);
|
|
176
|
+
print $c 'GET /hello-world?a';
|
|
177
|
+
$buf = join('', (0..9));
|
|
178
|
+
for (0..1023) { print $c $buf }
|
|
179
|
+
print $c " HTTP/1.0\r\n\r\n";
|
|
180
|
+
($status, $hdr) = slurp_hdr($c);
|
|
181
|
+
like($status, qr!\AHTTP/1\.[01] 414 \b!,
|
|
182
|
+
'414 on QUERY_STRING > (10 * 1024)');
|
|
183
|
+
|
|
184
|
+
$c = tcp_start($srv);
|
|
185
|
+
print $c 'GET /hello-world#a';
|
|
186
|
+
$buf = join('', (0..9), 'a'..'f');
|
|
187
|
+
for (0..63) { print $c $buf }
|
|
188
|
+
print $c " HTTP/1.0\r\n\r\n";
|
|
189
|
+
($status, $hdr) = slurp_hdr($c);
|
|
190
|
+
like($status, qr!\AHTTP/1\.[01] 414 \b!, '414 on FRAGMENT > (1024)');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# input tests
|
|
194
|
+
my ($blob_size, $blob_hash);
|
|
195
|
+
SKIP: {
|
|
196
|
+
skip 'SKIP_EXPENSIVE on', 1 if $ENV{SKIP_EXPENSIVE};
|
|
197
|
+
CORE::open(my $rh, '<', 't/random_blob') or
|
|
198
|
+
skip "t/random_blob not generated $!", 1;
|
|
199
|
+
$blob_size = -s $rh;
|
|
200
|
+
require Digest::MD5;
|
|
201
|
+
$blob_hash = Digest::MD5->new->addfile($rh)->hexdigest;
|
|
202
|
+
|
|
203
|
+
my $ck_hash = sub {
|
|
204
|
+
my ($sub, $path, %opt) = @_;
|
|
205
|
+
seek($rh, 0, SEEK_SET);
|
|
206
|
+
$c = tcp_start($srv);
|
|
207
|
+
$c->autoflush($opt{sync} // 0);
|
|
208
|
+
$PUT{$sub}->($rh, $c, $path, %opt);
|
|
209
|
+
defined($opt{overwrite}) and
|
|
210
|
+
print { $c } ('x' x $opt{overwrite});
|
|
211
|
+
$c->flush or die $!;
|
|
212
|
+
shutdown($c, SHUT_WR);
|
|
213
|
+
($status, $hdr) = slurp_hdr($c);
|
|
214
|
+
is(readline($c), $blob_hash, "$sub $path");
|
|
215
|
+
};
|
|
216
|
+
$ck_hash->('identity', '/rack_input', -s => $blob_size);
|
|
217
|
+
$ck_hash->('chunked_md5', '/rack_input');
|
|
218
|
+
$ck_hash->('identity', '/rack_input/size_first', -s => $blob_size);
|
|
219
|
+
$ck_hash->('identity', '/rack_input/rewind_first', -s => $blob_size);
|
|
220
|
+
$ck_hash->('chunked_md5', '/rack_input/size_first');
|
|
221
|
+
$ck_hash->('chunked_md5', '/rack_input/rewind_first');
|
|
222
|
+
|
|
223
|
+
$ck_hash->('identity', '/rack_input', -s => $blob_size, sync => 1);
|
|
224
|
+
$ck_hash->('chunked_md5', '/rack_input', sync => 1);
|
|
225
|
+
|
|
226
|
+
# ensure small overwrites don't get checksummed
|
|
227
|
+
$ck_hash->('identity', '/rack_input', -s => $blob_size,
|
|
228
|
+
overwrite => 1); # one extra byte
|
|
229
|
+
unlike(slurp($err_log), qr/ClientShutdown/,
|
|
230
|
+
'no overreads after client SHUT_WR');
|
|
231
|
+
|
|
232
|
+
# excessive overwrite truncated
|
|
233
|
+
$c = tcp_start($srv);
|
|
234
|
+
$c->autoflush(0);
|
|
235
|
+
print $c "PUT /rack_input HTTP/1.0\r\nContent-Length: 1\r\n\r\n";
|
|
236
|
+
if (1) {
|
|
237
|
+
local $SIG{PIPE} = 'IGNORE';
|
|
238
|
+
my $buf = "\0" x 8192;
|
|
239
|
+
my $n = 0;
|
|
240
|
+
my $end = time + 5;
|
|
241
|
+
$! = 0;
|
|
242
|
+
while (print $c $buf and time < $end) { ++$n }
|
|
243
|
+
ok($!, 'overwrite truncated') or diag "n=$n err=$! ".time;
|
|
244
|
+
undef $c;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# client shutdown early
|
|
248
|
+
$c = tcp_start($srv);
|
|
249
|
+
$c->autoflush(0);
|
|
250
|
+
print $c "PUT /rack_input HTTP/1.0\r\nContent-Length: 16384\r\n\r\n";
|
|
251
|
+
if (1) {
|
|
252
|
+
local $SIG{PIPE} = 'IGNORE';
|
|
253
|
+
print $c 'too short body';
|
|
254
|
+
shutdown($c, SHUT_WR);
|
|
255
|
+
vec(my $rvec = '', fileno($c), 1) = 1;
|
|
256
|
+
select($rvec, undef, undef, 10) or BAIL_OUT "timed out";
|
|
257
|
+
my $buf = <$c>;
|
|
258
|
+
is($buf, undef, 'server aborted after client SHUT_WR');
|
|
259
|
+
undef $c;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
$curl // skip 'no curl found in PATH', 1;
|
|
263
|
+
|
|
264
|
+
my ($copt, $cout);
|
|
265
|
+
my $url = "http://$host_port/rack_input";
|
|
266
|
+
my $do_curl = sub {
|
|
267
|
+
my (@arg) = @_;
|
|
268
|
+
pipe(my $cout, $copt->{1});
|
|
269
|
+
open $copt->{2}, '>', "$tmpdir/curl.err";
|
|
270
|
+
my $cpid = spawn($curl, '-sSf', @arg, $url, $copt);
|
|
271
|
+
close(delete $copt->{1});
|
|
272
|
+
is(readline($cout), $blob_hash, "curl @arg response");
|
|
273
|
+
is(waitpid($cpid, 0), $cpid, "curl @arg exited");
|
|
274
|
+
is($?, 0, "no error from curl @arg");
|
|
275
|
+
is(slurp("$tmpdir/curl.err"), '', "no stderr from curl @arg");
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
$do_curl->(qw(-T t/random_blob));
|
|
279
|
+
|
|
280
|
+
seek($rh, 0, SEEK_SET);
|
|
281
|
+
$copt->{0} = $rh;
|
|
282
|
+
$do_curl->('-T-');
|
|
283
|
+
|
|
284
|
+
diag 'testing Unicorn::PrereadInput...';
|
|
285
|
+
local $srv = tcp_server();
|
|
286
|
+
local $host_port = tcp_host_port($srv);
|
|
287
|
+
check_stderr;
|
|
288
|
+
truncate($errfh, 0);
|
|
289
|
+
|
|
290
|
+
my $pri = unicorn(qw(-E none t/preread_input.ru), { 3 => $srv });
|
|
291
|
+
$url = "http://$host_port/";
|
|
292
|
+
|
|
293
|
+
$do_curl->(qw(-T t/random_blob));
|
|
294
|
+
seek($rh, 0, SEEK_SET);
|
|
295
|
+
$copt->{0} = $rh;
|
|
296
|
+
$do_curl->('-T-');
|
|
297
|
+
|
|
298
|
+
my @pr_err = slurp("$tmpdir/err.log");
|
|
299
|
+
is(scalar(grep(/app dispatch:/, @pr_err)), 2, 'app dispatched twice');
|
|
300
|
+
|
|
301
|
+
# abort a chunked request by blocking curl on a FIFO:
|
|
302
|
+
$c = tcp_start($srv, "PUT / HTTP/1.1\r\nTransfer-Encoding: chunked");
|
|
303
|
+
close $c;
|
|
304
|
+
@pr_err = slurp("$tmpdir/err.log");
|
|
305
|
+
is(scalar(grep(/app dispatch:/, @pr_err)), 2,
|
|
306
|
+
'app did not dispatch on aborted request');
|
|
307
|
+
undef $pri;
|
|
308
|
+
check_stderr;
|
|
309
|
+
diag 'Unicorn::PrereadInput middleware tests done';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# ... more stuff here
|
|
313
|
+
|
|
314
|
+
# SIGHUP-able stuff goes here
|
|
315
|
+
|
|
316
|
+
if ('check_client_connection') {
|
|
317
|
+
print $conf_fh <<EOM; # appending to existing
|
|
318
|
+
check_client_connection true
|
|
319
|
+
after_fork { |_,_| File.open('$fifo', 'w') { |fp| fp.write "pid=#\$\$" } }
|
|
320
|
+
EOM
|
|
321
|
+
$ar->do_kill('HUP');
|
|
322
|
+
open my $fifo_fh, '<', $fifo;
|
|
323
|
+
my $wpid = readline($fifo_fh);
|
|
324
|
+
like($wpid, qr/\Apid=\d+\z/a , 'new worker ready');
|
|
325
|
+
$ck_early_hints->('ccc on');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if ('max_header_len internal API') {
|
|
329
|
+
undef $c;
|
|
330
|
+
my $req = 'GET / HTTP/1.0';
|
|
331
|
+
my $len = length($req."\r\n\r\n");
|
|
332
|
+
print $conf_fh <<EOM; # appending to existing
|
|
333
|
+
Unicorn::HttpParser.max_header_len = $len
|
|
334
|
+
EOM
|
|
335
|
+
$ar->do_kill('HUP');
|
|
336
|
+
open my $fifo_fh, '<', $fifo;
|
|
337
|
+
my $wpid = readline($fifo_fh);
|
|
338
|
+
like($wpid, qr/\Apid=\d+\z/a , 'new worker ready');
|
|
339
|
+
close $fifo_fh;
|
|
340
|
+
$wpid =~ s/\Apid=// or die;
|
|
341
|
+
ok(CORE::kill(0, $wpid), 'worker PID retrieved');
|
|
342
|
+
|
|
343
|
+
($status, $hdr) = do_req($srv, $req);
|
|
344
|
+
like($status, qr!\AHTTP/1\.[01] 200\b!, 'minimal request succeeds');
|
|
345
|
+
|
|
346
|
+
($status, $hdr) = do_req($srv, 'GET /xxxxxx HTTP/1.0');
|
|
347
|
+
like($status, qr!\AHTTP/1\.[01] 413\b!, 'big request fails');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
undef $ar;
|
|
352
|
+
|
|
353
|
+
check_stderr;
|
|
354
|
+
|
|
355
|
+
undef $tmpdir;
|
|
356
|
+
done_testing;
|
data/t/lib.perl
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!perl -w
|
|
2
|
+
# Copyright (C) unicorn hackers <unicorn-public@80x24.org>
|
|
3
|
+
# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
|
|
4
|
+
package UnicornTest;
|
|
5
|
+
use v5.14;
|
|
6
|
+
use parent qw(Exporter);
|
|
7
|
+
use autodie;
|
|
8
|
+
use Test::More;
|
|
9
|
+
use Time::HiRes qw(sleep time);
|
|
10
|
+
use IO::Socket::INET;
|
|
11
|
+
use POSIX qw(dup2 _exit setpgid :signal_h SEEK_SET F_SETFD);
|
|
12
|
+
use File::Temp 0.19 (); # 0.19 for ->newdir
|
|
13
|
+
our ($tmpdir, $errfh, $err_log, $u_sock, $u_conf, $daemon_pid,
|
|
14
|
+
$pid_file);
|
|
15
|
+
our @EXPORT = qw(unicorn slurp tcp_server tcp_start unicorn
|
|
16
|
+
$tmpdir $errfh $err_log $u_sock $u_conf $daemon_pid $pid_file
|
|
17
|
+
SEEK_SET tcp_host_port which spawn check_stderr unix_start slurp_hdr
|
|
18
|
+
do_req stop_daemon sleep time);
|
|
19
|
+
|
|
20
|
+
my ($base) = ($0 =~ m!\b([^/]+)\.[^\.]+\z!);
|
|
21
|
+
$tmpdir = File::Temp->newdir("unicorn-$base-XXXX", TMPDIR => 1);
|
|
22
|
+
$err_log = "$tmpdir/err.log";
|
|
23
|
+
$pid_file = "$tmpdir/pid";
|
|
24
|
+
$u_sock = "$tmpdir/u.sock";
|
|
25
|
+
$u_conf = "$tmpdir/u.conf.rb";
|
|
26
|
+
open($errfh, '>>', $err_log);
|
|
27
|
+
|
|
28
|
+
sub stop_daemon (;$) {
|
|
29
|
+
my ($is_END) = @_;
|
|
30
|
+
kill('TERM', $daemon_pid);
|
|
31
|
+
my $tries = 1000;
|
|
32
|
+
while (CORE::kill(0, $daemon_pid) && --$tries) { sleep(0.01) }
|
|
33
|
+
if ($is_END && CORE::kill(0, $daemon_pid)) { # after done_testing
|
|
34
|
+
CORE::kill('KILL', $daemon_pid);
|
|
35
|
+
die "daemon_pid=$daemon_pid did not die";
|
|
36
|
+
} else {
|
|
37
|
+
ok(!CORE::kill(0, $daemon_pid), 'daemonized unicorn gone');
|
|
38
|
+
undef $daemon_pid;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
END {
|
|
43
|
+
diag slurp($err_log) if $tmpdir;
|
|
44
|
+
stop_daemon(1) if defined $daemon_pid;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
sub check_stderr () {
|
|
48
|
+
my @log = slurp($err_log);
|
|
49
|
+
diag("@log") if $ENV{V};
|
|
50
|
+
my @err = grep(!/NameError.*Unicorn::Waiter/, grep(/error/i, @log));
|
|
51
|
+
@err = grep(!/failed to set accept_filter=/, @err);
|
|
52
|
+
@err = grep(!/perhaps accf_.*? needs to be loaded/, @err);
|
|
53
|
+
is_deeply(\@err, [], 'no unexpected errors in stderr');
|
|
54
|
+
is_deeply([grep(/SIGKILL/, @log)], [], 'no SIGKILL in stderr');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sub slurp_hdr {
|
|
58
|
+
my ($c) = @_;
|
|
59
|
+
local $/ = "\r\n\r\n"; # affects both readline+chomp
|
|
60
|
+
chomp(my $hdr = readline($c));
|
|
61
|
+
my ($status, @hdr) = split(/\r\n/, $hdr);
|
|
62
|
+
diag explain([ $status, \@hdr ]) if $ENV{V};
|
|
63
|
+
($status, \@hdr);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
sub tcp_server {
|
|
67
|
+
my %opt = (
|
|
68
|
+
ReuseAddr => 1,
|
|
69
|
+
Proto => 'tcp',
|
|
70
|
+
Type => SOCK_STREAM,
|
|
71
|
+
Listen => SOMAXCONN,
|
|
72
|
+
Blocking => 0,
|
|
73
|
+
@_,
|
|
74
|
+
);
|
|
75
|
+
eval {
|
|
76
|
+
die 'IPv4-only' if $ENV{TEST_IPV4_ONLY};
|
|
77
|
+
require IO::Socket::INET6;
|
|
78
|
+
IO::Socket::INET6->new(%opt, LocalAddr => '[::1]')
|
|
79
|
+
} || eval {
|
|
80
|
+
die 'IPv6-only' if $ENV{TEST_IPV6_ONLY};
|
|
81
|
+
IO::Socket::INET->new(%opt, LocalAddr => '127.0.0.1')
|
|
82
|
+
} || BAIL_OUT "failed to create TCP server: $! ($@)";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
sub tcp_host_port {
|
|
86
|
+
my ($s) = @_;
|
|
87
|
+
my ($h, $p) = ($s->sockhost, $s->sockport);
|
|
88
|
+
my $ipv4 = $s->sockdomain == AF_INET;
|
|
89
|
+
if (wantarray) {
|
|
90
|
+
$ipv4 ? ($h, $p) : ("[$h]", $p);
|
|
91
|
+
} else {
|
|
92
|
+
$ipv4 ? "$h:$p" : "[$h]:$p";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
sub unix_start ($@) {
|
|
97
|
+
my ($dst, @req) = @_;
|
|
98
|
+
my $s = IO::Socket::UNIX->new(Peer => $dst, Type => SOCK_STREAM) or
|
|
99
|
+
BAIL_OUT "unix connect $dst: $!";
|
|
100
|
+
$s->autoflush(1);
|
|
101
|
+
print $s @req, "\r\n\r\n" if @req;
|
|
102
|
+
$s;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
sub tcp_start ($@) {
|
|
106
|
+
my ($dst, @req) = @_;
|
|
107
|
+
my $addr = tcp_host_port($dst);
|
|
108
|
+
my $s = ref($dst)->new(
|
|
109
|
+
Proto => 'tcp',
|
|
110
|
+
Type => SOCK_STREAM,
|
|
111
|
+
PeerAddr => $addr,
|
|
112
|
+
) or BAIL_OUT "failed to connect to $addr: $!";
|
|
113
|
+
$s->autoflush(1);
|
|
114
|
+
print $s @req, "\r\n\r\n" if @req;
|
|
115
|
+
$s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
sub slurp {
|
|
119
|
+
open my $fh, '<', $_[0];
|
|
120
|
+
local $/ if !wantarray;
|
|
121
|
+
readline($fh);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
sub spawn {
|
|
125
|
+
my $env = ref($_[0]) eq 'HASH' ? shift : undef;
|
|
126
|
+
my $opt = ref($_[-1]) eq 'HASH' ? pop : {};
|
|
127
|
+
my @cmd = @_;
|
|
128
|
+
my $old = POSIX::SigSet->new;
|
|
129
|
+
my $set = POSIX::SigSet->new;
|
|
130
|
+
$set->fillset or die "sigfillset: $!";
|
|
131
|
+
sigprocmask(SIG_SETMASK, $set, $old) or die "SIG_SETMASK: $!";
|
|
132
|
+
pipe(my $r, my $w);
|
|
133
|
+
my $pid = fork;
|
|
134
|
+
if ($pid == 0) {
|
|
135
|
+
close $r;
|
|
136
|
+
$SIG{__DIE__} = sub {
|
|
137
|
+
warn(@_);
|
|
138
|
+
syswrite($w, my $num = $! + 0);
|
|
139
|
+
_exit(1);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
# pretend to be systemd (cf. sd_listen_fds(3))
|
|
143
|
+
my $cfd;
|
|
144
|
+
for ($cfd = 0; ($cfd < 3) || defined($opt->{$cfd}); $cfd++) {
|
|
145
|
+
my $io = $opt->{$cfd} // next;
|
|
146
|
+
my $pfd = fileno($io);
|
|
147
|
+
if ($pfd == $cfd) {
|
|
148
|
+
fcntl($io, F_SETFD, 0);
|
|
149
|
+
} else {
|
|
150
|
+
dup2($pfd, $cfd) // die "dup2($pfd, $cfd): $!";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (($cfd - 3) > 0) {
|
|
154
|
+
$env->{LISTEN_PID} = $$;
|
|
155
|
+
$env->{LISTEN_FDS} = $cfd - 3;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (defined(my $pgid = $opt->{pgid})) {
|
|
159
|
+
setpgid(0, $pgid) // die "setpgid(0, $pgid): $!";
|
|
160
|
+
}
|
|
161
|
+
$SIG{$_} = 'DEFAULT' for grep(!/^__/, keys %SIG);
|
|
162
|
+
if (defined(my $cd = $opt->{-C})) { chdir $cd }
|
|
163
|
+
$old->delset(POSIX::SIGCHLD) or die "sigdelset CHLD: $!";
|
|
164
|
+
sigprocmask(SIG_SETMASK, $old) or die "SIG_SETMASK: ~CHLD: $!";
|
|
165
|
+
@ENV{keys %$env} = values(%$env) if $env;
|
|
166
|
+
exec { $cmd[0] } @cmd;
|
|
167
|
+
die "exec @cmd: $!";
|
|
168
|
+
}
|
|
169
|
+
close $w;
|
|
170
|
+
sigprocmask(SIG_SETMASK, $old) or die "SIG_SETMASK(old): $!";
|
|
171
|
+
if (my $cerrnum = do { local $/, <$r> }) {
|
|
172
|
+
$! = $cerrnum;
|
|
173
|
+
die "@cmd PID=$pid died: $!";
|
|
174
|
+
}
|
|
175
|
+
$pid;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
sub which {
|
|
179
|
+
my ($file) = @_;
|
|
180
|
+
return $file if index($file, '/') >= 0;
|
|
181
|
+
for my $p (split(/:/, $ENV{PATH})) {
|
|
182
|
+
$p .= "/$file";
|
|
183
|
+
return $p if -x $p;
|
|
184
|
+
}
|
|
185
|
+
undef;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# returns an AutoReap object
|
|
189
|
+
sub unicorn {
|
|
190
|
+
my %env;
|
|
191
|
+
if (ref($_[0]) eq 'HASH') {
|
|
192
|
+
my $e = shift;
|
|
193
|
+
%env = %$e;
|
|
194
|
+
}
|
|
195
|
+
my @args = @_;
|
|
196
|
+
push(@args, {}) if ref($args[-1]) ne 'HASH';
|
|
197
|
+
$args[-1]->{2} //= $errfh; # stderr default
|
|
198
|
+
|
|
199
|
+
state $ruby = which($ENV{RUBY} // 'ruby');
|
|
200
|
+
state $lib = File::Spec->rel2abs('lib');
|
|
201
|
+
state $ver = $ENV{TEST_RUBY_VERSION} // `$ruby -e 'print RUBY_VERSION'`;
|
|
202
|
+
state $eng = $ENV{TEST_RUBY_ENGINE} // `$ruby -e 'print RUBY_ENGINE'`;
|
|
203
|
+
state $ext = File::Spec->rel2abs("test/$eng-$ver/ext/unicorn_http");
|
|
204
|
+
state $exe = File::Spec->rel2abs('bin/unicorn');
|
|
205
|
+
my $pid = spawn(\%env, $ruby, '-I', $lib, '-I', $ext, $exe, @args);
|
|
206
|
+
UnicornTest::AutoReap->new($pid);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
sub do_req ($@) {
|
|
210
|
+
my ($dst, @req) = @_;
|
|
211
|
+
my $c = ref($dst) ? tcp_start($dst, @req) : unix_start($dst, @req);
|
|
212
|
+
return $c if !wantarray;
|
|
213
|
+
my ($status, $hdr);
|
|
214
|
+
# read headers iff HTTP/1.x request, HTTP/0.9 remains supported
|
|
215
|
+
my ($first) = (join('', @req) =~ m!\A([^\r\n]+)!);
|
|
216
|
+
($status, $hdr) = slurp_hdr($c) if $first =~ m{\s*HTTP/\S+$};
|
|
217
|
+
my $bdy = do { local $/; <$c> };
|
|
218
|
+
close $c;
|
|
219
|
+
($status, $hdr, $bdy);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# automatically kill + reap children when this goes out-of-scope
|
|
223
|
+
package UnicornTest::AutoReap;
|
|
224
|
+
use v5.14;
|
|
225
|
+
use autodie;
|
|
226
|
+
|
|
227
|
+
sub new {
|
|
228
|
+
my (undef, $pid) = @_;
|
|
229
|
+
bless { pid => $pid, owner => $$ }, __PACKAGE__
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
sub do_kill {
|
|
233
|
+
my ($self, $sig) = @_;
|
|
234
|
+
kill($sig // 'TERM', $self->{pid});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
sub join {
|
|
238
|
+
my ($self, $sig) = @_;
|
|
239
|
+
my $pid = delete $self->{pid} or return;
|
|
240
|
+
kill($sig, $pid) if defined $sig;
|
|
241
|
+
my $ret = waitpid($pid, 0);
|
|
242
|
+
$ret == $pid or die "BUG: waitpid($pid) != $ret";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
sub DESTROY {
|
|
246
|
+
my ($self) = @_;
|
|
247
|
+
return if $self->{owner} != $$;
|
|
248
|
+
$self->join('TERM');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
package main; # inject ourselves into the t/*.t script
|
|
252
|
+
UnicornTest->import;
|
|
253
|
+
Test::More->import;
|
|
254
|
+
# try to ensure ->DESTROY fires:
|
|
255
|
+
$SIG{TERM} = sub { exit(15 + 128) };
|
|
256
|
+
$SIG{INT} = sub { exit(2 + 128) };
|
|
257
|
+
$SIG{PIPE} = sub { exit(13 + 128) };
|
|
258
|
+
1;
|
data/t/listener_names.ru
ADDED