unicorn-maintained 6.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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