source: LMDZ6/branches/Amaury_dev/tools/fcm/lib/FCM/Util/Shell.pm @ 5209

Last change on this file since 5209 was 5129, checked in by abarral, 4 months ago

Re-add removed by mistake fcm

File size: 9.0 KB
Line 
1# ------------------------------------------------------------------------------
2# Copyright (C) 2006-2021 British Crown (Met Office) & Contributors.
3#
4# This file is part of FCM, tools for managing and building source code.
5#
6# FCM is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# FCM is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with FCM. If not, see <http://www.gnu.org/licenses/>.
18# ------------------------------------------------------------------------------
19use strict;
20use warnings;
21
22# ------------------------------------------------------------------------------
23package FCM::Util::Shell;
24use base qw{FCM::Class::CODE};
25
26use FCM::Context::Event;
27use FCM::Util::Exception;
28use File::Spec::Functions qw{catfile file_name_is_absolute path};
29use IPC::Open3 qw{open3};
30use List::Util qw{first};
31use Scalar::Util qw{reftype};
32use Text::ParseWords qw{shellwords};
33
34our $BUFFER_SIZE = 4096;  # default buffer size
35our $TIME_OUT    = 0.005; # default time out for selecting a file handle
36
37my $E = 'FCM::Util::Exception';
38my %FUNCTION_OF = (e => \&_do_r, i => \&_do_w, o => \&_do_r);
39my @IOE = qw{i o e};
40my %ACTION_FUNC_FOR
41    = (e => \&_action_func_r, i => \&_action_func_w, o => \&_action_func_r);
42
43# Creates the class.
44__PACKAGE__->class(
45    {   buffer_size => {isa => '$', default => $BUFFER_SIZE},
46        time_out    => {isa => '$', default => $TIME_OUT},
47        util        => '&',
48    },
49    {   action_of => {
50            invoke        => \&_invoke,
51            invoke_simple => \&_invoke_simple,
52            which         => \&_which,
53        },
54    },
55);
56
57# Returns a CODE to deal with non-CODE read action.
58sub _action_func_r {
59    my ($arg_ref) = @_;
60    ${$arg_ref} ||= q{};
61    sub {${$arg_ref} .= $_[0]};
62}
63
64# Returns a CODE to deal with non-CODE write action.
65sub _action_func_w {
66    my ($arg_ref) = @_;
67    my @inputs
68        = ref($arg_ref) && reftype($arg_ref) eq 'ARRAY'  ? @{$arg_ref}
69        : ref($arg_ref) && reftype($arg_ref) eq 'SCALAR' ? (${$arg_ref})
70        :                                                  ()
71        ;
72    sub {shift(@inputs)};
73}
74
75# Gets output $value from a selected handle, and invokes $action->($value).
76sub _do_r {
77    my ($attrib_ref, $ctx) = @_;
78    my $n_bytes;
79    while (
80        my @handles = $ctx->get_select()->can_read($attrib_ref->{time_out})
81    ) {
82        my ($handle) = @handles;
83        my $buffer = q{};
84        my $n = sysread($handle, $buffer, $attrib_ref->{buffer_size});
85        if (!defined($n)) {
86            return;
87        }
88        $n_bytes += $n;
89        if ($n == 0) {
90            close($handle) || return;
91            return 0;
92        }
93        $ctx->get_action()->($buffer);
94    }
95    defined($n_bytes) ? $n_bytes : -1;
96}
97
98# Gets input from $action->() and writes to a selected handle if possible.
99# Handles buffering of STDIN to the command.
100sub _do_w {
101    my ($attrib_ref, $ctx) = @_;
102    my $n_bytes;
103    while (
104        my @handles = $ctx->get_select()->can_write($attrib_ref->{time_out})
105    ) {
106        my ($handle) = @handles;
107        if (!$ctx->get_buf()) {
108            $ctx->set_buf($ctx->get_action()->());
109            if (!defined($ctx->get_buf())) {
110                close($handle) || return;
111                return 0;
112            };
113            $ctx->set_buf_length(length($ctx->get_buf()));
114            $ctx->set_buf_offset(0);
115        }
116        my $n = syswrite(
117            $handle,
118            $ctx->get_buf(),
119            $attrib_ref->{buffer_size},
120            $ctx->get_buf_offset(),
121        );
122        if (!defined($n)) {
123            return;
124        }
125        $n_bytes += $n;
126        $ctx->set_buf_offset($ctx->get_buf_offset() + $n);
127        if ($ctx->get_buf_offset() >= $ctx->get_buf_length()) {
128            $ctx->set_buf(undef);
129            $ctx->set_buf_length(0);
130            $ctx->set_buf_offset(0);
131        }
132    }
133    defined($n_bytes) ? $n_bytes : -1;
134}
135
136# Invokes a command.
137sub _invoke {
138    my ($attrib_ref, $command_ref, $action_ref) = @_;
139    # Ensure that the command is an ARRAY
140    if (!ref($command_ref)) {
141        $command_ref = [shellwords($command_ref)];
142    }
143    # Check that the command exists in the PATH
144    if (!_which($attrib_ref, $command_ref->[0])) {
145        return $E->throw($E->SHELL_WHICH, $command_ref);
146    }
147    # Sets up the STDIN, STDOUT and STDERR to the command
148    my %ctx_of = map {($_, FCM::Util::Shell::Context->new())} @IOE;
149    $action_ref ||= {};
150    while (my ($key, $action) = each(%{$action_ref})) {
151        if (exists($ctx_of{$key})) {
152            if (reftype($action) eq 'CODE') {
153                $ctx_of{$key}->set_action($action);
154            }
155            else {
156                $ctx_of{$key}->set_action($ACTION_FUNC_FOR{$key}->($action));
157            }
158        }
159    }
160    # Calls the command with open3
161    my $timer = $attrib_ref->{util}->timer();
162    my $pid = eval {
163        open3((map {$ctx_of{$_}->get_handle()} @IOE), @{$command_ref});
164    };
165    if (my $e = $@) {
166        return $E->throw($E->SHELL_OPEN3, $command_ref, $e);
167    }
168    # Handles input/output of the command
169    for my $ctx (values(%ctx_of)) {
170        $ctx->get_select()->add($ctx->get_handle());
171    }
172    while (keys(%ctx_of)) {
173        while (my ($key, $ctx) = each(%ctx_of)) {
174            my $status = $FUNCTION_OF{$key}->($attrib_ref, $ctx);
175            if (!defined($status)) {
176                return $E->throw($E->SHELL_OS, $command_ref, $!);
177            }
178            if (!$status) {
179                delete($ctx_of{$key});
180            }
181        }
182    }
183    # Wait for command to finish
184    waitpid($pid, 0);
185    my $rc = $?;
186    $attrib_ref->{util}->event(
187        FCM::Context::Event->SHELL, $command_ref, $rc, $timer->(),
188    );
189    # Handles exceptions and signals
190    if ($rc) {
191        if ($rc == -1) {
192            return $E->throw($E->SHELL_OS, $command_ref, $!);
193        }
194        if ($rc & 127) {
195            return $E->throw($E->SHELL_SIGNAL, $command_ref, $rc & 127);
196        }
197    }
198    return $rc >> 8;
199}
200
201# Wraps _invoke.
202sub _invoke_simple {
203    my ($attrib_ref, $command_ref) = @_;
204    my ($e, $o);
205    my $rc = _invoke($attrib_ref, $command_ref, {e => \$e, o => \$o});
206    return {e => $e, o => $o, rc => $rc};
207}
208
209# Returns the full path to the command $name, if it exists in the PATH.
210sub _which {
211    my ($attrib_ref, $name) = @_;
212    if (file_name_is_absolute($name)) {
213        return $name;
214    }
215    use filetest 'access';
216    first {-f $_ && -x _} map {catfile($_, $name)} path();
217    no filetest 'access';
218}
219
220# ------------------------------------------------------------------------------
221package FCM::Util::Shell::Context;
222use base qw{FCM::Class::HASH};
223
224use IO::Select;
225use Symbol qw{gensym};
226
227# A context to hold the information for the command's STDIN, STDOUT or STDERR.
228# action => CODE to call to get more STDIN for the command or to send
229#           STDOUT/STDERR to when possible.
230# buf*   => A buffer (and its length and the current offset) to hold the STDIN
231#           that is yet to be written to the command.
232# handle => The command STDIN, STDOUT or STDERR.
233# select => The IO::Select object that tells us whether the handle is ready for
234#           I/O or not.
235__PACKAGE__->class(
236    {   action     => {isa => '&'},
237        buf        => {isa => '$'},
238        buf_length => {isa => '$'},
239        buf_offset => {isa => '$'},
240        handle     => {isa => '*', default => \&gensym},
241        'select'   => {isa => 'IO::Select', default => sub {IO::Select->new()}},
242    },
243);
244
245# ------------------------------------------------------------------------------
2461;
247__END__
248
249=head1 NAME
250
251FCM::Util::Shell
252
253=head1 SYNOPSIS
254
255    use FCM::Util;
256    $util = FCM::Util->new(\%attrib);
257    %action_of = {e => \&e_handler, i => \&i_handler, o => \&o_handler};
258    $rc = $util->shell(\@command, \%action_of);
259    %value_of = %{$util->shell_simple(\@command)};
260
261=head1 DESCRIPTION
262
263Wraps L<IPC::Open3|IPC::Open3> to provide an interface driven by callbacks.
264
265=head1 METHODS
266
267=over 4
268
269=item $class->new(\%attrib)
270
271Returns a new instance. The attributes that can be specified in %attrib are:
272
273=over 4
274
275=item {buffer_size}
276
277The size of the read buffer for reading from the standard output and standard
278error output of the command. The default is 4096.
279
280=item {time_out}
281
282The time to wait when selecting a file handle. The default is 0.001.
283
284=item {util}
285
286A CODE reference. The L<FCM::Util|FCM::Util> object that initialised this
287instance.
288
289=back
290
291=back
292
293See the description of the shell(), shell_simpl() and shell_which() methods in
294L<FCM::Util|FCM::Util> for detail.
295
296=head1 SEE ALSO
297
298L<IPC::Open3|IPC::Open3>
299
300Inspired by the CPAN module L<IPC::Cmd|IPC::Cmd> and friends.
301
302=head1 COPYRIGHT
303
304Copyright (C) 2006-2021 British Crown (Met Office) & Contributors.
305
306=cut
Note: See TracBrowser for help on using the repository browser.