File Coverage

blib/lib/Gnuplot/Builder/Process.pm
Criterion Covered Total %
statement 39 119 32.7
branch 4 38 10.5
condition 2 9 22.2
subroutine 13 27 48.1
pod 1 3 33.3
total 59 196 30.1


line stmt bran cond sub pod time code
1             package Gnuplot::Builder::Process;
2 38     38   40827 use strict;
  38         71  
  38         988  
3 38     38   189 use warnings;
  38         73  
  38         1118  
4 38     38   30647 use IPC::Open3 qw(open3);
  38         173232  
  38         2127  
5 38     38   297 use Carp;
  38         82  
  38         2139  
6 38     38   1266 use Gnuplot::Builder::PartiallyKeyedList;
  38         78  
  38         919  
7 38     38   30269 use POSIX qw(:sys_wait_h);
  38         307625  
  38         355  
8 38     38   58643 use File::Spec;
  38         75  
  38         1043  
9 38     38   30858 use Try::Tiny;
  38         56426  
  38         2304  
10 38     38   33835 use Encode ();
  38         436678  
  38         52780  
11              
12             sub _get_env {
13 228     228   492 my ($basename, @default) = @_;
14 228         423 my $name = "PERL_GNUPLOT_BUILDER_PROCESS_$basename";
15 228 100 66     1027 if(defined($ENV{$name}) && $ENV{$name} ne "") {
16 5         15 return $ENV{$name};
17             }else {
18 223 100       700 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             sub with_new_process {
77 0     0 0   my ($class, %args) = @_;
78 0           my $code = $args{do};
79 0 0         croak "do parameter is mandatory" if !defined($code);
80 0 0         my $async = defined($args{async}) ? $args{async} : $ASYNC;
81 0 0         my $no_stderr = defined($args{no_stderr}) ? $args{no_stderr} : $NO_STDERR;
82 0           my $process = $class->_new(capture => !$async, no_stderr => $no_stderr);
83 0           my $result = "";
84             try {
85 0     0     $code->($process->_writer);
86 0           $process->_close_input();
87 0 0         if(!$async) {
88 0           $result = $process->_wait_to_finish();
89             }
90             }catch {
91 0     0     my $e = shift;
92 0           $process->_terminate();
93 0           die $e;
94 0           };
95 0           return $result;
96             }
97              
98             ## create a new gnuplot process. it blocks if the number of processes
99             ## has reached $MAX_PROCESSES.
100             ##
101             ## Fields in %args are:
102             ##
103             ## capture (BOOL optional, default: false): If true, it keeps the
104             ## STDOUT and STDERR of the process so that it can read them
105             ## afterward. Otherwise, it just discards the output.
106             ##
107             ## no_stderr (BOOL optional, default: false): If true, STDERR is
108             ## discarded instead of being redirected to STDOUT.
109             sub _new {
110 0     0     my ($class, %args) = @_;
111 0           _clear_zombies();
112 0   0       while($MAX_PROCESSES > 0 && $processes->size() >= $MAX_PROCESSES) {
113             ## wait for the first process to finish. it's not the smartest
114             ## way, but is it possible to wait for specific set of
115             ## processes?
116 0           my $proc = $processes->get_at(0);
117 0           $proc->_waitpid(1);
118             }
119 0           my $capture = $args{capture};
120 0           my $no_stderr = $args{no_stderr};
121 0           my ($write_handle, $read_handle, $pid);
122              
123             ## open3() does not seem to work well with lexical filehandles, so we use fileno()
124 0 0         $pid = open3($write_handle,
    0          
125             $capture ? $read_handle : '>&'.fileno(_null_handle()),
126             $no_stderr ? '>&'.fileno(_null_handle()) : undef,
127             @COMMAND);
128 0           my $self = bless {
129             pid => $pid,
130             write_handle => $write_handle,
131             read_handle => $read_handle,
132             }, $class;
133 0           $processes->set($pid, $self);
134 0           return $self;
135             }
136              
137              
138             ########
139             ######## OBJECT METHODS
140             ########
141              
142             ## Return the writer code-ref for this process.
143             sub _writer {
144 0     0     my ($self) = @_;
145 0 0         croak "Input end is already closed" if not defined $self->{write_handle};
146 0           my $write_handle = $self->{write_handle};
147 0           my $pid = $self->{pid};
148             return sub {
149 0     0     my $msg = join "", @_;
150 0 0         $msg = Encode::encode($ENCODING, $msg) if defined $ENCODING;
151 0 0         $TAP->($pid, "write", $msg) if defined $TAP;
152 0           print $write_handle ($msg);
153 0           };
154             ## If we are serious about avoiding dead-lock, we must use
155             ## select() to check writability first and to read from the
156             ## read_handle. But I guess the dead-lock happens only if the
157             ## user inputs too much data and the gnuplot outputs too much
158             ## data to STDOUT/STDERR. That's rare.
159             }
160              
161             ## lexical sub because MockTool uses it, too.
162             my $_finishing_commands = sub {
163             if($PAUSE_FINISH) {
164             return ('pause mouse close', 'exit');
165             }else {
166             return ('exit');
167             }
168             };
169              
170             ## Close the input channel. You can call this method multiple times.
171             sub _close_input {
172 0     0     my ($self) = @_;
173 0 0         return if not defined $self->{write_handle};
174 0           my $writer = $self->_writer;
175 0           $writer->("\n");
176 0           foreach my $statement (qq{set print "-"}, qq{print '$END_SCRIPT_MARK'}, $_finishing_commands->()) {
177 0           $writer->($statement . "\n");
178             }
179 0           undef $writer;
180 0           close $self->{write_handle};
181 0           $self->{write_handle} = undef;
182             }
183              
184             sub _waitpid {
185 0     0     my ($self, $blocking) = @_;
186 0 0         my $result = waitpid($self->{pid}, $blocking ? 0 : WNOHANG);
187 0 0 0       if($result == $self->{pid} || $result == -1) {
188 0           $processes->delete($self->{pid});
189             }
190             }
191              
192             ## Blocks until the process finishes. It automatically close the input
193             ## channel if necessary.
194             ##
195             ## If "capture" attribute is true, it returns the output of the
196             ## gnuplot process. Otherwise it returns an empty string.
197             sub _wait_to_finish {
198 0     0     my ($self) = @_;
199 0           $self->_close_input();
200              
201 0           my $result = "";
202 0           my $read_handle = $self->{read_handle};
203 0 0         if(defined $read_handle) {
204 0           while(defined(my $line = <$read_handle>)) {
205 0           $result .= $line;
206              
207             ## Wait for $END_SCRIPT_MARK that we told the gnuplot to
208             ## print. It is not enough to wait for EOF from $read_handle,
209             ## because in some cases, $read_handle won't be closed even
210             ## after the gnuplot process exits. For example, in Linux
211             ## 'wxt' terminal, 'gnuplot --persist' process spawns its own
212             ## child process to handle the wxt window. That child process
213             ## inherits the file descriptors from the gnuplot process, and
214             ## it won't close the output fd. So $read_handle won't be
215             ## closed until we close the wxt window. This is not good
216             ## especially we are in REPL mode.
217 0           my $end_position = index($result, $END_SCRIPT_MARK);
218 0 0         if($end_position != -1) {
219 0           $result = substr($result, 0, $end_position);
220 0           last;
221             }
222             }
223 0           close $read_handle;
224             }
225             ## Do not actually wait for the process to finish, because it can
226             ## be a long-lasting process with plot windows.
227 0           return $result;
228             }
229              
230             sub _terminate {
231 0     0     my ($self) = @_;
232 0           kill 'TERM', $self->{pid};
233             }
234              
235             #### #### #### #### #### #### #### #### #### #### #### #### ####
236              
237             package Gnuplot::Builder::Process::MockTool;
238 38     38   281 use strict;
  38         75  
  38         875  
239 38     38   198 use warnings;
  38         115  
  38         6226  
240              
241             ## tools for a process who mocks gnuplot, i.e., the process who
242             ## communicates with Gnuplot::Builder::Process.
243              
244              
245             ## Receive data from Gnuplot::Builder::Process and execute the $code
246             ## with the received data.
247             sub receive_from_builder {
248 0     0     my ($input_handle, $output_handle, $code) = @_;
249 0           while(defined(my $line = <$input_handle>)) {
250 0           $code->($line);
251              
252             ## Windows does not signal EOF on $input_handle so we must
253             ## detect the end of script by ourselves.
254 0 0         if(index($line, $END_SCRIPT_MARK) != -1) {
255 0           print $output_handle "$END_SCRIPT_MARK\n";
256 0           $code->("$_\n") foreach $_finishing_commands->();
257 0           last;
258             }
259             }
260             }
261              
262              
263             1;
264              
265             __END__