File Coverage

blib/lib/Unix/PID/Tiny.pm
Criterion Covered Total %
statement 3 109 2.7
branch 0 78 0.0
condition 0 39 0.0
subroutine 1 12 8.3
pod 8 8 100.0
total 12 246 4.8


line stmt bran cond sub pod time code
1             package Unix::PID::Tiny;
2              
3 1     1   37216 use strict;
  1         2  
  1         1836  
4             $Unix::PID::Tiny::VERSION = 0.91;
5              
6             sub new {
7 0     0 1   my ( $self, $args_hr ) = @_;
8 0 0 0       $args_hr->{'minimum_pid'} = 11 if !exists $args_hr->{'minimum_pid'} || $args_hr->{'minimum_pid'} !~ m{\A\d+\z}ms; # this does what one assumes m{^\d+$} would do
9              
10 0 0         if ( defined $args_hr->{'ps_path'} ) {
11 0 0         $args_hr->{'ps_path'} .= '/' if $args_hr->{'ps_path'} !~ m{/$};
12 0 0 0       if ( !-d $args_hr->{'ps_path'} || !-x "$args_hr->{'ps_path'}ps" ) {
13 0           $args_hr->{'ps_path'} = '';
14             }
15             }
16             else {
17 0           $args_hr->{'ps_path'} = '';
18             }
19              
20 0           return bless { 'ps_path' => $args_hr->{'ps_path'}, 'minimum_pid' => $args_hr->{'minimum_pid'} }, $self;
21             }
22              
23             sub kill {
24 0     0 1   my ( $self, $pid, $give_kill_a_chance ) = @_;
25 0 0         $give_kill_a_chance = int $give_kill_a_chance if defined $give_kill_a_chance;
26 0           $pid = int $pid;
27 0           my $min = int $self->{'minimum_pid'};
28 0 0         if ( $pid < $min ) {
29              
30             # prevent bad args from killing the process group (IE '0')
31             # or general low level ones
32 0           warn "kill() called with integer value less than $min";
33 0           return;
34             }
35              
36             # CORE::kill 0, $pid : may be false but still running, see `perldoc -f kill`
37 0 0         if ( $self->is_pid_running($pid) ) {
38              
39             # RC from CORE::kill is not a boolean of if the PID was killed or not, only that it was signaled
40             # so it is not an indicator of "success" in killing $pid
41 0           _kill( 15, $pid ); # TERM
42 0           _kill( 2, $pid ); # INT
43 0           _kill( 1, $pid ); # HUP
44 0           _kill( 9, $pid ); # KILL
45              
46             # give kill() some time to take effect?
47 0 0         if ($give_kill_a_chance) {
48 0           sleep($give_kill_a_chance);
49             }
50 0 0         return if $self->is_pid_running($pid);
51             }
52 0           return 1;
53             }
54              
55             sub is_pid_running {
56 0     0 1   my ( $self, $check_pid ) = @_;
57              
58 0           $check_pid = int $check_pid;
59 0 0 0       return if !$check_pid || $check_pid < 0;
60              
61 0 0 0       return 1 if $> == 0 && _kill( 0, $check_pid ); # if we are superuser we can avoid the the system call. For details see `perldoc -f kill`
62              
63             # If the proc filesystem is available, it's a good test. If not, continue on to system call
64 0 0 0       return 1 if -e "/proc/$$" && -r "/proc/$$" && -r "/proc/$check_pid";
      0        
65              
66             # even if we are superuser, go ahead and call ps just in case CORE::kill 0's false RC was erroneous
67 0           my @outp = $self->_raw_ps( 'u', '-p', $check_pid );
68 0           chomp @outp;
69 0 0         return 1 if defined $outp[1];
70 0           return;
71             }
72              
73             sub pid_info_hash {
74 0     0 1   my ( $self, $pid ) = @_;
75 0           $pid = int $pid;
76 0 0 0       return if !$pid || $pid < 0;
77              
78 0           my @outp = $self->_raw_ps( 'u', '-p', $pid );
79 0           chomp @outp;
80 0           my %info;
81 0           @info{ split( /\s+/, $outp[0], 11 ) } = split( /\s+/, $outp[1], 11 );
82 0 0         return wantarray ? %info : \%info;
83             }
84              
85             sub _raw_ps {
86 0     0     my ( $self, @ps_args ) = @_;
87 0           my $psargs = join( ' ', @ps_args );
88 0           my @res = `$self->{'ps_path'}ps $psargs`;
89 0 0         return wantarray ? @res : join '', @res;
90             }
91              
92             sub get_pid_from_pidfile {
93 0     0 1   my ( $self, $pid_file ) = @_;
94              
95             # if this function is ever changed to use $self as a hash object, update pid_file() to not do a class method call
96 0 0         return 0 if !-e $pid_file;
97              
98 0 0         open my $pid_fh, '<', $pid_file or return;
99 0           chomp( my $pid = <$pid_fh> );
100 0           close $pid_fh;
101              
102 0           return int( abs($pid) );
103             }
104              
105             sub is_pidfile_running {
106 0     0 1   my ( $self, $pid_file ) = @_;
107 0   0       my $pid = $self->get_pid_from_pidfile($pid_file) || return;
108 0 0         return $pid if $self->is_pid_running($pid);
109 0           return;
110             }
111              
112             sub pid_file {
113 0     0 1   my ( $self, $pid_file, $newpid, $retry_conf ) = @_;
114 0 0         $newpid = $$ if !$newpid;
115              
116 0           my $rc = $self->pid_file_no_unlink( $pid_file, $newpid, $retry_conf );
117 0 0 0       if ( $rc && $newpid == $$ ) {
118              
119             # prevent forked childrens' END from killing parent's pid files
120             # 'unlink_end_use_current_pid_only' is undocumented as this may change, feedback welcome!
121             # 'carp_unlink_end' undocumented as it is only meant for testing (rt57462, use Test::Carp to test END behavior)
122 0 0         if ( $self->{'unlink_end_use_current_pid_only'} ) {
123 0           eval 'END { unlink $pid_file if $$ eq ' . $$ . '}'; ## no critic qw(ProhibitStringyEval)
124 0 0         if ( $self->{'carp_unlink_end'} ) {
125              
126             # eval 'END { require Carp;Carp::carp("[info] $$ !unlink $pid_file (current pid check)") if $$ ne ' . $$ . '}'; ## no critic qw(ProhibitStringyEval)
127 0           eval 'END { require Carp;Carp::carp("[info] $$ unlink $pid_file (current pid check)") if $$ eq ' . $$ . '}'; ## no critic qw(ProhibitStringyEval)
128             }
129             }
130             else {
131 0           eval 'END { unlink $pid_file if Unix::PID::Tiny->get_pid_from_pidfile($pid_file) eq $$ }'; ## no critic qw(ProhibitStringyEval)
132 0 0         if ( $self->{'carp_unlink_end'} ) {
133              
134             # eval 'END { require Carp;Carp::carp("[info] $$ !unlink $pid_file (pid file check)") if Unix::PID::Tiny->get_pid_from_pidfile($pid_file) ne $$ }'; ## no critic qw(ProhibitStringyEval)
135 0           eval 'END { require Carp;Carp::carp("[info] $$ unlink $pid_file (pid file check)") if Unix::PID::Tiny->get_pid_from_pidfile($pid_file) eq $$ }'; ## no critic qw(ProhibitStringyEval)
136             }
137             }
138             }
139              
140 0 0 0       return 1 if defined $rc && $rc == 1;
141 0 0 0       return 0 if defined $rc && $rc == 0;
142 0           return;
143             }
144              
145             *pid_file_no_cleanup = \&pid_file_no_unlink; # more intuitively named alias
146              
147             sub pid_file_no_unlink {
148 0     0 1   my ( $self, $pid_file, $newpid, $retry_conf ) = @_;
149 0 0         $newpid = $$ if !$newpid;
150              
151 0 0         if ( ref($retry_conf) eq 'ARRAY' ) {
    0          
152 0           $retry_conf->[0] = int( abs( $retry_conf->[0] ) );
153 0           for my $idx ( 1 .. scalar( @{$retry_conf} ) - 1 ) {
  0            
154 0 0         next if ref $retry_conf->[$idx] eq 'CODE';
155 0           $retry_conf->[$idx] = int( abs( $retry_conf->[$idx] ) );
156             }
157             }
158             elsif ( ref($retry_conf) eq 'HASH' ) {
159 0   0       $retry_conf->{'num_of_passes'} ||= 3;
160 0   0       $retry_conf->{'passes_config'} ||= [ 1, 2 ];
161 0           $retry_conf = [ int( $retry_conf->{'num_of_passes'} ), @{ $retry_conf->{'passes_config'} } ];
  0            
162             }
163             else {
164 0           $retry_conf = [ 3, 1, 2 ];
165             }
166              
167 0           my $passes = 0;
168 0           require Fcntl;
169              
170 0           EXISTS:
171             $passes++;
172 0 0         if ( -e $pid_file ) {
173 0           my $curpid = $self->get_pid_from_pidfile($pid_file);
174              
175             # TODO: narrow even more the race condition where $curpid stops running and a new PID is put in
176             # the file between when we pull in $curpid above and check to see if it is running/unlink below
177              
178 0 0 0       return 1 if int $curpid == $$ && $newpid == $$; # already setup
179 0 0         return if int $curpid == $$; # can't change it while $$ is alive
180 0 0         return if $self->is_pid_running( int $curpid );
181              
182 0           unlink $pid_file; # must be a stale PID file, so try to remove it for sysopen()
183             }
184              
185             # write only if it does not exist:
186 0           my $pid_fh = _sysopen($pid_file);
187 0 0         if ( !$pid_fh ) {
188 0 0         return 0 if $passes >= $retry_conf->[0];
189 0 0         if ( ref( $retry_conf->[$passes] ) eq 'CODE' ) {
190 0           $retry_conf->[$passes]->( $self, $pid_file, $passes );
191             }
192             else {
193 0 0         sleep( $retry_conf->[$passes] ) if $retry_conf->[$passes];
194             }
195 0           goto EXISTS;
196             }
197              
198 0           print {$pid_fh} int( abs($newpid) );
  0            
199 0           close $pid_fh;
200              
201 0           return 1;
202             }
203              
204             sub _sysopen {
205 0     0     my ($pid_file) = @_;
206 0 0         sysopen( my $pid_fh, $pid_file, Fcntl::O_WRONLY() | Fcntl::O_EXCL() | Fcntl::O_CREAT() ) || return;
207 0           return $pid_fh;
208             }
209              
210             sub _kill { ## no critic(RequireArgUnpacking
211 0     0     return CORE::kill(@_); # goto &CORE::kill; is problematic
212             }
213              
214             1;
215              
216             __END__