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.
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 ] ] })