File Coverage

blib/lib/Paranoid/Process.pm
Criterion Covered Total %
statement 160 256 62.5
branch 38 88 43.1
condition 21 36 58.3
subroutine 27 34 79.4
pod 15 15 100.0
total 261 429 60.8


line stmt bran cond sub pod time code
1             # Paranoid::Process -- Process management support for paranoid programs
2             #
3             # $Id: lib/Paranoid/Process.pm, 2.10 2022/03/08 00:01:04 acorliss Exp $
4             #
5             # This software is free software. Similar to Perl, you can redistribute it
6             # and/or modify it under the terms of either:
7             #
8             # a) the GNU General Public License
9             # as published by the
10             # Free Software Foundation ; either version 1
11             # , or any later version
12             # , or
13             # b) the Artistic License 2.0
14             # ,
15             #
16             # subject to the following additional term: No trademark rights to
17             # "Paranoid" have been or are conveyed under any of the above licenses.
18             # However, "Paranoid" may be used fairly to describe this unmodified
19             # software, in good faith, but not as a trademark.
20             #
21             # (c) 2005 - 2020, Arthur Corliss (corliss@digitalmages.com)
22             # (tm) 2008 - 2020, Paranoid Inc. (www.paranoid.com)
23             #
24             #####################################################################
25              
26             #####################################################################
27             #
28             # Environment definitions
29             #
30             #####################################################################
31              
32             package Paranoid::Process;
33              
34 49     49   21392 use 5.008;
  49         153  
35              
36 49     49   264 use strict;
  49         100  
  49         931  
37 49     49   213 use warnings;
  49         89  
  49         1289  
38 49     49   241 use vars qw($VERSION @EXPORT @EXPORT_OK %EXPORT_TAGS);
  49         69  
  49         2461  
39 49     49   225 use base qw(Exporter);
  49         398  
  49         3047  
40 49     49   300 use Paranoid;
  49         59  
  49         2004  
41 49     49   7358 use Paranoid::Debug qw(:all);
  49         109  
  49         7973  
42 49     49   20649 use POSIX qw(getuid setuid setgid WNOHANG setsid);
  49         277188  
  49         267  
43 49     49   62009 use Carp;
  49         117  
  49         109099  
