unicorn-maintained 6.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. checksums.yaml +7 -0
  2. data/.CHANGELOG.old +25 -0
  3. data/.document +28 -0
  4. data/.gitattributes +5 -0
  5. data/.gitignore +25 -0
  6. data/.mailmap +26 -0
  7. data/.manifest +149 -0
  8. data/.olddoc.yml +25 -0
  9. data/Application_Timeouts +77 -0
  10. data/CONTRIBUTORS +39 -0
  11. data/COPYING +674 -0
  12. data/DESIGN +99 -0
  13. data/Documentation/.gitignore +3 -0
  14. data/Documentation/unicorn.1 +222 -0
  15. data/Documentation/unicorn_rails.1 +207 -0
  16. data/FAQ +70 -0
  17. data/GIT-VERSION-FILE +1 -0
  18. data/GIT-VERSION-GEN +39 -0
  19. data/GNUmakefile +317 -0
  20. data/HACKING +112 -0
  21. data/ISSUES +102 -0
  22. data/KNOWN_ISSUES +79 -0
  23. data/LATEST +1 -0
  24. data/LICENSE +67 -0
  25. data/Links +58 -0
  26. data/NEWS +1 -0
  27. data/PHILOSOPHY +139 -0
  28. data/README +156 -0
  29. data/Rakefile +16 -0
  30. data/SIGNALS +123 -0
  31. data/Sandbox +104 -0
  32. data/TODO +3 -0
  33. data/TUNING +119 -0
  34. data/archive/.gitignore +3 -0
  35. data/archive/slrnpull.conf +4 -0
  36. data/bin/unicorn +128 -0
  37. data/bin/unicorn_rails +209 -0
  38. data/examples/big_app_gc.rb +2 -0
  39. data/examples/echo.ru +26 -0
  40. data/examples/init.sh +102 -0
  41. data/examples/logger_mp_safe.rb +25 -0
  42. data/examples/logrotate.conf +44 -0
  43. data/examples/nginx.conf +156 -0
  44. data/examples/unicorn.conf.minimal.rb +13 -0
  45. data/examples/unicorn.conf.rb +110 -0
  46. data/examples/unicorn.socket +11 -0
  47. data/examples/unicorn@.service +40 -0
  48. data/ext/unicorn_http/CFLAGS +13 -0
  49. data/ext/unicorn_http/c_util.h +116 -0
  50. data/ext/unicorn_http/common_field_optimization.h +128 -0
  51. data/ext/unicorn_http/epollexclusive.h +128 -0
  52. data/ext/unicorn_http/ext_help.h +38 -0
  53. data/ext/unicorn_http/extconf.rb +39 -0
  54. data/ext/unicorn_http/global_variables.h +97 -0
  55. data/ext/unicorn_http/httpdate.c +91 -0
  56. data/ext/unicorn_http/unicorn_http.c +4334 -0
  57. data/ext/unicorn_http/unicorn_http.rl +1040 -0
  58. data/ext/unicorn_http/unicorn_http_common.rl +76 -0
  59. data/lib/unicorn/app/old_rails/static.rb +59 -0
  60. data/lib/unicorn/app/old_rails.rb +35 -0
  61. data/lib/unicorn/cgi_wrapper.rb +147 -0
  62. data/lib/unicorn/configurator.rb +748 -0
  63. data/lib/unicorn/const.rb +21 -0
  64. data/lib/unicorn/http_request.rb +201 -0
  65. data/lib/unicorn/http_response.rb +93 -0
  66. data/lib/unicorn/http_server.rb +859 -0
  67. data/lib/unicorn/launcher.rb +62 -0
  68. data/lib/unicorn/oob_gc.rb +81 -0
  69. data/lib/unicorn/preread_input.rb +33 -0
  70. data/lib/unicorn/select_waiter.rb +6 -0
  71. data/lib/unicorn/socket_helper.rb +185 -0
  72. data/lib/unicorn/stream_input.rb +151 -0
  73. data/lib/unicorn/tee_input.rb +131 -0
  74. data/lib/unicorn/tmpio.rb +33 -0
  75. data/lib/unicorn/util.rb +90 -0
  76. data/lib/unicorn/version.rb +1 -0
  77. data/lib/unicorn/worker.rb +165 -0
  78. data/lib/unicorn.rb +136 -0
  79. data/man/man1/unicorn.1 +222 -0
  80. data/man/man1/unicorn_rails.1 +207 -0
  81. data/setup.rb +1586 -0
  82. data/t/.gitignore +4 -0
  83. data/t/GNUmakefile +5 -0
  84. data/t/README +49 -0
  85. data/t/active-unix-socket.t +117 -0
  86. data/t/bin/unused_listen +40 -0
  87. data/t/broken-app.ru +12 -0
  88. data/t/client_body_buffer_size.ru +14 -0
  89. data/t/client_body_buffer_size.t +80 -0
  90. data/t/detach.ru +11 -0
  91. data/t/env.ru +3 -0
  92. data/t/fails-rack-lint.ru +5 -0
  93. data/t/heartbeat-timeout.ru +12 -0
  94. data/t/heartbeat-timeout.t +62 -0
  95. data/t/integration.ru +115 -0
  96. data/t/integration.t +356 -0
  97. data/t/lib.perl +258 -0
  98. data/t/listener_names.ru +4 -0
  99. data/t/my-tap-lib.sh +201 -0
  100. data/t/oob_gc.ru +17 -0
  101. data/t/oob_gc_path.ru +17 -0
  102. data/t/pid.ru +3 -0
  103. data/t/preread_input.ru +22 -0
  104. data/t/reload-bad-config.t +54 -0
  105. data/t/reopen-logs.ru +13 -0
  106. data/t/reopen-logs.t +39 -0
  107. data/t/t0008-back_out_of_upgrade.sh +110 -0
  108. data/t/t0009-broken-app.sh +56 -0
  109. data/t/t0010-reap-logging.sh +55 -0
  110. data/t/t0012-reload-empty-config.sh +86 -0
  111. data/t/t0013-rewindable-input-false.sh +24 -0
  112. data/t/t0013.ru +12 -0
  113. data/t/t0014-rewindable-input-true.sh +24 -0
  114. data/t/t0014.ru +12 -0
  115. data/t/t0015-configurator-internals.sh +25 -0
  116. data/t/t0020-at_exit-handler.sh +49 -0
  117. data/t/t0021-process_detach.sh +29 -0
  118. data/t/t0022-listener_names-preload_app.sh +32 -0
  119. data/t/t0300-no-default-middleware.sh +20 -0
  120. data/t/t0301-no-default-middleware-ignored-in-config.sh +25 -0
  121. data/t/t0301.ru +13 -0
  122. data/t/t9001-oob_gc.sh +47 -0
  123. data/t/t9002-oob_gc-path.sh +75 -0
  124. data/t/test-lib.sh +125 -0
  125. data/t/winch_ttin.t +67 -0
  126. data/t/working_directory.t +94 -0
  127. data/test/aggregate.rb +15 -0
  128. data/test/benchmark/README +60 -0
  129. data/test/benchmark/dd.ru +18 -0
  130. data/test/benchmark/ddstream.ru +50 -0
  131. data/test/benchmark/readinput.ru +40 -0
  132. data/test/benchmark/stack.ru +8 -0
  133. data/test/benchmark/uconnect.perl +66 -0
  134. data/test/exec/README +5 -0
  135. data/test/exec/test_exec.rb +1029 -0
  136. data/test/test_helper.rb +306 -0
  137. data/test/unit/test_ccc.rb +91 -0
  138. data/test/unit/test_configurator.rb +175 -0
  139. data/test/unit/test_droplet.rb +28 -0
  140. data/test/unit/test_http_parser.rb +884 -0
  141. data/test/unit/test_http_parser_ng.rb +714 -0
  142. data/test/unit/test_request.rb +169 -0
  143. data/test/unit/test_server.rb +244 -0
  144. data/test/unit/test_signals.rb +188 -0
  145. data/test/unit/test_socket_helper.rb +159 -0
  146. data/test/unit/test_stream_input.rb +210 -0
  147. data/test/unit/test_tee_input.rb +303 -0
  148. data/test/unit/test_util.rb +131 -0
  149. data/test/unit/test_waiter.rb +34 -0
  150. data/unicorn.gemspec +48 -0
  151. 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;
@@ -0,0 +1,4 @@
1
+ use Rack::ContentLength
2
+ use Rack::ContentType, "text/plain"
3
+ names = Unicorn.listener_names.inspect # rely on preload_app=true
4
+ run(lambda { |_| [ 200, {}, [ names ] ] })