File Coverage

blib/lib/Server/Starter.pm
Criterion Covered Total %
statement 276 336 82.1
branch 113 190 59.4
condition 29 49 59.1
subroutine 28 32 87.5
pod 2 4 50.0
total 448 611 73.3


line stmt bran cond sub pod time code
1             package Server::Starter;
2              
3 64     64   7193019 use 5.008;
  64         482  
4 64     64   297 use strict;
  64         95  
  64         1158  
5 64     64   233 use warnings;
  64         94  
  64         1458  
6 64     64   296 use Carp;
  64         91  
  64         3094  
7 64     64   381 use Fcntl;
  64         92  
  64         12396  
8 64     64   6378 use IO::Handle;
  64         62962  
  64         2284  
9 64     64   6621 use IO::Socket::UNIX;
  64         194403  
  64         602  
10 64     64   32472 use POSIX qw(:sys_wait_h);
  64         79981  
  64         360  
11 64     64   28903 use Socket ();
  64         132  
  64         1205  
12 64     64   23358 use Server::Starter::Guard;
  64         140  
  64         1611  
13 64     64   321 use Fcntl qw(:flock);
  64         89  
  64         4875  
14              
15 64     64   336 use Exporter qw(import);
  64         114  
  64         235496  
16              
17             our $VERSION = '0.35';
18             our @EXPORT_OK = qw(start_server restart_server stop_server server_ports);
19              
20             my @signals_received;
21              
22             sub start_server {
23 49 50   49 1 108845078 my $opts = {
24             (@_ == 1 ? @$_[0] : @_),
25             };
26             $opts->{interval} = 1
27 49 50       2018 if not defined $opts->{interval};
28 49   100     2698 $opts->{signal_on_hup} ||= 'TERM';
29 49   50     2072 $opts->{signal_on_term} ||= 'TERM';
30 49   50     1719 $opts->{backlog} ||= Socket::SOMAXCONN();
31 49         511 for ($opts->{signal_on_hup}, $opts->{signal_on_term}) {
32             # normalize to the one that can be passed to kill
33 98         1396 tr/a-z/A-Z/;
34 98         2132 s/^SIG//i;
35             }
36              
37             # prepare args
38 49         585 my $ports = $opts->{port};
39 49         344 my $paths = $opts->{path};
40 49 100 66     1573 $ports = [ $ports ]
41             if ! ref $ports && defined $ports;
42 49 100 66     1278 $paths = [ $paths ]
43             if ! ref $paths && defined $paths;
44             croak "mandatory option ``exec'' is missing or is not an arrayref\n"
45 49 50 33     1492 unless $opts->{exec} && ref $opts->{exec} eq 'ARRAY';
46              
47             # set envs
48             $ENV{ENVDIR} = $opts->{envdir}
49 49 100       609 if defined $opts->{envdir};
50             $ENV{ENABLE_AUTO_RESTART} = $opts->{enable_auto_restart}
51 49 100       2056 if defined $opts->{enable_auto_restart};
52             $ENV{KILL_OLD_DELAY} = $opts->{kill_old_delay}
53 49 100       753 if defined $opts->{kill_old_delay};
54             $ENV{AUTO_RESTART_INTERVAL} = $opts->{auto_restart_interval}
55 49 100       848 if defined $opts->{auto_restart_interval};
56              
57 49         1444 my %loaded_env = _load_env();
58 49         533 my @loaded_env_keys = keys %loaded_env;
59 49         251 local @ENV{@loaded_env_keys} = map { $loaded_env{$_} } (@loaded_env_keys);
  4         64  
60              
61             # open log file
62 49         250 my $logfh;
63 49 50       293 if ($opts->{log_file}) {
64 0 0       0 if ($opts->{log_file} =~ /^\s*\|\s*/s) {
65 0         0 my $cmd = $';
66 0 0       0 open $logfh, '|-', $cmd
67             or die "failed to open pipe:$opts->{log_file}: $!";
68             } else {
69             open $logfh, '>>', $opts->{log_file}
70 0 0       0 or die "failed to open log file:$opts->{log_file}: $!";
71             }
72 0         0 $logfh->autoflush(1);
73             }
74            
75             # create guard that removes the status file
76 49         229 my $status_file_created;
77             my $status_file_guard = $opts->{status_file} && Server::Starter::Guard->new(
78             sub {
79 7     7   40 if ($status_file_created) {
80 7         766 unlink $opts->{status_file};
81             }
82             },
83 49   66     2032 );
84            
85 49         6566 print STDERR "start_server (pid:$$) starting now...\n";
86            
87             # start listening, setup envvar
88 49         340 my @sock;
89             my @sockenv;
90 49         593 for my $hostport (@$ports) {
91 41         210 my ($domain, $sa);
92 41         177 my $socktype = Socket::SOCK_STREAM();
93 41         108 my $fd;
94 41     41   960 my $sockopts = sub {};
95 41 100       615 if ($hostport =~ /^\s*(u?)(\d+)(?:\s*=(\d+))?\s*$/) {
    50          
96             # by default, only bind to IPv4 (for compatibility)
97 39         1428 ($hostport, $fd) = ($2, $3);
98 39 100       694 $socktype = Socket::SOCK_DGRAM()
99             if $1;
100 39         144 $domain = Socket::PF_INET;
101 39         1493 $sa = pack_sockaddr_in $hostport, Socket::inet_aton("0.0.0.0");
102             } elsif ($hostport =~ /^\s*(?:\[\s*|)([^\]]*)\s*(?:\]\s*|):\s*(u?)(\d+)(?:\s*=(\d+))?\s*$/) {
103 2         172 my ($host, $port) = ($1, $3);
104 2         106 $fd = $4;
105 2 50       40 $socktype = Socket::SOCK_DGRAM()
106             if $2;
107 2 50       22 if ($host =~ /:/) {
108             # IPv6
109 0         0 local $@;
110 0         0 eval {
111 0         0 $hostport = "[$host]:$port";
112 0 0       0 my $addr = Socket::inet_pton(Socket::AF_INET6(), $host)
113             or die "failed to resolve host:$host:$!";
114 0         0 $sa = Socket::pack_sockaddr_in6($port, $addr);
115 0         0 $domain = Socket::PF_INET6();
116             };
117 0 0       0 if ($@) {
118 0         0 die "No support for IPv6. Please update Perl (or Perl modules)";
119             }
120             $sockopts = sub {
121 0     0   0 my $sock = shift;
122 0         0 local $@;
123 0         0 eval {
124 0         0 setsockopt $sock, Socket::IPPROTO_IPV6(), Socket::IPV6_V6ONLY(), 1;
125             };
126 0         0 };
127             } else {
128             # IPv4
129 2         14 $domain = Socket::PF_INET;
130 2         10 $hostport = "$host:$port";
131 2 50       336 my $addr = gethostbyname $host
132             or die "failed to resolve host:$host:$!";
133 2         34 $sa = Socket::pack_sockaddr_in($port, $addr);
134             }
135             } else {
136 0         0 croak "invalid ``port'' value:$hostport\n"
137             }
138 41 50       4518 socket my $sock, $domain, $socktype, 0
139             or die "failed to create socket:$!";
140 41         413 setsockopt $sock, Socket::SOL_SOCKET, Socket::SO_REUSEADDR(), pack("l", 1);
141 41         219 $sockopts->($sock);
142 41 50       614 bind $sock, $sa
143             or die "failed to bind to $hostport:$!";
144 41 100       265 if ($socktype != Socket::SOCK_DGRAM()) {
145             listen $sock, $opts->{backlog}
146 37 50       595 or die "listen(2) failed:$!";
147             }
148 41 50       685 fcntl($sock, F_SETFD, my $flags = '')
149             or die "fcntl(F_SETFD, 0) failed:$!";
150 41 100       246 if (defined $fd) {
151 2 50       106 POSIX::dup2($sock->fileno, $fd)
152             or die "dup2(2) failed(${fd}): $!";
153 2         196 print STDERR "socket is duplicated to file descriptor ${fd}\n";
154 2         20 close $sock;
155 2         10 push @sockenv, "$hostport=$fd";
156             } else {
157 39         2168 push @sockenv, "$hostport=" . $sock->fileno;
158             }
159 41         1108 push @sock, $sock;
160             }
161             my $path_remove_guard = Server::Starter::Guard->new(
162             sub {
163             -S $_ and unlink $_
164 16   33 16   1691 for @$paths;
165             },
166 49         2241 );
167 49         278 for my $path (@$paths) {
168 6 50       198 if (-S $path) {
169 0         0 warn "removing existing socket file:$path";
170 0 0       0 unlink $path
171             or die "failed to remove existing socket file:$path:$!";
172             }
173 6         52 unlink $path;
174 6         344 my $saved_umask = umask(0);
175             my $sock = IO::Socket::UNIX->new(
176             Listen => $opts->{backlog},
177 6 50       760 Local => $path,
178             ) or die "failed to listen to file $path:$!";
179 6         6490 umask($saved_umask);
180 6 50       92 fcntl($sock, F_SETFD, my $flags = '')
181             or die "fcntl(F_SETFD, 0) failed:$!";
182 6         126 push @sockenv, "$path=" . $sock->fileno;
183 6         108 push @sock, $sock;
184             }
185 49         1845 $ENV{SERVER_STARTER_PORT} = join ";", @sockenv;
186 49         837 $ENV{SERVER_STARTER_GENERATION} = 0;
187            
188             # setup signal handlers
189             _set_sighandler($_, sub {
190 61     61   470 push @signals_received, $_[0];
191 49         1520 }) for (qw/INT TERM HUP ALRM/);
192 49         1075 $SIG{PIPE} = 'IGNORE';
193            
194             # setup status monitor
195 49         1331 my ($current_worker, %old_workers, $last_restart_time);
196             my $update_status = $opts->{status_file}
197             ? sub {
198 40     40   794 my $tmpfn = "$opts->{status_file}.$$";
199 40 50       41986 open my $tmpfh, '>', $tmpfn
200             or die "failed to create temporary file:$tmpfn:$!";
201 40         271 $status_file_created = 1;
202             my %gen_pid = (
203             ($current_worker
204             ? ($ENV{SERVER_STARTER_GENERATION} => $current_worker)
205             : ()),
206 40 100       714 map { $old_workers{$_} => $_ } keys %old_workers,
  9         571  
207             );
208             print $tmpfh "$_:$gen_pid{$_}\n"
209 40         992 for sort keys %gen_pid;
210 40         1332 close $tmpfh;
211             rename $tmpfn, $opts->{status_file}
212 40 50       130323 or die "failed to rename $tmpfn to $opts->{status_file}:$!";
213       25     } : sub {
214 49 100       890 };
215              
216             # now that setup is complete, redirect outputs to the log file (if specified)
217 49 50       255 if ($logfh) {
218 0         0 STDOUT->flush;
219 0         0 STDERR->flush;
220 0 0       0 open STDOUT, '>&=', $logfh
221             or die "failed to dup STDOUT to file: $!";
222 0 0       0 open STDERR, '>&=', $logfh
223             or die "failed to dup STDERR to file: $!";
224 0         0 close $logfh;
225 0         0 undef $logfh;
226             }
227              
228             # daemonize
229 49 100       252 if ($opts->{daemonize}) {
230 8         20233 my $pid = fork;
231 8 50       575 die "fork failed:$!"
232             unless defined $pid;
233 8 100       151 if ($pid != 0) {
234 2         186 $path_remove_guard->dismiss;
235 2         217 exit 0;
236             }
237             # in child process
238 6         756 POSIX::setsid();
239 6         7520 $pid = fork;
240 6 50       275 die "fork failed:$!"
241             unless defined $pid;
242 6 100       1265 if ($pid != 0) {
243 2         121 $path_remove_guard->dismiss;
244 2         147 exit 0;
245             }
246             # do not close STDIN if `--port=n=0`.
247 4 50       354 unless (grep /=0$/, @sockenv) {
248 4         134 close STDIN;
249 4 50       230 open STDIN, '<', '/dev/null'
250             or die "reopen failed: $!";
251             }
252             }
253              
254             # open pid file
255             my $pid_file_guard = sub {
256 45 100   45   376 return unless $opts->{pid_file};
257             open my $fh, '>', $opts->{pid_file}
258 4 50       992 or die "failed to open file:$opts->{pid_file}: $!";
259 4 50       114 flock($fh, LOCK_EX)
260             or die "flock failed($opts->{pid_file}): $!";
261 4         174 print $fh "$$\n";
262 4         416 $fh->flush();
263             return Server::Starter::Guard->new(
264             sub {
265             unlink $opts->{pid_file}
266 2 50       118 or warn "failed to unlink file:$opts->{pid_file}:$!";
267 2         186 close $fh;
268             },
269 4         304 );
270 45         973 }->();
271              
272             # setup the start_worker function
273             my $start_worker = sub {
274 68     68   160 my $pid;
275 68         214 while (1) {
276 78         921 $ENV{SERVER_STARTER_GENERATION}++;
277 78         96435 $pid = fork;
278 78 50       3791 die "fork(2) failed:$!"
279             unless defined $pid;
280 78 100       2250 if ($pid == 0) {
281 29         935 my @args = @{$opts->{exec}};
  29         1758  
282             # child process
283 29 100       826 if (defined $opts->{dir}) {
284 2 50       151 chdir $opts->{dir} or die "failed to chdir:$opts->{dir}:$!";
285             }
286 29         412 { exec { $args[0] } @args };
  29         267  
  29         0  
287 0         0 print STDERR "failed to exec $args[0]$!";
288 0         0 exit(255);
289             }
290 49         3764 print STDERR "starting new worker $pid\n";
291 49         46217596 sleep $opts->{interval};
292 49 100 100     3298 if ((grep { $_ ne 'HUP' } @signals_received)
  7         252  
293             || waitpid($pid, WNOHANG) <= 0) {
294 39         425 last;
295             }
296 10         692 print STDERR "new worker $pid seems to have failed to start, exit status:$?\n";
297             }
298             # ready, update the environment
299 39         395 $current_worker = $pid;
300 39         258 $last_restart_time = time;
301 39         620 $update_status->();
302 45         1061 };
303              
304             # setup the wait function
305             my $wait = sub {
306 72     72   423 my $block = @signals_received == 0;
307 72         225 my @r;
308 72 100 100     819 if ($block && $ENV{ENABLE_AUTO_RESTART}) {
309 32         246 alarm(1);
310 32         224 @r = _wait3($block);
311 32         232 alarm(0);
312             } else {
313 40         555 @r = _wait3($block);
314             }
315 72         865 return @r;
316 45         902 };
317              
318             # setup the cleanup function
319             my $cleanup = sub {
320 16     16   77 my $sig = shift;
321 16 50       297 my $term_signal = $sig eq 'TERM' ? $opts->{signal_on_term} : 'TERM';
322 16         342 $old_workers{$current_worker} = $ENV{SERVER_STARTER_GENERATION};
323 16         137 undef $current_worker;
324 16         2666 print STDERR "received $sig, sending $term_signal to all workers:",
325             join(',', sort keys %old_workers), "\n";
326             kill $term_signal, $_
327 16         738 for sort keys %old_workers;
328 16         121 while (%old_workers) {
329 16 50       162 if (my @r = _wait3(1)) {
330 16         110 my ($died_worker, $status) = @r;
331 16         1258 print STDERR "worker $died_worker died, status:$status\n";
332 16         108 delete $old_workers{$died_worker};
333 16         195 $update_status->();
334             }
335             }
336 16         2235 print STDERR "exiting\n";
337 45         587 };
338              
339             # the main loop
340 45         298 $start_worker->();
341 28         108 while (1) {
342             # wait for next signal (or when auto-restart becomes necessary)
343 72         409 my @r = $wait->();
344             # reload env if necessary
345 72         548 my %loaded_env = _load_env();
346 72         708 my @loaded_env_keys = keys %loaded_env;
347 72         760 local @ENV{@loaded_env_keys} = map { $loaded_env{$_} } (@loaded_env_keys);
  5         65  
348             $ENV{AUTO_RESTART_INTERVAL} ||= 360
349 72 100 50     472 if $ENV{ENABLE_AUTO_RESTART};
350             # restart if worker died
351 72 100       425 if (@r) {
352 12         69 my ($died_worker, $status) = @r;
353 12 100       123 if ($died_worker == $current_worker) {
354 2         44 print STDERR "worker $died_worker died unexpectedly with status:$status, restarting\n";
355 2         20 $start_worker->();
356             } else {
357 10         952 print STDERR "old worker $died_worker died, status:$status\n";
358 10         146 delete $old_workers{$died_worker};
359 10         90 $update_status->();
360             }
361             }
362             # handle signals
363 71         285 my $restart;
364 71         366 while (@signals_received) {
365 61         452 my $sig = shift @signals_received;
366 61 100       364 if ($sig eq 'HUP') {
    100          
367 17         1870 print STDERR "received HUP, spawning a new worker\n";
368 17         101 $restart = 1;
369 17         53 last;
370             } elsif ($sig eq 'ALRM') {
371             # skip
372             } else {
373 16         155 return $cleanup->($sig);
374             }
375             }
376 55 100 100     434 if (! $restart && $ENV{ENABLE_AUTO_RESTART}) {
377 30         106 my $auto_restart_interval = $ENV{AUTO_RESTART_INTERVAL};
378 30         78 my $elapsed_since_restart = time - $last_restart_time;
379 30 100 66     199 if ($elapsed_since_restart >= $auto_restart_interval && ! %old_workers) {
    50          
380 4         160 print STDERR "autorestart triggered (interval=$auto_restart_interval)\n";
381 4         20 $restart = 1;
382             } elsif ($elapsed_since_restart >= $auto_restart_interval * 2) {
383 0         0 print STDERR "autorestart triggered (forced, interval=$auto_restart_interval)\n";
384 0         0 $restart = 1;
385             }
386             }
387             # restart if requested
388 55 100       216 if ($restart) {
389 21         233 $old_workers{$current_worker} = $ENV{SERVER_STARTER_GENERATION};
390 21         101 $start_worker->();
391 10         1098 print STDERR "new worker is now running, sending $opts->{signal_on_hup} to old workers:";
392 10 50       159 if (%old_workers) {
393 10         814 print STDERR join(',', sort keys %old_workers), "\n";
394             } else {
395 0         0 print STDERR "none\n";
396             }
397 10 50       168 my $kill_old_delay = defined $ENV{KILL_OLD_DELAY} ? $ENV{KILL_OLD_DELAY} : $ENV{ENABLE_AUTO_RESTART} ? 5 : 0;
    100          
398 10 100       140 if ($kill_old_delay != 0) {
399 4         203 print STDERR "sleeping $kill_old_delay secs before killing old workers\n";
400 4         33 while ($kill_old_delay > 0) {
401 4   50     10000827 $kill_old_delay -= sleep $kill_old_delay || 1;
402             }
403             }
404 10         1129 print STDERR "killing old workers\n";
405             kill $opts->{signal_on_hup}, $_
406 10         1636 for sort keys %old_workers;
407             }
408             }
409              
410 0         0 die "unreachable";
411             }
412              
413             sub restart_server {
414 0 0   0 0 0 my $opts = {
415             (@_ == 1 ? @$_[0] : @_),
416             };
417             die "--restart option requires --pid-file and --status-file to be set as well\n"
418 0 0 0     0 unless $opts->{pid_file} && $opts->{status_file};
419            
420             # get pid
421 0         0 my $pid = do {
422             open my $fh, '<', $opts->{pid_file}
423 0 0       0 or die "failed to open file:$opts->{pid_file}:$!";
424 0         0 my $line = <$fh>;
425 0         0 chomp $line;
426 0         0 $line;
427             };
428            
429             # function that returns a list of active generations in sorted order
430             my $get_generations = sub {
431             open my $fh, '<', $opts->{status_file}
432 0 0   0   0 or die "failed to open file:$opts->{status_file}:$!";
433 0         0 my %gen;
434 0         0 while (my $line = <$fh>) {
435 0 0       0 if ($line =~ /^(\d+):/) {
436 0         0 $gen{$1} = 1;
437             }
438             }
439 0         0 sort { $a <=> $b } keys %gen;
  0         0  
440 0         0 };
441            
442             # wait for this generation
443 0         0 my $wait_for = do {
444 0 0       0 my @gens = $get_generations->()
445             or die "no active process found in the status file";
446 0         0 pop(@gens) + 1;
447             };
448            
449             # send HUP
450 0 0       0 kill 'HUP', $pid
451             or die "failed to send SIGHUP to the server process:$!";
452            
453             # wait for the generation
454 0         0 while (1) {
455 0         0 my @gens = $get_generations->();
456 0 0 0     0 last if scalar(@gens) == 1 && $gens[0] == $wait_for;
457 0         0 sleep 1;
458             }
459             }
460              
461             sub stop_server {
462 2 50   2 0 1029869 my $opts = {
463             (@_ == 1 ? @$_[0] : @_),
464             };
465             die "--stop option requires --pid-file to be set as well\n"
466 2 50       23 unless $opts->{pid_file};
467              
468             # get pid
469             open my $fh, '+<', $opts->{pid_file}
470 2 50       174 or die "failed to open file:$opts->{pid_file}:$!";
471 2         13 my $pid = do {
472 2         40 my $line = <$fh>;
473 2         15 chomp $line;
474 2         11 $line;
475             };
476              
477 2         189 print STDERR "stop_server (pid:$$) stopping now (pid:$pid)...\n";
478              
479             # send TERM
480 2 50       106 kill 'TERM', $pid
481             or die "failed to send SIGTERM to the server process:$!";
482              
483             # wait process
484 2 50       3872 flock($fh, LOCK_EX)
485             or die "flock failed($opts->{pid_file}): $!";
486 2         1012 close $fh;
487             }
488              
489             sub server_ports {
490             die "no environment variable SERVER_STARTER_PORT. Did you start the process using server_starter?",
491 0 0   0 1 0 unless defined $ENV{SERVER_STARTER_PORT};
492             my %ports = map {
493 0         0 +(split /=/, $_, 2)
494 0         0 } split /;/, $ENV{SERVER_STARTER_PORT};
495 0         0 \%ports;
496             }
497              
498             sub _load_env {
499 121     121   9990 my $dn = $ENV{ENVDIR};
500 121 100 66     1347 return if !defined $dn or !-d $dn;
501 13         33 my $d;
502 13 50       888 opendir($d, $dn) or return;
503 13         30 my %env;
504 13         369 while (my $n = readdir($d)) {
505 35 100       553 next if $n =~ /^\./;
506 9 50       392 open my $fh, '<', "$dn/$n" or next;
507 9         264 chomp(my $v = <$fh>);
508 9 50       191 $env{$n} = $v if defined $v;
509             }
510 13         255 return %env;
511             }
512              
513             our $sighandler_should_die;
514             my $sighandler_got_sig;
515              
516             sub _set_sighandler {
517 197     197   776 my ($sig, $proc) = @_;
518             $SIG{$sig} = sub {
519 63     63   1133 $proc->(@_);
520 63         627 $sighandler_got_sig = 1;
521 63 100       1440 die "got signal"
522             if $sighandler_should_die;
523 197         12912 };
524             }
525              
526             sub _wait3 {
527 92     92   7390 my $block = shift;
528 92         246 my $pid = -1;
529 92 100       354 if ($block) {
530 85         327 local $@;
531 85         342 eval {
532 85         296 $sighandler_got_sig = 0;
533 85         452 local $sighandler_should_die = 1;
534 85 50       393 die "exit from eval"
535             if $sighandler_got_sig;
536 85         81470103 $pid = wait();
537             };
538 85 50 66     1372 if ($pid == -1 && $@) {
539 56         794 $! = Errno::EINTR;
540             }
541             } else {
542 7         75 $pid = waitpid(-1, WNOHANG);
543             }
544 92 100       1082 return $pid > 0 ? ($pid, $?) : ();
545             }
546              
547             1;
548             __END__