File Coverage

blib/lib/Proc/Terminator.pm
Criterion Covered Total %
statement 108 135 80.0
branch 28 50 56.0
condition 7 14 50.0
subroutine 20 20 100.0
pod 1 4 25.0
total 164 223 73.5


line stmt bran cond sub pod time code
1             package Proc::Terminator::Ctx;
2 1     1   933 use strict;
  1         3  
  1         45  
3 1     1   6 use warnings;
  1         2  
  1         38  
4 1     1   31 use POSIX qw(errno_h);
  1         3  
  1         8  
5             my $DEBUG = $ENV{PROC_TERMINATOR_DEBUG};
6              
7 1     1   1925 use Moo;
  1         16851  
  1         8  
8             has pid => (
9             is =>'ro',
10             required => 1,
11             isa => sub {
12             ($_[0] && $_[0] > 0) or die "PID must be a positive number!"
13             },
14             );
15              
16             has siglist => (
17             is => 'rw',
18             required => 0,
19             isa => sub { ref $_[0] eq 'ARRAY' or die "Siglist must be an array reference" },
20             default => sub { [] }
21             );
22              
23             has last_sent => (
24             is => 'rw',
25             default => sub { 0 }
26             );
27              
28             has error => (
29             is => 'rw',
30             default => sub { "" }
31             );
32              
33             sub try_kill {
34 15     15 0 42 my ($self,$do_kill) = @_;
35            
36 15 100       265 if (kill(0, $self->pid) == 0) {
37 2         35 my $errno_save = $!;
38 2 50       12 $DEBUG and warn "Kill with signal=0 returned 0 (dead!)";
39 2 50       7 if ($errno_save != ESRCH) {
40 0         0 $self->error($errno_save);
41 0         0 warn $errno_save;
42 0         0 return -1;
43             }
44             # else, == ESRCH
45 2         5 return 1;
46             }
47            
48 13 100       45 if (!$do_kill) {
49 7 50       25 $DEBUG and warn "We were not requested to proceed with signal. Returning";
50 7         24 return 0;
51             }
52 6         11 my $sig = shift @{$self->siglist};
  6         453  
53              
54 6 100       616 if (!defined $sig) {
55 1 50       39 $DEBUG and warn "Cannot kill ${\$self->pid} because no signals remain";
  0         0  
56 1         10 return -1;
57             }
58 5 50       29 $DEBUG and warn "Using signal $sig for ${\$self->pid}";
  0         0  
59            
60 5 50       135 if (kill($sig, $self->pid) == 1) {
61 5         18 return 0;
62             }
63            
64 0 0       0 if ($! == ESRCH) {
65 0         0 return 1;
66             } else {
67 0         0 warn $!;
68 0         0 return -1;
69             }
70             }
71              
72             # This class represents a single 'batch' of PIDs each withe
73             package Proc::Terminator::Batch;
74 1     1   2225 use strict;
  1         2  
  1         23  
75 1     1   4 use warnings;
  1         1  
  1         33  
76 1     1   5 use POSIX qw(:errno_h);
  1         2  
  1         10  
77 1     1   676 use Time::HiRes qw(sleep time);
  1         2  
  1         15  
78 1     1   117 use Moo;
  1         2  
  1         4  
