2 # Copyright (C) unicorn hackers <unicorn-public@80x24.org>
3 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
6 use parent
qw(Exporter);
9 use Socket
qw(SOMAXCONN);
10 use Time
::HiRes
qw(sleep time);
14 use POSIX
qw(dup2 _exit setpgid :signal_h SEEK_SET F_SETFD);
15 use File
::Temp
0.19 (); # 0.19 for ->newdir
16 our ($tmpdir, $errfh, $err_log, $u_sock, $u_conf, $daemon_pid,
17 $pid_file, $wtest_sock, $fifo);
18 our @EXPORT = qw(unicorn slurp tcp_server tcp_start unicorn
19 $tmpdir $errfh $err_log $u_sock $u_conf $daemon_pid $pid_file
21 SEEK_SET tcp_host_port which spawn check_stderr unix_start slurp_hdr
22 do_req stop_daemon sleep time mkfifo_die kill_until_dead write_file);
24 my ($base) = ($0 =~ m!\b([^/]+)\.[^\.]+\z!);
25 $tmpdir = File::Temp->newdir("unicorn-$base-XXXX", TMPDIR => 1);
27 $wtest_sock = "$tmpdir/wtest.sock";
28 $err_log = "$tmpdir/err.log";
29 $pid_file = "$tmpdir/pid";
30 $fifo = "$tmpdir/fifo";
31 $u_sock = "$tmpdir/u.sock";
32 $u_conf = "$tmpdir/u.conf.rb";
33 open($errfh, '>>', $err_log);
35 if (my $t = $ENV{TAIL}) {
36 my @tail = $t =~ /tail/ ? split(/\s+/, $t) : (qw(tail -F));
40 open STDOUT
, '>&', \
*STDERR
;
42 die "exec(@tail): $!";
46 UnicornTest
::AutoReap
->new($pid);
49 sub kill_until_dead
($;%) {
51 my $tries = $opt{tries
} // 1000;
52 my $sig = $opt{sig
} // 0;
53 while (CORE
::kill($sig, $pid) && --$tries) { sleep(0.01) }
54 $tries or croak
"PID: $pid died after signal ($sig)";
57 sub stop_daemon
(;$) {
59 kill('TERM', $daemon_pid);
60 kill_until_dead
$daemon_pid;
61 if ($is_END && CORE
::kill(0, $daemon_pid)) { # after done_testing
62 CORE
::kill('KILL', $daemon_pid);
63 die "daemon_pid=$daemon_pid did not die";
65 ok
(!CORE
::kill(0, $daemon_pid), 'daemonized unicorn gone');
71 diag slurp
($err_log) if $tmpdir;
72 stop_daemon
(1) if defined $daemon_pid;
75 sub check_stderr
(@
) {
77 slurp
($err_log) if !@log;
78 diag
("@log") if $ENV{V
};
79 my @err = grep(!/NameError.*Unicorn::Waiter/, grep(/error/i, @log));
80 @err = grep(!/failed to set accept_filter=/, @err);
81 @err = grep(!/perhaps accf_.*? needs to be loaded/, @err);
82 is_deeply
(\
@err, [], 'no unexpected errors in stderr');
83 is_deeply
([grep(/SIGKILL/, @log)], [], 'no SIGKILL in stderr');
88 local $/ = "\r\n\r\n"; # affects both readline+chomp
89 chomp(my $hdr = readline($c));
90 my ($status, @hdr) = split(/\r\n/, $hdr);
91 diag explain
([ $status, \
@hdr ]) if $ENV{V
};
95 sub unix_server
(;$@
) {
96 my $l = shift // $u_sock;
97 IO
::Socket
::UNIX
->new(Listen
=> SOMAXCONN
, Local
=> $l, Blocking
=> 0,
98 Type
=> SOCK_STREAM
, @_);
101 sub unix_connect
($) {
102 IO
::Socket
::UNIX
->new(Peer
=> $_[0], Type
=> SOCK_STREAM
);
115 die 'IPv4-only' if $ENV{TEST_IPV4_ONLY
};
116 require IO
::Socket
::INET6
;
117 IO
::Socket
::INET6
->new(%opt, LocalAddr
=> '[::1]')
119 die 'IPv6-only' if $ENV{TEST_IPV6_ONLY
};
120 IO
::Socket
::INET
->new(%opt, LocalAddr
=> '127.0.0.1')
121 } || BAIL_OUT
"failed to create TCP server: $! ($@)";
126 my ($h, $p) = ($s->sockhost, $s->sockport);
127 my $ipv4 = $s->sockdomain == AF_INET
;
129 $ipv4 ?
($h, $p) : ("[$h]", $p);
131 $ipv4 ?
"$h:$p" : "[$h]:$p";
135 sub unix_start
($@
) {
136 my ($dst, @req) = @_;
137 my $s = unix_connect
($dst) or BAIL_OUT
"unix connect $dst: $!";
139 print $s @req, "\r\n\r\n" if @req;
144 my ($dst, @req) = @_;
145 my $addr = tcp_host_port
($dst);
146 my $s = ref($dst)->new(
150 ) or BAIL_OUT
"failed to connect to $addr: $!";
152 print $s @req, "\r\n\r\n" if @req;
157 open my $fh, '<', $_[0];
158 local $/ if !wantarray;
163 my $env = ref($_[0]) eq 'HASH' ?
shift : undef;
164 my $opt = ref($_[-1]) eq 'HASH' ?
pop : {};
166 my $old = POSIX
::SigSet
->new;
167 my $set = POSIX
::SigSet
->new;
168 $set->fillset or die "sigfillset: $!";
169 sigprocmask
(SIG_SETMASK
, $set, $old) or die "SIG_SETMASK: $!";
174 $SIG{__DIE__
} = sub {
176 syswrite($w, my $num = $! + 0);
180 # pretend to be systemd (cf. sd_listen_fds(3))
182 for ($cfd = 0; ($cfd < 3) || defined($opt->{$cfd}); $cfd++) {
183 my $io = $opt->{$cfd} // next;
184 my $pfd = fileno($io);
186 fcntl($io, F_SETFD
, 0);
188 dup2
($pfd, $cfd) // die "dup2($pfd, $cfd): $!";
191 if (($cfd - 3) > 0) {
192 $env->{LISTEN_PID
} = $$;
193 $env->{LISTEN_FDS
} = $cfd - 3;
196 if (defined(my $pgid = $opt->{pgid
})) {
197 setpgid
(0, $pgid) // die "setpgid(0, $pgid): $!";
199 $SIG{$_} = 'DEFAULT' for grep(!/^__/, keys %SIG);
200 if (defined(my $cd = $opt->{-C
})) { chdir $cd }
201 $old->delset(POSIX
::SIGCHLD
) or die "sigdelset CHLD: $!";
202 sigprocmask
(SIG_SETMASK
, $old) or die "SIG_SETMASK: ~CHLD: $!";
203 @ENV{keys %$env} = values(%$env) if $env;
204 exec { $cmd[0] } @cmd;
208 sigprocmask
(SIG_SETMASK
, $old) or die "SIG_SETMASK(old): $!";
209 if (my $cerrnum = do { local $/, <$r> }) {
211 die "@cmd PID=$pid died: $!";
218 return $file if index($file, '/') >= 0;
219 for my $p (split(/:/, $ENV{PATH
})) {
226 # returns an AutoReap object
229 if (ref($_[0]) eq 'HASH') {
234 push(@args, {}) if ref($args[-1]) ne 'HASH';
235 $args[-1]->{2} //= $errfh; # stderr default
237 state $ruby = which
($ENV{RUBY
} // 'ruby');
238 state $lib = File
::Spec
->rel2abs('lib');
239 state $ver = $ENV{TEST_RUBY_VERSION
} // `$ruby -e 'print RUBY_VERSION'`;
240 state $eng = $ENV{TEST_RUBY_ENGINE
} // `$ruby -e 'print RUBY_ENGINE'`;
241 state $ext = File
::Spec
->rel2abs("test/$eng-$ver/ext/unicorn_http");
242 state $exe = File
::Spec
->rel2abs("test/$eng-$ver/bin/unicorn");
243 state $rl = $ENV{RUBYLIB
} ?
"$lib:$ext:$ENV{RUBYLIB}" : "$lib:$ext";
245 my $pid = spawn
(\
%env, $ruby, $exe, @args);
246 UnicornTest
::AutoReap
->new($pid);
250 my ($dst, @req) = @_;
251 my $c = ref($dst) ? tcp_start
($dst, @req) : unix_start
($dst, @req);
252 return $c if !wantarray;
254 # read headers iff HTTP/1.x request, HTTP/0.9 remains supported
255 my ($first) = (join('', @req) =~ m!\A([^\r\n]+)!);
256 ($status, $hdr) = slurp_hdr
($c) if $first =~ m{\s*HTTP/\S+$};
257 my $bdy = do { local $/; <$c> };
259 ($status, $hdr, $bdy);
262 sub mkfifo_die
($;$) {
263 POSIX
::mkfifo
($_[0], $_[1] // 0600) or croak
"mkfifo: $!";
266 sub write_file
($$@
) { # mode, filename, LIST (for print)
267 open(my $fh, shift, shift);
269 # return $fh for futher writes if user wants it:
270 defined(wantarray) && !wantarray ?
$fh : close $fh;
273 # automatically kill + reap children when this goes out-of-scope
274 package UnicornTest
::AutoReap
;
279 my (undef, $pid) = @_;
280 bless { pid
=> $pid, owner
=> $$ }, __PACKAGE__
284 my ($self, $sig) = @_;
285 kill($sig // 'TERM', $self->{pid
});
289 my ($self, $sig) = @_;
290 my $pid = delete $self->{pid
} or return;
291 kill($sig, $pid) if defined $sig;
292 my $ret = waitpid($pid, 0);
293 $ret == $pid or die "BUG: waitpid($pid) != $ret";
298 return if $self->{owner
} != $$;
302 package main
; # inject ourselves into the t/*.t script
305 # try to ensure ->DESTROY fires:
306 $SIG{TERM
} = sub { exit(15 + 128) };
307 $SIG{INT
} = sub { exit(2 + 128) };
308 $SIG{PIPE
} = sub { exit(13 + 128) };