File Coverage

blib/lib/Feersum/Runner.pm
Criterion Covered Total %
statement 226 599 37.7
branch 79 386 20.4
condition 21 142 14.7
subroutine 33 53 62.2
pod 4 4 100.0
total 363 1184 30.6


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