File Coverage

blib/lib/Feersum/Runner.pm
Criterion Covered Total %
statement 216 573 37.7
branch 84 362 23.2
condition 21 151 13.9
subroutine 34 55 61.8
pod 4 4 100.0
total 359 1145 31.3


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