File Coverage

blib/lib/PAGI/Server/Runner.pm
Criterion Covered Total %
statement 213 274 77.7
branch 100 178 56.1
condition 45 70 64.2
subroutine 25 27 92.5
pod 7 7 100.0
total 390 556 70.1


line stmt bran cond sub pod time code
1             package PAGI::Server::Runner;
2              
3 14     14   504647 use strict;
  14         38  
  14         468  
4 14     14   53 use warnings;
  14         23  
  14         900  
5              
6             our $VERSION = '0.002001';
7              
8 14     14   3449 use Getopt::Long qw(GetOptionsFromArray :config pass_through no_auto_abbrev no_ignore_case);
  14         55303  
  14         66  
9 14     14   9263 use Pod::Usage;
  14         767143  
  14         1848  
10 14     14   151 use File::Spec;
  14         27  
  14         444  
11 14     14   4435 use POSIX qw(setsid);
  14         53953  
  14         77  
12 14     14   16405 use FindBin ();
  14         10239  
  14         42538  
13              
14             =head1 NAME
15              
16             PAGI::Server::Runner - PAGI application loader and server runner
17              
18             =head1 SYNOPSIS
19              
20             # Command line usage via pagi-server
21             pagi-server PAGI::App::Directory root=/var/www
22             pagi-server ./app.pl -p 8080
23             pagi-server # serves current directory
24              
25             # With environment modes
26             pagi-server -E development app.pl # enable Lint middleware
27             pagi-server -E production app.pl # no auto-middleware
28             PAGI_ENV=production pagi-server app.pl
29              
30             # Programmatic usage
31             use PAGI::Server::Runner;
32              
33             PAGI::Server::Runner->run(@ARGV);
34              
35             =head1 DESCRIPTION
36              
37             PAGI::Server::Runner is a loader and runner for PAGI applications, similar to
38             L for PSGI. It handles CLI argument parsing, app loading
39             (from files or modules), environment modes, and server orchestration.
40              
41             The runner is designed to be server-agnostic. Common options like host,
42             port, and daemonize are handled by the runner, while server-specific
43             options are passed through to the server backend.
44              
45             =head1 ENVIRONMENT MODES
46              
47             PAGI::Server::Runner supports environment modes similar to Plack's C<-E> flag:
48              
49             =over 4
50              
51             =item development
52              
53             Auto-enables L with strict mode to catch
54             specification violations early (when PAGI-Tools is installed). This is
55             the default when running interactively (TTY detected).
56              
57             =item production
58              
59             No middleware is auto-enabled. This is the default when running
60             non-interactively (no TTY, e.g., systemd, docker, cron).
61              
62             =item none
63              
64             Explicit opt-out of all auto-middleware, regardless of TTY detection.
65              
66             =back
67              
68             Mode is determined by (in order of precedence):
69              
70             1. -E / --env command line flag
71             2. PAGI_ENV environment variable
72             3. Auto-detection: TTY = development, no TTY = production
73              
74             After determining the mode, the runner sets C to the resolved
75             value. This allows your application to check C<$ENV{PAGI_ENV}> to know
76             what mode it's running in, similar to Plack's C.
77              
78             Use C<--no-default-middleware> to disable auto-middleware while keeping
79             the mode for other purposes.
80              
81             =head1 APP LOADING
82              
83             The runner supports three ways to specify an application:
84              
85             =head2 Module Name
86              
87             If the app specifier contains C<::>, it's treated as a module name:
88              
89             pagi-server PAGI::App::Directory root=/var/www show_hidden=1
90              
91             The module is loaded, instantiated with the provided key=value arguments,
92             and C is called to get the PAGI app coderef.
93              
94             =head2 File Path
95              
96             If the app specifier contains C or ends with C<.pl> or C<.psgi>,
97             it's treated as a file path:
98              
99             pagi-server ./app.pl
100             pagi-server /path/to/myapp.psgi
101              
102             The file is loaded via C and must return a coderef.
103              
104             For compatibility with Plack's C, the runner localizes C<$0> to the
105             app file before loading it. This ensures C resolves to the app
106             file directory inside the app. If C was already loaded, the runner
107             also calls C to refresh its cached path.
108              
109             =head2 Default
110              
111             If no app is specified, defaults to serving the current directory:
112              
113             pagi-server # same as: PAGI::App::Directory root=.
114              
115             =head1 METHODS
116              
117             =head2 run
118              
119             PAGI::Server::Runner->run(@ARGV);
120              
121             Class method that creates a runner, parses options, loads the app,
122             and runs the server. This is the main entry point for CLI usage.
123              
124             =head2 new
125              
126             my $runner = PAGI::Server::Runner->new(%options);
127              
128             Creates a new runner instance. Most users should use C instead.
129              
130             =cut
131              
132             sub new {
133 66     66 1 1199227 my ($class, %args) = @_;
134              
135             return bless {
136             # Runner options (common to all servers)
137             host => $args{host},
138             port => $args{port},
139             server => $args{server},
140             env => $args{env},
141             quiet => $args{quiet} // 0,
142             loop => $args{loop},
143             access_log => $args{access_log},
144             no_access_log => $args{no_access_log} // 0,
145             daemonize => $args{daemonize} // 0,
146             pid_file => $args{pid_file},
147             user => $args{user},
148             group => $args{group},
149             libs => $args{libs} // [],
150             modules => $args{modules} // [],
151             eval => $args{eval},
152             default_middleware => $args{default_middleware},
153              
154             # Internal state
155             app => undef,
156             app_spec => undef,
157             app_args => {},
158             server_options => $args{server_options} // {},
159 66   100     2141 argv => [],
      50        
      100        
      50        
      50        
      100        
160             }, $class;
161             }
162              
163             =head2 parse_options
164              
165             $runner->parse_options(@args);
166              
167             Parses CLI options from the argument list. Common options are stored
168             in the runner object. Server-specific options are passed separately
169             via the C hashref (see L).
170              
171             =head3 Common Options (handled by Runner)
172              
173             -a, --app FILE Load app from file (legacy option)
174             -e CODE Inline app code (like perl -e)
175             -M MODULE Load MODULE before -e (repeatable, like perl -M)
176             -o, --host HOST Bind address (default: 127.0.0.1)
177             -p, --port PORT Bind port (default: 5000)
178             -s, --server CLASS Server class (default: PAGI::Server)
179             -E, --env MODE Environment mode (development, production, none)
180             -I, --lib PATH Add PATH to @INC (repeatable)
181             -l, --loop BACKEND Event loop backend (EV, Epoll, UV, Poll)
182             -D, --daemonize Run as background daemon
183             --access-log FILE Access log file (default: STDERR)
184             --no-access-log Disable access logging
185             --pid FILE Write PID to file
186             --user USER Run as specified user (after binding)
187             --group GROUP Run as specified group (after binding)
188             -q, --quiet Suppress startup messages
189             --default-middleware Toggle mode middleware (default: on)
190             -v, --version Show version info
191             --help Show help
192              
193             Example with C<-e> and C<-M>:
194              
195             pagi-server -MPAGI::App::File -e 'PAGI::App::File->new(root => ".")->to_app'
196              
197             =head3 Server-Specific Options
198              
199             Server-specific options should be parsed by the server's CLI wrapper
200             (e.g., C) and passed to Runner via the C
201             hashref parameter. This keeps Runner server-agnostic.
202              
203             See L for available options when using pagi-server.
204              
205             =cut
206              
207             sub parse_options {
208 30     30 1 145 my ($self, @args) = @_;
209              
210             # Check for server_options hashref passed from bin/pagi-server
211 30         84 for my $i (0 .. $#args) {
212 116 100 66     269 if ($args[$i] eq 'server_options' && ref($args[$i + 1]) eq 'HASH') {
213 10         52 $self->{server_options} = $args[$i + 1];
214 10         28 splice @args, $i, 2;
215 10         15 last;
216             }
217             }
218              
219             # Pre-process cuddled options like -MModule or -e"code" → -M Module, -e "code"
220             # This matches Plack::Runner behavior for perl-like flags
221 30 100       61 @args = map { /^(-[IMMe])(.+)/ ? ($1, $2) : $_ } @args;
  107         276  
222              
223 30         75 my %opts;
224             my @libs;
225 30         0 my @modules;
226              
227             # Parse runner options, pass through unknown for server
228             GetOptionsFromArray(
229             \@args,
230             # App loading
231             'a|app=s' => \$opts{app},
232             'e=s' => \$opts{eval},
233             'I|lib=s' => \@libs,
234             'M=s' => \@modules,
235              
236             # Network
237             'o|host=s' => \$opts{host},
238             'p|port=i' => \$opts{port},
239              
240             # Server selection (future: pluggable servers)
241             's|server=s' => \$opts{server},
242              
243             # Environment/mode
244             'E|env=s' => \$opts{env},
245              
246             # Event loop
247             'l|loop=s' => \$opts{loop},
248              
249             # Logging
250             'access-log=s' => \$opts{access_log},
251             'no-access-log' => \$opts{no_access_log},
252              
253             # Daemon/process
254             'D|daemonize' => \$opts{daemonize},
255             'pid=s' => \$opts{pid_file},
256             'user=s' => \$opts{user},
257             'group=s' => \$opts{group},
258              
259             # Output
260             'q|quiet' => \$opts{quiet},
261             'default-middleware!' => \$opts{default_middleware},
262              
263             # Help/version
264             'help' => \$opts{help},
265             'v|version' => \$opts{version},
266 30 50       261 ) or die "Error parsing options\n";
267              
268             # Store the selected server class before the version/help early-return
269             # so --version can report the configured server (see _show_version).
270 30 100       31712 $self->{server} = $opts{server} if defined $opts{server};
271              
272             # Handle help/version flags
273 30 100       76 if ($opts{version}) {
274 1         3 $self->{show_version} = 1;
275 1         5 return;
276             }
277 29 100       60 if ($opts{help}) {
278 1         3 $self->{show_help} = 1;
279 1         4 return;
280             }
281              
282             # Apply parsed options
283 28 100       63 $self->{host} = $opts{host} if defined $opts{host};
284 28 100       69 $self->{port} = $opts{port} if defined $opts{port};
285 28 100       61 $self->{env} = $opts{env} if defined $opts{env};
286 28 100       57 $self->{loop} = $opts{loop} if defined $opts{loop};
287 28 50       60 $self->{access_log} = $opts{access_log} if defined $opts{access_log};
288 28 50       72 $self->{no_access_log} = $opts{no_access_log} if $opts{no_access_log};
289 28 100       53 $self->{daemonize} = $opts{daemonize} if $opts{daemonize};
290 28 100       58 $self->{pid_file} = $opts{pid_file} if defined $opts{pid_file};
291 28 100       51 $self->{user} = $opts{user} if defined $opts{user};
292 28 100       58 $self->{group} = $opts{group} if defined $opts{group};
293 28 100       53 $self->{quiet} = $opts{quiet} if $opts{quiet};
294             $self->{default_middleware} = $opts{default_middleware}
295 28 100       71 if defined $opts{default_middleware};
296              
297             # Add library paths
298 28 50       57 push @{$self->{libs}}, @libs if @libs;
  0         0  
299              
300             # Store -M modules for loading
301 28 100       60 push @{$self->{modules}}, @modules if @modules;
  2         7  
302              
303             # Store -e eval code
304 28 100       56 $self->{eval} = $opts{eval} if defined $opts{eval};
305              
306             # Legacy --app flag
307 28 100       49 if (defined $opts{app}) {
308 5         11 $self->{app_spec} = $opts{app};
309             }
310              
311             # Remaining args go to argv (app spec and app args)
312 28         30 push @{$self->{argv}}, @args;
  28         110  
313             }
314              
315             =head2 mode
316              
317             my $mode = $runner->mode;
318              
319             Returns the current environment mode. Determines mode by checking
320             (in order): explicit C<-E> flag, C environment variable,
321             or auto-detection based on TTY.
322              
323             =cut
324              
325             sub mode {
326 50     50 1 438 my ($self) = @_;
327              
328 50 100       164 return $self->{env} if defined $self->{env};
329 46 100       270 return $ENV{PAGI_ENV} if defined $ENV{PAGI_ENV};
330 13 50       179 return -t STDIN ? 'development' : 'production';
331             }
332              
333             =head2 load_app
334              
335             my $app = $runner->load_app;
336              
337             Loads the PAGI application based on the app specifier from command
338             line arguments. Returns the app coderef.
339              
340             =cut
341              
342             sub load_app {
343 19     19 1 1320 my ($self) = @_;
344              
345             # Add library paths to @INC before loading
346 19 50       34 if (@{$self->{libs}}) {
  19         86  
347 0         0 unshift @INC, @{$self->{libs}};
  0         0  
348             }
349              
350             # Load -M modules before evaluating -e code
351 19         45 for my $module (@{$self->{modules}}) {
  19         93  
352             # Handle Module=import,args syntax like perl -M
353 0         0 my ($mod, $imports) = split /=/, $module, 2;
354 0         0 eval "require $mod";
355 0 0       0 die "Cannot load module $mod: $@\n" if $@;
356 0 0       0 if (defined $imports) {
357 0         0 my @imports = split /,/, $imports;
358 0         0 $mod->import(@imports);
359             } else {
360 0         0 $mod->import;
361             }
362             }
363              
364             # Handle -e inline code
365 19 100       63 if (defined $self->{eval}) {
366 2         6 my $code = $self->{eval};
367 2         251 my $app = eval $code;
368 2 50       7 die "Error evaluating -e code: $@\n" if $@;
369 2 100 50     14 die "-e code must return a coderef, got " . (ref($app) || 'non-reference') . "\n"
370             unless ref $app eq 'CODE';
371 1         2 $self->{app_spec} = '-e';
372 1         2 $self->{app} = $app;
373 1         3 return $app;
374             }
375              
376             # Get app spec from argv if not set via --app
377 17         41 my @argv = @{$self->{argv}};
  17         44  
378 17 100 66     100 if (!$self->{app_spec} && @argv) {
379 7         27 my $first = $argv[0];
380             # Check if first arg looks like an app spec (not a key=value)
381 7 50       32 if ($first !~ /=/) {
382 7         14 $self->{app_spec} = shift @argv;
383 7         19 $self->{argv} = \@argv;
384             }
385             }
386              
387 17         94 my $app_spec = $self->{app_spec};
388              
389             # Default: serve current directory
390 17         25 my %app_args;
391 17 50       86 if (!defined $app_spec) {
392 0         0 $app_spec = 'PAGI::App::Directory';
393 0         0 %app_args = (root => '.');
394             } else {
395             # Parse constructor args (key=value pairs) from remaining argv
396 17         43 %app_args = $self->_parse_app_args(@{$self->{argv}});
  17         202  
397             }
398              
399 17         73 $self->{app_spec} = $app_spec;
400 17         39 $self->{app_args} = \%app_args;
401              
402 17         111 my $app;
403 17 100       68 if ($self->_is_module_name($app_spec)) {
    100          
404 1         3 $app = $self->_load_module($app_spec, %app_args);
405             }
406             elsif ($self->_is_file_path($app_spec)) {
407 15         55 $app = $self->_load_file($app_spec);
408             }
409             else {
410             # Ambiguous - try as file first, then module
411 1 50       51 if (-f $app_spec) {
412 0         0 $app = $self->_load_file($app_spec);
413             }
414             else {
415 1         19 $app = $self->_load_module($app_spec, %app_args);
416             }
417             }
418              
419 14         60 $self->{app} = $app;
420 14         41 return $app;
421             }
422              
423             =head2 prepare_app
424              
425             my $app = $runner->prepare_app;
426              
427             Loads the app and wraps it with mode-appropriate middleware.
428             In development mode (with default_middleware enabled), wraps
429             with L if available (requires PAGI-Tools).
430              
431             =cut
432              
433             sub prepare_app {
434 11     11 1 141 my ($self) = @_;
435              
436 11         87 my $app = $self->load_app;
437              
438             # Wrap with mode middleware unless disabled
439 11   100     105 my $use_middleware = $self->{default_middleware} // 1;
440              
441 11 50 66     110 if ($use_middleware && $self->mode eq 'development') {
442 0 0       0 if (eval { require PAGI::Middleware::Lint; 1 }) {
  0 0       0  
  0         0  
443 0         0 $app = PAGI::Middleware::Lint->new(strict => 1)->wrap($app);
444             warn "PAGI development mode - Lint middleware enabled\n"
445 0 0       0 unless $self->{quiet};
446             }
447             elsif (!$self->{quiet}) {
448 0         0 warn "PAGI development mode - install PAGI-Tools for Lint middleware\n";
449             }
450             }
451              
452 11         21 $self->{app} = $app;
453 11         19 return $app;
454             }
455              
456             =head2 load_server
457              
458             my $server = $runner->load_server;
459              
460             Constructs the configured server class (C<-s CLASS>, default
461             L) according to the server runner contract documented in
462             L: C<< $class->new(%options) >> followed by
463             C<< $server->run >>. Any class implementing that contract works here.
464              
465             Creates the server instance with the prepared app and configuration.
466             Parses server-specific options and passes them to the server constructor.
467              
468             =cut
469              
470             sub load_server {
471 16     16 1 89 my ($self) = @_;
472              
473 16   100     90 my $server_class = $self->{server} // 'PAGI::Server';
474              
475             # Load server class
476 16         28 my $server_file = $server_class;
477 16         67 $server_file =~ s{::}{/}g;
478 16         32 $server_file .= '.pm';
479              
480 16         36 eval { require $server_file };
  16         5666  
481 16 50       1200 if ($@) {
482 0         0 die "Cannot load server '$server_class': $@\n";
483             }
484              
485             # Get server-specific options (passed from bin/pagi-server or similar)
486 16   50     23 my %server_opts = %{$self->{server_options} // {}};
  16         218  
487              
488             # Handle access log
489             # Production mode disables logging by default for performance
490             # Use --access-log to explicitly enable in production
491 16         28 my $access_log;
492 16         28 my $disable_log = 0;
493              
494 16 50       119 if ($self->{no_access_log}) {
    50          
    50          
495             # Explicit --no-access-log
496 0         0 $disable_log = 1;
497             }
498             elsif ($self->{access_log}) {
499             # Explicit --access-log FILE
500             open $access_log, '>>', $self->{access_log}
501 0 0       0 or die "Cannot open access log $self->{access_log}: $!\n";
502             }
503             elsif ($self->mode eq 'production') {
504             # Production mode: disable logging by default
505 16         90 $disable_log = 1;
506             }
507             # else: development mode uses server default (STDERR)
508              
509             # Build server
510             # Omit host/port when socket or listen is provided (mutually exclusive)
511 16   100     87 my $has_socket_or_listen = exists $server_opts{socket} || exists $server_opts{listen};
512             return $server_class->new(
513             app => $self->{app},
514             ($has_socket_or_listen ? () : (
515             host => $self->{host} // '127.0.0.1',
516             port => $self->{port} // 5000,
517             )),
518             quiet => $self->{quiet} // 0,
519 16 100 50     565 ($self->{loop} ? (loop_type => $self->{loop}) : ()),
    50 100        
    50 50        
      33        
520             (defined $access_log || $disable_log
521             ? (access_log => $access_log) : ()),
522             %server_opts,
523             );
524             }
525              
526              
527             =head2 run
528              
529             PAGI::Server::Runner->run(@ARGV);
530             $runner->run(@ARGV);
531              
532             Main entry point. Parses options, loads the app, creates the server,
533             and delegates to C<< $server->run() >> which manages the event loop.
534              
535             =cut
536              
537             # Package variable for END block cleanup
538             our $_current_runner;
539              
540             sub run {
541 9     9 1 14 my $self = shift;
542              
543             # Support both class and instance method
544 9 50       24 unless (ref $self) {
545 9         50 $self = $self->new;
546             }
547              
548             # Parse options
549 9         39 $self->parse_options(@_);
550              
551             # Export resolved mode to environment so apps can check it
552             # (similar to Plack's PLACK_ENV)
553 9         34 $ENV{PAGI_ENV} = $self->mode;
554              
555             # Configure Future::IO for IO::Async if available
556             # This enables Future::IO-based libraries (Async::Redis, etc.) and
557             # PAGI::SSE->every() to work seamlessly under pagi-server
558 9         33 $self->_configure_future_io;
559              
560             # Handle --version
561 9 50       40 if ($self->{show_version}) {
562 0         0 $self->_show_version;
563 0         0 return;
564             }
565              
566             # Handle --help
567 9 50       60 if ($self->{show_help}) {
568 0         0 $self->_show_help;
569 0         0 return;
570             }
571              
572             # Prepare app (load + wrap with middleware)
573 9         43 $self->prepare_app;
574              
575             # Create server
576 9         78 my $server = $self->load_server;
577              
578             # Daemonize before running (bind errors will be lost in daemon mode,
579             # but this is acceptable for production where systemd/docker is preferred)
580 9 50       133 if ($self->{daemonize}) {
581 0         0 $self->_daemonize;
582             }
583              
584             # Write PID file (after daemonizing so we record the daemon's PID)
585 9 50       25 if ($self->{pid_file}) {
586 0         0 $self->_write_pid_file($self->{pid_file});
587             # Store for END block cleanup
588 0         0 $_current_runner = $self;
589             }
590              
591             # Drop privileges
592 9 50 33     53 if ($self->{user} || $self->{group}) {
593 0         0 $self->_drop_privileges;
594             }
595              
596             # Run server (server owns the event loop)
597 9         26 $server->run;
598              
599             # Cleanup PID file on normal exit
600 6 50       645 $self->_remove_pid_file if $self->{_pid_file_path};
601             }
602              
603             # END block for PID file cleanup on abnormal exit
604             END {
605 13 0 33 13   2137800 if ($_current_runner && $_current_runner->{_pid_file_path}) {
606 0           $_current_runner->_remove_pid_file;
607             }
608             }
609              
610             # Internal methods
611              
612             sub _configure_future_io {
613 9     9   17 my ($self) = @_;
614              
615             # Try to configure Future::IO for IO::Async
616             # This enables seamless use of Future::IO-based libraries under pagi-server
617 9         15 my $configured = eval {
618 9         4768 require Future::IO::Impl::IOAsync;
619 9         497109 1;
620             };
621              
622 9 50       46 if ($configured) {
623             # Report in non-production mode
624 9 50 33     50 if ($self->mode ne 'production' && !$self->{quiet}) {
625 0         0 warn "Future::IO configured for IO::Async\n";
626             }
627             }
628             # If Future::IO::Impl::IOAsync not installed, that's fine - user just
629             # won't have Future::IO integration. Apps that need it will get a
630             # helpful error message from PAGI::SSE->every() or similar.
631             }
632              
633             sub _is_module_name {
634 21     21   63 my ($self, $spec) = @_;
635 21         159 return $spec =~ /::/;
636             }
637              
638             sub _is_file_path {
639 21     21   43 my ($self, $spec) = @_;
640 21   100     145 return $spec =~ m{/} || $spec =~ /\.(?:pl|psgi)$/i;
641             }
642              
643             sub _load_module {
644 2     2   7 my ($self, $module, %args) = @_;
645              
646             # Validate module name (basic security check)
647 2 50       13 die "Invalid module name: $module\n"
648             unless $module =~ /^[A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*$/;
649              
650             # Try to load the module
651 2         4 my $file = $module;
652 2         5 $file =~ s{::}{/}g;
653 2         3 $file .= '.pm';
654              
655 2         3 eval { require $file };
  2         588  
656 2 100       7 if ($@) {
657 1         7 die "Cannot find module '$module': $@\n";
658             }
659              
660             # Check for to_app method
661 1 50 33     19 unless ($module->can('new') && $module->can('to_app')) {
662 0         0 die "Module '$module' does not have new() and to_app() methods\n";
663             }
664              
665             # Get the module's actual file path for correct home directory detection
666 1         3 my $module_file = $INC{$file};
667              
668             # Instantiate and get app (pass _caller_file for correct home dir)
669 1         4 my $instance = $module->new(%args, _caller_file => $module_file);
670 1         3 my $app = $instance->to_app;
671              
672 1 50       4 unless (ref $app eq 'CODE') {
673 0         0 die "Module '$module' to_app() did not return a coderef\n";
674             }
675              
676 1         6 return $app;
677             }
678              
679             sub _load_file {
680 15     15   32 my ($self, $file) = @_;
681              
682             # Convert to absolute path
683 15         524 $file = File::Spec->rel2abs($file);
684              
685 15 100       450 die "App file not found: $file\n" unless -f $file;
686              
687             # Match plackup behavior so FindBin::Bin resolves to the app file directory
688 14         216 local $0 = $file;
689 14         48 local @ARGV = ($file);
690 14 50       69 if (exists $INC{'FindBin.pm'}) {
691 14         131 FindBin::again();
692             }
693              
694 14         8106 my $app = do $file;
695              
696 14 50       11097 if ($@) {
697 0         0 die "Error loading $file: $@\n";
698             }
699 14 0 33     90 if (!defined $app && $!) {
700 0         0 die "Error reading $file: $!\n";
701             }
702 14 100       54 unless (ref $app eq 'CODE') {
703 1   50     4 my $type = ref($app) || 'non-reference';
704 1         15 die "App file must return a coderef, got: $type\n";
705             }
706              
707 13         121 return $app;
708             }
709              
710             sub _parse_app_args {
711 18     18   115 my ($self, @args) = @_;
712              
713 18         67 my %result;
714 18         80 for my $arg (@args) {
715 4 50       17 if ($arg =~ /^([^=]+)=(.*)$/) {
716 4         13 $result{$1} = $2;
717             }
718             else {
719 0         0 warn "Ignoring argument without '=': $arg\n";
720             }
721             }
722 18         91 return %result;
723             }
724              
725             sub _show_help {
726 0     0   0 my ($self) = @_;
727              
728 0         0 print <<'HELP';
729             Usage: pagi-server [options] [app] [key=value ...]
730              
731             Common Options (handled by Runner):
732             -I, --lib PATH Add PATH to @INC (repeatable, like perl -I)
733             -a, --app FILE Load app from file (legacy option)
734             -o, --host HOST Bind address (default: 127.0.0.1)
735             -p, --port PORT Bind port (default: 5000)
736             -s, --server CLASS Server class (default: PAGI::Server)
737             -E, --env MODE Environment mode (development, production, none)
738             -l, --loop BACKEND Event loop backend (EV, Epoll, UV, Poll)
739             --access-log FILE Access log file (default: STDERR)
740             --no-access-log Disable access logging
741             -D, --daemonize Run as background daemon
742             --pid FILE Write PID to file
743             --user USER Run as specified user (after binding)
744             --group GROUP Run as specified group (after binding)
745             -q, --quiet Suppress startup messages
746             --no-default-middleware Disable mode-based middleware
747             -v, --version Show version info
748             --help Show this help
749              
750             Environment Modes:
751             development Auto-enable Lint middleware if PAGI-Tools installed (default if TTY)
752             production No auto-middleware (default if no TTY)
753             none Explicit opt-out of all auto-middleware
754              
755             App can be:
756             Module name: pagi-server PAGI::App::Directory root=/var/www
757             File path: pagi-server ./app.pl
758             Default: pagi-server (serves current directory)
759              
760             Server-specific options are handled by the server CLI (e.g., pagi-server).
761             See: perldoc pagi-server
762              
763             HELP
764             }
765              
766             sub _show_version {
767 3     3   58 my ($self) = @_;
768              
769 3   50     9 my $server_class = $self->{server} // 'PAGI::Server';
770 3         13 (my $server_file = $server_class) =~ s{::}{/}g;
771 3         5 eval { require "$server_file.pm" };
  3         321  
772              
773 3         9 require File::Basename;
774 3         111 my $prog = File::Basename::basename($0);
775 3   50     6 my $pagi_version = eval { require PAGI; PAGI->VERSION } // 'unknown';
  3         905  
  0         0  
776 3   100     41 my $server_version = $server_class->VERSION // 'unknown';
777              
778 3         27 print "$prog (PAGI $pagi_version, $server_class $server_version)\n";
779             }
780              
781             sub _daemonize {
782 0     0   0 my ($self) = @_;
783              
784             # First fork - parent exits, child continues
785 0         0 my $pid = fork();
786 0 0       0 die "Cannot fork: $!" unless defined $pid;
787 0 0       0 exit(0) if $pid; # Parent exits
788              
789             # Child becomes session leader
790 0 0       0 setsid() or die "Cannot create new session: $!";
791              
792             # Second fork - prevent acquiring a controlling terminal
793 0         0 $pid = fork();
794 0 0       0 die "Cannot fork: $!" unless defined $pid;
795 0 0       0 exit(0) if $pid; # First child exits
796              
797             # Grandchild continues as daemon
798             # Change to root directory to avoid blocking unmounts
799 0 0       0 chdir('/') or die "Cannot chdir to /: $!";
800              
801             # Clear umask
802 0         0 umask(0);
803              
804             # Redirect standard file descriptors to /dev/null
805 0 0       0 open(STDIN, '<', '/dev/null') or die "Cannot redirect STDIN: $!";
806 0 0       0 open(STDOUT, '>', '/dev/null') or die "Cannot redirect STDOUT: $!";
807 0 0       0 open(STDERR, '>', '/dev/null') or die "Cannot redirect STDERR: $!";
808              
809 0         0 return $$; # Return daemon PID
810             }
811              
812             sub _write_pid_file {
813 2     2   346 my ($self, $pid_file) = @_;
814              
815 2 50       332 open(my $fh, '>', $pid_file)
816             or die "Cannot write PID file $pid_file: $!\n";
817 2         54 print $fh "$$\n";
818 2         116 close($fh);
819              
820             # Store for cleanup
821 2         36 $self->{_pid_file_path} = $pid_file;
822             }
823              
824             sub _remove_pid_file {
825 3     3   941 my ($self) = @_;
826              
827 3 50       13 return unless $self->{_pid_file_path};
828 3         273 unlink($self->{_pid_file_path});
829             }
830              
831             sub _drop_privileges {
832 5     5   96 my ($self) = @_;
833              
834 5         11 my $user = $self->{user};
835 5         8 my $group = $self->{group};
836              
837 5 100 100     26 return unless $user || $group;
838              
839             # Must be root to change user/group
840 4 50       42 if ($> != 0) {
841 0         0 die "Must run as root to use --user/--group\n";
842             }
843              
844             # Change group first (while still root)
845 4 100       11 if ($group) {
846 2         372 my $gid = getgrnam($group);
847 2 50       31 die "Unknown group: $group\n" unless defined $gid;
848              
849             # Set both real and effective GID
850 0         0 $( = $) = $gid;
851 0 0       0 die "Cannot change to group $group: $!\n" if $) != $gid;
852             }
853              
854             # Then change user
855 2 50       10 if ($user) {
856 2         887 my ($uid, $gid) = (getpwnam($user))[2, 3];
857 2 50       37 die "Unknown user: $user\n" unless defined $uid;
858              
859             # If no group specified, use user's primary group
860 0 0       0 unless ($group) {
861 0         0 $( = $) = $gid;
862             }
863              
864             # Set both real and effective UID
865 0         0 $< = $> = $uid;
866 0 0       0 die "Cannot change to user $user: $!\n" if $> != $uid;
867             }
868             }
869              
870             1;
871              
872             __END__