File Coverage

lib/Class/Usul/IPC.pm
Criterion Covered Total %
statement 41 127 32.2
branch 1 34 2.9
condition 0 41 0.0
subroutine 13 19 68.4
pod 8 8 100.0
total 63 229 27.5


line stmt bran cond sub pod time code
1             package Class::Usul::IPC;
2              
3 18     18   110 use namespace::autoclean;
  18         51  
  18         95  
4              
5 18     18   1282 use Class::Null;
  18         36  
  18         386  
6 18     18   89 use Class::Usul::Constants qw( EXCEPTION_CLASS FALSE NUL OK SPC TRUE );
  18         36  
  18         147  
7 18         94 use Class::Usul::Functions qw( arg_list get_user io loginid
8 18     18   15496 merge_attributes throw );
  18         36  
9 18     18   30716 use Class::Usul::IPC::Cmd;
  18         113  
  18         156  
10 18     18   155 use Class::Usul::Time qw( time2str );
  18         37  
  18         932  
11 18     18   122 use Class::Usul::Types qw( Bool ConfigProvider LoadableClass Logger );
  18         22  
  18         186  
12 18     18   24735 use English qw( -no_match_vars );
  18         51  
  18         159  
13 18     18   5538 use Module::Load::Conditional qw( can_load );
  18         36  
  18         921  
14 18     18   148 use Unexpected::Functions qw( Unspecified );
  18         52  
  18         215  
15 18     18   4841 use Moo;
  18         38  
  18         122  
