File Coverage

blib/lib/Proc/Guard.pm
Criterion Covered Total %
statement 32 108 29.6
branch 1 54 1.8
condition 0 18 0.0
subroutine 9 17 52.9
pod 4 7 57.1
total 46 204 22.5


line stmt bran cond sub pod time code
1             package Proc::Guard;
2 1     1   49874 use strict;
  1         3  
  1         24  
3 1     1   4 use warnings;
  1         1  
  1         19  
4 1     1   20 use 5.00800;
  1         3  
5             our $VERSION = '0.07_01';
6 1     1   5 use Carp ();
  1         1  
  1         93  
7              
8             our $EXIT_STATUS;
9              
10             # killer helper borrowed from IPC::Cmd
11             # to send both SIGTERM and SIGKILL on Unix
12             # and to avoid hanging on Windows
13             # where waitpid after killing doesn't work "by design"
14             #
15              
16             my $HAVE_MONOTONIC;
17              
18             BEGIN {
19 1     1   3 eval {
20 1         391 require POSIX; POSIX->import();
  1         4822  
21 1         2402 require Time::HiRes; Time::HiRes->import();
  1         1051  
22             };
23              
24 1         56 eval {
25 1         4 my $wait_start_time = Time::HiRes::clock_gettime(&Time::HiRes::CLOCK_MONOTONIC);
26             };
27 1 50       26 if ($@) {
28 0         0 $HAVE_MONOTONIC = 0;
29             }
30             else {
31 1         363 $HAVE_MONOTONIC = 1;
32             }
33             }
34              
35             sub get_monotonic_time {
36 0 0   0 0   if ($HAVE_MONOTONIC) {
37 0           return Time::HiRes::clock_gettime(&Time::HiRes::CLOCK_MONOTONIC);
38             }
39             else {
40 0           return time();
41             }
42             }
43              
44             sub adjust_monotonic_start_time {
45 0     0 0   my ($ref_vars, $now, $previous) = @_;
46              
47             # workaround only for those systems which don't have
48             # Time::HiRes::CLOCK_MONOTONIC (Mac OSX in particular)
49 0 0         return if $HAVE_MONOTONIC;
50              
51             # don't have previous monotonic value (only happens once
52             # in the beginning of the program execution)
53 0 0         return unless $previous;
54              
55 0           my $time_diff = $now - $previous;
56              
57             # adjust previously saved time with the skew value which is
58             # either negative when clock moved back or more than 5 seconds --
59             # assuming that event loop does happen more often than once
60             # per five seconds, which might not be always true (!) but
61             # hopefully that's ok, because it's just a workaround
62 0 0 0       if ($time_diff > 5 || $time_diff < 0) {
63 0           foreach my $ref_var (@{$ref_vars}) {
  0            
64 0 0         if (defined($$ref_var)) {
65 0           $$ref_var = $$ref_var + $time_diff;
66             }
67             }
68             }
69             }
70              
71              
72              
73              
74             #
75             # give process a chance sending TERM,
76             # waiting for a while (2 seconds)
77             # and killing it with KILL
78             sub kill_gently {
79 0     0 0   my ($pid, $opts) = @_;
80              
81 0 0         $opts = {} unless $opts;
82 0 0         $opts->{'wait_time'} = 2 unless defined($opts->{'wait_time'});
83 0 0         $opts->{'first_kill_type'} = 'just_process' unless $opts->{'first_kill_type'};
84 0 0         $opts->{'final_kill_type'} = 'just_process' unless $opts->{'final_kill_type'};
85              
86 0 0         if ($opts->{'first_kill_type'} eq 'just_process') {
    0          
87 0           kill(15, $pid);
88             }
89             elsif ($opts->{'first_kill_type'} eq 'process_group') {
90 0           kill(-15, $pid);
91             }
92              
93 0           my $do_wait = 1;
94 0           my $child_finished = 0;
95              
96 0           my $wait_start_time = get_monotonic_time();
97 0           my $now;
98             my $previous_monotonic_value;
99              
100 0           while ($do_wait) {
101 0           $previous_monotonic_value = $now;
102 0           $now = get_monotonic_time();
103              
104 0           adjust_monotonic_start_time([\$wait_start_time], $now, $previous_monotonic_value);
105              
106 0 0         if ($now > $wait_start_time + $opts->{'wait_time'}) {
107 0           $do_wait = 0;
108 0           next;
109             }
110              
111 0           my $waitpid = waitpid($pid, POSIX::WNOHANG);
112              
113 0 0         if ($waitpid eq -1) {
114 0           $child_finished = 1;
115 0           $do_wait = 0;
116 0           next;
117             }
118              
119 0           Time::HiRes::usleep(250000); # quarter of a second
120             }
121              
122 0 0         if (!$child_finished) {
123 0 0         if ($opts->{'final_kill_type'} eq 'just_process') {
    0          
124 0           kill(9, $pid);
125             }
126             elsif ($opts->{'final_kill_type'} eq 'process_group') {
127 0           kill(-9, $pid);
128             }
129             }
130             }
131              
132             # functional interface
133             our @EXPORT = qw/proc_guard/;
134 1     1   6 use Exporter 'import';
  1         1  
  1         97  
135             sub proc_guard {
136 0     0 1   return Proc::Guard->new(do {
137 0 0 0       if (@_==1 && ref($_[0]) && ref($_[0]) eq 'CODE') {
      0        
138 0           +{ code => $_[0] }
139             } else {
140 0           +{ command => [@_] }
141             }
142             });
143             }
144              
145             # OOish interface
146 1     1   6 use POSIX;
  1         1  
  1         4  
147 1     1   2606 use Errno qw/EINTR ECHILD/;
  1         979  
  1         93  
148             use Class::Accessor::Lite 0.05 (
149 1         4 rw => ['pid'],
150 1     1   384 );
  1         924  
151              
152             sub new {
153 0     0 1   my $class = shift;
154 0 0         my %args = @_==1 ? %{$_[0]} : @_;
  0            
155              
156 0           my $self = bless {
157             _owner_pid => $$,
158             auto_start => 1,
159             %args,
160             }, $class;
161              
162 0 0 0       if ($self->{command} && !ref($self->{command})) {
163 0           $self->{command} = [$self->{command}];
164             }
165 0 0 0       unless ($self->{command} || $self->{code}) {
166 0           Carp::croak("'command' or 'code' is required.");
167             }
168              
169             $self->start()
170 0 0         if $self->{auto_start};
171              
172 0           return $self;
173             }
174              
175             sub start {
176 0     0 1   my $self = shift;
177              
178 0           my $pid = fork();
179 0 0         die "fork failed: $!" unless defined $pid;
180 0 0         if ($pid == 0) { # child
181 0 0         if ($self->{command}) {
182 0           exec @{$self->{command}};
  0            
183 0           die "cannot exec @{$self->{command}}: $!";
  0            
184             } else {
185 0           $self->{code}->();
186 0           exit(0); # exit after work
187             }
188             }
189 0           $self->pid($pid);
190             }
191              
192             sub stop {
193 0     0 1   my ( $self, $sig ) = @_;
194             return
195 0 0         unless defined $self->pid;
196              
197 0           kill_gently $self->pid;
198 0           $self->pid(undef);
199             }
200              
201             sub DESTROY {
202 0     0     my $self = shift;
203 0 0 0       if (defined $self->pid && $$ == $self->{_owner_pid}) {
204 0           local $?; # "END" function and destructors can change the exit status by modifying $?.(perldoc -f exit)
205 0           $self->stop()
206             }
207             }
208              
209             1;
210             __END__