File Coverage

lib/PAGI/Runner.pm
Criterion Covered Total %
statement 207 266 77.8
branch 98 174 56.3
condition 41 64 64.0
subroutine 25 28 89.2
pod 7 7 100.0
total 378 539 70.1


line stmt bran cond sub pod time code
1             package PAGI::Runner;
2              
3 5     5   804727 use strict;
  5         9  
  5         203  
4 5     5   34 use warnings;
  5         10  
  5         359  
5 5     5   4016 use Getopt::Long qw(GetOptionsFromArray :config pass_through no_auto_abbrev no_ignore_case);
  5         57996  
  5         22  
6 5     5   4567 use Pod::Usage;
  5         339861  
  5         733  
7 5     5   46 use File::Spec;
  5         5  
  5         149  
8 5     5   625 use POSIX qw(setsid);
  5         6154  
  5         42  
9 5     5   1924 use FindBin ();
  5         1148  
  5         81  
10              
11 5     5   2716 use PAGI;
  5         13  
  5         16509  
12              
13              
14             =head1 NAME
15              
16             PAGI::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::Runner;
32              
33             PAGI::Runner->run(@ARGV);
34              
35             =head1 DESCRIPTION
36              
37             PAGI::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::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. This is the default when running
55             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::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::Runner->new(%options);
127              
128             Creates a new runner instance. Most users should use C instead.
129              
130             =cut
131              
132             sub new {
133 59     59 1 2752553 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 59   100     2498 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 21     21 1 145 my ($self, @args) = @_;
209              
210             # Check for server_options hashref passed from bin/pagi-server
211 21         66 for my $i (0 .. $#args) {
212 75 100 66     145 if ($args[$i] eq 'server_options' && ref($args[$i + 1]) eq 'HASH') {
213 2         8 $self->{server_options} = $args[$i + 1];
214 2         5 splice @args, $i, 2;
215 2         4 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 21 100       40 @args = map { /^(-[IMMe])(.+)/ ? ($1, $2) : $_ } @args;
  74         186  
222              
223 21         58 my %opts;
224             my @libs;
225 21         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 21 50       212 ) or die "Error parsing options\n";
267              
268             # Handle help/version flags
269 21 50       26247 if ($opts{version}) {
270 0         0 $self->{show_version} = 1;
271 0         0 return;
272             }
273 21 100       47 if ($opts{help}) {
274 1         5 $self->{show_help} = 1;
275 1         5 return;
276             }
277              
278             # Apply parsed options
279 20 100       42 $self->{host} = $opts{host} if defined $opts{host};
280 20 100       58 $self->{port} = $opts{port} if defined $opts{port};
281 20 50       35 $self->{server} = $opts{server} if defined $opts{server};
282 20 100       39 $self->{env} = $opts{env} if defined $opts{env};
283 20 100       38 $self->{loop} = $opts{loop} if defined $opts{loop};
284 20 50       42 $self->{access_log} = $opts{access_log} if defined $opts{access_log};
285 20 50       38 $self->{no_access_log} = $opts{no_access_log} if $opts{no_access_log};
286 20 100       39 $self->{daemonize} = $opts{daemonize} if $opts{daemonize};
287 20 100       38 $self->{pid_file} = $opts{pid_file} if defined $opts{pid_file};
288 20 100       35 $self->{user} = $opts{user} if defined $opts{user};
289 20 100       38 $self->{group} = $opts{group} if defined $opts{group};
290 20 100       37 $self->{quiet} = $opts{quiet} if $opts{quiet};
291             $self->{default_middleware} = $opts{default_middleware}
292 20 100       48 if defined $opts{default_middleware};
293              
294             # Add library paths
295 20 50       38 push @{$self->{libs}}, @libs if @libs;
  0         0  
296              
297             # Store -M modules for loading
298 20 100       28 push @{$self->{modules}}, @modules if @modules;
  2         6  
299              
300             # Store -e eval code
301 20 100       36 $self->{eval} = $opts{eval} if defined $opts{eval};
302              
303             # Legacy --app flag
304 20 100       36 if (defined $opts{app}) {
305 1         2 $self->{app_spec} = $opts{app};
306             }
307              
308             # Remaining args go to argv (app spec and app args)
309 20         20 push @{$self->{argv}}, @args;
  20         91  
310             }
311              
312             =head2 mode
313              
314             my $mode = $runner->mode;
315              
316             Returns the current environment mode. Determines mode by checking
317             (in order): explicit C<-E> flag, C environment variable,
318             or auto-detection based on TTY.
319              
320             =cut
321              
322             sub mode {
323 24     24 1 399 my ($self) = @_;
324              
325 24 100       70 return $self->{env} if defined $self->{env};
326 20 100       74 return $ENV{PAGI_ENV} if defined $ENV{PAGI_ENV};
327 16 50       388 return -t STDIN ? 'development' : 'production';
328             }
329              
330             =head2 load_app
331              
332             my $app = $runner->load_app;
333              
334             Loads the PAGI application based on the app specifier from command
335             line arguments. Returns the app coderef.
336              
337             =cut
338              
339             sub load_app {
340 20     20 1 1086 my ($self) = @_;
341              
342             # Add library paths to @INC before loading
343 20 50       54 if (@{$self->{libs}}) {
  20         333  
344 0         0 unshift @INC, @{$self->{libs}};
  0         0  
345             }
346              
347             # Load -M modules before evaluating -e code
348 20         65 for my $module (@{$self->{modules}}) {
  20         69  
349             # Handle Module=import,args syntax like perl -M
350 1         4 my ($mod, $imports) = split /=/, $module, 2;
351 1         90 eval "require $mod";
352 1 50       5 die "Cannot load module $mod: $@\n" if $@;
353 1 50       4 if (defined $imports) {
354 0         0 my @imports = split /,/, $imports;
355 0         0 $mod->import(@imports);
356             } else {
357 1         7 $mod->import;
358             }
359             }
360              
361             # Handle -e inline code
362 20 100       131 if (defined $self->{eval}) {
363 4         8 my $code = $self->{eval};
364 4         365 my $app = eval $code;
365 4 50       25 die "Error evaluating -e code: $@\n" if $@;
366 4 100 50     24 die "-e code must return a coderef, got " . (ref($app) || 'non-reference') . "\n"
367             unless ref $app eq 'CODE';
368 3         7 $self->{app_spec} = '-e';
369 3         5 $self->{app} = $app;
370 3         8 return $app;
371             }
372              
373             # Get app spec from argv if not set via --app
374 16         125 my @argv = @{$self->{argv}};
  16         116  
375 16 100 100     126 if (!$self->{app_spec} && @argv) {
376 3         4 my $first = $argv[0];
377             # Check if first arg looks like an app spec (not a key=value)
378 3 50       12 if ($first !~ /=/) {
379 3         7 $self->{app_spec} = shift @argv;
380 3         7 $self->{argv} = \@argv;
381             }
382             }
383              
384 16         47 my $app_spec = $self->{app_spec};
385              
386             # Default: serve current directory
387 16         32 my %app_args;
388 16 100       92 if (!defined $app_spec) {
389 7         21 $app_spec = 'PAGI::App::Directory';
390 7         41 %app_args = (root => '.');
391             } else {
392             # Parse constructor args (key=value pairs) from remaining argv
393 9         11 %app_args = $self->_parse_app_args(@{$self->{argv}});
  9         60  
394             }
395              
396 16         64 $self->{app_spec} = $app_spec;
397 16         42 $self->{app_args} = \%app_args;
398              
399 16         27 my $app;
400 16 100       108 if ($self->_is_module_name($app_spec)) {
    50          
401 10         70 $app = $self->_load_module($app_spec, %app_args);
402             }
403             elsif ($self->_is_file_path($app_spec)) {
404 6         30 $app = $self->_load_file($app_spec);
405             }
406             else {
407             # Ambiguous - try as file first, then module
408 0 0       0 if (-f $app_spec) {
409 0         0 $app = $self->_load_file($app_spec);
410             }
411             else {
412 0         0 $app = $self->_load_module($app_spec, %app_args);
413             }
414             }
415              
416 13         46 $self->{app} = $app;
417 13         36 return $app;
418             }
419              
420             =head2 prepare_app
421              
422             my $app = $runner->prepare_app;
423              
424             Loads the app and wraps it with mode-appropriate middleware.
425             In development mode (with default_middleware enabled), wraps
426             with L.
427              
428             =cut
429              
430             sub prepare_app {
431 10     10 1 194 my ($self) = @_;
432              
433 10         141 my $app = $self->load_app;
434              
435             # Wrap with mode middleware unless disabled
436 10   100     58 my $use_middleware = $self->{default_middleware} // 1;
437              
438 10 50 66     53 if ($use_middleware && $self->mode eq 'development') {
439 0         0 require PAGI::Middleware::Lint;
440 0         0 $app = PAGI::Middleware::Lint->new(strict => 1)->wrap($app);
441              
442             warn "PAGI development mode - Lint middleware enabled\n"
443 0 0       0 unless $self->{quiet};
444             }
445              
446 10         19 $self->{app} = $app;
447 10         30 return $app;
448             }
449              
450             =head2 load_server
451              
452             my $server = $runner->load_server;
453              
454             Creates the server instance with the prepared app and configuration.
455             Parses server-specific options and passes them to the server constructor.
456              
457             =cut
458              
459             sub load_server {
460 10     10 1 152 my ($self) = @_;
461              
462 10   50     61 my $server_class = $self->{server} // 'PAGI::Server';
463              
464             # Load server class
465 10         16 my $server_file = $server_class;
466 10         46 $server_file =~ s{::}{/}g;
467 10         21 $server_file .= '.pm';
468              
469 10         26 eval { require $server_file };
  10         3470  
470 9 50       53 if ($@) {
471 0         0 die "Cannot load server '$server_class': $@\n";
472             }
473              
474             # Get server-specific options (passed from bin/pagi-server or similar)
475 9   50     12 my %server_opts = %{$self->{server_options} // {}};
  9         38  
476              
477             # Handle access log
478             # Production mode disables logging by default for performance
479             # Use --access-log to explicitly enable in production
480 9         14 my $access_log;
481 9         11 my $disable_log = 0;
482              
483 9 50       54 if ($self->{no_access_log}) {
    50          
    50          
484             # Explicit --no-access-log
485 0         0 $disable_log = 1;
486             }
487             elsif ($self->{access_log}) {
488             # Explicit --access-log FILE
489             open $access_log, '>>', $self->{access_log}
490 0 0       0 or die "Cannot open access log $self->{access_log}: $!\n";
491             }
492             elsif ($self->mode eq 'production') {
493             # Production mode: disable logging by default
494 9         15 $disable_log = 1;
495             }
496             # else: development mode uses server default (STDERR)
497              
498             # Build server
499             # Omit host/port when socket or listen is provided (mutually exclusive)
500 9   100     38 my $has_socket_or_listen = exists $server_opts{socket} || exists $server_opts{listen};
501             return $server_class->new(
502             app => $self->{app},
503             ($has_socket_or_listen ? () : (
504             host => $self->{host} // '127.0.0.1',
505             port => $self->{port} // 5000,
506             )),
507             quiet => $self->{quiet} // 0,
508 9 100 50     169 ($self->{loop} ? (loop_type => $self->{loop}) : ()),
    50 100        
    50 50        
      33        
509             (defined $access_log || $disable_log
510             ? (access_log => $access_log) : ()),
511             %server_opts,
512             );
513             }
514              
515              
516             =head2 run
517              
518             PAGI::Runner->run(@ARGV);
519             $runner->run(@ARGV);
520              
521             Main entry point. Parses options, loads the app, creates the server,
522             and delegates to C<< $server->run() >> which manages the event loop.
523              
524             =cut
525              
526             # Package variable for END block cleanup
527             our $_current_runner;
528              
529             sub run {
530 1     1 1 3 my $self = shift;
531              
532             # Support both class and instance method
533 1 50       3 unless (ref $self) {
534 1         3 $self = $self->new;
535             }
536              
537             # Parse options
538 1         4 $self->parse_options(@_);
539              
540             # Export resolved mode to environment so apps can check it
541             # (similar to Plack's PLACK_ENV)
542 1         4 $ENV{PAGI_ENV} = $self->mode;
543              
544             # Configure Future::IO for IO::Async if available
545             # This enables Future::IO-based libraries (Async::Redis, etc.) and
546             # PAGI::SSE->every() to work seamlessly under pagi-server
547 1         4 $self->_configure_future_io;
548              
549             # Handle --version
550 1 50       4 if ($self->{show_version}) {
551 0         0 $self->_show_version;
552 0         0 return;
553             }
554              
555             # Handle --help
556 1 50       4 if ($self->{show_help}) {
557 0         0 $self->_show_help;
558 0         0 return;
559             }
560              
561             # Prepare app (load + wrap with middleware)
562 1         6 $self->prepare_app;
563              
564             # Create server
565 1         3 my $server = $self->load_server;
566              
567             # Daemonize before running (bind errors will be lost in daemon mode,
568             # but this is acceptable for production where systemd/docker is preferred)
569 1 50       17 if ($self->{daemonize}) {
570 0         0 $self->_daemonize;
571             }
572              
573             # Write PID file (after daemonizing so we record the daemon's PID)
574 1 50       4 if ($self->{pid_file}) {
575 0         0 $self->_write_pid_file($self->{pid_file});
576             # Store for END block cleanup
577 0         0 $_current_runner = $self;
578             }
579              
580             # Drop privileges
581 1 50 33     5 if ($self->{user} || $self->{group}) {
582 0         0 $self->_drop_privileges;
583             }
584              
585             # Run server (server owns the event loop)
586 1         4 $server->run;
587              
588             # Cleanup PID file on normal exit
589 0 0       0 $self->_remove_pid_file if $self->{_pid_file_path};
590             }
591              
592             # END block for PID file cleanup on abnormal exit
593             END {
594 5 0 33 5   7644 if ($_current_runner && $_current_runner->{_pid_file_path}) {
595 0         0 $_current_runner->_remove_pid_file;
596             }
597             }
598              
599             # Internal methods
600              
601             sub _configure_future_io {
602 1     1   2 my ($self) = @_;
603              
604             # Try to configure Future::IO for IO::Async
605             # This enables seamless use of Future::IO-based libraries under pagi-server
606 1         1 my $configured = eval {
607 1         557 require Future::IO::Impl::IOAsync;
608 1         61612 1;
609             };
610              
611 1 50       5 if ($configured) {
612             # Report in non-production mode
613 1 50 33     6 if ($self->mode ne 'production' && !$self->{quiet}) {
614 0         0 warn "Future::IO configured for IO::Async\n";
615             }
616             }
617             # If Future::IO::Impl::IOAsync not installed, that's fine - user just
618             # won't have Future::IO integration. Apps that need it will get a
619             # helpful error message from PAGI::SSE->every() or similar.
620             }
621              
622             sub _is_module_name {
623 20     20   51 my ($self, $spec) = @_;
624 20         188 return $spec =~ /::/;
625             }
626              
627             sub _is_file_path {
628 11     11   20 my ($self, $spec) = @_;
629 11   100     764 return $spec =~ m{/} || $spec =~ /\.(?:pl|psgi)$/i;
630             }
631              
632             sub _load_module {
633 10     10   24 my ($self, $module, %args) = @_;
634              
635             # Validate module name (basic security check)
636 10 50       108 die "Invalid module name: $module\n"
637             unless $module =~ /^[A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*$/;
638              
639             # Try to load the module
640 10         12 my $file = $module;
641 10         70 $file =~ s{::}{/}g;
642 10         14 $file .= '.pm';
643              
644 10         21 eval { require $file };
  10         3113  
645 10 100       35 if ($@) {
646 1         9 die "Cannot find module '$module': $@\n";
647             }
648              
649             # Check for to_app method
650 9 50 33     144 unless ($module->can('new') && $module->can('to_app')) {
651 0         0 die "Module '$module' does not have new() and to_app() methods\n";
652             }
653              
654             # Get the module's actual file path for correct home directory detection
655 9         24 my $module_file = $INC{$file};
656              
657             # Instantiate and get app (pass _caller_file for correct home dir)
658 9         30 my $instance = $module->new(%args, _caller_file => $module_file);
659 9         22 my $app = $instance->to_app;
660              
661 9 50       43 unless (ref $app eq 'CODE') {
662 0         0 die "Module '$module' to_app() did not return a coderef\n";
663             }
664              
665 9         47 return $app;
666             }
667              
668             sub _load_file {
669 6     6   22 my ($self, $file) = @_;
670              
671             # Convert to absolute path
672 6         317 $file = File::Spec->rel2abs($file);
673              
674 6 100       243 die "App file not found: $file\n" unless -f $file;
675              
676             # Match plackup behavior so FindBin::Bin resolves to the app file directory
677 5         82 local $0 = $file;
678 5         26 local @ARGV = ($file);
679 5 50       33 if (exists $INC{'FindBin.pm'}) {
680 5         69 FindBin::again();
681             }
682              
683 5         4041 my $app = do $file;
684              
685 5 50       19 if ($@) {
686 0         0 die "Error loading $file: $@\n";
687             }
688 5 0 33     13 if (!defined $app && $!) {
689 0         0 die "Error reading $file: $!\n";
690             }
691 5 100       17 unless (ref $app eq 'CODE') {
692 1   50     3 my $type = ref($app) || 'non-reference';
693 1         14 die "App file must return a coderef, got: $type\n";
694             }
695              
696 4         38 return $app;
697             }
698              
699             sub _parse_app_args {
700 10     10   33 my ($self, @args) = @_;
701              
702 10         10 my %result;
703 10         42 for my $arg (@args) {
704 5 50       22 if ($arg =~ /^([^=]+)=(.*)$/) {
705 5         16 $result{$1} = $2;
706             }
707             else {
708 0         0 warn "Ignoring argument without '=': $arg\n";
709             }
710             }
711 10         36 return %result;
712             }
713              
714             sub _show_help {
715 0     0   0 my ($self) = @_;
716              
717 0         0 print <<'HELP';
718             Usage: pagi-server [options] [app] [key=value ...]
719              
720             Common Options (handled by Runner):
721             -I, --lib PATH Add PATH to @INC (repeatable, like perl -I)
722             -a, --app FILE Load app from file (legacy option)
723             -o, --host HOST Bind address (default: 127.0.0.1)
724             -p, --port PORT Bind port (default: 5000)
725             -s, --server CLASS Server class (default: PAGI::Server)
726             -E, --env MODE Environment mode (development, production, none)
727             -l, --loop BACKEND Event loop backend (EV, Epoll, UV, Poll)
728             --access-log FILE Access log file (default: STDERR)
729             --no-access-log Disable access logging
730             -D, --daemonize Run as background daemon
731             --pid FILE Write PID to file
732             --user USER Run as specified user (after binding)
733             --group GROUP Run as specified group (after binding)
734             -q, --quiet Suppress startup messages
735             --no-default-middleware Disable mode-based middleware
736             -v, --version Show version info
737             --help Show this help
738              
739             Environment Modes:
740             development Auto-enable Lint middleware (default if TTY)
741             production No auto-middleware (default if no TTY)
742             none Explicit opt-out of all auto-middleware
743              
744             App can be:
745             Module name: pagi-server PAGI::App::Directory root=/var/www
746             File path: pagi-server ./app.pl
747             Default: pagi-server (serves current directory)
748              
749             Server-specific options are handled by the server CLI (e.g., pagi-server).
750             See: perldoc pagi-server
751              
752             HELP
753             }
754              
755             sub _show_version {
756 0     0   0 my ($self) = @_;
757              
758 0         0 require PAGI;
759 0         0 require PAGI::Server;
760 0         0 print "pagi-server (PAGI $PAGI::VERSION, PAGI::Server $PAGI::Server::VERSION)\n";
761             }
762              
763             sub _daemonize {
764 0     0   0 my ($self) = @_;
765              
766             # First fork - parent exits, child continues
767 0         0 my $pid = fork();
768 0 0       0 die "Cannot fork: $!" unless defined $pid;
769 0 0       0 exit(0) if $pid; # Parent exits
770              
771             # Child becomes session leader
772 0 0       0 setsid() or die "Cannot create new session: $!";
773              
774             # Second fork - prevent acquiring a controlling terminal
775 0         0 $pid = fork();
776 0 0       0 die "Cannot fork: $!" unless defined $pid;
777 0 0       0 exit(0) if $pid; # First child exits
778              
779             # Grandchild continues as daemon
780             # Change to root directory to avoid blocking unmounts
781 0 0       0 chdir('/') or die "Cannot chdir to /: $!";
782              
783             # Clear umask
784 0         0 umask(0);
785              
786             # Redirect standard file descriptors to /dev/null
787 0 0       0 open(STDIN, '<', '/dev/null') or die "Cannot redirect STDIN: $!";
788 0 0       0 open(STDOUT, '>', '/dev/null') or die "Cannot redirect STDOUT: $!";
789 0 0       0 open(STDERR, '>', '/dev/null') or die "Cannot redirect STDERR: $!";
790              
791 0         0 return $$; # Return daemon PID
792             }
793              
794             sub _write_pid_file {
795 3     3   408 my ($self, $pid_file) = @_;
796              
797 3 50       586 open(my $fh, '>', $pid_file)
798             or die "Cannot write PID file $pid_file: $!\n";
799 3         124 print $fh "$$\n";
800 3         199 close($fh);
801              
802             # Store for cleanup
803 3         79 $self->{_pid_file_path} = $pid_file;
804             }
805              
806             sub _remove_pid_file {
807 5     5   20994 my ($self) = @_;
808              
809 5 50       38 return unless $self->{_pid_file_path};
810 5         789 unlink($self->{_pid_file_path});
811             }
812              
813             sub _drop_privileges {
814 5     5   71 my ($self) = @_;
815              
816 5         12 my $user = $self->{user};
817 5         11 my $group = $self->{group};
818              
819 5 100 100     50 return unless $user || $group;
820              
821             # Must be root to change user/group
822 4 50       24 if ($> != 0) {
823 0         0 die "Must run as root to use --user/--group\n";
824             }
825              
826             # Change group first (while still root)
827 4 100       11 if ($group) {
828 2         316 my $gid = getgrnam($group);
829 2 50       29 die "Unknown group: $group\n" unless defined $gid;
830              
831             # Set both real and effective GID
832 0         0 $( = $) = $gid;
833 0 0       0 die "Cannot change to group $group: $!\n" if $) != $gid;
834             }
835              
836             # Then change user
837 2 50       9 if ($user) {
838 2         1902 my ($uid, $gid) = (getpwnam($user))[2, 3];
839 2 50       52 die "Unknown user: $user\n" unless defined $uid;
840              
841             # If no group specified, use user's primary group
842 0 0         unless ($group) {
843 0           $( = $) = $gid;
844             }
845              
846             # Set both real and effective UID
847 0           $< = $> = $uid;
848 0 0         die "Cannot change to user $user: $!\n" if $> != $uid;
849             }
850             }
851              
852             1;
853              
854             __END__