File Coverage

blib/lib/FCGI/Daemon.pm
Criterion Covered Total %
statement 32 187 17.1
branch 0 92 0.0
condition 0 35 0.0
subroutine 11 18 61.1
pod 5 5 100.0
total 48 337 14.2


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2 1     1   75748 use strict;
  1         2  
  1         45  
3             #use warnings;
4             #use diagnostics -verbose;
5             ## no critic (TestingAndDebugging::RequireUseWarnings)
6             package FCGI::Daemon;
7             our $VERSION = '0.20220206';
8 1     1   13 use 5.14.2;
  1         3  
9 1     1   1168 use English '-no_match_vars';
  1         4439  
  1         6  
10 1     1   828 use BSD::Resource; # on Debian available as libbsd-resource-perl
  1         3931  
  1         4  
11 1     1   689 use FCGI 0.82; # on Debian available as libfcgi-perl
  1         876  
  1         27  
12 1     1   430 use FCGI::ProcManager 0.28; # on Debian available as libfcgi-procmanager-perl
  1         10045  
  1         57  
13 1     1   1815 use Getopt::Std;
  1         50  
  1         60  
14 1     1   444 use autouse 'Pod::Usage'=>qw(pod2usage);
  1         643  
  1         6  
15              
16             =head1 NAME
17              
18             FCGI::Daemon - Perl-aware Fast CGI daemon for use with nginx web server.
19              
20             =head1 VERSION
21              
22             Version 0.20220206
23              
24             =begin comment
25             =cut
26              
27             my %o;
28              
29             __PACKAGE__->run() unless caller(); # modulino i.e. executable rather than module
30              
31             =head2 help()
32             print help screen extracted from POD
33             =cut
34 0 0   0 1   sub help { pod2usage(-verbose=>$ARG[0],-noperldoc=>1) and exit; } ## no critic
35              
36             =head2 dieif()
37             exit handler
38             =cut
39             sub dieif {
40 0 0   0 1   if($ARG[0]){
41 0           my $err=$ARG[1];
42 0           for my $f ( @o{'pidfile','sockfile'} ) {
43 0 0         unlink $f if -f $f;
44             }
45 0           print "Error - $err:\n",$ARG[0],"\n";
46 0           exit 1;
47             }
48             }
49              
50             =head2 run()
51             Modulino-style main routine
52             =cut
53             sub run {
54 0 0   0 1   getopts('hde:f:q:p:s:g:u:m:c:l:w:',\%o) or help(0);
55 0 0         help(2) if $o{'h'};
56              
57 0   0       $o{sockfile}=$o{'s'}||'/var/run/fcgi-daemon.sock';
58 0 0 0       $o{pidfile}=$o{'p'}||'/var/run/fcgi-daemon.pid' if $o{'d'};
59 0 0         $o{prefork}=defined $o{'w'} ? $o{'w'} : 1;
60 0 0         $o{queue}=defined $o{'q'} ? $o{'q'} : 96;
61 0   0       $o{rlimit_vmem}=($o{'m'}||512)*1024*1024;
62 0   0       $o{rlimit_cpu}=$o{'c'}||32;
63 0 0         $o{max_evals}=defined $o{'e'} ? $o{'e'} : 10240; #max evals before exit - paranoid to free memory if leaks
64 0   0       $o{file_pattern}=$o{'f'}||qr{\.pl};
65 0   0       $o{leak_threshold}=$o{'l'}||1.3;
66              
67 0 0 0       if($REAL_USER_ID==$EFFECTIVE_USER_ID and $EFFECTIVE_USER_ID==0){ # if run as root
68 0   0       $o{gid}=$o{g}||'www-data'; $o{gid_num}=scalar getgrnam($o{gid});
  0            
69 0   0       $o{uid}=$o{u}||'www-data'; $o{uid_num}=scalar getpwnam($o{uid});
  0            
70             }
71              
72             local $SIG{INT}= local $SIG{TERM}= sub{
73             # actually FCGI::ProcManager override our TERM handler so .sock and .pid files will be removed only by sysv script... :(
74 0 0   0     $o{fcgi_pm}->pm_remove_pid_file() if $o{fcgi_pm};
75 0           for my $f ( @o{'pidfile','sockfile'} ) {
76 0 0         unlink $f if -f $f;
77             }
78 0 0         $o{fcgi_pm}->pm_die() if $o{fcgi_pm}; #pm_die() does not return
79 0           exit 0;
80 0           };
81              
82             # daemonize
83 0 0         if($o{'d'}){
84 0           chdir '/'; # this is good practice for unmounting
85 0           local $PROGRAM_NAME='FCGI::Daemon';
86 0 0         defined(my $pid=fork) or die "Can't fork: $!";
87 0 0         exit if $pid;
88 1 0   1   457 eval {use POSIX qw(setsid); POSIX::setsid();} or die q{Can't start a new session: }.$OS_ERROR;
  1         7  
  1         7  
  0            
  0            
89 0           open *STDIN,'<','/dev/null';
90 0           open *STDOUT,'>>','/dev/null';
91 0           open *STDERR,'>>','/dev/null';
92 0           umask 022;
93             }
94              
95 0           my %req_env;
96             $o{fcgi_pm}=FCGI::ProcManager->new({n_processes=>$o{prefork},
97             die_timeout=>28,
98             pid_fname=>$o{pidfile}
99 0           });
100 0           print "Opening socket $o{sockfile}\n";
101             my $rqst=FCGI::Request(\*STDIN,\*STDOUT,\*STDERR,\%req_env,
102 0 0         FCGI::OpenSocket($o{sockfile},$o{prefork}*$o{queue}),
103             FCGI::FAIL_ACCEPT_ON_INTR())
104             or die "Error: Unable to create FCGI::Request...";
105              
106 0 0 0       if(defined $o{gid_num} and defined $o{uid_num}){ # if run as root
107             chown $o{uid_num},$o{gid_num},$o{sockfile} # chown SOCKfile
108 0 0         or dieif($OS_ERROR,'Unable to chown SOCKfile');
109             }
110              
111 0           $o{fcgi_pm}->pm_manage(); # from now on we are worker process
112              
113             # drop privileges if run as root
114 0 0 0       if(defined $o{gid_num} and defined $o{uid_num}){
115 0           my $gid = getgrnam($o{gid});
116 0           $EFFECTIVE_GROUP_ID = "$gid $gid";
117 0           dieif($OS_ERROR,'Unable to effective group_id to '.$o{gid});
118 0           $REAL_GROUP_ID = $gid;
119 0           dieif($OS_ERROR,'Unable to change real group_id to '.$o{gid});
120 0           my $uid = getpwnam($o{uid});
121 0           $EFFECTIVE_USER_ID = $uid;
122 0           dieif($OS_ERROR,'Unable to change effective user_id to '.$o{uid});
123 0           $REAL_USER_ID = $uid;
124 0           dieif($OS_ERROR,'Unable to change real user_id to '.$o{uid});
125             }
126              
127             ## set rlimit(s)
128             setrlimit(RLIMIT_AS, $o{rlimit_vmem}, $o{rlimit_vmem})
129 0 0         or warn "Unable to set RLIMIT_AS.\n";
130             setrlimit(RLIMIT_CPU, $o{rlimit_cpu}, $o{rlimit_cpu})
131 0 0         or warn "Unable to set RLIMIT_CPU.\n";
132              
133             REQ_LOOP: # main loop
134 0           while($rqst->Accept()>=0){
135              
136 0           $req_env{'PATH_INFO'}=$req_env{'SCRIPT_FILENAME'};
137 0           $req_env{'SCRIPT_FILENAME'}=get_file_from_path($req_env{SCRIPT_FILENAME});
138 0           $req_env{'PATH_INFO'}=~s/$req_env{'SCRIPT_FILENAME'}//;
139 0           $req_env{'SCRIPT_NAME'}=$req_env{'SCRIPT_FILENAME'};
140 0           $req_env{'SCRIPT_NAME'}=~s/$req_env{'DOCUMENT_ROOT'}//;
141              
142             # check if script (exacutable, readable, non-zero size)
143 0 0         unless(-x -s -r $req_env{'SCRIPT_FILENAME'}){
144 0           print "Content-type: text/plain\r\n\r\n";
145 0           $_="Error: No such CGI app - $req_env{SCRIPT_FILENAME} may not exist or is not executable by this process.\n";
146 0           print $_;
147 0           print {*STDERR} $_;
  0            
148 0           next;
149             }
150              
151 0           local @ENV{keys %req_env}=values %req_env;
152 0 0         chdir $1 if $req_env{'SCRIPT_FILENAME'}=~m{^(.*)\/}; # cd to the script's local directory
153              
154             # Fast Perl-CGI processing
155 0 0 0       if($o{max_evals}>0 and $req_env{'SCRIPT_FILENAME'}=~m{$o{file_pattern}\z}){ # detect if perl script
156 0           my %allvars;
157 0           @allvars{keys %main::}=();
158             {
159 0     0     local *CORE::GLOBAL::exit=sub { die 'notr3a11yeXit' };
  0            
  0            
160 0           local $0=$req_env{SCRIPT_FILENAME}; #fixes FindBin (in English $0 means $PROGRAM_NAME)
161 1     1   506 no strict; ## no critic :: default for Perl5
  1         2  
  1         524  
162 0           do $0; # do $0; could be enough for strict scripts
163 0 0         if($EVAL_ERROR){
164 0           $EVAL_ERROR=~s{\n+\z}{};
165 0 0         print {*STDERR} "$0\n$EVAL_ERROR\n\b" unless $EVAL_ERROR =~ m{^notr3a11yeXit};
  0            
166             }
167             }
168              
169             #untested experimental callback to execute on script exit
170             #$_{$req_env{SCRIPT_FILENAME}}->{SIGTERM}() if defined $_{$req_env{SCRIPT_FILENAME}}->{SIGTERM};
171             #Perl scripts can cache persistent data in $_{$0}->{mydata}
172             #However if you store too much data it may trigger termination by rlimit
173             #After DO/EVAL $_{$0}->{'SIGTERM'} is being called so termination handler
174             #can be used to close DB connections etc.
175             #$_{$0}->{'SIGTERM'}=sub { print "I closed my handles"; };
176              
177 0           foreach(keys %main::){ # cleanup garbage after do()
178 0 0         next if exists $allvars{$_};
179 0 0         next if m{::$};
180 0 0         next if m{^_};
181 0           delete $main::{$_};
182             }
183              
184 0 0         if(open my $STAT,'<',"/proc/$$/status"){
185 0           my %stat;
186 0           while(my ($k,$v)=split /\:\s+/,<$STAT>){
187 0           chop $v;
188 0           $stat{$k}=$v;
189             }
190 0           close $STAT;
191             # check if child takes too much resident memory and terminate if necessary
192 0 0         if($stat{VmSize}/$stat{VmRSS}<$o{leak_threshold}){
193 0           print {*STDERR} 'fcgi-daemon :: terminating child - memory leak? '
194 0           ."VmSize:$stat{VmSize}; VmRSS:$stat{VmRSS}; Ratio:".$stat{VmSize}/$stat{VmRSS};
195 0           exit;
196             }
197             }
198 0 0         exit unless --$o{max_evals};
199 0           next REQ_LOOP;
200             }
201              
202             # Normal CGI processing
203 0           $o{fcgi_pm}->pm_pre_dispatch();
204 0           local $OUTPUT_AUTOFLUSH=1; # select below is equivalent of: my $oldfh=select($CERR); $|=1; select($oldfh);
205 0           pipe my($PERR),my($CERR); select((select($CERR),$OUTPUT_AUTOFLUSH=1)[0]); #prepare child-to-parent pipe and swith off buffering
  0            
206 0           pipe my($CIN),my($PIN); select((select($PIN),$OUTPUT_AUTOFLUSH=1)[0]); #prepare parent-to-child pipe and swith off buffering
  0            
207              
208 0           my $pid=open my($COUT),"-|"; ## fork and pipe to us
209 0 0         unless(defined $pid){
210 0           print "Content-type: text/plain\r\n\r\n"
211             ."Error: CGI app returned no output - Executing $req_env{SCRIPT_FILENAME} failed !\n";
212 0           next;
213             }
214 0           $rqst->Detach(); # needed to restore original STDIN,STDOUT,STDERR
215              
216 0 0         unless($pid){ #### Child ####
217 0           close $PIN; # <--perhaps not really necessary
218 0 0         open *STDIN,'<&=',$CIN or die 'unable to reopen STDIN';
219 0 0         open *STDERR,'>&=',$CERR or die 'unable to reopen STDERR';
220 0 0         exec $req_env{'SCRIPT_FILENAME'} or die "exec failed"; # running the cgi app (exec does not return so child terminates here)
221             }else{ #### Parent ####
222 0           close $CIN; # <--perhaps not really necessary
223 0           close $CERR; # *must* close child's file handle to avoid deadlock
224 0           $rqst->Attach(); #reattach FCGI's STDIN,STDOUT,STDERR
225              
226             ## send STDIN to child
227 0           my $buffer;
228             #print {$PIN} $buffer while (read *STDIN,$buffer,$ENV{'CONTENT_LENGTH'}); ## longer version below may be safer for very long input.
229 0 0 0       if($req_env{'REQUEST_METHOD'} =~ /(?:POST|PUT)/ and $req_env{'CONTENT_LENGTH'}!=0){
230 0           my $bytes=0;
231 0           while ($bytes<$req_env{'CONTENT_LENGTH'}){
232 0           $bytes+=read *STDIN,$buffer,($req_env{'CONTENT_LENGTH'}-$bytes);
233 0 0 0       last if ($bytes==0 or not defined $bytes);
234 0           print {$PIN} $buffer;
  0            
235             } }
236 0           close $PIN;
237              
238 1     1   487 use IO::Select; # non-blocking read from child's redirected STDOUT and STDERR
  1         1479  
  1         487  
239 0           my $sel = IO::Select->new($COUT,$PERR);
240 0           while(my @ready=$sel->can_read){
241 0           for my $FH (@ready){
242 0 0         if(0==sysread $FH,$buffer,4096){
243 0           $sel->remove($FH);
244 0           close $FH;
245             }else{
246 0 0         print {$FH==$COUT ? *STDOUT:*STDERR} $buffer;
  0            
247             } } }
248 0           waitpid $pid,0;
249 0           $rqst->Finish();
250             }
251 0           $o{fcgi_pm}->pm_post_dispatch();
252             }
253 0           return;
254             }
255              
256             # overriding process names
257             sub FCGI::ProcManager::pm_change_process_name {
258 0     0 1   my ($self,$name)=@_;
259 0           my %p=( 'perl-fcgi-pm' =>'FCGI::Daemon',
260             'perl-fcgi' =>'FCGI::Daemon-worker',
261             );
262 0 0         $0=$p{$name} if $p{$name} ne ''; ## no critic
263 0           return;
264             }
265              
266             =head2 get_file_from_path()
267             Find first file in path
268             =cut
269             sub get_file_from_path {
270 0     0 1   local $_=shift;
271 0           my $file='';
272 0           for(split '/',$_){
273 0 0         next if $_ eq '';
274 0           $file.='/'.$_;
275 0 0         last if -f -s $file;
276             }
277 0           return $file;
278             }
279              
280             1;
281             __END__