44              
45             ($VERSION) = ( q$Revision: 2.10 $ =~ /(\d+(?:\.\d+)+)/sm );
46              
47             @EXPORT = qw(switchUser daemonize);
48             @EXPORT_OK = (
49             @EXPORT, qw(MAXCHILDREN childrenCount installChldHandler
50             sigchld pfork pcommFork ptranslateUser ptranslateGroup pcapture
51             installSIGH uninstallSIGH installSIGD uninstallSIGD
52             ) );
53             %EXPORT_TAGS = (
54             all => [@EXPORT_OK],
55             misc => [qw(pcapture)],
56             pfork => [
57             qw(MAXCHILDREN childrenCount installChldHandler
58             sigchld pfork pcommFork daemonize)
59             ],
60             signal => [qw(installSIGH uninstallSIGH installSIGD uninstallSIGD)],
61             user => [qw(switchUser ptranslateUser ptranslateGroup)],
62             );
63              
64             #####################################################################
65             #
66             # Module code follows
67             #
68             #####################################################################
69              
70             {
71             my %signals = ( map { $_ => [] } keys %SIG );
72             my %original = %SIG;
73              
74             sub _sigHandler {
75              
76             # Purpose: Runs all code refs assigned to the signal
77             # Returns: Boolean
78             # Usage: _sigHandler($signal);
79              
80 0     0   0 my ( $signal, @sargs ) = @_;
81 0         0 my $sref;
82              
83 0         0 local $SIG{$signal} = 'IGNORE';
84 0         0 foreach $sref ( @{ $signals{$signal} } ) {
  0         0  
85 0         0 &$sref( $signal, @sargs );
86             }
87              
88 0         0 return 1;
89             }
90              
91             sub installSIGD {
92              
93             # Purpose: Installs dispatcher for sig handlers that have code refs
94             # assigned
95             # Returns: Boolean
96             # Usage: $rv = installSIGD();
97              
98 0     0 1 0 my $sig;
99              
100 0         0 subPreamble(PDLEVEL1);
101              
102 0         0 foreach $sig ( keys %signals ) {
103 0 0       0 $SIG{$sig} = \&_sigHandler if scalar @{ $signals{$sig} };
  0         0  
104             }
105              
106 0         0 subPostamble( PDLEVEL1, '$', 1 );
107              
108 0         0 return 1;
109             }
110              
111             sub uninstallSIGD {
112              
113             # Purpose: Uninstalls the dispatcher
114             # Returns: Boolean
115             # Usage: $rv = uninstallSIGD();
116              
117 0     0 1 0 subPreamble(PDLEVEL1);
118              
119 0         0 foreach ( keys %original ) {
120             $SIG{$_} = $original{$_}
121             if defined $SIG{$_}
122 0 0 0     0 and $SIG{$_} eq \&_sigHandler;
123             }
124              
125 0         0 subPostamble( PDLEVEL1, '$', 1 );
126              
127 0         0 return 1;
128             }
129              
130             sub installSIGH ($\&) {
131              
132             # Purpose: Assigns a code ref to a signal array
133             # Returns: Boolean
134             # Usage: $rv = installSIGH($signal, $sref);
135              
136 0     0 1 0 my ( $signal, $sref ) = @_;
137 0         0 my $rv = 1;
138              
139 0         0 subPreamble( PDLEVEL1, '$\&', $signal, $sref );
140              
141 0 0       0 if ( exists $signals{$signal} ) {
142 0 0       0 if ( grep { $_ eq $sref } @{ $signals{$signal} } ) {
  0         0  
  0         0  
143 0         0 pdebug( '%s handler already installed', PDLEVEL2, $signal );
144             } else {
145 0         0 push @{ $signals{$signal} }, $sref;
  0         0  
146 0         0 pdebug( '%s handler installed', PDLEVEL2, $signal );
147             }
148             } else {
149 0         0 pdebug( 'unknown signal: %s', PDLEVEL1, $signal );
150 0         0 $rv = 0;
151             }
152              
153 0         0 subPostamble( PDLEVEL1, '$', $rv );
154              
155 0         0 return $rv;
156             }
157              
158             sub uninstallSIGH ($\&) {
159              
160             # Purpose: Removes a code ref for a signal array
161             # Returns: Boolean
162             # Usage: $rv = uninstallSIGH($signal, $sref);
163              
164 0     0 1 0 my ( $signal, $sref ) = @_;
165 0         0 my $rv = 1;
166 0         0 my ( $sigh, $i );
167              
168 0         0 subPreamble( PDLEVEL1, '$\&', $signal, $sref );
169              
170 0 0       0 if ( exists $signals{$signal} ) {
171 0 0       0 if ( grep { $_ eq $sref } @{ $signals{$signal} } ) {
  0         0  
  0         0  
172 0         0 $i = 0;
173 0         0 foreach $sigh ( @{ $signals{$signal} } ) {
  0         0  
174 0 0       0 if ( $sigh eq $sref ) {
175 0         0 splice @{ $signals{$signal} }, $i, 1;
  0         0  
176 0         0 last;
177             }
178 0         0 $i++;
179             }
180 0         0 pdebug( '%s handler removed', PDLEVEL2, $signal );
181             } else {
182 0         0 pdebug( 'no %s handler to remove', PDLEVEL2, $signal );
183             }
184             } else {
185 0         0 pdebug( 'unknown signal: %s', PDLEVEL1, $signal );
186 0         0 $rv = 0;
187             }
188              
189 0         0 subPostamble( PDLEVEL1, '$', $rv );
190              
191 0         0 return $rv;
192             }
193             }
194              
195             {
196             my $maxChildren = 0;
197             my $numChildren = 0;
198             my @forkedPIDs = ();
199             my $chldRef = undef;
200              
201             sub MAXCHILDREN : lvalue {
202              
203             # Purpose: Gets/sets $maxChildren
204             # Returns: $maxChildren
205             # Usage: $max = MAXCHILDREN;
206             # Usage: MAXCHILDREN = 20;
207              
208 276     276 1 1131 $maxChildren;
209             }
210 551     551 1 69066732 sub childrenCount { return $numChildren }
211 221     221   1947 sub _incrChildren { $numChildren++ }
212 128     128   437 sub _decrChildren { $numChildren-- }
213              
214             sub _resetChildren {
215 36     36   871 @forkedPIDs = ();
216 36         701 $numChildren = 0;
217             }
218              
219             sub installChldHandler (\&) {
220              
221             # Purpose: Installs a code reference to execute whenever a child
222             # exits
223             # Returns: True (1)
224             # Usage: installChldHandler(&foo);
225              
226 7     7 1 77 $chldRef = shift;
227              
228 7         126 return 1;
229             }
230 140     140   684 sub _chldHandler { return $chldRef }
231              
232 221     221   2914 sub _addPID { push @forkedPIDs, shift }
233              
234             sub _grepPID {
235 128     128   541 my $pid = shift;
236 128         444 return scalar grep { $_ == $pid } @forkedPIDs;
  388         2608  
237             }
238              
239             sub _delPID {
240 128     128   413 my $pid = shift;
241 128         311 @forkedPIDs = grep { $_ != $pid } @forkedPIDs;
  388         809  
242 128         239 return 1;
243             }
244             }
245              
246             sub sigchld {
247              
248             # Purpose: Default signal handler for SIGCHLD
249             # Returns: True (1)
250             # Usage: $SIG{CHLD} = \&sigchld;
251              
252 140     140 1 13759139 my ($pid);
253 140         914 my $sref = _chldHandler();
254              
255             # Remove the signal handler so we're not preempted
256 140     0   4703 local $SIG{CHLD} = sub {1};
  0         0  
257              
258             # Process children exit values
259 140         597 do {
260 268         7010 $pid = waitpid -1, WNOHANG;
261 268 100 66     2262 if ( $pid > 0 and _grepPID($pid) ) {
262 128         646 _decrChildren();
263 128         482 _delPID($pid);
264 128         879 pdebug( 'child %d reaped w/rv: %s', PDLEVEL1, $pid, $? );
265 128         676 pdebug( 'children remaining: %s', PDLEVEL1, childrenCount() );
266              
267             # Call the user's sig handler if defined
268 128 100       879 &$sref( $pid, $? ) if defined $sref;
269             }
270             } until $pid < 1;
271              
272 140         3408 return 1;
273             }
274              
275             sub daemonize {
276              
277             # Purpose: Daemonizes process and disassociates with the terminal
278             # Returns: True unless there are errors.
279             # Usage: daemonize();
280              
281 0     0 1 0 my ( $rv, $pid );
282              
283 0         0 subPreamble(PDLEVEL1);
284              
285 0         0 $pid = fork;
286              
287             # Exit if we're the parent process
288 0 0       0 exit 0 if $pid;
289              
290 0 0       0 if ( defined $pid ) {
291              
292             # Fork was successful, close parent file descriptors
293 0 0       0 $rv = open( STDIN, '/dev/null' ) and open( STDOUT, '>/dev/null' );
294              
295             # Create a new process group
296 0 0       0 unless ($rv) {
297 0         0 setsid();
298 0         0 $rv = open STDERR, '>&STDOUT';
299 0 0       0 croak "Can't dup stdout: $!" unless $rv;
300 0         0 chdir '/';
301             }
302              
303             } else {
304 0         0 Paranoid::ERROR =
305             pdebug( 'Failed to daemonize process: %s', PDLEVEL1, $! );
306 0         0 $rv = 0;
307             }
308              
309 0         0 subPostamble( PDLEVEL1, '$', $rv );
310              
311 0         0 return $rv;
312             }
313              
314             sub pfork {
315              
316             # Purpose: Replacement for Perl's fork function. Blocks until a child
317             # exists if MAXCHILDREN is exceeded.
318             # Returns: Return value of children handler if installed, otherwise
319             # undef.
320             # Usage: $rv = pfork();
321              
322 257     257 1 16500998 my $max = MAXCHILDREN();
323 257         1008 my $rv;
324              
325 257         1839 subPreamble(PDLEVEL1);
326              
327             # Check children limits and wait, if necessary
328 257 100       3440 if ($max) {
329 77         327 while ( $max <= childrenCount() ) { sleep 1 }
  62         53175099  
330             }
331              
332             # Fork and return
333 257         224162 $rv = fork;
334 257 50       12024 if ( defined $rv ) {
335 257 100       2678 if ( $rv > 0 ) {
336 221         6508 _incrChildren();
337 221         3769 _addPID($rv);
338             } else {
339 36         3220 _resetChildren();
340             }
341             }
342              
343 257         11619 subPostamble( PDLEVEL1, '$', $rv );
344              
345 257         1614 return $rv;
346             }
347              
348             sub pcommFork (\$\$) {
349              
350             # Purpose: Creates pipes for bi-directional communiation, then calls
351             # pfork()
352             # Returns: Return value of children handler if installed, otherwise
353             # undef.
354             # Usage: $rv = pcommFork($procr, $procw);
355              
356 2     2 1 26 my ( $procr, $procw ) = @_;
357 2         8 my ( $tp, $fp, $tc, $fc, $sfd, $rv );
358              
359 2         20 subPreamble( PDLEVEL1, '\*\*', $procr, $procw );
360              
361 2 50 33     276 if ( pipe( $fc, $tp ) and pipe( $fp, $tc ) ) {
362 2         24 $sfd = select $tc;
363 2         28 $| = 1;
364 2         10 select $tp;
365 2         4 $| = 1;
366 2         16 select $sfd;
367              
368 2         8 $rv = pfork();
369 2 50       44 if ( defined $rv ) {
370 2 100       47 if ($rv) {
371 1         28 close $tp;
372 1         17 close $fp;
373 1         20 $$procw = $tc;
374 1         4 $$procr = $fc;
375             } else {
376 1         27 close $tc;
377 1         14 close $fc;
378 1         13 $$procw = $tp;
379 1         3 $$procr = $fp;
380             }
381             } else {
382 0         0 $$procr = undef;
383 0         0 $$procw = undef;
384 0         0 close $tp;
385 0         0 close $fp;
386 0         0 close $tc;
387 0         0 close $fc;
388             }
389             } else {
390 0         0 pdebug( 'failed to create pipes: %s', PDLEVEL1, $! );
391             }
392              
393 2         38 subPostamble( PDLEVEL1, '$', $rv );
394              
395 2         78 return $rv;
396             }
397              
398             sub ptranslateUser {
399              
400             # Purpose: Translates a string account name into the UID
401             # Returns: UID if found, undef if not
402             # Usage: $uid = ptranslateUser($user);
403              
404 37     37 1 28519 my $user = shift;
405 37         111 my ( $uuid, @pwentry, $rv );
406              
407 37         133 subPreamble( PDLEVEL1, '$', $user );
408              
409 37 50 33     204 if ( defined $user and length $user ) {
410              
411 37         13097 setpwent;
412 37   100     131 do {
413 468         20649 @pwentry = getpwent;
414 468 100 100     3402 $uuid = $pwentry[2] if @pwentry && $user eq $pwentry[0];
415             } until defined $uuid || !@pwentry;
416 37         598 endpwent;
417 37 100       134 $rv = $uuid if defined $uuid;
418             }
419              
420 37         223 subPostamble( PDLEVEL1, '$', $rv );
421              
422 37         130 return $rv;
423             }
424              
425             sub ptranslateGroup {
426              
427             # Purpose: Translates a string group name into the UID
428             # Returns: GID if found, undef if not
429             # Usage: $gid = ptranslateGroup($group);
430              
431 38     38 1 22674 my $group = shift;
432 38         95 my ( $ugid, @pwentry, $rv );
433              
434 38         153 subPreamble( PDLEVEL1, '$', $group );
435              
436 38 50 33     271 if ( defined $group and length $group ) {
437              
438 38         1286 setgrent;
439 38   100     152 do {
440 957         3478 @pwentry = getgrent;
441 957 100 100     3957 $ugid = $pwentry[2] if @pwentry && $group eq $pwentry[0];
442             } until defined $ugid || !@pwentry;
443 38         771 endgrent;
444 38 100       189 $rv = $ugid if defined $ugid;
445             }
446              
447 38         176 subPostamble( PDLEVEL1, '$', $rv );
448              
449 38         114 return $rv;
450             }
451              
452             sub switchUser {
453              
454             # Purpose: Switches to the user/group specified
455             # Returns: True (1) if successful, False (0) if not
456             # Usage: $rv = swithUser($user);
457             # Usage: $rv = swithUser($user, $group);
458              
459 1     1 1 19553 my $user = shift;
460 1         51 my $group = shift;
461 1         16 my $rv = 1;
462 1         18 my ( @pwentry, $duid, $dgid );
463              
464 1         46 subPreamble( PDLEVEL1, '$;$', $user, $group );
465              
466             # Validate arguments
467 1 50 33     39 croak 'Mandatory argument of either user or group must be passed'
468             unless defined $user || defined $group;
469              
470             # First switch the group
471 1 50       17 if ( defined $group ) {
472              
473             # Look up named group
474 1 50       32 unless ( $group =~ /^\d+$/s ) {
475 1         10 $dgid = ptranslateGroup($group);
476 1 50       12 unless ( defined $dgid ) {
477 0         0 Paranoid::ERROR = pdebug( 'couldn\'t identify group (%s)',
478             PDLEVEL1, $group );
479 0         0 $rv = 0;
480             }
481             }
482              
483             # Switch to group
484 1 50       5 if ($rv) {
485 1         14 pdebug( 'switching to GID %s', PDLEVEL2, $dgid );
486 1 50       52 unless ( setgid($dgid) ) {
487 0         0 Paranoid::ERROR =
488             pdebug( 'couldn\'t switch to group (%s): %s',
489             PDLEVEL1, $group, $! );
490 0         0 $rv = 0;
491             }
492             }
493             }
494              
495             # Second, switch the user
496 1 50 33     15 if ( $rv && defined $user ) {
497              
498             # Look up named user
499 0 0       0 unless ( $user =~ /^\d+$/s ) {
500 0         0 $duid = ptranslateUser($user);
501 0 0       0 unless ( defined $duid ) {
502 0         0 Paranoid::ERROR =
503             pdebug( 'couldn\'t identify user (%s)', PDLEVEL1, $user );
504 0         0 $rv = 0;
505             }
506             }
507              
508             # Switch to user
509 0 0       0 if ($rv) {
510 0         0 pdebug( 'switching to UID %s', PDLEVEL2, $duid );
511 0 0       0 unless ( setuid($duid) ) {
512 0         0 Paranoid::ERROR = pdebug( 'couldn\'t switch to user (%s): %s',
513             PDLEVEL1, $user, $! );
514 0         0 $rv = 0;
515             }
516             }
517             }
518              
519 1         13 subPostamble( PDLEVEL1, '$', $rv );
520              
521 1         12 return $rv;
522             }
523              
524             sub pcapture ($\$\$) {
525              
526             # Purpose: Captures the output and exit code of the specified shell
527             # command. Output incorporates STDERR via redirection.
528             # Returns: True (1) if command exits cleanly, False (0) otherwise
529             # Usage: $rv = pcapture($cmd, $crv, $out);
530              
531 4     4 1 2822 my $cmd = shift;
532 4         13 my $cref = shift;
533 4         7 my $oref = shift;
534 4         16 my $rv = -1;
535 4         7 my ( $sigchld, $cored, $signal );
536              
537 4         14 subPreamble( PDLEVEL1, '$\$\$', $cmd, $cref, $oref );
538 4         15 pdebug( 'entering w/(%s)(%s)(%s)', PDLEVEL1, $cmd, $cref, $oref );
539 4         12 pIn();
540              
541 4 50       26 if ( defined $cmd ) {
542              
543             # Massage the command string
544 4         16 $cmd = "( $cmd ) 2>&1";
545              
546             # Backup SIGCHLD handler and set it to something safe
547 4 50       14 if ( defined $SIG{CHLD} ) {
548 4         5 $sigchld = $SIG{CHLD};
549 4     4   63 $SIG{CHLD} = sub {1};
  4         62  
550             }
551              
552             # Execute and snarf the output
553 4         33 pdebug( 'executing command', PDLEVEL2 );
554 4         10907 $$oref = `$cmd`;
555 4         169 $$cref = $?;
556 4         31 $cored = $$cref & 128;
557 4         11 $signal = $$cref & 127;
558 4         93 pdebug( 'command exited with raw rv: %s', PDLEVEL2, $$cref );
559              
560             # Restore SIGCHLD handler
561 4 50       93 $SIG{CHLD} = $sigchld if defined $SIG{CHLD};
562              
563             # Check the return value
564 4 100 66     67 if ( $$cref == -1 or $$cref == 32512 ) {
    50          
565              
566             # Command failed to execute
567 1         23 Paranoid::ERROR =
568             pdebug( 'command failed to execute: %s', PDLEVEL1, $! );
569 1         7 $rv = -1;
570              
571             } elsif ($signal) {
572              
573             # Exited with signal (and core?)
574 0         0 Paranoid::ERROR =
575             pdebug( 'command died with signal: %s', PDLEVEL1, $signal );
576 0 0       0 pdebug( "command exited with core dump", PDLEVEL1 ) if $cored;
577 0         0 $rv = -1;
578              
579             } else {
580              
581             # Command exited normally
582 3         12 $$cref >>= 8;
583 3 100       19 $rv = $$cref == 0 ? 1 : 0;
584 3         25 pdebug( 'command exited with rv: %s', PDLEVEL1, $$cref );
585             }
586              
587 4         52 subPostamble( PDLEVEL1, '$', $rv );
588              
589 4         97 return $rv;
590             }
591             }
592              
593             1;
594              
595             __END__