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   1514 use strict;
  3           17  
  3           144  
132               our $DEBUG = 0; # set to 1 for some debugging output
133                
134 3       3   1338 use parent qw( Alien::Base );
  3           1035  
  3           16  
135                
136 3       3   159200 use File::Spec;
  3           21  
  3           66  
137 3       3   14 use File::Temp qw/tempfile/;
  3           6  
  3           122  
138 3       3   1455 use File::Which;
  3           3068  
  3           155  
139 3       3   20 use Time::HiRes qw/usleep/;
  3           6  
  3           17  
140 3       3   1915 use POSIX ":sys_wait_h";
  3           19977  
  3           15  
141 3       3   4656 use Fcntl qw/SEEK_SET/;
  3           6  
  3           142  
142 3       3   2820 use Env qw( @PATH );
  3           8300  
  3           17  
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.040';
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 776 my $module =shift;
163 2           4 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         11 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 10 my ($class) = @_;
186 4     50     13 $class ||= __PACKAGE__;
187                
188 4           5 my $exec_path;
189               # GNUPLOT_BINARY overrides at runtime
190 4 100         25 if($ENV{'GNUPLOT_BINARY'}) {
191 1           6 $exec_path = $ENV{'GNUPLOT_BINARY'};
192               } else {
193 3           29 local $ENV{PATH} = $ENV{PATH};
194 3           21 unshift @PATH, $class->bin_dir;
195 3           1334 $exec_path = which("gnuplot");
196               }
197                
198 4           474 return $exec_path;
199               }
200                
201               sub load_gnuplot {
202 4       4 0 219 my ($class) = @_;
203 4     50     15 $class ||= __PACKAGE__;
204                
205 4           12 my $exec_path = $class->exe;
206 4           14 $class->check_gnuplot($exec_path);
207               }
208                
209               sub check_gnuplot {
210 4       4 0 8 my $exec_path = pop @_;
211                
212 4 100         117 unless(-x $exec_path) {
213 1           16 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           15 my($pid);
229 3           18 my ($undef, $file) = tempfile();
230                
231               # Create command file
232 3           2116 open FOO, ">${file}_gzinta";
233 3           68 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           190 close FOO;
235                
236 3 50         20 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           2522 $pid = fork();
257 3 50         489 if(defined($pid)) {
258 3 50         155 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   2290 no warnings;
  3           9  
  3           2736  
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         114 if($DEBUG) { print "waiting for pid $pid (up to 20 iterations of 100ms)"; flush STDOUT; }
  0           0  
  0           0  
277 3           116 for (1..20) {
278 12 50         194 if($DEBUG) { print "."; flush STDOUT; }
  0           0  
  0           0  
279 12 100         385 if(waitpid($pid,WNOHANG)) {
280 3           215 $pid=0;
281 3           48 last;
282               }
283 9           901992 usleep(1e5);
284               }
285 3 50         129 if($DEBUG) { print "\n"; flush STDOUT; }
  0           0  
  0           0  
286              
287 3 50         44 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           498 open FOO, "<$file";
302 3           985 my @lines = ;
303 3           206 close FOO;
304 3           377 unlink $file;
305 3           414 unlink $file."_gzinta";
306              
307               ##############################
308               # Whew. Now parse out the 'GNUPLOT' and version number...
309 3           86 my $lines = join("", map { chomp $_; $_} @lines);
  465           1052  
  465           1726  
310 3 50         297 $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              
316 3 50         121 $lines =~ m/Version (\d+\.\d+) (patchlevel (\d+))?/ or die qq{
317               Alien::Gnuplot: the executable file $exec_path claims to be gnuplot, but
318               I could not parse a version number from its output. Sorry, I give up.
319                
320               };
321              
322 3           91 $version = $1;
323 3           57 $pl = $3;
324 3           31 $executable = $exec_path;
325              
326               ##############################
327               # Parse out available terminals and put them into the
328               # global list and hash.
329 3           21 @terms = ();
330 3           24 %terms = ();
331 3           79 my $reading_terms = 0;
332 3           31 for my $line(@lines) {
333 123 100         500 last if($line =~ m/CcColors/);
334 120 100         414 if(!$reading_terms) {
335 42 100         193 if($line =~ m/^Available terminal types\:/) {
336 3           22 $reading_terms = 1;
337               }
338               } else {
339 78           224 $line =~ s/^Press return for more\:\s*//;
340 78 50         463 $line =~ m/^\s*(\w+)\s(.*[^\s])\s*$/ || next;
341 78           394 push(@terms, $1);
342 78           599 $terms{$1} = $2;
343               }
344               }
345              
346               ##############################
347               # Parse out available colors and put them into that global list and hash.
348 3           15 @colors = ();
349 3           12 %colors = ();
350              
351 3           13 for my $line(@lines) {
352 465 100         1415 last if($line =~ m/FfFinished/);
353 462 100         2072 next unless( $line =~ m/\s+([\w\-0-9]+)\s+(\#......)/);
354 333           2038 $colors{$1} = $2;
355               }
356 3           1302 @colors = sort keys %colors;
357               }
358                
359               sub import {
360 3       3   46 my $pkg = shift;
361 3           22 $pkg->SUPER::import(@_);
362 3           60512 $pkg->load_gnuplot();
363               };
364                
365                
366               1;
367