File Coverage

blib/lib/Feersum/Runner.pm
Criterion Covered Total %
statement 232 591 39.2
branch 98 380 25.7
condition 27 172 15.7
subroutine 35 55 63.6
pod 4 4 100.0
total 396 1202 32.9


line stmt bran cond sub pod time code
1             package Feersum::Runner;
2 10     10   1800675 use warnings;
  10         20  
  10         597  
3 10     10   42 use strict;
  10         20  
  10         248  
4              
5 10     10   3887 use EV;
  10         15317  
  10         388  
6 10     10   4198 use Feersum;
  10         20  
  10         595  
7 10         1516 use Socket qw/SOMAXCONN SOL_SOCKET SO_REUSEADDR AF_INET SOCK_STREAM
8 10     10   53 inet_aton pack_sockaddr_in/;
  10         26  
9             BEGIN {
10             # IPv6 support (Socket 1.95+, Perl 5.14+)
11 10         242 eval { Socket->import(qw/AF_INET6 inet_pton pack_sockaddr_in6/); 1 }
  10         578  
12 10 50   10   24 or do {
13 0         0 *AF_INET6 = sub () { undef };
14 0         0 *inet_pton = sub { undef };
  0         0  
15 0         0 *pack_sockaddr_in6 = sub { undef };
  0         0  
16             };
17             }
18             BEGIN {
19             # SO_REUSEPORT may not be available on all systems
20 10         154 eval { Socket->import('SO_REUSEPORT'); 1 }
  10         293  
21 10 50   10   22 or *SO_REUSEPORT = sub () { undef };
22             }
23 10     10   4928 use POSIX ();
  10         53421  
  10         310  
24 10     10   55 use Scalar::Util qw/weaken/;
  10         15  
  10         470  
25 10     10   358 use Guard ();
  10         470  
  10         158  
26 10     10   32 use Carp qw/carp croak/;
  10         10  
  10         361  
27 10     10   1478 use File::Spec::Functions 'rel2abs';
  10         2382  
  10         550  
28              
29 10     10   43 use constant DEATH_TIMER => 5.0; # seconds
  10         12  
  10         592  
30 10     10   32 use constant DEATH_TIMER_INCR => 2.0; # seconds
  10         12  
  10         301  
31 10     10   32 use constant DEFAULT_HOST => 'localhost';
  10         10  
  10         280  
32 10     10   28 use constant DEFAULT_PORT => 5000;
  10         11  
  10         390  
33 10   50 10   34 use constant MAX_PRE_FORK => $ENV{FEERSUM_MAX_PRE_FORK} || 1000;
  10         93  
  10         59288  
