File Coverage

blib/lib/Alien/Gnuplot.pm
Criterion Covered Total %
statement 104 138 75.3
branch 29 44 65.9
path n/a
condition 2 4 50.0
subroutine 15 15 100.0
pod 0 4 0.0
total 150 205 73.1


line stmt bran path cond sub pod time code
1               =head1 NAME
2                
3               Alien::Gnuplot - Find and verify functionality of the gnuplot executable.
4                
5               =head1 SYNOPSIS
6                
7               package MyGnuplotter;
8                
9               use strict;
10                
11               use Alien::Gnuplot;
12                
13               $gnuplot = $Alien::Gnuplot::executable;
14                
15               `$gnuplot < /tmp/plotfile`;
16                
17               1;
18                
19               =head1 DESCRIPTION
20                
21               Alien::Gnuplot verifies existence and sanity of the gnuplot external
22               application. It only declares one access method,
23               C, which does the actual work and is
24               called automatically at load time. Alien::Gnuplot doesn't have any
25               actual plotting methods - making use of gnuplot, once it is found and
26               verified, is up to you or your client module.
27                
28               Using Alien::Gnuplot checks for existence of the executable, verifies
29               that it runs properly, and sets several global variables to describe
30               the properties of the gnuplot it found:
31                
32               =over 3
33                
34               =item * C<$Alien::Gnuplot::executable>
35                
36               gets the path to the gnuplot executable.
37                
38               =item * C<$Alien::Gnuplot::version>
39                
40               gets the self-reported version number of the executable.
41                
42               =item * C<$Alien::Gnuplot::pl>
43                
44               gets the self-reported patch level.
45                
46               =item * C<@Alien::Gnuplot::terms>
47                
48               gets a list of the names of all supported terminal devices.
49                
50               =item * C<%Alien::Gnuplot::terms>
51                
52               gets a key for each supported terminal device; values are the 1-line
53               description from gnuplot. This is useful for testing whether a
54               particular terminal is supported.
55                
56               =item * C<@Alien::Gnuplot::colors>
57                
58               gets a list of the names of all named colors recognized by this gnuplot.
59                
60               =item * C<%Alien::Gnuplot::colors>
61                
62               gets a key for each named color; values are the C<#RRGGBB> form of the color.
63               This is useful for decoding colors, or for checking whether a particular color
64               name is recognized. All the color names are lowercase alphanumeric.
65                
66               =back
67                
68               You can point Alien::Gnuplot to a particular path for gnuplot, by
69               setting the environment variable GNUPLOT_BINARY to the path. Otherwise
70               your path will be searched (using File::Spec) for the executable file.
71                
72               If there is no executable application in your path or in the location
73               pointed to by GNUPLOT_BINARY, then the module throws an exception.
74               You can also verify that it has not completed successfully, by
75               examining $Alien::Gnuplot::version, which is undefined in case of
76               failure and contains the gnuplot version string on success.
77                
78               If you think the global state of the gnuplot executable may have
79               changed, you can either reload the module or explicitly call
80               C to force a fresh inspection of
81               the executable.
82                
83               =head1 INSTALLATION STRATEGY
84                
85               When you install Alien::Gnuplot, it checks that gnuplot itself is
86               installed as well. If it is not, then Alien::Gnuplot attempts to
87               use one of several common package managers to install gnuplot for you.
88               If it can't find one of those, if dies (and refuses to install), printing
89               a friendly message about how to get gnuplot before throwing an error.
90                
91               In principle, gnuplot could be automagically downloaded and built,
92               but it is distributed via Sourceforge -- which obfuscates interior
93               links, making such tools surprisingly difficult to write.
94                
95               =head1 CROSS-PLATFORM BEHAVIOR
96                
97               On POSIX systems, including Linux and MacOS, Alien::Gnuplot uses
98               fork/exec to invoke the gnuplot executable and asynchronously monitor
99               it for hangs. Microsoft Windows process control is more difficult, so
100               if $^O contains "MSWin32", a simpler system call is used, that is
101               riskier -- it involves waiting for the unknown executable to complete.
102                
103               =head1 REPOSITORIES
104                
105               Gnuplot's main home page is at L.
106                
107               Alien::Gnuplot development is at L.
108                
109               A major client module for Alien::Gnuplot is PDL::Graphics::Gnuplot, which
110               can be found at L.
111               PDL is at L.
112                
113               =head1 AUTHOR
114                
115               Craig DeForest
116                
117               (with special thanks to Chris Marshall, Juergen Mueck, and
118               Sisyphus for testing and debugging on the Microsoft platform)
119                
120               =head1 COPYRIGHT AND LICENSE
121                
122               Copyright (C) 2013 Craig DeForest
123                
124               This library is free software; you can redistribute it and/or modify
125               it under the same terms as Perl itself.
126                
127               =cut
128                
129               package Alien::Gnuplot;
130                
131 3       3   16468 use strict;
  3           6  
  3           174  
132               our $DEBUG = 0; # set to 1 for some debugging output
133                
134 3       3   1388 use parent qw( Alien::Base );
  3           872  
  3           15  
