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.08 2020/12/31 12:10:06 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 32     32   13735 use 5.008;
  32         121  
35              
36 32     32   166 use strict;
  32         64  
  32         628  
37 32     32   144 use warnings;
  32         66  
  32         941  
38 32     32   167 use vars qw($VERSION @EXPORT @EXPORT_OK %EXPORT_TAGS);
  32         63  
  32         1924  
39 32     32   173 use base qw(Exporter);
  32         70  
  32         2580  
40 32     32   198 use Paranoid;
  32         78  
  32         1840  
41 32     32   8931 use Paranoid::Debug qw(:all);
  32         65  
  32         5811  
42 32     32   18716 use POSIX qw(getuid setuid setgid WNOHANG setsid);
  32         212428  
  32         167  
43 32     32   47233 use Carp;
  32         64  
  32         90863  
44              
45             ($VERSION) = ( q$Revision: 2.08 $ =~ /(\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 191     191 1 759 $maxChildren;
217             }
218 487     487 1 63069608 sub childrenCount { return $numChildren }
219 151     151   712 sub _incrChildren { $numChildren++ }
220 113     113   342 sub _decrChildren { $numChildren-- }
221              
222             sub _resetChildren {
223 21     21   581 @forkedPIDs = ();
224 21         479 $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         126 return 1;
237             }
238 119     119   486 sub _chldHandler { return $chldRef }
239              
240 151     151   1347 sub _addPID { push @forkedPIDs, shift }
241              
242             sub _grepPID {
243 119     119   397 my $pid = shift;
244 119         424 return scalar grep { $_ == $pid } @forkedPIDs;
  318         1340  
245             }
246              
247             sub _delPID {
248 113     113   219 my $pid = shift;
249 113         276 @forkedPIDs = grep { $_ != $pid } @forkedPIDs;
  318         874  
250 113         302 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 119     119 1 14824514 my ($pid);
261 119         700 my $sref = _chldHandler();
262              
263             # Remove the signal handler so we're not preempted
264 119     0   4008 local $SIG{CHLD} = sub {1};
  0         0  
265              
266             # Process children exit values
267 119         446 do {
268 238         5334 $pid = waitpid -1, WNOHANG;
269 238 100 100     2091 if ( $pid > 0 and _grepPID($pid) ) {
270 113         436 _decrChildren();
271 113         485 _delPID($pid);
272 113         847 pdebug( 'child %d reaped w/rv: %s', PDLEVEL1, $pid, $? );
273 113         446 pdebug( 'children remaining: %s', PDLEVEL1, childrenCount() );
274              
275             # Call the user's sig handler if defined
276 113 100       581 &$sref( $pid, $? ) if defined $sref;
277             }
278             } until $pid < 1;
279              
280 119         2815 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 172     172 1 17152574 my $max = MAXCHILDREN();
333 172         440 my $rv;
334              
335 172         1059 pdebug( 'entering', PDLEVEL1 );
336 172         639 pIn();
337              
338             # Check children limits and wait, if necessary
339 172 100       404 if ($max) {
340 77         197 while ( $max <= childrenCount() ) { sleep 1 }
  62         52072943  
341             }
342              
343             # Fork and return
344 172         440174 $rv = fork;
345 172 50       10033 if ( defined $rv ) {
346 172 100       2418 if ( $rv > 0 ) {
347 151         5078 _incrChildren();
348 151         17678 _addPID($rv);
349             } else {
350 21         7046 _resetChildren();
351             }
352             }
353              
354 172         7056 pOut();
355 172         4215 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
356              
357 172         1168 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 16 my ( $procr, $procw ) = @_;
369 2         12 my ( $tp, $fp, $tc, $fc, $sfd, $rv );
370              
371 2         26 pdebug( 'entering', PDLEVEL1 );
372 2         20 pIn();
373              
374 2 50 33     288 if ( pipe( $fc, $tp ) and pipe( $fp, $tc ) ) {
375 2         26 $sfd = select $tc;
376 2         16 $| = 1;
377 2         16 select $tp;
378 2         16 $| = 1;
379 2         16 select $sfd;
380              
381 2         12 $rv = pfork();
382 2 50       48 if ( defined $rv ) {
383 2 100       34 if ($rv) {
384 1         36 close $tp;
385 1         19 close $fp;
386 1         15 $$procw = $tc;
387 1         15 $$procr = $fc;
388             } else {
389 1         30 close $tc;
390 1         19 close $fc;
391 1         23 $$procw = $tp;
392 1         6 $$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         31 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 19417 my $user = shift;
419 37         111 my ( $uuid, @pwentry, $rv );
420              
421 37         132 pdebug( 'entering w/(%s)', PDLEVEL1, $user );
422 37         111 pIn();
423              
424 37 50 33     277 if ( defined $user and length $user ) {
425              
426 37         14495 setpwent;
427 37   100     131 do {
428 468         25445 @pwentry = getpwent;
429 468 100 100     4068 $uuid = $pwentry[2] if @pwentry && $user eq $pwentry[0];
430             } until defined $uuid || !@pwentry;
431 37         703 endpwent;
432 37 100       203 $rv = $uuid if defined $uuid;
433             }
434              
435 37         177 pOut();
436 37         111 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
437              
438 37         131 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 18955 my $group = shift;
448 38         132 my ( $ugid, @pwentry, $rv );
449              
450 38         133 pdebug( 'entering w/(%s)', PDLEVEL1, $group );
451 38         116 pIn();
452              
453 38 50 33     340 if ( defined $group and length $group ) {
454              
455 38         1231 setgrent;
456 38   100     133 do {
457 957         4359 @pwentry = getgrent;
458 957 100 100     4933 $ugid = $pwentry[2] if @pwentry && $group eq $pwentry[0];
459             } until defined $ugid || !@pwentry;
460 38         785 endgrent;
461 38 100       207 $rv = $ugid if defined $ugid;
462             }
463              
464 38         171 pOut();
465 38         134 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 20044 my $user = shift;
478 1         53 my $group = shift;
479 1         21 my $rv = 1;
480 1         82 my ( @pwentry, $duid, $dgid );
481              
482             # Validate arguments
483 1 50 33     66 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         7 pIn();
488              
489             # First switch the group
490 1 50       14 if ( defined $group ) {
491              
492             # Look up named group
493 1 50       29 unless ( $group =~ /^\d+$/s ) {
494 1         24 $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       20 if ($rv) {
504 1         28 pdebug( 'switching to GID %s', PDLEVEL2, $dgid );
505 1 50       57 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     38 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         3 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
540              
541 1         18 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 2871 my $cmd = shift;
552 4         14 my $cref = shift;
553 4         6 my $oref = shift;
554 4         5 my $rv = -1;
555 4         6 my ( $sigchld, $cored, $signal );
556              
557 4         22 pdebug( 'entering w/(%s)(%s)(%s)', PDLEVEL1, $cmd, $cref, $oref );
558 4         13 pIn();
559              
560 4 50       45 if ( defined $cmd ) {
561              
562             # Massage the command string
563 4         10 $cmd = "( $cmd ) 2>&1";
564              
565             # Backup SIGCHLD handler and set it to something safe
566 4 50       20 if ( defined $SIG{CHLD} ) {
567 4         9 $sigchld = $SIG{CHLD};
568 4     4   92 $SIG{CHLD} = sub {1};
  4         75  
569             }
570              
571             # Execute and snarf the output
572 4         28 pdebug( 'executing command', PDLEVEL2 );
573 4         11842 $$oref = `$cmd`;
574 4         181 $$cref = $?;
575 4         22 $cored = $$cref & 128;
576 4         11 $signal = $$cref & 127;
577 4         122 pdebug( 'command exited with raw rv: %s', PDLEVEL2, $$cref );
578              
579             # Restore SIGCHLD handler
580 4 50       84 $SIG{CHLD} = $sigchld if defined $SIG{CHLD};
581              
582             # Check the return value
583 4 100 66     111 if ( $$cref == -1 or $$cref == 32512 ) {
    50          
584              
585             # Command failed to execute
586 1         27 Paranoid::ERROR =
587             pdebug( 'command failed to execute: %s', PDLEVEL1, $! );
588 1         9 $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         15 $$cref >>= 8;
602 3 100       10 $rv = $$cref == 0 ? 1 : 0;
603 3         23 pdebug( 'command exited with rv: %s', PDLEVEL1, $$cref );
604             }
605              
606 4         30 pOut();
607 4         11 pdebug( 'leaving w/rv: %s', PDLEVEL1, $rv );
608              
609 4         113 return $rv;
610             }
611             }
612              
613             1;
614              
615             __END__