34              
35             our $INSTANCE;
36             sub new { ## no critic (RequireArgUnpacking)
37 24     24 1 20400 my $c = shift;
38 24 100       282 if ($INSTANCE) {
39             croak "Only one Feersum::Runner instance can be active at a time"
40 2 50       7 if $INSTANCE->{running};
41             # Clean up old instance state before creating new one
42 2         6 $INSTANCE->_cleanup();
43 2         3 undef $INSTANCE;
44             }
45 24         543 $INSTANCE = bless {quiet=>1, @_, running=>0}, $c;
46 24         101 return $INSTANCE;
47             }
48              
49             sub _cleanup {
50 25     25   46 my $self = shift;
51 25 100       69 return if $self->{_cleaned_up};
52 23         56 $self->{_cleaned_up} = 1;
53 23 100       138 if (my $f = $self->{endjinn}) {
54 6     0   113 $f->request_handler(sub{});
55 6         114 $f->unlisten();
56             }
57 23         105 $self->{_quit} = undef;
58 23         134 $self->{running} = 0;
59 23 50       159 if (my $file = $self->{pid_file}) {
60 0 0       0 unlink $file if -f $file;
61             }
62 23         274 return;
63             }
64              
65             sub DESTROY {
66 20     20   11878 local $@;
67 20         44 $_[0]->_cleanup();
68             }
69              
70             sub _create_socket {
71 13     13   35 my ($self, $listen, $use_reuseport) = @_;
72 13   50     97 my $backlog = $self->{backlog} || SOMAXCONN;
73              
74 13         15 my $sock;
75 13 50       111 if ($listen =~ m#^[/\.]+\w#) {
76 0         0 require IO::Socket::UNIX;
77 0 0       0 if (-S $listen) {
78 0 0       0 unlink $listen or carp "unlink stale socket '$listen': $!";
79             }
80 0         0 my $saved = umask(0);
81 0         0 $sock = eval {
82 0         0 IO::Socket::UNIX->new(
83             Local => rel2abs($listen),
84             Listen => $backlog,
85             );
86             };
87 0         0 my $err = $@;
88 0         0 umask($saved); # Restore umask even if socket creation failed
89 0 0       0 die $err if $err;
90 0 0       0 croak "couldn't bind to socket" unless $sock;
91 0 0       0 $sock->blocking(0) || do { close($sock); croak "couldn't unblock socket: $!"; };
  0         0  
  0         0  
92             }
93             else {
94 13         187 require IO::Socket::INET;
95             # SO_REUSEPORT must be set BEFORE bind for multiple sockets per port
96 13 50 33     77 if ($use_reuseport && defined SO_REUSEPORT) {
97             # Parse listen address - handle IPv6 bracketed notation [host]:port
98 0         0 my ($host, $port, $is_ipv6);
99 0 0       0 if ($listen =~ /^\[([^\]]+)\]:(\d*)$/) {
    0          
    0          
100             # IPv6 with port: [::1]:8080
101 0   0     0 ($host, $port, $is_ipv6) = ($1, $2 || 0, 1);
102             } elsif ($listen =~ /^\[([^\]]+)\]$/) {
103             # IPv6 without port: [::1]
104 0         0 ($host, $port, $is_ipv6) = ($1, 0, 1);
105             } elsif ($listen =~ /:.*:/) {
106             # Bare IPv6 - reject ambiguous cases that look like host:port
107 0 0       0 if ($listen =~ /:(\d{1,5})$/) {
108 0         0 my $maybe_port = $1;
109             # 5 digits = definitely a port; >=1024 = likely a port
110 0 0 0     0 if ($maybe_port <= 65535 && (length($maybe_port) == 5 || $maybe_port >= 1024)) {
      0        
111 0         0 croak "ambiguous IPv6 address '$listen': use bracket notation [host]:port " .
112             "(e.g., [::1]:$maybe_port or [2001:db8::1]:$maybe_port)";
113             }
114             }
115 0         0 ($host, $port, $is_ipv6) = ($listen, 0, 1);
116             } else {
117             # IPv4: host:port
118 0         0 ($host, $port) = split /:/, $listen, 2;
119 0   0     0 $host ||= '0.0.0.0';
120 0   0     0 $port ||= 0;
121 0         0 $is_ipv6 = 0;
122             }
123              
124             # Validate port range (0-65535)
125 0 0 0     0 if ($port !~ /^\d+$/ || $port > 65535) {
126 0         0 croak "invalid port '$port': must be 0-65535";
127             }
128              
129 0         0 my ($domain, $sockaddr);
130 0 0       0 if ($is_ipv6) {
131 0 0       0 defined AF_INET6()
132             or croak "IPv6 not supported on this system";
133 0 0       0 my $addr = inet_pton(AF_INET6(), $host)
134             or croak "couldn't resolve IPv6 address '$host'";
135 0         0 $domain = AF_INET6();
136 0         0 $sockaddr = pack_sockaddr_in6($port, $addr);
137             } else {
138 0 0       0 my $addr = inet_aton($host)
139             or croak "couldn't resolve address '$host'";
140 0         0 $domain = AF_INET();
141 0         0 $sockaddr = pack_sockaddr_in($port, $addr);
142             }
143              
144             # Create socket with correct address family
145 0 0       0 socket($sock, $domain, SOCK_STREAM(), 0)
146             or croak "couldn't create socket: $!";
147             setsockopt($sock, SOL_SOCKET, SO_REUSEADDR, pack("i", 1))
148 0 0       0 or do { close($sock); croak "setsockopt SO_REUSEADDR failed: $!"; };
  0         0  
  0         0  
149             setsockopt($sock, SOL_SOCKET, SO_REUSEPORT, pack("i", 1))
150 0 0       0 or do { close($sock); croak "setsockopt SO_REUSEPORT failed: $!"; };
  0         0  
  0         0  
151             bind($sock, $sockaddr)
152 0 0       0 or do { close($sock); croak "couldn't bind to socket: $!"; };
  0         0  
  0         0  
153             listen($sock, $backlog)
154 0 0       0 or do { close($sock); croak "couldn't listen: $!"; };
  0         0  
  0         0  
155              
156             # Wrap in IO::Handle for ->blocking() method
157 0         0 require IO::Handle;
158 0         0 bless $sock, 'IO::Handle';
159             $sock->blocking(0)
160 0 0       0 || do { close($sock); croak "couldn't unblock socket: $!"; };
  0         0  
  0         0  
161             }
162             else {
163             # Validate port in listen address for better error messages
164 13 50       119 if ($listen =~ /:(\d+)$/) {
    0          
165 13         147 my $port = $1;
166 13 50       53 croak "invalid port '$port': must be 0-65535" if $port > 65535;
167             } elsif ($listen =~ /:(\S+)$/) {
168 0         0 my $port = $1;
169 0 0       0 croak "invalid port '$port': must be numeric" unless $port =~ /^\d+$/;
170             }
171 13         263 $sock = IO::Socket::INET->new(
172             LocalAddr => $listen,
173             ReuseAddr => 1,
174             Proto => 'tcp',
175             Listen => $backlog,
176             Blocking => 0,
177             );
178 13 50       9303 croak "couldn't bind to socket: $!" unless $sock;
179             }
180             }
181 13         26 return $sock;
182             }
183              
184              
185             # Option name -> Feersum setter method. Used by both _prepare (cold start,
186             # consumes from $self) and _apply_settings (hot_restart child, preserves
187             # $self). Adding a new "set this value if defined" setting in one place but
188             # not the other would silently break hot_restart workers, so both paths
189             # iterate the same list.
190             my @SIMPLE_SETTINGS = (
191             [keepalive => 'set_keepalive'],
192             [reverse_proxy => 'set_reverse_proxy'],
193             [proxy_protocol => 'set_proxy_protocol'],
194             [psgix_io => 'set_psgix_io'],
195             [read_timeout => 'read_timeout'],
196             [header_timeout => 'header_timeout'],
197             [write_timeout => 'write_timeout'],
198             [max_connection_reqs => 'max_connection_reqs'],
199             [read_priority => 'read_priority'],
200             [write_priority => 'write_priority'],
201             [accept_priority => 'accept_priority'],
202             [max_accept_per_loop => 'max_accept_per_loop'],
203             [max_connections => 'max_connections'],
204             [max_read_buf => 'max_read_buf'],
205             [max_body_len => 'max_body_len'],
206             [max_uri_len => 'max_uri_len'],
207             [wbuf_low_water => 'wbuf_low_water'],
208             );
209              
210             # Walk @SIMPLE_SETTINGS plus the can()-gated extras. $consume=1 deletes from
211             # $self after applying (cold-start path); $consume=0 leaves $self intact
212             # (hot_restart child re-applying preserved config from master).
213             sub _apply_simple_settings {
214 10     10   16 my ($self, $f, $consume) = @_;
215 10         35 for my $pair (@SIMPLE_SETTINGS) {
216 170         384 my ($opt, $meth) = @$pair;
217 170 50       234 my $val = $consume ? delete $self->{$opt} : $self->{$opt};
218 170 100       335 $f->$meth($val) if defined $val;
219             }
220 10 50       115 if ($f->can('max_h2_concurrent_streams')) {
221             my $v = $consume ? delete $self->{max_h2_concurrent_streams}
222 10 50       28 : $self->{max_h2_concurrent_streams};
223 10 50       23 $f->max_h2_concurrent_streams($v) if defined $v;
224             }
225             }
226              
227             # Install the periodic watcher that triggers graceful shutdown once the
228             # worker has served max_requests_per_worker requests. Returns the watcher
229             # SV; caller must keep the reference alive (a my-scoped lexical works).
230             # Used by both the non-prefork hot_restart generation and pre_fork workers.
231             sub _install_max_requests_watcher {
232 0     0   0 my ($self, $f) = @_;
233 0 0       0 my $max = $self->{max_requests_per_worker} or return;
234 0         0 my $w; $w = EV::timer(1, 1, sub {
235 0 0   0   0 if ($f->total_requests >= $max) {
236 0         0 $f->graceful_shutdown(sub { POSIX::_exit(0) });
  0         0  
237 0         0 undef $w;
238             }
239 0         0 });
240 0         0 return $w;
241             }
242              
243             sub _apply_tls_to_listeners {
244 0     0   0 my ($self, $f, $n_listeners, $tls, $sni) = @_;
245 0         0 for my $i (0 .. $n_listeners - 1) {
246 0         0 $f->set_tls(listener => $i, %$tls);
247             }
248 0 0       0 if ($sni) {
249 0 0       0 croak "sni must be an array reference" unless ref $sni eq 'ARRAY';
250 0         0 for my $entry (@$sni) {
251 0         0 for my $i (0 .. $n_listeners - 1) {
252 0         0 $f->set_tls(listener => $i, %$entry);
253             }
254             }
255             }
256             }
257              
258             sub _normalize_listen {
259 14     14   25 my $self = shift;
260 14 50 33     164 if (defined $self->{listen} && !ref $self->{listen}) {
261 0         0 $self->{listen} = [ $self->{listen} ];
262             }
263             $self->{listen} ||=
264 14   0     105 [ ($self->{host}||DEFAULT_HOST).':'.($self->{port}||DEFAULT_PORT) ];
      0        
      50        
265             croak "listen must be an array reference"
266 14 50       81 if ref $self->{listen} ne 'ARRAY';
267             croak "listen array cannot be empty"
268 14 50       28 if @{$self->{listen}} == 0;
  14         57  
269 14         25 $self->{_listen_addrs} = [ @{$self->{listen}} ];
  14         53  
270             }
271              
272             # Fold the flat tls_cert_file/tls_key_file shorthand and the top-level h2 flag
273             # into $self->{tls} (a hashref), in place, and validate. Shared by the
274             # cold-start _prepare path and the hot_restart master so that generation
275             # children - which apply TLS via _apply_settings, not _prepare - get identical
276             # TLS/H2 configuration. Does NOT consume $self->{tls} (hot_restart re-reads it
277             # for each generation).
278             sub _normalize_tls_config {
279 14     14   28 my $self = shift;
280 14 100       43 if (!$self->{tls}) {
281 11 100       30 if (my $cert = delete $self->{tls_cert_file}) {
    100          
282             my $key = delete $self->{tls_key_file}
283 2 100       102 or croak "tls_cert_file requires tls_key_file";
284 1         3 $self->{tls} = { cert_file => $cert, key_file => $key };
285             } elsif (delete $self->{tls_key_file}) {
286 1         95 croak "tls_key_file requires tls_cert_file";
287             }
288             } else {
289             # tls hash takes precedence; discard flat options
290 3         3 delete $self->{tls_cert_file};
291 3         5 delete $self->{tls_key_file};
292             }
293 12 100       43 if (my $tls = $self->{tls}) {
    100          
294 4 50       11 croak "tls must be a hash reference" unless ref $tls eq 'HASH';
295 4 50       6 croak "tls requires cert_file" unless $tls->{cert_file};
296 4 50       7 croak "tls requires key_file" unless $tls->{key_file};
297             # H2 is off by default; only enable if h2 => 1 was passed
298 4 100       7 $tls->{h2} = 1 if delete $self->{h2};
299             } elsif (delete $self->{h2}) {
300 1         122 croak "h2 requires TLS (provide tls_cert_file and tls_key_file, or a tls hash)";
301             }
302             # sni without a base tls config would otherwise be silently ignored,
303             # leaving the listeners on plaintext - fail loudly like h2 above
304             croak "sni requires TLS (provide tls_cert_file and tls_key_file, or a tls hash)"
305 11 100 66     127 if $self->{sni} && !$self->{tls};
306 10         13 return;
307             }
308              
309             sub _prepare {
310 13     13   51 my $self = shift;
311              
312 13         58 $self->_normalize_listen();
313              
314             # Validate pre_fork early (before socket creation) to fail fast
315 13 100       88 if ($self->{pre_fork}) {
316 3         18 my $n = $self->{pre_fork};
317 3 50 33     161 if ($n !~ /^\d+$/ || $n < 1) {
318 0         0 croak "pre_fork must be a positive integer";
319             }
320 3 50       33 if ($n > MAX_PRE_FORK) {
321 0         0 croak "pre_fork=$n exceeds maximum of " . MAX_PRE_FORK;
322             }
323             }
324              
325             # Enable reuseport automatically in prefork mode if SO_REUSEPORT available
326 13   0     67 my $use_reuseport = $self->{reuseport} && $self->{pre_fork} && defined SO_REUSEPORT;
327 13         33 $self->{_use_reuseport} = $use_reuseport;
328              
329 13         187 my $f = Feersum->endjinn;
330              
331             # EPOLLEXCLUSIVE must be set BEFORE use_socket() so the separate accept epoll
332             # is created with EPOLLEXCLUSIVE flag (Linux 4.5+)
333 13 50 66     321 if ($self->{epoll_exclusive} && $self->{pre_fork} && $^O eq 'linux') {
      33        
334 2         136 $f->set_epoll_exclusive(1);
335             }
336              
337             # accept_priority must also be set BEFORE use_socket(): each accept watcher
338             # captures srvr->accept_priority when created (setup_accept_watcher), so a
339             # later assignment only affects listeners added afterwards.
340 13 100 66     77 if (defined $self->{accept_priority} && $f->can('accept_priority')) {
341 1         2 my $val = $self->{accept_priority};
342 1 50       6 croak "accept_priority must be an integer" unless $val =~ /^-?\d+$/;
343 1 50 33     86 croak "accept_priority must be between -2 and 2" if $val < -2 || $val > 2;
344 0         0 $f->accept_priority($val);
345             }
346              
347 12         34 my @socks;
348 12         110 for my $listen (@{$self->{_listen_addrs}}) {
  12         48  
349 13         41 my $sock = $self->_create_socket($listen, $use_reuseport);
350 13         35 push @socks, $sock;
351 13         54 $f->use_socket($sock);
352             }
353 12         49 $self->{sock} = $socks[0]; # backward compat: primary socket
354 12         67 $self->{_socks} = \@socks; # all sockets
355              
356             # Validate priorities (-2..+2 per libev) before applying
357 12         34 for my $prio_name (qw/read_priority write_priority accept_priority/) {
358 33         53 my $val = $self->{$prio_name};
359 33 100       52 next unless defined $val;
360 4 50       23 croak "$prio_name must be an integer" unless $val =~ /^-?\d+$/;
361 4 100 100     286 croak "$prio_name must be between -2 and 2" if $val < -2 || $val > 2;
362             }
363 10 50       50 if (defined(my $val = $self->{max_accept_per_loop})) {
364 0 0 0     0 croak "max_accept_per_loop must be a positive integer"
365             unless $val =~ /^\d+$/ && $val > 0;
366             }
367 10 50       39 if (defined(my $val = $self->{max_connections})) {
368 0 0       0 croak "max_connections must be a non-negative integer"
369             unless $val =~ /^\d+$/;
370             }
371 10         41 $self->_apply_simple_settings($f, 1); # consume from $self
372              
373             # Fold flat tls_cert_file/tls_key_file shorthand + h2 into $self->{tls}
374             # (also used by the hot_restart master via _normalize_tls_config).
375 10         36 $self->_normalize_tls_config;
376              
377             # TLS configuration: apply to all listeners
378 8 100       29 if (my $tls = delete $self->{tls}) {
379 2 50 33     280 -f $tls->{cert_file} && -r _
380             or croak "tls cert_file '$tls->{cert_file}': not found or not readable";
381 0 0 0     0 -f $tls->{key_file} && -r _
382             or croak "tls key_file '$tls->{key_file}': not found or not readable";
383              
384 0 0       0 if ($f->has_tls()) {
385 0         0 $self->_apply_tls_to_listeners($f, scalar(@socks), $tls, $self->{sni});
386 0         0 $self->{_tls_config} = $tls; # for reuseport workers
387 0 0       0 $self->{quiet} or warn "Feersum [$$]: TLS enabled on "
388             . scalar(@socks) . " listener(s)\n";
389             } else {
390 0         0 croak "tls option requires Feersum compiled with TLS support (need picotls submodule + OpenSSL; see Alien::OpenSSL)";
391             }
392             }
393              
394 6         25 $self->{endjinn} = $f;
395 6         12 return;
396             }
397              
398             # for overriding:
399             sub assign_request_handler {
400 3     3 1 6 my ($self, $app) = @_;
401 3 50       12 if (my $log_cb = $self->{access_log}) {
402 0         0 my $orig = $app;
403             $app = sub {
404 0     0   0 my $r = shift;
405 0         0 my $t0 = EV::now();
406 0         0 my $method = $r->method;
407 0         0 my $uri = $r->uri;
408             $r->response_guard(Guard::guard(sub {
409 0         0 $log_cb->($method, $uri, EV::now() - $t0);
410 0         0 }));
411 0         0 $orig->($r);
412 0         0 };
413             }
414 3         27 return $self->{endjinn}->request_handler($app);
415             }
416              
417             sub run {
418 3     3 1 165 my $self = shift;
419 3         119 weaken $self;
420              
421 3         137 $self->{running} = 1;
422 3   33     283 my $app = shift || $self->{app};
423 3 50       124 $self->{quiet} or warn "Feersum [$$]: starting...\n";
424              
425             # Hot restart mode: entry process creates sockets, then manages
426             # generation children that each load a fresh app with clean modules.
427 3 50       131 if ($self->{hot_restart}) {
428 0 0       0 croak "hot_restart requires app_file" unless $self->{app_file};
429 0         0 $self->_daemonize_and_write_pid();
430 0         0 $self->_run_hot_restart_master(); # creates sockets, then drops privs
431 0         0 return;
432             }
433              
434 3         120 $self->_prepare(); # bind() on listen sockets
435 3         100 $self->_daemonize_and_write_pid();
436 3         22 $self->_drop_privs(); # after bind, before app load
437              
438             # preload_app => 0: fork workers first, each loads the app independently.
439             # Default (preload_app unset or true): load app once, fork inherits via COW.
440 3 50 33     41 if ($self->{pre_fork} && defined $self->{preload_app} && !$self->{preload_app}) {
      33        
441             $self->{_app_loader} = sub {
442 0   0 0   0 my $a = $app || $self->{app};
443 0 0 0     0 if (!$a && $self->{app_file}) {
444 0         0 local ($@, $!);
445 0         0 $a = do(rel2abs($self->{app_file}));
446 0 0 0     0 warn "couldn't load $self->{app_file}: " . ($@ || $!) if $@ || !$a;
      0        
447             }
448 0 0       0 croak "app not defined or failed to compile" unless $a;
449 0         0 $self->assign_request_handler($a);
450 0         0 };
451             # Set a no-op handler on parent so it doesn't crash if it briefly
452             # re-accepts during non-reuseport worker respawn
453             $self->{endjinn}->request_handler(sub {
454 0     0   0 $_[0]->send_response(503, ['Content-Type'=>'text/plain'], \"Service Unavailable\n");
455 0         0 });
456 0 0   0   0 $self->{_quit} = EV::signal 'QUIT', sub { $self && $self->quit };
  0         0  
457 0         0 $self->_start_pre_fork;
458             } else {
459 3   33     44 $app ||= delete $self->{app};
460 3 50 33     19 if (!$app && $self->{app_file}) {
461 3         113 local ($@, $!);
462 3         75 $app = do(rel2abs($self->{app_file}));
463 3 50       12 warn "couldn't parse $self->{app_file}: $@" if $@;
464 3 50 33     19 warn "couldn't do $self->{app_file}: $!" if ($! && !defined $app);
465 3 50       14 warn "couldn't run $self->{app_file}: didn't return anything"
466             unless $app;
467             }
468 3 50       6 croak "app not defined or failed to compile" unless $app;
469              
470 3         22 $self->assign_request_handler($app);
471              
472 3 50   6   198 $self->{_quit} = EV::signal 'QUIT', sub { $self && $self->quit };
  6         174  
473              
474 3 50       36 $self->_start_pre_fork if $self->{pre_fork};
475             }
476 3         29338898 EV::run;
477 3 50       16 $self->{quiet} or warn "Feersum [$$]: done\n";
478 3         66 $self->_cleanup();
479 3         117 return;
480             }
481              
482             # Hot restart master: creates sockets once, then manages generations.
483             # Each generation is a forked child that runs _apply_settings + app load + serve.
484             # SIGHUP -> fork new gen -> if ready -> SIGQUIT old gen.
485             sub _run_hot_restart_master {
486 1     1   6 my ($self) = @_;
487 1         4 my $quiet = $self->{quiet};
488              
489 1 50       2 $quiet or warn "Feersum [$$]: hot restart master starting\n";
490              
491 1         4 $self->_normalize_listen();
492             # Validate pre_fork early (before socket creation) to fail fast; the
493             # generation children never run _prepare, where this otherwise happens.
494             # An unvalidated non-positive value would fork zero workers yet still
495             # unlisten() and report ready - a silent outage.
496 1 50       2 if ($self->{pre_fork}) {
497 1         2 my $n = $self->{pre_fork};
498 1 50 33     7 if ($n !~ /^\d+$/ || $n < 1) {
499 1         82 croak "pre_fork must be a positive integer";
500             }
501 0 0       0 if ($n > MAX_PRE_FORK) {
502 0         0 croak "pre_fork=$n exceeds maximum of " . MAX_PRE_FORK;
503             }
504             }
505             # Fold flat TLS keys + h2 into $self->{tls} once, here in the master, so
506             # every generation child's _apply_settings sees the complete TLS/H2 config
507             # (these children never run _prepare, which is where folding otherwise
508             # happens). Without this, hot_restart silently drops flat tls_cert_file/
509             # tls_key_file and the h2 flag.
510 0         0 $self->_normalize_tls_config();
511              
512             # Create listen sockets in the master (shared across generations via fork).
513             # Use SO_REUSEPORT if configured - reuseport workers need all sockets
514             # on the same addr:port to have the flag set.
515 0   0     0 $self->{_listen_addrs} ||= [ @{$self->{listen}} ];
  0         0  
516 0   0     0 my $use_reuseport = $self->{reuseport} && $self->{pre_fork} && defined SO_REUSEPORT;
517 0         0 my @socks;
518 0         0 for my $listen (@{$self->{_listen_addrs}}) {
  0         0  
519 0         0 my $sock = $self->_create_socket($listen, $use_reuseport);
520 0         0 push @socks, $sock;
521             }
522 0         0 $self->{_master_socks} = \@socks;
523              
524             # Drop privileges after sockets are bound (privileged ports are now open)
525 0         0 $self->_drop_privs();
526              
527 0         0 my $gen = 0;
528 0         0 my $current_pid;
529             my $pending_pid; # generation being started (not yet $current_pid)
530 0         0 my $shutting_down = 0;
531 0   0     0 my $startup_timeout = $self->{startup_timeout} // 10;
532              
533             # Fork a generation child. The child inherits listen sockets via fork,
534             # registers them with use_socket, then calls _apply_settings (which
535             # applies TLS and other settings without consuming from $self), loads the
536             # app file fresh, then serves.
537             my $fork_generation = sub {
538 0     0   0 $gen++;
539 0         0 my $pid = fork;
540 0 0       0 croak "fork generation: $!" unless defined $pid;
541              
542 0 0       0 if ($pid == 0) {
543             # === Generation child ===
544 0         0 EV::default_loop()->loop_fork;
545 0 0       0 $quiet or warn "Feersum [$$]: gen $gen loading app\n";
546              
547             # Sockets were created in the master and inherited via fork -
548             # register them with this generation's Feersum instance.
549 0         0 my $f = Feersum->endjinn;
550             # EPOLLEXCLUSIVE must be set BEFORE use_socket() so each accept
551             # watcher is created with it (mirrors _prepare); otherwise
552             # non-reuseport hot_restart workers inherit plain accept watchers
553             # and lose the thundering-herd mitigation.
554 0 0 0     0 if ($self->{epoll_exclusive} && $self->{pre_fork} && $^O eq 'linux'
      0        
      0        
555             && $f->can('set_epoll_exclusive')) {
556 0         0 $f->set_epoll_exclusive(1);
557             }
558             # accept_priority also before use_socket (see _prepare); the XS
559             # setter clamps to libev's valid range.
560 0 0 0     0 if (defined $self->{accept_priority} && $f->can('accept_priority')) {
561 0         0 $f->accept_priority($self->{accept_priority});
562             }
563 0         0 for my $sock (@socks) {
564 0         0 $f->use_socket($sock);
565             }
566 0         0 $self->{_socks} = \@socks;
567 0         0 $self->{sock} = $socks[0];
568              
569             # Apply server settings (preserved in $self for later generations)
570 0         0 $self->_apply_settings($f);
571              
572             # Load app fresh (fork gave us clean copy-on-write memory)
573 0         0 my $app_file = rel2abs($self->{app_file});
574 0         0 local ($@, $!);
575 0         0 my $app = do $app_file;
576 0 0 0     0 if ($@ || !$app || ref $app ne 'CODE') {
      0        
577 0   0     0 warn "Feersum [$$]: gen $gen: failed to load $app_file: "
578             . ($@ || $! || "not a coderef") . "\n";
579 0         0 POSIX::_exit(1);
580             }
581              
582 0         0 $self->{endjinn} = $f;
583 0         0 $self->assign_request_handler($app);
584              
585 0         0 my ($quit_w, $death_w);
586             $quit_w = EV::signal 'QUIT', sub {
587 0 0       0 return if $self->{_shutdown};
588 0         0 $self->{_shutdown} = 1;
589             my $gt = $self->{graceful_timeout}
590             // $ENV{FEERSUM_GRACEFUL_TIMEOUT}
591 0   0     0 // DEATH_TIMER;
      0        
592 0 0       0 if ($self->{_n_kids}) {
593             # Parent of live workers: broadcast SIGQUIT to the process
594             # group (includes self; guarded by _shutdown above) and
595             # let the _fork_another reaper break the loop once the
596             # last worker exits. Calling graceful_shutdown on the
597             # supervisor's own (connection-less) instance would fire
598             # the callback synchronously and _exit(0) before the
599             # workers have drained. Checked via the live worker count,
600             # not pre_fork: with zero live workers there is no reaper
601             # event to wait for, so fall through to graceful_shutdown.
602 0         0 kill POSIX::SIGQUIT, -$$;
603 0         0 $gt += DEATH_TIMER_INCR; # outlast the workers' deadline
604             }
605             else {
606 0         0 $f->graceful_shutdown(sub { POSIX::_exit(0) });
  0         0  
607             }
608             $death_w = EV::timer($gt, 0, sub {
609 0         0 POSIX::_exit(1);
610 0         0 });
611 0         0 };
612              
613 0 0       0 if ($self->{pre_fork}) {
614 0         0 $f->set_multiprocess(1);
615             # Set reuseport flag for _fork_another workers
616             $self->{_use_reuseport} = $self->{reuseport}
617 0   0     0 && $self->{pre_fork} && defined SO_REUSEPORT;
618             # (epoll_exclusive already set before use_socket above)
619 0 0       0 POSIX::setsid() or warn "Feersum [$$]: setsid failed: $!\n";
620 0         0 $self->{_kids} = [];
621 0         0 $self->{_n_kids} = 0;
622 0         0 $self->_fork_another($_) for (1 .. $self->{pre_fork});
623 0         0 $f->unlisten(); # parent of workers doesn't accept
624             }
625              
626 0         0 my $mrw;
627 0 0       0 if (!$self->{pre_fork}) {
628 0 0       0 $self->{after_fork}->() if $self->{after_fork};
629 0         0 $mrw = $self->_install_max_requests_watcher($f);
630             }
631              
632             # Signal master: ready to serve (after workers are forked)
633 0         0 kill 'USR2', getppid();
634              
635             $quiet or warn "Feersum [$$]: gen $gen ready"
636 0 0       0 . ($self->{pre_fork} ? " ($self->{pre_fork} workers)" : "") . "\n";
    0          
637 0         0 EV::run;
638 0         0 POSIX::_exit(0);
639             }
640              
641 0         0 return $pid;
642 0         0 };
643              
644             # Fork first generation
645 0         0 $pending_pid = $fork_generation->();
646 0 0       0 unless (_wait_for_ready($pending_pid, $quiet, $gen, \$shutting_down, $startup_timeout)) {
647 0 0       0 kill 'KILL', $pending_pid if kill(0, $pending_pid);
648 0         0 waitpid($pending_pid, 0);
649 0         0 croak "first generation failed to start";
650             }
651 0         0 $current_pid = $pending_pid;
652 0         0 $pending_pid = undef;
653              
654 0 0       0 $quiet or warn "Feersum [$$]: master ready (gen $gen, pid $current_pid)\n";
655              
656             my $hup = EV::signal 'HUP', sub {
657 0 0 0 0   0 return if $shutting_down || $pending_pid; # debounce rapid HUPs
658 0 0       0 $quiet or warn "Feersum [$$]: HUP - spawning gen " . ($gen + 1) . "\n";
659              
660 0         0 my $old_pid = $current_pid;
661 0         0 $pending_pid = $fork_generation->();
662              
663 0 0       0 if (_wait_for_ready($pending_pid, $quiet, $gen, \$shutting_down, $startup_timeout)) {
664 0 0       0 $quiet or warn "Feersum [$$]: gen $gen ready (pid $pending_pid), retiring old (pid $old_pid)\n";
665 0         0 $current_pid = $pending_pid;
666 0         0 $pending_pid = undef;
667 0 0       0 kill 'QUIT', $old_pid if $old_pid;
668             } else {
669 0         0 warn "Feersum [$$]: gen $gen failed, keeping old (pid $old_pid)\n";
670 0 0       0 kill 'KILL', $pending_pid if kill(0, $pending_pid);
671 0         0 waitpid($pending_pid, 0);
672 0         0 $pending_pid = undef;
673             }
674 0         0 };
675              
676             my $quit = EV::signal 'QUIT', sub {
677 0 0   0   0 return if $shutting_down;
678 0         0 $shutting_down = 1;
679 0 0       0 $quiet or warn "Feersum [$$]: master shutting down\n";
680 0 0       0 kill 'QUIT', $current_pid if $current_pid;
681             # Also kill $pending_pid in case QUIT raced with a HUP reload:
682             # the pending gen may be about to be promoted to $current_pid.
683 0 0       0 kill 'QUIT', $pending_pid if $pending_pid;
684 0         0 };
685              
686             my $int = EV::signal 'INT', sub {
687 0 0   0   0 return if $shutting_down;
688 0         0 $shutting_down = 1;
689 0 0       0 $quiet or warn "Feersum [$$]: master interrupted\n";
690 0 0       0 kill 'QUIT', $current_pid if $current_pid;
691 0 0       0 kill 'QUIT', $pending_pid if $pending_pid;
692 0         0 };
693              
694             # Reap children; restart if active generation dies unexpectedly
695             my $reap = EV::child 0, 0, sub {
696 0     0   0 my $kid = $_[0]->rpid;
697 0         0 my $status = $_[0]->rstatus >> 8;
698 0 0       0 $quiet or warn "Feersum [$$]: child $kid exited ($status)\n";
699             # Ignore pending generation deaths - handled by _wait_for_ready
700 0 0 0     0 return if $pending_pid && $kid == $pending_pid;
701 0 0 0     0 if ($current_pid && $kid == $current_pid) {
702 0         0 $current_pid = undef;
703 0 0       0 EV::break if $shutting_down;
704 0 0 0     0 unless ($shutting_down || $pending_pid) {
705 0         0 warn "Feersum [$$]: active generation died, restarting\n";
706 0         0 $pending_pid = $fork_generation->();
707 0 0       0 if (_wait_for_ready($pending_pid, $quiet, $gen, \$shutting_down, $startup_timeout)) {
708 0         0 $current_pid = $pending_pid;
709             } else {
710             # Replacement also failed - kill it and shut down
711 0         0 warn "Feersum [$$]: replacement generation also failed, giving up\n";
712 0 0       0 kill 'KILL', $pending_pid if kill(0, $pending_pid);
713 0         0 waitpid($pending_pid, 0);
714 0         0 EV::break;
715             }
716 0         0 $pending_pid = undef;
717             }
718             }
719 0         0 };
720              
721 0         0 EV::run;
722             # Cleanup
723 0         0 for my $sock (@socks) { close($sock) }
  0         0  
724 0         0 waitpid(-1, POSIX::WNOHANG()) for 1..100;
725 0 0       0 $quiet or warn "Feersum [$$]: master done\n";
726             }
727              
728             # Wait for a generation child to signal readiness (USR2) or fail.
729             # Uses RUN_ONCE loop to avoid EV::break propagating to the outer EV::run.
730             sub _wait_for_ready {
731 0     0   0 my ($pid, $quiet, $gen, $shutdown_ref, $timeout) = @_;
732 0   0     0 $timeout //= 10;
733 0         0 my $ready = 0;
734 0         0 my $done = 0;
735 0     0   0 my $usr2 = EV::signal 'USR2', sub { $ready = 1; $done = 1 };
  0         0  
  0         0  
736             my $fail = EV::child $pid, 0, sub {
737 0     0   0 warn "Feersum [$$]: gen $gen (pid $pid) died during startup\n";
738 0         0 $done = 1;
739 0         0 };
740             my $to = EV::timer($timeout, 0, sub {
741 0     0   0 warn "Feersum [$$]: gen $gen startup timeout\n";
742 0         0 $done = 1;
743 0         0 });
744 0   0     0 EV::run(EV::RUN_ONCE) until $done || ($shutdown_ref && $$shutdown_ref);
      0        
745 0         0 return $ready;
746             }
747              
748             # Apply server settings to a Feersum instance (without consuming from $self).
749             # Used by hot_restart generations to re-apply settings from the master's config.
750             sub _apply_settings {
751 0     0   0 my ($self, $f) = @_;
752 0         0 $self->_apply_simple_settings($f, 0); # preserve $self for re-use
753             # Gate on pre_fork like _prepare: EPOLLEXCLUSIVE is only meaningful with
754             # multiple accepting workers (the generation child already set it before
755             # use_socket; this keeps the two settings paths consistent).
756             $f->set_epoll_exclusive($self->{epoll_exclusive} && $self->{pre_fork} ? 1 : 0)
757 0 0 0     0 if defined $self->{epoll_exclusive} && $f->can('set_epoll_exclusive');
    0 0        
758              
759             # TLS
760 0 0       0 if (my $tls = $self->{tls}) {
761 0 0       0 if ($f->has_tls()) {
762 0 0       0 my $n = scalar @{$self->{_master_socks} || $self->{_socks}};
  0         0  
763 0         0 $self->_apply_tls_to_listeners($f, $n, $tls, $self->{sni});
764 0         0 $self->{_tls_config} = $tls; # for reuseport workers
765             } else {
766             # Match _prepare's loud failure: never silently serve plaintext on
767             # a TLS-configured listener just because this build lacks TLS.
768 0         0 warn "Feersum [$$]: tls configured but TLS not compiled in"
769             . " - aborting generation\n";
770 0         0 POSIX::_exit(1);
771             }
772             }
773             }
774              
775             sub _fork_another {
776 20     20   60 my ($self, $slot) = @_;
777              
778 20         23340 my $pid = fork;
779 20 50       654 croak "failed to fork: $!" unless defined $pid;
780 20 50       179 unless ($pid) {
781 0         0 EV::default_loop()->loop_fork;
782 0 0       0 $self->{quiet} or warn "Feersum [$$]: starting\n";
783 0         0 delete $self->{_kids};
784 0         0 delete $self->{pre_fork};
785 0         0 $self->{_n_kids} = 0;
786              
787             # With SO_REUSEPORT, each child creates its own sockets
788             # This eliminates accept() contention for better scaling
789 0 0       0 if ($self->{_use_reuseport}) {
790 0         0 $self->{endjinn}->unlisten();
791 0 0       0 for my $old_sock (@{$self->{_socks} || []}) {
  0         0  
792             close($old_sock)
793 0 0       0 or do { warn "close parent socket in child: $!"; POSIX::_exit(1); };
  0         0  
  0         0  
794             }
795 0         0 my @new_socks;
796             eval {
797 0         0 for my $listen (@{$self->{_listen_addrs}}) {
  0         0  
798 0         0 my $sock = $self->_create_socket($listen, 1);
799 0         0 push @new_socks, $sock;
800 0         0 $self->{endjinn}->use_socket($sock);
801             }
802 0         0 1;
803 0 0       0 } or do {
804 0         0 warn "Feersum [$$]: child socket creation failed: $@";
805 0         0 POSIX::_exit(1);
806             };
807 0         0 $self->{sock} = $new_socks[0];
808 0         0 $self->{_socks} = \@new_socks;
809              
810             # Re-apply TLS config + SNI on new listeners
811 0 0       0 if (my $tls = $self->{_tls_config}) {
812             $self->_apply_tls_to_listeners(
813 0         0 $self->{endjinn}, scalar(@new_socks), $tls, $self->{sni});
814             }
815             }
816              
817             # Per-worker app loading (preload_app => 0)
818 0 0       0 if (my $loader = $self->{_app_loader}) {
819 0         0 eval { $loader->() };
  0         0  
820 0 0       0 if ($@) {
821 0         0 warn "Feersum [$$]: worker app load failed: $@";
822 0         0 POSIX::_exit(1);
823             }
824             }
825              
826 0 0       0 if (my $cb = $self->{after_fork}) { $cb->() }
  0         0  
827              
828 0         0 my $max_reqs_w = $self->_install_max_requests_watcher($self->{endjinn});
829              
830 0         0 eval { EV::run; }; ## no critic (RequireCheckingReturnValueOfEval)
  0         0  
831 0 0       0 carp $@ if $@;
832 0 0       0 POSIX::_exit($@ ? 1 : 0); # _exit avoids running parent's END blocks
833             }
834              
835 20         601 weaken $self; # prevent circular ref with watcher callback
836 20         222 $self->{_n_kids}++;
837             $self->{_kids}[$slot] = EV::child $pid, 0, sub {
838 20     20   76 my $w = shift;
839 20 50       78 return unless $self; # guard against destruction during shutdown
840 20 50       70 $self->{quiet} or warn "Feersum [$$]: child $pid exited ".
841             "with rstatus ".$w->rstatus."\n";
842 20         40 $self->{_n_kids}--;
843 20 50       81 if ($self->{_shutdown}) {
844 20 100       44 unless ($self->{_n_kids}) {
845 3         91 $self->{_death} = undef;
846 3         21 EV::break(EV::BREAK_ALL());
847             }
848 20         2295 return;
849             }
850             # Without SO_REUSEPORT, parent needs to accept during respawn
851 0 0       0 unless ($self->{_use_reuseport}) {
852 0         0 my $feersum = $self->{endjinn};
853 0 0       0 my @socks = @{$self->{_socks} || [$self->{sock}]};
  0         0  
854 0         0 my $all_valid = 1;
855 0         0 for my $sock (@socks) {
856 0 0       0 unless (defined fileno $sock) {
857 0         0 $all_valid = 0;
858 0         0 last;
859             }
860             }
861 0 0       0 if ($all_valid) {
862 0         0 for my $sock (@socks) {
863 0         0 $feersum->accept_on_fd(fileno $sock);
864             }
865 0         0 $self->_fork_another($slot);
866 0         0 $feersum->unlisten;
867             } else {
868 0         0 carp "fileno returned undef during respawn, cannot respawn worker";
869             }
870             }
871             else {
872             # With SO_REUSEPORT, just spawn new child (it creates its own socket)
873 0         0 $self->_fork_another($slot);
874             }
875 20         2924 };
876 20         1134 return;
877             }
878              
879             sub _start_pre_fork {
880 3     3   5 my $self = shift;
881              
882             # pre_fork value already validated in _prepare()
883 3         33 $self->{endjinn}->set_multiprocess(1);
884              
885 3 50       432 POSIX::setsid() or croak "setsid() failed: $!";
886              
887 3         35 $self->{_kids} = [];
888 3         19 $self->{_n_kids} = 0;
889 3         28 $self->_fork_another($_) for (1 .. $self->{pre_fork});
890              
891             # Parent stops accepting - children handle connections
892 3         455 $self->{endjinn}->unlisten();
893              
894             # With SO_REUSEPORT, parent can close its sockets entirely
895             # Children have their own sockets
896 3 50       83 if ($self->{_use_reuseport}) {
897 0 0       0 for my $sock (@{$self->{_socks} || []}) {
  0         0  
898 0 0       0 close($sock)
899             or warn "close parent socket after fork: $!";
900             }
901 0         0 $self->{sock} = undef;
902 0         0 $self->{_socks} = [];
903             }
904 3         40 return;
905             }
906              
907             sub _daemonize_and_write_pid {
908 3     3   9 my $self = shift;
909              
910 3 50       30 if ($self->{daemonize}) {
    50          
911 0         0 my $pid = fork;
912 0 0       0 croak "daemonize fork: $!" unless defined $pid;
913 0 0       0 if ($pid) {
914 0 0       0 if (my $file = $self->{pid_file}) {
915 0 0       0 open my $fh, '>', $file or croak "Cannot write pid_file '$file': $!";
916 0         0 print $fh "$pid\n";
917 0         0 close $fh;
918             }
919 0         0 POSIX::_exit(0);
920             }
921 0         0 POSIX::setsid();
922 0 0       0 open STDIN, '<', '/dev/null' or croak "redirect stdin: $!";
923 0 0       0 open STDOUT, '>', '/dev/null' or croak "redirect stdout: $!";
924             open STDERR, '>', '/dev/null' or croak "redirect stderr: $!"
925 0 0 0     0 unless $ENV{FEERSUM_DEBUG};
926             } elsif (my $file = $self->{pid_file}) {
927 0 0       0 open my $fh, '>', $file or croak "Cannot write pid_file '$file': $!";
928 0         0 print $fh "$$\n";
929 0         0 close $fh;
930             }
931             }
932              
933             sub _drop_privs {
934 5     5   12 my $self = shift;
935 5 100       21 if (my $group = $self->{group}) {
936 1         102 my $gid = getgrnam($group);
937 1 50       97 croak "Unknown group '$group'" unless defined $gid;
938             # Setting $) clears supplemental groups AND sets effective GID (via
939             # setgroups + setgid). Without this, supplemental groups like wheel,
940             # sudo, docker, shadow inherited from root are retained after setuid.
941             # $)-assignment magic discards the setgroups/setegid return values,
942             # so errno is the only failure signal - clear any stale value first
943             # (getgrnam may leave errno set even on success).
944 0         0 $! = 0;
945 0         0 $) = "$gid $gid";
946 0 0       0 croak "setgroups/setegid($gid): $!" if $!;
947 0 0       0 POSIX::setgid($gid) or croak "setgid($gid): $!";
948             # Verify drop took effect AND supplemental groups were cleared
949             # (some LSMs/seccomp policies silently no-op setgroups). A successful
950             # drop reads back as "gid gid": real gid plus the one-entry setgroups
951             # list ($) = "g g" sets setgroups([g]), not an empty list), so require
952             # every token to equal $gid rather than expecting a single token.
953 0         0 my @rg = split ' ', $(;
954             croak "setgid($gid) verification failed: real GID list is @rg"
955 0 0 0     0 unless @rg && !grep { $_ != $gid } @rg;
  0         0  
956             }
957 4 100       16 if (my $user = $self->{user}) {
958 1         155 my $uid = getpwnam($user);
959 1 50       207 croak "Unknown user '$user'" unless defined $uid;
960 0 0       0 POSIX::setuid($uid) or croak "setuid($uid): $!";
961             # Verify the privilege drop actually happened.
962 0 0 0     0 croak "setuid($uid) verification failed: \$<=$<, \$>=$>"
963             unless $< == $uid && $> == $uid;
964             }
965             }
966              
967             sub quit {
968 6     6 1 48 my $self = shift;
969 6 100       15177 return if $self->{_shutdown};
970              
971 3         155 $self->{_shutdown} = 1;
972 3 50       39 $self->{quiet} or warn "Feersum [$$]: shutting down...\n";
973             my $death = $self->{graceful_timeout}
974             // $ENV{FEERSUM_GRACEFUL_TIMEOUT}
975 3   33     171 // DEATH_TIMER;
      50        
976              
977 3 50       107 if ($self->{_n_kids}) {
978             # in parent, broadcast SIGQUIT to the process group (including self,
979             # but protected by _shutdown flag above)
980 3         435 kill POSIX::SIGQUIT, -$$;
981 3         35 $death += DEATH_TIMER_INCR;
982             }
983             else {
984             # in child or solo process
985 0     0   0 $self->{endjinn}->graceful_shutdown(sub { POSIX::_exit(0) });
  0         0  
986             }
987              
988 3     0   293 $self->{_death} = EV::timer $death, 0, sub { POSIX::_exit(1) };
  0         0  
989 3         125 return;
990             }
991              
992             1;
993             __END__