135                
136 3       3   142220 use File::Spec;
  3           5  
  3           80  
137 3       3   13 use File::Temp qw/tempfile/;
  3           7  
  3           144  
138 3       3   1304 use File::Which;
  3           3804  
  3           202  
139 3       3   26 use Time::HiRes qw/usleep/;
  3           5  
  3           21  
140 3       3   1658 use POSIX ":sys_wait_h";
  3           21701  
  3           17  
141 3       3   4822 use Fcntl qw/SEEK_SET/;
  3           27  
  3           1584  
142 3       3   4491 use Env qw( @PATH );
  3           7292  
  3           15  
143                
144               # VERSION here is for CPAN to parse -- it is the version of the module itself. But we
145               # overload the system VERSION to compare a required version against gnuplot itself, rather
146               # than against the module version.
147                
148               our $VERSION = '1.043';
149                
150               # On install, try to make sure at least this version is present.
151               our $GNUPLOT_RECOMMENDED_VERSION = '4.6';
152                
153               our $executable; # Holds the path to the found gnuplot
154               our $version; # Holds the found version number
155               our $pl; # Holds the found patchlevel
156               our @terms;
157               our %terms;
158               our @colors;
159               our %colors;
160                
161               sub VERSION {
162 2       2 0 263069 my $module =shift;
163 2           5 my $req_v = shift;
164               # Need this line when using
165               #
166               # use Alien::Gnuplot 4.4;
167               #
168               # to check Gnuplot version.
169 2 50         7 $module->load_gnuplot unless $version; # already have version
170 2 100         18 unless($req_v <= $version) {
171 1           12 die qq{
172                
173               Alien::Gnuplot: Found gnuplot version $version, but you requested $req_v.
174               You should upgrade gnuplot, either by reinstalling Alien::Gnuplot or
175               getting it yourself from L.
176                
177               };
178               }
179               }
180                
181               sub exe {
182               ##############################
183               # Search the path for the executable
184               #
185 4       4 0 14 my ($class) = @_;
186 4     50     13 $class ||= __PACKAGE__;
187                
188 4           26 my $exec_path;
189               # GNUPLOT_BINARY overrides at runtime
190 4 100         19 if($ENV{'GNUPLOT_BINARY'}) {
191 1           4 $exec_path = $ENV{'GNUPLOT_BINARY'};
192               } else {
193 3           49 local $ENV{PATH} = $ENV{PATH};
194 3           36 unshift @PATH, $class->bin_dir;
195 3           1585 $exec_path = which("gnuplot");
196               }
197                
198 4           453 return $exec_path;
199               }
200                
201               sub load_gnuplot {
202 4       4 0 286831 my ($class) = @_;
203 4     50     19 $class ||= __PACKAGE__;
204                
205 4           21 my $exec_path = $class->exe;
206 4           19 $class->check_gnuplot($exec_path);
207               }
208                
209               sub check_gnuplot {
210 4       4 0 9 my $exec_path = pop @_;
211                
212 4 100         188 unless(-x $exec_path) {
213 1           18 die q{
214               Alien::Gnuplot: no executable gnuplot found! If you have gnuplot,
215               you can put its exact location in your GNUPLOT_BINARY environment
216               variable or make sure your PATH contains it. If you do not have
217               gnuplot, you can reinstall Alien::Gnuplot (and its installation
218               script will try to install gnuplot) or get it yourself from L.
219               };
220               }
221              
222               ##############################
223               # Execute the executable to make sure it's really gnuplot, and parse
224               # out its reported version. This is complicated by gnuplot's shenanigans
225               # with STDOUT and STDERR, so we fork and redirect everything to a file.
226               # The parent process gives the daughter 2 seconds to report progress, then
227               # kills it dead.
228 3           7 my($pid);
229 3           25 my ($undef, $file) = tempfile();
230                
231               # Create command file
232 3           3005 open FOO, ">${file}_gzinta";
233 3           63 print FOO "show version\nset terminal\n\n\n\n\n\n\n\n\n\nprint \"CcColors\"\nshow colornames\n\n\n\n\n\n\n\nprint \"FfFinished\"\nexit\n";
234 3           147 close FOO;
235                
236 3 50         21 if($^O =~ /MSWin32/i) {
237                
238 0 0         0 if( $exec_path =~ m/([\"\*\?\<\>\|])/ ) {
239 0           0 die "Alien::Gnuplot: Invalid character '$1' in path to gnuplot -- I give up" ;
240               }
241              
242               # Microsoft Windows sucks at IPC (and many other things), so
243               # use "system" instead of civilized fork/exec.
244               # This leaves us vulnerable to gnuplot itself hanging, but
245               # sidesteps the problem of waitpid hanging on Strawberry Perl.
246 0           0 open FOO, ">&STDOUT";
247 0           0 open BAR, ">&STDERR";
248 0           0 open STDOUT,">$file";
249 0           0 open STDERR,">$file";
250 0           0 system(qq{"$exec_path" < ${file}_gzinta});
251 0           0 open STDOUT,">&FOO";
252 0           0 open STDERR,">&BAR";
253 0           0 close FOO;
254 0           0 close BAR;
255               } else {
256 3           6052 $pid = fork();
257 3 50         407 if(defined($pid)) {
258 3 50         289 if(!$pid) {
259               # daughter
260 0           0 open BAR, ">&STDERR"; # preserve stderr
261 0           0 eval {
262 0           0 open STDOUT, ">$file";
263 0           0 open STDERR, ">&STDOUT";
264 0           0 open STDIN, "<${file}_gzinta";
265 0           0 seek STDIN, 0, SEEK_SET;
266 3       3   2185 no warnings;
  3           5  
  3           3804  
267 0           0 exec($exec_path);
268 0           0 print BAR "Execution of $exec_path failed!\n";
269 0           0 exit(1);
270               };
271 0           0 print STDERR "Alien::Gnuplot: Unknown problems spawning '$exec_path' to probe gnuplot.\n";
272 0           0 exit(2); # there was a problem!
273               } else {
274               # parent
275               # Assume we're more POSIX-compliant...
276 3 50         207 if($DEBUG) { print "waiting for pid $pid (up to 20 iterations of 100ms)"; flush STDOUT; }
  0           0  
  0           0  
277 3           274 for (1..20) {
278 11 50         184 if($DEBUG) { print "."; flush STDOUT; }
  0           0  
  0           0  
279 11 100         395 if(waitpid($pid,WNOHANG)) {
280 3           39 $pid=0;
281 3           15 last;
282               }
283 8           805571 usleep(1e5);
284               }
285 3 50         26 if($DEBUG) { print "\n"; flush STDOUT; }
  0           0  
  0           0  
286              
287 3 50         39 if($pid) {
288 0 0         0 if( $DEBUG) { print "gnuplot didn't complete. Killing it dead...\n"; flush STDOUT; }
  0           0  
  0           0  
289 0           0 kill 9,$pid; # zap
290 0           0 waitpid($pid,0); # reap
291               }
292               } #end of parent case
293               } else {
294               # fork returned undef - error.
295 0           0 die "Alien::Gnuplot: Couldn't fork to test gnuplot! ($@)\n";
296               }
297               }
298              
299               ##############################
300               # Read what gnuplot had to say, and clean up our mess...
301 3           258 open FOO, "<$file";
302 3           621 my @lines = ;
303 3           40 close FOO;
304 3           280 unlink $file;
305 3           384 unlink $file."_gzinta";
306              
307               ##############################
308               # Whew. Now parse out the 'GNUPLOT' and version number...
309 3           40 my $lines = join("", map { chomp $_; $_} @lines);
  465           499  
  465           604  
310 3 50         183 $lines =~ s/\s+G N U P L O T\s*// or die qq{
311               Alien::Gnuplot: the executable '$exec_path' appears not to be gnuplot,
312               or perhaps there was a problem running it. You can remove it or set
313               your GNUPLOT_BINARY variable to an actual gnuplot.
314                
315               Raw output from Gnuplot:
316               $lines
317               };
318              
319 3 50         90 $lines =~ m/Version (\d+\.\d+) (patchlevel (\d+))?/ or die qq{
320               Alien::Gnuplot: the executable file $exec_path claims to be gnuplot, but
321               I could not parse a version number from its output. Sorry, I give up.
322                
323               Raw output from Gnuplot:
324               $lines
325               };
326              
327 3           43 $version = $1;
328 3           23 $pl = $3;
329 3           42 $executable = $exec_path;
330              
331               ##############################
332               # Parse out available terminals and put them into the
333               # global list and hash.
334 3           9 @terms = ();
335 3           10 %terms = ();
336 3           9 my $reading_terms = 0;
337 3           15 for my $line(@lines) {
338 123 100         201 last if($line =~ m/CcColors/);
339 120 100         151 if(!$reading_terms) {
340 42 100         83 if($line =~ m/^Available terminal types\:/) {
341 3           9 $reading_terms = 1;
342               }
343               } else {
344 78           94 $line =~ s/^Press return for more\:\s*//;
345 78 50         174 $line =~ m/^\s*(\w+)\s(.*[^\s])\s*$/ || next;
346 78           212 push(@terms, $1);
347 78           267 $terms{$1} = $2;
348               }
349               }
350              
351               ##############################
352               # Parse out available colors and put them into that global list and hash.
353 3           8 @colors = ();
354 3           10 %colors = ();
355              
356 3           9 for my $line(@lines) {
357 465 100         575 last if($line =~ m/FfFinished/);
358 462 100         889 next unless( $line =~ m/\s+([\w\-0-9]+)\s+(\#......)/);
359 333           723 $colors{$1} = $2;
360               }
361 3           859 @colors = sort keys %colors;
362               }
363                
364               sub import {
365 3       3   34 my $pkg = shift;
366 3           51 $pkg->SUPER::import(@_);
367 3           59612 $pkg->load_gnuplot();
368               };
369                
370                
371               1;
372