File Coverage

blib/lib/Gnuplot/Builder/Process.pm
Criterion Covered Total %
statement 39 125 31.2
branch 4 42 9.5
condition 2 15 13.3
subroutine 13 27 48.1
pod 1 3 33.3
total 59 212 27.8


line stmt bran cond sub pod time code
1             package Gnuplot::Builder::Process;
2 38     38   160682 use strict;
  38         58  
  38         1262  
3 38     38   142 use warnings;
  38         54  
  38         1599  
4 38     38   15711 use IPC::Open3 qw(open3);
  38         153582  
  38         10143  
5 38     38   241 use Carp;
  38         57  
  38         1731  
6 38     38   971 use Gnuplot::Builder::PartiallyKeyedList;
  38         62  
  38         781  
7 38     38   16244 use POSIX qw(:sys_wait_h);
  38         284116  
  38         233  
8 38     38   53809 use File::Spec;
  38         61  
  38         875  
9 38     38   17105 use Try::Tiny;
  38         67952  
  38         2218  
10 38     38   17450 use Encode ();
  38         552747  
  38         48411  
11              
12             sub _get_env {
13 228     228   395 my ($basename, @default) = @_;
14 228         555 my $name = "PERL_GNUPLOT_BUILDER_PROCESS_$basename";
15 228 100 66     689 if(defined($ENV{$name}) && $ENV{$name} ne "") {
16 5         10 return $ENV{$name};
17             }else {
18 223 100       584 return wantarray ? @default : $default[0];
19             }
20             }
21              
22             our $ASYNC = _get_env("ASYNC", 0);
23             our $NO_STDERR = _get_env("NO_STDERR", 0);
24             our @COMMAND = _get_env("COMMAND", qw(gnuplot --persist));
25             our $MAX_PROCESSES = _get_env("MAX_PROCESSES", 2);
26             our $PAUSE_FINISH = _get_env("PAUSE_FINISH", 0);
27             our $TAP = undef;
28             our $ENCODING = _get_env("ENCODING", undef);
29              
30             my $END_SCRIPT_MARK = '@@@@@@_END_OF_GNUPLOT_BUILDER_@@@@@@';
31             my $processes = Gnuplot::Builder::PartiallyKeyedList->new;
32              
33             sub _clear_zombies {
34 0     0   0 $_->_waitpid(0) foreach $processes->get_all_values(); ## cannot use each() method because _waitpid() manipulates $processes...
35             }
36              
37             {
38             my $null_handle;
39             sub _null_handle {
40 0 0   0   0 return $null_handle if defined $null_handle;
41 0         0 my $devnull = File::Spec->devnull();
42 0 0       0 open $null_handle, ">", $devnull or confess("Cannot open $devnull: $!");
43 0         0 return $null_handle;
44             }
45             }
46              
47             ## PUBLIC ONLY IN TESTS: number of processes it keeps now
48 0     0 0 0 sub FOR_TEST_process_num { $processes->size }
49              
50             ## PUBLIC ONLY IN TESTS
51             *FOR_TEST_clear_zombies = *_clear_zombies;
52              
53             ## Documented public method.
54             sub wait_all {
55 1     1 1 5 while($processes->size > 0) {
56 0           my $proc = $processes->get_at(0);
57 0           $proc->_waitpid(1);
58             }
59             }
60              
61             ## create a new gnuplot process, create a writer to it and run the
62             ## given code. If the code throws an exception, the process is
63             ## terminated. It returns the output of the gnuplot process.
64             ##
65             ## Fields in %args are:
66             ##
67             ## do (CODE-REF mandatory): the code to execute. $do->($writer).
68             ##
69             ## async (BOOL optional, default = false): If set to true, it won't
70             ## wait for the gnuplot process to finish. In this case, the return
71             ## value is an empty string.
72             ##
73             ## no_stderr (BOOL optional, default = $NO_STDERR): If set to true,
74             ## the return value won't include gnuplot's STDERR. It just includes
75             ## STDOUT.
76             ##
77             ## on_exit (CODE-REF optional, default: undef): See _new method.
78             sub with_new_process {
79 0     0 0   my ($class, %args) = @_;
80 0           my $code = $args{do};
81 0 0         croak "do parameter is mandatory" if !defined($code);
82 0 0         my $async = defined($args{async}) ? $args{async} : $ASYNC;
83 0 0         my $no_stderr = defined($args{no_stderr}) ? $args{no_stderr} : $NO_STDERR;
84 0           my $process = $class->_new(capture => !$async, no_stderr => $no_stderr, on_exit => $args{on_exit});
85 0           my $result = "";
86             try {
87 0     0     $code->($process->_writer);
88 0           $process->_close_input();
89 0 0         if(!$async) {
90 0           $result = $process->_wait_to_finish();
91             }
92             }catch {
93 0     0     my $e = shift;
94 0           $process->_terminate();
95 0           die $e;
96 0           };
97 0           return $result;
98             }
99              
100             ## create a new gnuplot process. it blocks if the number of processes
101             ## has reached $MAX_PROCESSES.
102             ##
103             ## Fields in %args are:
104             ##
105             ## capture (BOOL optional, default: false): If true, it keeps the
106             ## STDOUT and STDERR of the process so that it can read them
107             ## afterward. Otherwise, it just discards the output.
108             ##
109             ## no_stderr (BOOL optional, default: false): If true, STDERR is
110             ## discarded instead of being redirected to STDOUT.
111             ##
112             ## on_exit (CODE-REF optional, default: undef): If set, this callback is called when the process
113             ## exits. It's called like $on_exit->($status), where $status is the $? value for the process.
114             sub _new {
115 0     0     my ($class, %args) = @_;
116 0           _clear_zombies();
117 0           my $on_exit = $args{on_exit};
118 0 0 0       if(defined($on_exit) && ref($on_exit) ne 'CODE') {
119 0           croak("on_exit must be a CODE-REF");
120             }
121 0   0       while($MAX_PROCESSES > 0 && $processes->size() >= $MAX_PROCESSES) {
122             ## wait for the first process to finish. it's not the smartest
123             ## way, but is it possible to wait for specific set of
124             ## processes?
125 0           my $proc = $processes->get_at(0);
126 0           $proc->_waitpid(1);
127             }
128 0           my $capture = $args{capture};
129 0           my $no_stderr = $args{no_stderr};
130 0           my ($write_handle, $read_handle, $pid);
131              
132             ## open3() does not seem to work well with lexical filehandles, so we use fileno()
133 0 0         $pid = open3($write_handle,
    0          
134             $capture ? $read_handle : '>&'.fileno(_null_handle()),
135             $no_stderr ? '>&'.fileno(_null_handle()) : undef,
136             @COMMAND);
137 0           my $self = bless {
138             pid => $pid,
139             write_handle => $write_handle,
140             read_handle => $read_handle,
141             on_exit => $on_exit,
142             }, $class;
143 0           $processes->set($pid, $self);
144 0           return $self;
145             }
146              
147              
148             ########
149             ######## OBJECT METHODS
150             ########
151              
152             ## Return the writer code-ref for this process.
153             sub _writer {
154 0     0     my ($self) = @_;
155 0 0         croak "Input end is already closed" if not defined $self->{write_handle};
156 0           my $write_handle = $self->{write_handle};
157 0           my $pid = $self->{pid};
158             return sub {
159 0     0     my $msg = join "", @_;
160 0 0         $msg = Encode::encode($ENCODING, $msg) if defined $ENCODING;
161 0 0         $TAP->($pid, "write", $msg) if defined $TAP;
162 0           print $write_handle ($msg);
163 0           };
164             ## If we are serious about avoiding dead-lock, we must use
165             ## select() to check writability first and to read from the
166             ## read_handle. But I guess the dead-lock happens only if the
167             ## user inputs too much data and the gnuplot outputs too much
168             ## data to STDOUT/STDERR. That's rare.
169             }
170              
171             ## lexical sub because MockTool uses it, too.
172             my $_finishing_commands = sub {
173             if($PAUSE_FINISH) {
174             return ('pause mouse close', 'exit');
175             }else {
176             return ('exit');
177             }
178             };
179              
180             ## Close the input channel. You can call this method multiple times.
181             sub _close_input {
182 0     0     my ($self) = @_;
183 0 0         return if not defined $self->{write_handle};
184 0           my $writer = $self->_writer;
185 0           $writer->("\n");
186 0           foreach my $statement (qq{set print "-"}, qq{print '$END_SCRIPT_MARK'}, $_finishing_commands->()) {
187 0           $writer->($statement . "\n");
188             }
189 0           undef $writer;
190 0           close $self->{write_handle};
191 0           $self->{write_handle} = undef;
192             }
193              
194             sub _waitpid {
195 0     0     my ($self, $blocking) = @_;
196 0 0         my $result = waitpid($self->{pid}, $blocking ? 0 : WNOHANG);
197 0           my $status = $?;
198 0 0 0       if($result == $self->{pid} || $result == -1) {
199 0           $processes->delete($self->{pid});
200             }
201 0 0 0       if($result == $self->{pid} && defined($self->{on_exit})) {
202 0           $self->{on_exit}->($status);
203             }
204             }
205              
206             ## Blocks until the process finishes. It automatically close the input
207             ## channel if necessary.
208             ##
209             ## If "capture" attribute is true, it returns the output of the
210             ## gnuplot process. Otherwise it returns an empty string.
211             sub _wait_to_finish {
212 0     0     my ($self) = @_;
213 0           $self->_close_input();
214              
215 0           my $result = "";
216 0           my $read_handle = $self->{read_handle};
217 0 0         if(defined $read_handle) {
218 0           while(defined(my $line = <$read_handle>)) {
219 0           $result .= $line;
220              
221             ## Wait for $END_SCRIPT_MARK that we told the gnuplot to
222             ## print. It is not enough to wait for EOF from $read_handle,
223             ## because in some cases, $read_handle won't be closed even
224             ## after the gnuplot process exits. For example, in Linux
225             ## 'wxt' terminal, 'gnuplot --persist' process spawns its own
226             ## child process to handle the wxt window. That child process
227             ## inherits the file descriptors from the gnuplot process, and
228             ## it won't close the output fd. So $read_handle won't be
229             ## closed until we close the wxt window. This is not good
230             ## especially we are in REPL mode.
231 0           my $end_position = index($result, $END_SCRIPT_MARK);
232 0 0         if($end_position != -1) {
233 0           $result = substr($result, 0, $end_position);
234 0           last;
235             }
236             }
237 0           close $read_handle;
238             }
239             ## Do not actually wait for the process to finish, because it can
240             ## be a long-lasting process with plot windows.
241 0           return $result;
242             }
243              
244             sub _terminate {
245 0     0     my ($self) = @_;
246 0           kill 'TERM', $self->{pid};
247             }
248              
249             #### #### #### #### #### #### #### #### #### #### #### #### ####
250              
251             package Gnuplot::Builder::Process::MockTool;
252 38     38   300 use strict;
  38         71  
  38         949  
253 38     38   138 use warnings;
  38         55  
  38         6828  
254              
255             ## tools for a process who mocks gnuplot, i.e., the process who
256             ## communicates with Gnuplot::Builder::Process.
257              
258              
259             ## Receive data from Gnuplot::Builder::Process and execute the $code
260             ## with the received data.
261             sub receive_from_builder {
262 0     0     my ($input_handle, $output_handle, $code) = @_;
263 0           while(defined(my $line = <$input_handle>)) {
264 0           $code->($line);
265              
266             ## Windows does not signal EOF on $input_handle so we must
267             ## detect the end of script by ourselves.
268 0 0         if(index($line, $END_SCRIPT_MARK) != -1) {
269 0           print $output_handle "$END_SCRIPT_MARK\n";
270 0           $code->("$_\n") foreach $_finishing_commands->();
271 0           last;
272             }
273             }
274             }
275              
276              
277             1;
278              
279             __END__