File Coverage

blib/lib/Server/Starter.pm
Criterion Covered Total %
statement 281 330 85.1
branch 112 186 60.2
condition 29 52 55.7
subroutine 29 32 90.6
pod 2 4 50.0
total 453 604 75.0


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