16              
17             # Public attributes
18             has 'cache_ttys' => is => 'ro', isa => Bool, default => TRUE;
19              
20             has 'config' => is => 'ro', isa => ConfigProvider, required => TRUE;
21              
22             has 'log' => is => 'ro', isa => Logger, required => TRUE;
23              
24             has 'table_class' => is => 'lazy', isa => LoadableClass, coerce => TRUE,
25             default => 'Class::Usul::Response::Table';
26              
27             # Private functions
28             my $_cmd_matches = sub {
29             my ($cmd, $pattern) = @_;
30              
31             return !$pattern || $cmd =~ m{ $pattern }msx ? TRUE : FALSE;
32             };
33              
34             my $_new_proc_process_table = sub {
35             my $cache_ttys = shift;
36              
37             can_load( modules => { 'Proc::ProcessTable' => '0' } )
38             and return Proc::ProcessTable->new( cache_ttys => $cache_ttys );
39              
40             return Class::Null->new;
41             };
42              
43             my $_new_process_table = sub {
44             my ($class, $rows, $count) = @_;
45              
46             # TODO: Rewrite this to match macros.tt. Delete Response::Table
47             return $class->new
48             ( count => $count,
49             fields => [ qw( uid pid ppid start time size state tty cmd ) ],
50             labels => { uid => 'User', pid => 'PID',
51             ppid => 'PPID', start => 'Start Time',
52             tty => 'TTY', time => 'Time',
53             size => 'Size', state => 'State',
54             cmd => 'Command' },
55             typelist => { pid => 'numeric', ppid => 'numeric',
56             start => 'date', size => 'numeric',
57             time => 'numeric' },
58             values => $rows,
59             wrap => { cmd => 1 }, );
60             };
61              
62             my $_proc_belongs_to_user = sub {
63             my ($puid, $user) = @_;
64              
65             return (!$user || $user eq 'All' || $user eq loginid $puid) ? TRUE : FALSE;
66             };
67              
68             my $_pscomp = sub {
69             my ($arg1, $arg2) = @_; my $result;
70              
71             $result = $arg1->{uid} cmp $arg2->{uid};
72             $result = $arg1->{pid} <=> $arg2->{pid} if ($result == 0);
73              
74             return $result;
75             };
76              
77             my $_set_fields = sub {
78             my ($has, $p) = @_; my $flds = {};
79              
80             $flds->{id } = $has->{pid } ? $p->pid : NUL;
81             $flds->{pid } = $has->{pid } ? $p->pid : NUL;
82             $flds->{ppid } = $has->{ppid } ? $p->ppid : NUL;
83             $flds->{start} = $has->{start } ? time2str( '%d/%m %H:%M', $p->start ) : NUL;
84             $flds->{state} = $has->{state } ? $p->state : NUL;
85             $flds->{tty } = $has->{ttydev} ? $p->ttydev : NUL;
86             $flds->{time } = $has->{time } ? int $p->time / 1_000_000 : NUL;
87             $flds->{uid } = $has->{uid } ? getpwuid $p->uid : NUL;
88              
89             if ($has->{ttydev} and $p->ttydev) {
90             $flds->{tty} = $p->ttydev;
91             }
92             elsif ($has->{ttynum} and $p->ttynum) {
93             $flds->{tty} = $p->ttynum;
94             }
95             else { $flds->{tty} = NUL }
96              
97             if ($has->{rss} and $p->rss) {
98             $flds->{size} = int $p->rss/1_024;
99             }
100             elsif ($has->{size} and $p->size) {
101             $flds->{size} = int $p->size/1_024;
102             }
103             else { $flds->{size} = NUL }
104              
105             if ($has->{exec} and $p->exec) {
106             $flds->{cmd} = substr $p->exec, 0, 64;
107             }
108             elsif ($has->{cmndline} and $p->cmndline) {
109             $flds->{cmd} = substr $p->cmndline, 0, 64;
110             }
111             elsif ($has->{fname} and $p->fname) {
112             $flds->{cmd} = substr $p->fname, 0, 64;
113             }
114             else { $flds->{cmd} = NUL }
115              
116             return $flds;
117             };
118              
119             my $_signal_cmd = sub {
120             my ($cmd, $flag, $sig, $pids) = @_; my $opts = [];
121              
122             $sig and push @{ $opts }, '-o', "sig=${sig}";
123             $flag and push @{ $opts }, '-o', 'flag=one';
124              
125             return [ $cmd, '-nc', 'signal_process', @{ $opts }, '--', @{ $pids || [] } ];
126             };
127              
128             # Construction
129             around 'BUILDARGS' => sub {
130             my ($orig, $self, @args) = @_; my $attr = $orig->( $self, @args );
131              
132             my $builder = $attr->{builder} or return $attr;
133              
134             merge_attributes $attr, $builder, [ 'config', 'log' ];
135              
136             return $attr;
137             };
138              
139             # Public methods
140             sub child_list {
141 0     0 1 0 my ($self, $pid, $procs) = @_; my ($child, $ppt); my @pids = ();
  0         0  
  0         0  
142              
143 0 0       0 unless (defined $procs) {
144 0         0 $ppt = $_new_proc_process_table->( $self->cache_ttys );
145 0         0 $procs = { map { $_->pid => $_->ppid } @{ $ppt->table } };
  0         0  
  0         0  
146             }
147              
148 0 0       0 if (exists $procs->{ $pid }) {
149 0         0 for $child (grep { $procs->{ $_ } == $pid } keys %{ $procs }) {
  0         0  
  0         0  
150 0         0 push @pids, $self->child_list( $child, $procs ); # Recurse
151             }
152              
153 0         0 push @pids, $pid;
154             }
155              
156 0         0 return sort { $a <=> $b } @pids;
  0         0  
157             }
158              
159             sub list_pids_by_file_system {
160 0 0   0 1 0 my ($self, $fsystem) = @_; $fsystem or return ();
  0         0  
161              
162 0         0 my $opts = { err => 'null', expected_rv => 1 };
163             # TODO: Make fuser OS dependent
164 0   0     0 my $data = $self->run_cmd( "fuser ${fsystem}", $opts )->out || NUL;
165              
166 0         0 $data =~ s{ [^0-9\s] }{}gmx; $data =~ s{ \s+ }{ }gmx;
  0         0  
167              
168 0 0       0 return sort { $a <=> $b } grep { defined && length } split SPC, $data;
  0         0  
  0         0  
169             }
170              
171             sub popen {
172 98     98 1 219926 return shift->run_cmd( @_ );
173             }
174              
175             sub process_exists {
176 0     0 1 0 my ($self, @args) = @_; my $args = arg_list @args;
  0         0  
177              
178 0         0 my $pid = $args->{pid}; my ($io, $file);
  0         0  
179              
180 0 0 0     0 $file = $args->{file} and $io = io( $file ) and $io->is_file
      0        
181             and $pid = $io->chomp->lock->getline;
182              
183 0 0 0     0 (not $pid or $pid !~ m{ \d+ }mx) and return FALSE;
184              
185 0 0       0 return (CORE::kill 0, $pid) ? TRUE : FALSE;
186             }
187              
188             sub process_table {
189 0     0 1 0 my ($self, @args) = @_; my $args = arg_list @args;
  0         0  
190              
191 0         0 my $pat = $args->{pattern};
192 0   0     0 my $ptype = $args->{type } // 1;
193 0   0     0 my $user = $args->{user } // get_user->name;
194 0         0 my $ppt = $_new_proc_process_table->( $self->cache_ttys );
195 0         0 my $has = { map { $_ => TRUE } $ppt->fields };
  0         0  
196 0         0 my @rows = ();
197 0         0 my $count = 0;
198              
199 0 0       0 if ($ptype == 3) {
200 0         0 my %procs = map { $_->pid => $_ } @{ $ppt->table };
  0         0  
  0         0  
201 0         0 my @pids = $self->list_pids_by_file_system( $args->{fsystem} );
202              
203 0         0 for my $p (grep { defined } map { $procs{ $_ } } @pids) {
  0         0  
  0         0  
204 0         0 push @rows, $_set_fields->( $has, $p );
205 0         0 $count++;
206             }
207             }
208             else {
209 0         0 for my $p (@{ $ppt->table }) {
  0         0  
210 0 0 0     0 if ( ($ptype == 1 and $_proc_belongs_to_user->( $p->uid, $user ))
      0        
      0        
211             or ($ptype == 2 and $_cmd_matches->( $p->cmndline, $pat ))) {
212 0         0 push @rows, $_set_fields->( $has, $p );
213 0         0 $count++;
214             }
215             }
216             }
217              
218             return $_new_process_table->
219 0         0 ( $self->table_class, [ sort { $_pscomp->( $a, $b ) } @rows ], $count );
  0         0  
220             }
221              
222             sub run_cmd {
223 374     374 1 41981090 my ($self, $cmd, @args) = @_; my $attr = arg_list @args;
  374         3258  
224              
225 374 50       1650 $attr->{cmd } = $cmd or throw Unspecified, [ 'command' ];
226 374         1680 $attr->{log } = $self->log;
227 374         9057 $attr->{rundir } = $self->config->rundir;
228 374         10430 $attr->{tempdir} = $self->config->tempdir;
229              
230 374         15907 return Class::Usul::IPC::Cmd->new( $attr )->run_cmd;
231             }
232              
233             sub signal_process {
234 0     0 1   my ($self, @args) = @_;
235              
236 0 0         is_hashref $args[ 0 ]
237             or return $self->run_cmd( $_signal_cmd->( $self->config->suid, @args ) );
238              
239 0           my ($file, $io); my $args = $args[ 0 ];
  0            
240              
241 0   0       my $sig = $args->{sig} // 'TERM'; my $pids = $args->{pids} // [];
  0   0        
242              
243 0 0         $args->{pid} and push @{ $pids }, $args->{pid};
  0            
244              
245 0 0 0       if ($file = $args->{file} and $io = io( $file ) and $io->is_file) {
      0        
246 0           push @{ $pids }, $io->chomp->lock->getlines;
  0            
247 0 0         $sig eq 'TERM' and unlink $file;
248             }
249              
250 0 0 0       (defined $pids->[0] and $pids->[0] =~ m{ \d+ }mx) or throw 'Process id bad';
251              
252 0           for my $mpid (@{ $pids }) {
  0            
253 0 0 0       if (exists $args->{flag} and $args->{flag} =~ m{ one }imx) {
254 0           CORE::kill $sig, $mpid; next;
  0            
255             }
256              
257 0           my @pids = reverse $self->child_list( $mpid );
258              
259 0           CORE::kill $sig, $_ for (@pids);
260              
261 0 0         $args->{force} or next;
262              
263 0           sleep 3; @pids = reverse $self->child_list( $mpid );
  0            
264              
265 0           CORE::kill 'KILL', $_ for (@pids);
266             }
267              
268 0           return OK;
269             }
270              
271             sub signal_process_as_root {
272 0     0 1   my ($self, @args) = @_; return $self->signal_process( arg_list @args );
  0            
273             }
274              
275             1;
276              
277             __END__
278              
279             =pod
280              
281             =encoding utf-8
282              
283             =head1 Name
284              
285             Class::Usul::IPC - List / create / delete processes
286              
287             =head1 Synopsis
288              
289             use Class::Usul;
290             use Class::Usul::IPC;
291              
292             my $ipc = Class::Usul::IPC->new( builder => Class::Usul->new );
293              
294             $result_object = $ipc->run_cmd( [ qw( ls -l ) ] );
295              
296             =head1 Description
297              
298             Displays the process table and allows signals to be sent to selected
299             processes
300              
301             =head1 Configuration and Environment
302              
303             Defines these attributes;
304              
305             =over 3
306              
307             =item C<cache_ttys>
308              
309             Boolean that defaults to true. Passed to L<Proc::ProcessTable>
310              
311             =item C<config>
312              
313             A required instance of type C<ConfigProvider>
314              
315             =item C<log>
316              
317             A required instance of type C<Logger>
318              
319             =item C<table_class>
320              
321             A lazy evaluated C<LoadableClass> that defaults to
322             L<Class::Usul::Response::Table>
323              
324             =back
325              
326             =head1 Subroutines/Methods
327              
328             =head2 C<BUILDARGS>
329              
330             Extracts C<config> and C<log> objects from the C<builder> attribute if it is
331             supplied to the constructor
332              
333             =head2 C<child_list>
334              
335             @pids = $self->child_list( $pid );
336              
337             Called with a process id for an argument this method returns a list of child
338             process ids
339              
340             =head2 C<list_pids_by_file_system>
341              
342             @pids = $self->list_pids_by_file_system( $file_system );
343              
344             Returns the list of process ids produced by the C<fuser> command
345              
346             =head2 C<popen>
347              
348             $response = $self->popen( $cmd, @opts );
349              
350             Uses L<IPC::Open3> to fork a command and pipe the lines of input into
351             it. Returns a C<Class::Usul::Response::IPC> object. The response
352             object's C<out> method returns the B<STDOUT> from the command. Throws
353             in the event of an error. See L</run_cmd> for a full list of options and
354             response attributes
355              
356             =head2 C<process_exists>
357              
358             $bool = $self->process_exists( file => $path, pid => $pid );
359              
360             Tests for the existence of the specified process. Either specify a
361             path to a file containing the process id or specify the id directly
362              
363             =head2 C<process_table>
364              
365             Generates the process table data used by the L<HTML::FormWidget> table
366             subclass. Called by L<Class::Usul::Model::Process/proc_table>
367              
368             =head2 C<run_cmd>
369              
370             $response = $self->run_cmd( $cmd, $opts );
371              
372             Runs the given command. If C<$cmd> is a string then an implementation based on
373             the L<IPC::Open3> function is used. If C<$cmd> is an array reference then an
374             implementation using C<fork> and C<exec> in L<Class::Usul::IPC::Cmd> is used to
375             execute the command. If the command contains pipes then an implementation based
376             on L<IPC::Run> is used if it is installed. If L<IPC::Run> is not installed then
377             the arrayref is joined with spaces and the C<system> implementation is
378             used. The C<$opts> hash reference and the C<$response> object are described
379             in L<Class::Usul::IPC::Cmd>
380              
381             On C<MSWin32> the L</popen> method is used instead. That method does not
382             support the C<async> option
383              
384             =head2 C<signal_process>
385              
386             Send a signal the the selected processes. Invokes the C<suid> root wrapper
387              
388             =head2 C<signal_process_as_root>
389              
390             $self->signal_process( [{] param => value, ... [}] );
391              
392             This is called by processes running as root to send signals to
393             selected processes. The passed parameters can be either a list of key
394             value pairs or a hash ref. Either a single C<pid>, or an array ref
395             C<pids>, or C<file> must be passwd. The C<file> parameter should be a
396             path to a file containing process ids one per line. The C<sig> defaults to
397             C<TERM>. If the C<flag> parameter is set to C<one> then the given signal
398             will be sent once to each selected process. Otherwise each process and
399             all of it's children will be sent the signal. If the C<force>
400             parameter is set to true the after a grace period each process and
401             it's children are sent signal C<KILL>
402              
403             =head1 Diagnostics
404              
405             None
406              
407             =head1 Dependencies
408              
409             =over 3
410              
411             =item L<Class::Usul>
412              
413             =item L<Class::Usul::Constants>
414              
415             =item L<Class::Usul::IPC::Cmd>
416              
417             =item L<Class::Usul::Response::Table>
418              
419             =item L<Module::Load::Conditional>
420              
421             =item L<Proc::ProcessTable>
422              
423             =item L<Try::Tiny>
424              
425             =back
426              
427             =head1 Incompatibilities
428              
429             There are no known incompatibilities in this module
430              
431             =head1 Bugs and Limitations
432              
433             There are no known bugs in this module.
434             Please report problems to the address below.
435             Patches are welcome
436              
437             =head1 Author
438              
439             Peter Flanigan, C<< <pjfl@cpan.org> >>
440              
441             =head1 License and Copyright
442              
443             Copyright (c) 2017 Peter Flanigan. All rights reserved
444              
445             This program is free software; you can redistribute it and/or modify it
446             under the same terms as Perl itself. See L<perlartistic>
447              
448             This program is distributed in the hope that it will be useful,
449             but WITHOUT WARRANTY; without even the implied warranty of
450             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE
451              
452             =cut
453              
454             # Local Variables:
455             # mode: perl
456             # tab-width: 3
457             # End: