File Coverage

blib/lib/Sys/Cmd/Process.pm
Criterion Covered Total %
statement 122 139 87.7
branch 44 72 61.1
condition 25 52 48.0
subroutine 23 25 92.0
pod 11 16 68.7
total 225 304 74.0


line stmt bran cond sub pod time code
1             package Sys::Cmd::Process;
2 2     2   28 use v5.18;
  2         8  
3 2     2   14 use warnings;
  2         4  
  2         112  
4 2     2   12 use Carp qw[];
  2         4  
  2         46  
5 2     2   8 use Log::Any qw[$log];
  2         16  
  2         14  
6              
7             our $VERSION = 'v0.986.3';
8              
9             ### START Class::Inline ### v0.0.1 Thu Dec 11 13:24:57 2025
10             require Carp;
11             our ( @_CLASS, $_FIELDS, %_NEW );
12              
13             sub new {
14 31     31 0 108 my $class = shift;
15 31   33     213 my $CLASS = ref $class || $class;
16 31   66     150 $_NEW{$CLASS} //= do {
17 2         6 my ( %seen, @new, @build );
18 2         8 my @possible = ($CLASS);
19 2         8 while (@possible) {
20 2         4 my $c = shift @possible;
21 2     2   810 no strict 'refs';
  2         6  
  2         5760  
22 2 50       4 push @new, $c . '::_NEW' if exists &{ $c . '::_NEW' };
  2         18  
23 2 50       4 push @build, $c . '::BUILD' if exists &{ $c . '::BUILD' };
  2         10  
24 2         6 $seen{$c}++;
25 2 50       4 if ( exists &{ $c . '::DOES' } ) {
  2         12  
26 0         0 push @possible, grep { not $seen{$_}++ } $c->DOES('*');
  0         0  
27             }
28 2         4 push @possible, grep { not $seen{$_}++ } @{ $c . '::ISA' };
  0         0  
  2         76  
29             }
30 2         18 [ [ reverse(@new) ], [ reverse(@build) ] ];
31             };
32 31 50       370 my $self = { @_ ? @_ > 1 ? @_ : %{ $_[0] } : () };
  0 50       0  
33 31         89 bless $self, $CLASS;
34 31         181 my $attrs = { map { ( $_ => 1 ) } keys %$self };
  157         386  
35 31         82 map { $self->$_($attrs) } @{ $_NEW{$CLASS}->[0] };
  31         159  
  31         106  
36             {
37 31         101 local $Carp::CarpLevel = 3;
  31         87  
38             Carp::carp("Sys::Cmd::Process: unexpected argument '$_'")
39 31         113 for keys %$attrs
40             }
41 31         53 map { $self->$_ } @{ $_NEW{$CLASS}->[1] };
  0         0  
  31         81  
