File Coverage

blib/lib/Paranoid/Process.pm
Criterion Covered Total %
statement 170 276 61.5
branch 38 88 43.1
condition 22 36 61.1
subroutine 27 34 79.4
pod 15 15 100.0
total 272 449 60.5


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.09 2021/12/28 15:46:49 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   27240 use 5.008;
  49         202  
35              
36 49     49   321 use strict;
  49         80  
  49         1067  
37 49     49   305 use warnings;
  49         112  
  49         1828  
38 49     49   352 use vars qw($VERSION @EXPORT @EXPORT_OK %EXPORT_TAGS);
  49         87  
  49         3173  
39 49     49   287 use base qw(Exporter);
  49         93  
  49         3660  
40 49     49   281 use Paranoid;
  49         98  
  49         2479  
41 49     49   9011 use Paranoid::Debug qw(:all);
  49         134  
  49         10217  
42 49     49   29039 use POSIX qw(getuid setuid setgid WNOHANG setsid);
  49         337491  
  49         354  
43 49     49   77002 use Carp;
  49         117  
  49         142393  
44              
45             ($VERSION) = ( q$Revision: 2.09 $ =~ /(\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 pdebug( 'entering', PDLEVEL1 );
101 0         0 pIn();
102              
103 0         0 foreach $sig ( keys %signals ) {
104 0 0       0 $SIG{$sig} = \&_sigHandler if scalar @{ $signals{$sig} };
  0         0  
105             }
106              
107 0         0 pOut();
108 0         0 pdebug( 'leaving w/rv: 1', PDLEVEL1 );
109              
110 0         0 return 1;
111             }
112              
113             sub uninstallSIGD {
114              
115             # Purpose: Uninstalls the dispatcher
116             # Returns: Boolean
117             # Usage: $rv = uninstallSIGD();
118              
119 0     0 1 0 pdebug( 'entering', PDLEVEL1 );
120 0         0 pIn();
121              
122 0         0 foreach ( keys %original ) {
123             $SIG{$_} = $original{$_}
124             if defined $SIG{$_}
125 0 0 0     0 and $SIG{$_} eq \&_sigHandler;
126             }
127              
128 0         0 pOut();
129 0         0 pdebug( 'leaving w/rv: 1', PDLEVEL1 );
130              
131 0         0 return 1;
132             }
133              
134             sub installSIGH ($\&) {
135              
136             # Purpose: Assigns a code ref to a signal array
137             # Returns: Boolean
138             # Usage: $rv = installSIGH($signal, $sref);
139              
140 0     0 1 0 my ( $signal, $sref ) = @_;
141 0         0 my $rv = 1;
142              
143 0         0 pdebug( 'entering w/%s, %s', PDLEVEL1, $signal, $sref );
144 0         0 pIn();
145              
146 0 0       0 if ( exists $signals{$signal} ) {
147 0 0       0 if ( grep { $_ eq $sref } @{ $signals{$signal} } ) {
  0         0  
  0         0  
148 0         0 pdebug( '%s handler already installed', PDLEVEL2, $signal );
149             } else {
150 0         0 push @{ $signals{$signal} }, $sref;
  0         0  
151 0         0 pdebug( '%s handler installed', PDLEVEL2, $signal );
152             }
153             } else {
154 0         0 pdebug( 'unknown signal: %s', PDLEVEL1, $signal );
155 0         0 $rv = 0;
156             }
157              
158 0         0 pOut();
159 0         0 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
160              
161 0         0 return $rv;
162             }
163              
164             sub uninstallSIGH ($\&) {
165              
166             # Purpose: Removes a code ref for a signal array
167             # Returns: Boolean
168             # Usage: $rv = uninstallSIGH($signal, $sref);
169              
170 0     0 1 0 my ( $signal, $sref ) = @_;
171 0         0 my $rv = 1;
172 0         0 my ( $sigh, $i );
173              
174 0         0 pdebug( 'entering w/%s, %s', PDLEVEL1, $signal, $sref );
175 0         0 pIn();
176              
177 0 0       0 if ( exists $signals{$signal} ) {
178 0 0       0 if ( grep { $_ eq $sref } @{ $signals{$signal} } ) {
  0         0  
  0         0  
179 0         0 $i = 0;
180 0         0 foreach $sigh ( @{ $signals{$signal} } ) {
  0         0  
181 0 0       0 if ( $sigh eq $sref ) {
182 0         0 splice @{ $signals{$signal} }, $i, 1;
  0         0  
183 0         0 last;
184             }
185 0         0 $i++;
186             }
187 0         0 pdebug( '%s handler removed', PDLEVEL2, $signal );
188             } else {
189 0         0 pdebug( 'no %s handler to remove', PDLEVEL2, $signal );
190             }
191             } else {
192 0         0 pdebug( 'unknown signal: %s', PDLEVEL1, $signal );
193 0         0 $rv = 0;
194             }
195              
196 0         0 pOut();
197 0         0 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
198              
199 0         0 return $rv;
200             }
201             }
202              
203             {
204             my $maxChildren = 0;
205             my $numChildren = 0;
206             my @forkedPIDs = ();
207             my $chldRef = undef;
208              
209             sub MAXCHILDREN : lvalue {
210              
211             # Purpose: Gets/sets $maxChildren
212             # Returns: $maxChildren
213             # Usage: $max = MAXCHILDREN;
214             # Usage: MAXCHILDREN = 20;
215              
216 276     276 1 1050 $maxChildren;
217             }
218 569     569 1 82080237 sub childrenCount { return $numChildren }
219 221     221   2263 sub _incrChildren { $numChildren++ }
220 128     128   325 sub _decrChildren { $numChildren-- }
221              
222             sub _resetChildren {
223 36     36   983 @forkedPIDs = ();
224 36         822 $numChildren = 0;
225             }
226              
227             sub installChldHandler (\&) {
228              
229             # Purpose: Installs a code reference to execute whenever a child
230             # exits
231             # Returns: True (1)
232             # Usage: installChldHandler(&foo);
233              
234 7     7 1 84 $chldRef = shift;
235              
236 7         182 return 1;
237             }
238 140     140   720 sub _chldHandler { return $chldRef }
239              
240 221     221   2698 sub _addPID { push @forkedPIDs, shift }
241              
242             sub _grepPID {
243 134     134   567 my $pid = shift;
244 134         647 return scalar grep { $_ == $pid } @forkedPIDs;
  388         2054  
245             }
246              
247             sub _delPID {
248 128     128   362 my $pid = shift;
249 128         491 @forkedPIDs = grep { $_ != $pid } @forkedPIDs;
  388         1029  
250 128         319 return 1;
251             }
252             }
253              
254             sub sigchld {
255              
256             # Purpose: Default signal handler for SIGCHLD
257             # Returns: True (1)
258             # Usage: $SIG{CHLD} = \&sigchld;
259              
260 140     140 1 3291190 my ($pid);
261 140         1324 my $sref = _chldHandler();
262              
263             # Remove the signal handler so we're not preempted
264 140     0   5285 local $SIG{CHLD} = sub {1};
  0         0  
265              
266             # Process children exit values
267 140         700 do {
268 274         6741 $pid = waitpid -1, WNOHANG;
269 274 100 100     2618 if ( $pid > 0 and _grepPID($pid) ) {
270 128         1134 _decrChildren();
271 128         565 _delPID($pid);
272 128         968 pdebug( 'child %d reaped w/rv: %s', PDLEVEL1, $pid, $? );
273 128         591 pdebug( 'children remaining: %s', PDLEVEL1, childrenCount() );
274              
275             # Call the user's sig handler if defined
276 128 100       697 &$sref( $pid, $? ) if defined $sref;
277             }
278             } until $pid < 1;
279              
280 140         3729 return 1;
281             }
282              
283             sub daemonize {
284              
285             # Purpose: Daemonizes process and disassociates with the terminal
286             # Returns: True unless there are errors.
287             # Usage: daemonize();
288              
289 0     0 1 0 my ( $rv, $pid );
290              
291 0         0 pdebug( 'entering', PDLEVEL1 );
292 0         0 pIn();
293              
294 0         0 $pid = fork;
295              
296             # Exit if we're the parent process
297 0 0       0 exit 0 if $pid;
298              
299 0 0       0 if ( defined $pid ) {
300              
301             # Fork was successful, close parent file descriptors
302 0 0       0 $rv = open( STDIN, '/dev/null' ) and open( STDOUT, '>/dev/null' );
303              
304             # Create a new process group
305 0 0       0 unless ($rv) {
306 0         0 setsid();
307 0         0 $rv = open STDERR, '>&STDOUT';
308 0 0       0 die "Can't dup stdout: $!" unless $rv;
309 0         0 chdir '/';
310             }
311              
312             } else {
313 0         0 Paranoid::ERROR =
314             pdebug( 'Failed to daemonize process: %s', PDLEVEL1, $! );
315 0         0 $rv = 0;
316             }
317              
318 0         0 pOut();
319 0         0 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
320              
321 0         0 return $rv;
322             }
323              
324             sub pfork {
325              
326             # Purpose: Replacement for Perl's fork function. Blocks until a child
327             # exists if MAXCHILDREN is exceeded.
328             # Returns: Return value of children handler if installed, otherwise
329             # undef.
330             # Usage: $rv = pfork();
331              
332 257     257 1 19496005 my $max = MAXCHILDREN();
333 257         570 my $rv;
334              
335 257         1427 pdebug( 'entering', PDLEVEL1 );
336 257         1502 pIn();
337              
338             # Check children limits and wait, if necessary
339 257 100       751 if ($max) {
340 77         475 while ( $max <= childrenCount() ) { sleep 1 }
  71         54571474  
341             }
342              
343             # Fork and return
344 257         228276 $rv = fork;
345 257 50       13405 if ( defined $rv ) {
346 257 100       3413 if ( $rv > 0 ) {
347 221         8474 _incrChildren();
348 221         3041 _addPID($rv);
349             } else {
350 36         3443 _resetChildren();
351             }
352             }
353              
354 257         11126 pOut();
355 257         5506 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
356              
357 257         2119 return $rv;
358             }
359              
360             sub pcommFork (\$\$) {
361              
362             # Purpose: Creates pipes for bi-directional communiation, then calls
363             # pfork()
364             # Returns: Return value of children handler if installed, otherwise
365             # undef.
366             # Usage: $rv = pcommFork($procr, $procw);
367              
368 2     2 1 12 my ( $procr, $procw ) = @_;
369 2         22 my ( $tp, $fp, $tc, $fc, $sfd, $rv );
370              
371 2         24 pdebug( 'entering', PDLEVEL1 );
372 2         22 pIn();
373              
374 2 50 33     284 if ( pipe( $fc, $tp ) and pipe( $fp, $tc ) ) {
375 2         22 $sfd = select $tc;
376 2         22 $| = 1;
377 2         8 select $tp;
378 2         10 $| = 1;
379 2         20 select $sfd;
380              
381 2         108 $rv = pfork();
382 2 50       62 if ( defined $rv ) {
383 2 100       38 if ($rv) {
384 1         34 close $tp;
385 1         16 close $fp;
386 1         19 $$procw = $tc;
387 1         19 $$procr = $fc;
388             } else {
389 1         34 close $tc;
390 1         9 close $fc;
391 1         15 $$procw = $tp;
392 1         5 $$procr = $fp;
393             }
394             } else {
395 0         0 $$procr = undef;
396 0         0 $$procw = undef;
397 0         0 close $tp;
398 0         0 close $fp;
399 0         0 close $tc;
400 0         0 close $fc;
401             }
402             } else {
403 0         0 pdebug( 'failed to create pipes: %s', PDLEVEL1, $! );
404             }
405              
406 2         39 pOut();
407 2         40 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
408              
409 2         88 return $rv;
410             }
411              
412             sub ptranslateUser {
413              
414             # Purpose: Translates a string account name into the UID
415             # Returns: UID if found, undef if not
416             # Usage: $uid = ptranslateUser($user);
417              
418 37     37 1 22262 my $user = shift;
419 37         112 my ( $uuid, @pwentry, $rv );
420              
421 37         151 pdebug( 'entering w/(%s)', PDLEVEL1, $user );
422 37         166 pIn();
423              
424 37 50 33     315 if ( defined $user and length $user ) {
425              
426 37         19061 setpwent;
427 37   100     170 do {
428 468         26270 @pwentry = getpwent;
429 468 100 100     4307 $uuid = $pwentry[2] if @pwentry && $user eq $pwentry[0];
430             } until defined $uuid || !@pwentry;
431 37         725 endpwent;
432 37 100       204 $rv = $uuid if defined $uuid;
433             }
434              
435 37         242 pOut();
436 37         167 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
437              
438 37         150 return $rv;
439             }
440              
441             sub ptranslateGroup {
442              
443             # Purpose: Translates a string group name into the UID
444             # Returns: GID if found, undef if not
445             # Usage: $gid = ptranslateGroup($group);
446              
447 38     38 1 21946 my $group = shift;
448 38         140 my ( $ugid, @pwentry, $rv );
449              
450 38         172 pdebug( 'entering w/(%s)', PDLEVEL1, $group );
451 38         152 pIn();
452              
453 38 50 33     372 if ( defined $group and length $group ) {
454              
455 38         1551 setgrent;
456 38   100     152 do {
457 957         5507 @pwentry = getgrent;
458 957 100 100     5027 $ugid = $pwentry[2] if @pwentry && $group eq $pwentry[0];
459             } until defined $ugid || !@pwentry;
460 38         930 endgrent;
461 38 100       210 $rv = $ugid if defined $ugid;
462             }
463              
464 38         208 pOut();
465 38         153 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
466              
467 38         132 return $rv;
468             }
469              
470             sub switchUser {
471              
472             # Purpose: Switches to the user/group specified
473             # Returns: True (1) if successful, False (0) if not
474             # Usage: $rv = swithUser($user);
475             # Usage: $rv = swithUser($user, $group);
476              
477 1     1 1 22270 my $user = shift;
478 1         84 my $group = shift;
479 1         34 my $rv = 1;
480 1         20 my ( @pwentry, $duid, $dgid );
481              
482             # Validate arguments
483 1 50 33     70 croak 'Mandatory argument of either user or group must be passed'
484             unless defined $user || defined $group;
485              
486 1         64 pdebug( 'entering w/(%s)(%s)', PDLEVEL1, $user, $group );
487 1         20 pIn();
488              
489             # First switch the group
490 1 50       13 if ( defined $group ) {
491              
492             # Look up named group
493 1 50       43 unless ( $group =~ /^\d+$/s ) {
494 1         18 $dgid = ptranslateGroup($group);
495 1 50       6 unless ( defined $dgid ) {
496 0         0 Paranoid::ERROR = pdebug( 'couldn\'t identify group (%s)',
497             PDLEVEL1, $group );
498 0         0 $rv = 0;
499             }
500             }
501              
502             # Switch to group
503 1 50       13 if ($rv) {
504 1         21 pdebug( 'switching to GID %s', PDLEVEL2, $dgid );
505 1 50       62 unless ( setgid($dgid) ) {
506 0         0 Paranoid::ERROR =
507             pdebug( 'couldn\'t switch to group (%s): %s',
508             PDLEVEL1, $group, $! );
509 0         0 $rv = 0;
510             }
511             }
512             }
513              
514             # Second, switch the user
515 1 50 33     27 if ( $rv && defined $user ) {
516              
517             # Look up named user
518 0 0       0 unless ( $user =~ /^\d+$/s ) {
519 0         0 $duid = ptranslateUser($user);
520 0 0       0 unless ( defined $duid ) {
521 0         0 Paranoid::ERROR =
522             pdebug( 'couldn\'t identify user (%s)', PDLEVEL1, $user );
523 0         0 $rv = 0;
524             }
525             }
526              
527             # Switch to user
528 0 0       0 if ($rv) {
529 0         0 pdebug( 'switching to UID %s', PDLEVEL2, $duid );
530 0 0       0 unless ( setuid($duid) ) {
531 0         0 Paranoid::ERROR = pdebug( 'couldn\'t switch to user (%s): %s',
532             PDLEVEL1, $user, $! );
533 0         0 $rv = 0;
534             }
535             }
536             }
537              
538 1         13 pOut();
539 1         14 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
540              
541 1         5 return $rv;
542             }
543              
544             sub pcapture ($\$\$) {
545              
546             # Purpose: Captures the output and exit code of the specified shell
547             # command. Output incorporates STDERR via redirection.
548             # Returns: True (1) if command exits cleanly, False (0) otherwise
549             # Usage: $rv = pcapture($cmd, $crv, $out);
550              
551 4     4 1 3419 my $cmd = shift;
552 4         6 my $cref = shift;
553 4         15 my $oref = shift;
554 4         9 my $rv = -1;
555 4         7 my ( $sigchld, $cored, $signal );
556              
557 4         24 pdebug( 'entering w/(%s)(%s)(%s)', PDLEVEL1, $cmd, $cref, $oref );
558 4         13 pIn();
559              
560 4 50       27 if ( defined $cmd ) {
561              
562             # Massage the command string
563 4         13 $cmd = "( $cmd ) 2>&1";
564              
565             # Backup SIGCHLD handler and set it to something safe
566 4 50       14 if ( defined $SIG{CHLD} ) {
567 4         9 $sigchld = $SIG{CHLD};
568 4     4   98 $SIG{CHLD} = sub {1};
  4         94  
569             }
570              
571             # Execute and snarf the output
572 4         27 pdebug( 'executing command', PDLEVEL2 );
573 4         12340 $$oref = `$cmd`;
574 4         221 $$cref = $?;
575 4         20 $cored = $$cref & 128;
576 4         31 $signal = $$cref & 127;
577 4         145 pdebug( 'command exited with raw rv: %s', PDLEVEL2, $$cref );
578              
579             # Restore SIGCHLD handler
580 4 50       100 $SIG{CHLD} = $sigchld if defined $SIG{CHLD};
581              
582             # Check the return value
583 4 100 66     136 if ( $$cref == -1 or $$cref == 32512 ) {
    50          
584              
585             # Command failed to execute
586 1         39 Paranoid::ERROR =
587             pdebug( 'command failed to execute: %s', PDLEVEL1, $! );
588 1         14 $rv = -1;
589              
590             } elsif ($signal) {
591              
592             # Exited with signal (and core?)
593 0         0 Paranoid::ERROR =
594             pdebug( 'command died with signal: %s', PDLEVEL1, $signal );
595 0 0       0 pdebug( "command exited with core dump", PDLEVEL1 ) if $cored;
596 0         0 $rv = -1;
597              
598             } else {
599              
600             # Command exited normally
601 3         8 $$cref >>= 8;
602 3 100       30 $rv = $$cref == 0 ? 1 : 0;
603 3         37 pdebug( 'command exited with rv: %s', PDLEVEL1, $$cref );
604             }
605              
606 4         37 pOut();
607 4         11 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
608              
609 4         128 return $rv;
610             }
611             }
612              
613             1;
614              
615             __END__