79              
80             has procs => (
81             is => 'rw',
82             isa => sub { ref $_[0] eq 'HASH' or die "Expected hash reference!" },
83             default => sub { { } },
84             );
85              
86             has grace_period => ( is => 'rw', default => sub { 0.75 });
87             has max_wait => ( is => 'rw', default => sub { 10 });
88             has interval => (is => 'rw', default => sub { 0.25 });
89             has badprocs => (is => 'rw',
90             isa => sub { ref $_[0] eq 'ARRAY' or die "Expected arrayref!" },
91             default => sub { [ ] } );
92             has begin_time => (is => 'rw', default => sub { 0 });
93              
94             sub with_pids {
95 3     3 0 27 my ($cls,$pids,%options) = @_;
96 3 50       60 $pids = ref $pids ? $pids : [ $pids ];
97            
98 3   100     69 my $siglist = delete $options{siglist} ||
99             [ @Proc::Terminator::DefaultSignalOrder ];
100            
101 3         6 my %procs;
102 3         44 foreach my $pid (@$pids) {
103 3         318 $procs{$pid} = Proc::Terminator::Ctx->new(
104             pid => $pid,
105             siglist => [ @$siglist ],
106             last_sent => 0);
107             }
108            
109 3   50     149 my $self = $cls->new(
      50        
      100        
110             procs => \%procs,
111             max_wait => delete $options{max_wait} || 10,
112             interval => delete $options{interval} || 0.25,
113             grace_period => delete $options{grace_period} || 0.75,
114             );
115 3         110 return $self;
116             }
117              
118             sub _check_one_proc {
119 15     15   37 my ($self,$ctx,$now) = @_;
120            
121 15         114 my $do_send_kill = $now - $ctx->last_sent > $self->grace_period;
122            
123 15 100       57 if ($do_send_kill) {
124 6         21 $ctx->last_sent($now);
125 6 50       21 $DEBUG and warn("Will send signal to ${\$ctx->pid}");
  0         0  
126             }
127            
128 15         92 my $ret = $ctx->try_kill($do_send_kill);
129            
130 15 100       55 if ($ret) {
131 3         73 delete $self->procs->{$ctx->pid};
132 3 100       30 if ($ret == -1) {
133 1         16 push @{ $self->badprocs }, $ctx;
  1         38  
134             }
135             }
136            
137 15         64 return $ret;
138             }
139              
140             # The point of abstracting this is so that this module may be integrated
141             # within event loops, where this method is called by a timer, or something.
142             sub loop_once {
143 15     15 0 1359 my $self = shift;
144 15         37 my @ctxs = values %{ $self->procs };
  15         992  
145            
146 15 50       929 if (!scalar @ctxs) {
147 0 0       0 $DEBUG and warn "Nothing left to check..";
148 0 0       0 if (@{$self->badprocs}) {
  0         0  
149 0         0 return undef;
150             }
151 0         0 return 0; #nothing left to do
152             }
153            
154 15         84 my $now = time();
155            
156 15 50 33     809 if ($self->max_wait &&
157             ($now - $self->begin_time > $self->max_wait)) {
158             # do one last sweep?
159 0         0 while (my ($pid,$ctx) = each %{$self->procs}) {
  0         0  
160 0 0 0     0 if (kill(0, $pid) == 0 && $! == ESRCH) {
161 0         0 delete $self->procs->{$pid};
162             } else {
163 0         0 push @{$self->badprocs}, $ctx;
  0         0  
164             }
165             }
166 0 0       0 if (@{$self->badprocs}) {
  0         0  
167 0         0 return undef;
168             }
169 0         0 return 0;
170             }
171 15         88 $self->_check_one_proc($_, $now) foreach (@ctxs);
172 15 100       30 if (keys %{$self->procs}) {
  15         413  
173 12         113 return scalar keys %{$self->procs};
  12         987  
174             } else {
175 3 100       20 if (@{$self->badprocs}) {
  3         51  
176 1         17 return undef;
177             }
178 2         759 return 0;
179             }
180             }
181              
182              
183              
184             package Proc::Terminator;
185 1     1   1117 use warnings;
  1         4  
  1         29  
186 1     1   5 use strict;
  1         1  
  1         38  
187 1     1   4 use Time::HiRes qw(time sleep);
  1         2  
  1         6  
188 1     1   101 use POSIX qw(:signal_h :sys_wait_h :errno_h);
  1         13  
  1         4  
189 1     1   922 use base qw(Exporter);
  1         1  
  1         131  
190              
191             our $VERSION = 0.05;
192              
193             our @DefaultSignalOrder = (
194             SIGINT,
195             SIGQUIT,
196             SIGTERM,
197             SIGKILL
198             );
199              
200             our @EXPORT = qw(proc_terminate);
201 1     1   1148 use Data::Dumper;
  1         6017  
  1         236  
202             # Kill a bunch of processes
203             sub proc_terminate {
204 3     3 1 7657 my ($pids, %options) = @_;
205            
206 3         78 my $batch = Proc::Terminator::Batch->with_pids($pids, %options);
207            
208 3         42 $batch->begin_time(time());
209             #print Dumper($batch);
210 3         27 while ($batch->loop_once) {
211 12 50       252 $DEBUG and warn "Sleeping for ${\$batch->interval} seconds";
  0         0  
212 12         2556620 sleep($batch->interval);
213             }
214            
215 3         9 my @badprocs = map { $_->pid } @{$batch->badprocs};
  1         24  
  3         68  
216            
217 3 50       21 if (wantarray) {
218 0         0 return @badprocs;
219             } else {
220 3         38 return !@badprocs;
221             }
222             }
223              
224             __END__