42 31         6476 $self;
43             }
44              
45             sub _NEW {
46 31     31   54 CORE::state $fix_FIELDS = do {
47 2 50       8 $_FIELDS = { @_CLASS > 1 ? @_CLASS : %{ $_CLASS[0] } };
  2         52  
48 2 50       16 $_FIELDS = $_FIELDS->{'FIELDS'} if exists $_FIELDS->{'FIELDS'};
49             };
50 31 50       101 if ( my @missing = grep { not exists $_[0]->{$_} } 'cmd', 'pid', 'stderr',
  155         389  
51             'stdin', 'stdout' )
52             {
53 0         0 Carp::croak( 'Sys::Cmd::Process required initial argument(s): '
54             . join( ', ', @missing ) );
55             }
56 0         0 $_[0]{'_ret'} = eval { $_FIELDS->{'_ret'}->{'isa'}->( $_[0]{'_ret'} ) }
57 31 50       98 if exists $_[0]{'_ret'};
58 31 50 0     93 delete $_[0]{'_ret'} || Carp::confess( 'Sys::Cmd::Process _ret: ' . $@ )
59             if $@;
60 31         75 $_[0]{'cmd'} = eval { $_FIELDS->{'cmd'}->{'isa'}->( $_[0]{'cmd'} ) };
  31         208  
61 31 50 0     85 delete $_[0]{'cmd'} || Carp::confess( 'Sys::Cmd::Process cmd: ' . $@ )
62             if $@;
63 31         84 map { delete $_[1]->{$_} } 'cmd', 'on_exit', 'pid', 'status', 'stderr',
  217         468  
64             'stdin', 'stdout';
65             }
66              
67             sub __RO {
68 0     0   0 my ( undef, undef, undef, $sub ) = caller(1);
69 0         0 Carp::confess("attribute $sub is read-only");
70             }
71              
72             sub _ret {
73 117 100   117   255 if ( @_ > 1 ) {
74 31         68 $_[0]{'_ret'} = eval { $_FIELDS->{'_ret'}->{'isa'}->( $_[1] ) };
  31         160  
75 31 50 0     97 delete $_[0]{'_ret'}
76             || Carp::confess( 'invalid (Sys::Cmd::Process::_ret) value: ' . $@ )
77             if $@;
78             }
79 117   66     282 $_[0]{'_ret'} //= eval {
80             $_FIELDS->{'_ret'}->{'isa'}
81 1         7 ->( $_FIELDS->{'_ret'}->{'default'}->( $_[0] ) );
82             };
83 117 100 33     784 delete $_[0]{'_ret'}
84             || Carp::confess( 'invalid (Sys::Cmd::Process::_ret) default: ' . $@ )
85             if $@;
86 116         539 $_[0]{'_ret'};
87             }
88 62     62 0 236 sub has__ret { exists $_[0]{'_ret'} }
89 18 50 50 18 0 84 sub cmd { __RO() if @_ > 1; $_[0]{'cmd'} // undef }
  18         266  
90              
91             sub core {
92 24 50   24 1 233 __RO() if @_ > 1;
93 24   33     267 $_[0]{'core'} //= $_FIELDS->{'core'}->{'default'}->( $_[0] );
94             }
95              
96             sub exit {
97 117 50   117 1 256 __RO() if @_ > 1;
98 117   100     1079 $_[0]{'exit'} //= $_FIELDS->{'exit'}->{'default'}->( $_[0] );
99             }
100 0     0 0 0 sub has_exit { exists $_[0]{'exit'} }
101              
102             sub on_exit {
103 31 50   31 0 80 if ( @_ > 1 ) { $_[0]{'on_exit'} = $_[1] }
  0         0  
104 31   100     252 $_[0]{'on_exit'} // undef;
105             }
106 160 50 50 160 1 128918 sub pid { __RO() if @_ > 1; $_[0]{'pid'} // undef }
  160         561580  
107              
108             sub signal {
109 86 50   86 1 201 __RO() if @_ > 1;
110 86   100     521 $_[0]{'signal'} //= $_FIELDS->{'signal'}->{'default'}->( $_[0] );
111             }
112              
113             sub status {
114 31 50   31 1 51500 if ( @_ > 1 ) { $_[0]{'status'} = $_[1] }
  31         97  
115 31   33     99 $_[0]{'status'} //= $_FIELDS->{'status'}->{'default'};
116             }
117 166 50 50 166 1 5560 sub stderr { __RO() if @_ > 1; $_[0]{'stderr'} // undef }
  166         11142211  
118 179 50 50 179 1 24814 sub stdin { __RO() if @_ > 1; $_[0]{'stdin'} // undef }
  179         1684  
119 152 50 50 152 1 25341 sub stdout { __RO() if @_ > 1; $_[0]{'stdout'} // undef }
  152         10170  
120             @_CLASS = grep 1, ### END Class::Inline ###
121             {
122             cmd => {
123             isa => sub {
124             ref $_[0] eq 'ARRAY' || Sys::Cmd::_croak("cmd must be ARRAYREF");
125             @{ $_[0] } || Sys::Cmd::_croak("Missing cmd elements");
126             if ( grep { !defined $_ } @{ $_[0] } ) {
127             Sys::Cmd::_croak('cmd array cannot contain undef elements');
128             }
129             $_[0];
130             },
131             required => 1,
132             },
133             pid => {
134             is => 'ro',
135             required => 1,
136             },
137             stdin => {
138             is => 'ro',
139             required => 1,
140             },
141             stdout => {
142             is => 'ro',
143             required => 1,
144             },
145             stderr => {
146             is => 'ro',
147             required => 1,
148             },
149             _ret => {
150             is => 'rw',
151             isa => sub {
152             defined( $_[0] )
153             or die Data::Dumper::Dumper( \@_ ) . "_ret must be defined! @_";
154             $_[0];
155             },
156             init_arg => undef,
157             predicate => 1,
158             default => sub {
159             Sys::Cmd::_croak(
160             'Process status values invalid before wait_child()');
161             },
162             },
163             exit => {
164             is => 'ro',
165             init_arg => undef,
166             predicate => 1,
167             default => sub { my $r = $_[0]->_ret; $r < 0 ? $r : $r >> 8 },
168             },
169             signal => {
170             is => 'ro',
171             init_arg => undef,
172             default => sub { $_[0]->_ret & 127 },
173             },
174             core => {
175             is => 'ro',
176             init_arg => undef,
177             default => sub { $_[0]->_ret & 128 },
178             },
179             status => {
180             is => 'rw',
181             default => 'Running',
182             },
183             on_exit => { is => 'rw', },
184             };
185              
186             sub cmdline {
187 18     18 1 4946 my $self = shift;
188 18 50       52 if (wantarray) {
189 18         36 return @{ $self->cmd };
  18         84  
190             }
191             else {
192 0         0 return join( ' ', @{ $self->cmd } );
  0         0  
193             }
194             }
195              
196             sub close {
197 50     50 1 793 my $self = shift;
198              
199 50         213 foreach my $h (qw/stdin stdout stderr/) {
200              
201             # may not be defined during global destruction
202 150 50       3524 my $fh = $self->$h or next;
203 150 100       569 $fh->opened or next;
204 64 100       708 if ( $h eq 'stderr' ) {
205             warn sprintf( '[%d] uncollected stderr: %s', $self->pid // -1, $_ )
206 24   0     82 for $self->stderr->getlines;
207             }
208 64 50       206 $fh->close || Carp::carp "error closing $h: $!";
209             }
210              
211 50         867 return;
212             }
213              
214             sub wait_child {
215 62     62 1 810 my $self = shift;
216 62   50     231 my $pid = $self->pid // return;
217 62 100       190 return $self->exit if $self->has__ret;
218              
219 31         194 my $ret = -1; # default means: bad execution for some reasons
220              
221 31 50       115 if ( $pid > 0 ) {
222              
223 31         176 local $?;
224 31         153 local $!;
225              
226 31         82 my $pid = waitpid $self->pid, 0;
227 31         111 $ret = $?;
228              
229 31 50       105 if ( $pid != $self->pid ) {
230 0         0 warn
231             sprintf( 'Could not reap child process %d (waitpid returned: %d)',
232             $self->pid, $pid );
233 0         0 $ret = 0;
234             }
235              
236 31 50       211 if ( $ret == -1 ) {
237              
238             # So waitpid returned a PID but then sets $? to this
239             # strange value? (Strange in that tests randomly show it to
240             # be invalid.) Most likely a perl bug; I think that waitpid
241             # got interrupted and when it restarts/resumes the status
242             # is lost.
243             #
244             # See http://www.perlmonks.org/?node_id=641620 for a
245             # possibly related discussion.
246             #
247             # However, since I localised $? and $! above I haven't seen
248             # this problem again, so I hope that is a good enough work
249             # around. Lets warn any way so that we know when something
250             # dodgy is going on.
251 0         0 warn __PACKAGE__
252             . ' received invalid child exit status for pid '
253             . $self->pid
254             . ' Setting to 0';
255 0         0 $ret = 0;
256              
257             }
258             }
259              
260 31         177 $self->_ret($ret);
261              
262 31 100       121 if ( my $subref = $self->on_exit ) {
263 2         12 $subref->($self);
264             }
265              
266             $self->status(
267 31         69 do {
268 31 100       88 if ( $self->signal != 0 ) {
    100          
269 4         16 $log->warn(
270             'Killed',
271             {
272             pid => $self->pid,
273             signal => $self->signal,
274             core => $self->core
275             }
276             );
277             }
278             elsif ( $self->exit != 0 ) {
279 2         18 $log->warn(
280             'Non-zero exit',
281             {
282             pid => $self->pid,
283             exit => $self->exit
284             }
285             );
286             }
287             else {
288 25         105 'Terminated';
289             }
290             }
291             );
292              
293 31   100     98 not( $self->exit or $self->signal );
294             }
295              
296             sub DESTROY {
297 31     31   11334 my $self = shift;
298 31         115 $self->close;
299 31         79 $self->wait_child;
300             }
301              
302             1;
303              
304             __END__