File Coverage

blib/lib/Spectrum/CLI.pm
Criterion Covered Total %
statement 24 214 11.2
branch 0 132 0.0
condition 0 18 0.0
subroutine 8 16 50.0
pod 6 6 100.0
total 38 386 9.8


line stmt bran cond sub pod time code
1             # Spectrum::CLI - a perl module for use with Spectrum's Command Line Interface
2             # Copyright (C) 1999-2003 Dave Plonka
3             #
4             # This program is free software; you can redistribute it and/or modify
5             # it under the terms of the GNU General Public License as published by
6             # the Free Software Foundation; either version 2 of the License, or
7             # (at your option) any later version.
8             #
9             # This program is distributed in the hope that it will be useful,
10             # but WITHOUT ANY WARRANTY; without even the implied warranty of
11             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12             # GNU General Public License for more details.
13             #
14             # You should have received a copy of the GNU General Public License
15             # along with this program; if not, write to the Free Software
16             # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17              
18             # $Id: CLI.pm,v 1.16 2003/12/16 15:50:04 dplonka Exp $
19             # Dave Plonka
20              
21             package Spectrum::CLI;
22              
23 1     1   597 use strict;
  1         2  
  1         30  
24 1     1   4 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);
  1         1  
  1         66  
25 1     1   845 use IO::File;
  1         9591  
  1         133  
26 1     1   835 use IO::Socket;
  1         17516  
  1         3  
27 1     1   1279 use IPC::Open2;
  1         3499  
  1         48  
28 1     1   6 use File::Basename;
  1         2  
  1         105  
29 1     1   844 use POSIX qw(setsid);
  1         7574  
  1         7  
30 1     1   1753 use Sys::Hostname;
  1         1114  
  1         2272  
31              
32             require Exporter;
33             require AutoLoader;
34              
35             @Spectrum::CLI::ISA = qw(Exporter AutoLoader);
36             # convert the RCS revision to a reasonable Exporter VERSION:
37             '$Revision: 1.16 $' =~ m/(\d+)\.(\d+)/ && (( $Spectrum::CLI::VERSION ) = sprintf("%d.%03d", $1, $2));
38              
39             sub new {
40 0     0 1   my $class = shift;
41 0           my $self = {};
42 0           $self->{verbose} = 0;
43 0           $ENV{CLISESSID} = $$; # default
44 0           $ENV{CLIMNAMEWIDTH} = 1024; # default
45 0           my $daemon; # absolute path to VnmShd
46 0           while (@_) {
47 0 0         if ('HASH' eq ref($_[0])) { # a hashref
48 0   0       $self->{verbose} = $_[0]->{verbose} || $_[0]->{Verbose};
49 0           $self->{Verbose} = $_[0]->{Verbose};
50 0 0         if (defined $_[0]->{CLISESSID}) {
51 0           $ENV{CLISESSID} = $_[0]->{CLISESSID}
52             }
53 0 0         if ($_[0]->{CLIMNAMEWIDTH}) {
54 0           $ENV{CLIMNAMEWIDTH} = $_[0]->{CLIMNAMEWIDTH}
55             }
56 0 0         if ($_[0]->{VNMSHRCPATH}) {
57 0           $ENV{VNMSHRCPATH} = $_[0]->{VNMSHRCPATH}
58             }
59 0 0         if ($_[0]->{localhostname}) {
60 0           $self->{localhostname} = $_[0]->{localhostname}
61             }
62 0 0         if ($_[0]->{timeout}) {
63 0           $self->{timeout} = $_[0]->{timeout}
64             }
65             shift
66 0           } else { # scalar
67 0 0         if (m|^/|) { # it's an absolute path
68 0           $self->{dir} = shift
69             } else { # it's the VNM name
70 0           $self->{vnm} = shift
71             }
72             }
73             }
74 0 0         if ('' eq $self->{dir}) {
75 0 0         if ($ENV{SPEC_ROOT}) {
    0          
76 0           $self->{dir} = "$ENV{SPEC_ROOT}/vnmsh"
77             } elsif ($ENV{SPECROOT}) {
78 0           $self->{dir} = "$ENV{SPECROOT}/vnmsh"
79             }
80             }
81              
82 0 0         if ('' eq $self->{dir}) {
83 0 0         warn "must set SPEC_ROOT/SPECROOT environment variable or pass full path to vnmsh directory to \"new\" method!\n" if $self->{verbose};
84             return undef
85 0           }
86              
87 0 0 0       if ('' eq $ENV{VNMSHRCPATH} && -f "$self->{dir}/.vnmshrc") {
88 0           $ENV{VNMSHRCPATH} = "$self->{dir}/.vnmshrc"
89             }
90              
91             # discover which vnmsh commands are available:
92 0           my $entry;
93 0           my $dir = $self->{dir}; # kludge since perl doesn't like this in a glob
94 0           foreach $entry (<$dir/*>) {
95 0           my $cmd = basename $entry;
96 0           $cmd =~ s/.exe$//;
97 0 0         if ($cmd =~ m;VnmShd$;) {
98 0           $daemon = $entry;
99             }
100 0 0         next if $cmd =~ m;(Vnm|stop)Shd$;; # skip these executables
101 0 0 0       push(@{$self->{command}}, $cmd) if (-f $entry && -x _)
  0            
102              
103             }
104 0 0         warn "valid vnmsh commands: @{$self->{command}}\n" if $self->{Verbose};
  0            
105              
106             # We don't want the vnmsh/connect command to launch VnmShd for us...
107             # This is because (as of this writing - Spectrum 5.0r1) the vnmsh/connect
108             # command not setting the close-on-exec flag before fork(2)ing/exec(2)ing
109             # a VnmShd process. This causes our perl read from pipe to hang (since
110             # the other end of the pipe is still open for writing until VnmShd
111             # terminates - ugh.)
112             #
113             # Unfortunately we now have a race condition here: If VnmShd shuts down
114             # (e.g. is stopped intentionally) between when we start it and when we
115             # run vnmsh/connect (which will restart VnmShd), we could hang
116             # indefinitely in the read on the pipe from vnmsh/connect.
117             # Then, when this new VnmShd terminates (e.g. is stopped intentionally)
118             # read(2) will return with EOF, and we'll think we have a successful
119             # connection - even though VnmShd is no longer running.
120             # Ugh.
121             #
122             # All is not lost though - that would be an unlikely chain of events.
123             # It should be able to be avoided by starting VnmShd "manually" before
124             # any Spectrum::CLI scripts are run, and then shutting VnmShd down until
125             # you know that no Spectrum::CLI scripts are running.
126             #
127             # This could all be fixed by fcntl(fd, F_GETFD, 0) followed by
128             # fcntl(fd, F_SETFD, FD_CLOEXEC | flags) in vnmsh/connect.
129             #
130             # In hindsight it probably wasn't such a good idea for Spectrum to have
131             # vnmsh/connect launch VnmShd when its not already running. This runs
132             # counter to the Unix permissions mechanism since any old Spectrum user
133             # could be running VnmShd and doing SSAPI I/O on behalf of other users.
134             # (IMHO, VnmShd should be run by the Spectrum install user.)
135              
136             # { Check to see if VnmShd is running (and start it if it's not):
137              
138             # determine which port VnmShd is to use (by parsing the config file):
139             {
140 0           my $fh = new IO::File "<$ENV{VNMSHRCPATH}";
  0            
141 0 0         if (!ref($fh)) {
142 0 0         warn "open \"$ENV{VNMSHRCPATH}\", \"r\": $!\n" if $self->{verbose}
143             } else {
144 0           while (<$fh>) {
145 0 0         next if (m/^\s*#/); # skip comments
146 0 0         if (m/\s*vsh_tcp_port\s*=\s*(0x([0-9a-f]+)|(\d+))/i) {
147 0 0         $self->{port} = $2? hex($2) : $3
148             }
149             }
150             }
151 0           undef $fh;
152             }
153              
154 0 0         my $s = new IO::Socket::INET (Proto => 'tcp',
    0          
155             PeerAddr =>
156             ($self->{localhostname}?
157             $self->{localhostname} : hostname),
158             PeerPort =>
159             ($self->{port}? $self->{port}: 7777));
160 0 0         if (!ref($s)) {
161 0 0         warn "no VnmShd (yet)...\n" if $self->{Verbose};
162             # start VnmShd
163 0 0         if ('' eq $daemon) {
164 0 0         warn("$self->{dir}/VnmShd not found!\n") if $self->{verbose};
165             return undef
166 0           }
167 0           my @command = ($daemon);
168             # FIXME stat(2)/access(2) VnmShd
169 0 0         warn "fork, exec @command...\n" if $self->{Verbose};
170 0           my $pid = fork();
171 0 0         die "fork: $!\n" if (-1 == $pid);
172 0 0         if (0 == $pid) { # child
173             # dissociate child from parent
174 0 0         setsid() or die "setsid: $!\n";
175             # Hmm... perhaps I should daemonize properly by chdir(2)ing,
176             # and association STDIN, STDOUT, and STDERR with "/dev/null".
177             # For the time being I'd like to see error messages (if any), so
178             # we'll leave it alone. Besides this is no sloppier than how
179             # vnmsh/connect leaves things (when it launches VnmShd).
180 0           exec @command;
181 0           die "exec \"@command\": $!\n"
182             } else { # parent
183 0 0         warn "waiting a bit...\n" if $self->{Verbose};
184 0 0         sleep($self->{timeout}? $self->{timeout} : 5)
185             }
186             }
187 0           undef $s;
188              
189             # }
190              
191 0           my @command = ('connect');
192 0 0         push(@command, $self->{vnm}) if $self->{vnm};
193              
194 0 0         warn("dir: $self->{dir}\n" .
195             "CLISESSID=$ENV{CLISESSID}\n" .
196             "CLIMNAMEWIDTH=$ENV{CLIMNAMEWIDTH}\n" .
197             "VNMSHRCPATH=$ENV{VNMSHRCPATH}\n") if $self->{Verbose};
198              
199 0 0         warn "@command...\n" if $self->{Verbose};
200 0           my $fh = new IO::File "$self->{dir}/@command 2>&1 |";
201 0 0         if (!ref($fh)) {
202 0 0         warn("failed to pipe from \"@command\": $!\n") if $self->{verbose};
203             return undef
204 0           }
205 0           @{$self->{results}} = <$fh>;
  0            
206 0           $fh->close;
207 0           $self->{status} = int($?/256);
208 0 0         if (0 != $self->{status}) {
209 0 0         warn("\"@command\" failed - exit status: ", $self->{status}, "\n") if $self->{verbose};
210             return undef
211 0           }
212              
213 0           bless($self, $class)
214             }
215              
216             sub dir {
217 0     0 1   my $self = shift;
218 0 0         die unless ref($self);
219 0 0         if (@_) {
220 0           $self->{dir} = shift
221             }
222 0           $self->{dir}
223             }
224              
225             sub verbose {
226 0     0 1   my $self = shift;
227 0 0         die unless ref($self);
228 0 0         if (@_) {
229 0           $self->{verbose} = shift
230             } else {
231 0           $self->{verbose}
232             }
233             }
234              
235             sub Verbose {
236 0     0 1   my $self = shift;
237 0 0         die unless ref($self);
238 0 0         if (@_) {
239 0           $self->{Verbose} = shift
240             } else {
241 0           $self->{Verbose}
242             }
243             }
244              
245             sub results {
246 0     0 1   my $self = shift;
247 0 0         die unless ref($self);
248 0           @{$self->{results}}
  0            
249             }
250              
251             sub status {
252 0     0 1   my $self = shift;
253 0 0         die unless ref($self);
254 0           $self->{status}
255             }
256              
257             sub AUTOLOAD {
258 0     0     my $self = shift;
259 0           my $call = $Spectrum::CLI::AUTOLOAD;
260 0           $call =~ s/^.*:://;
261              
262             # There seems to be some problems with buffered I/O, let's flush it here
263             # before we fork(2) any processes, just to be safe.
264 0           flush STDOUT;
265 0           flush STDERR;
266              
267             # There seems to be an intermittent problem with SIGPIPE being delivered.
268             # Let's try to avoid that issue:
269 0           my $saved_pipe_handler = $SIG{PIPE};
270 0           $SIG{PIPE} = 'IGNORE';
271              
272             # { save the hashref, if one was passed, and remove it from the argument list
273 0           my $hashref;
274             my $n;
275 0           for ($n=0; $n <= @_; $n++) {
276 0 0         if ('HASH' eq ref($_[$n])) {
277 0           $hashref = splice(@_, $n, 1)
278             }
279             }
280             # }
281              
282 0           my @command = (split(m/_/, $call), @_);
283              
284 0 0         if (!grep { $command[0] eq $_ } @{$self->{command}}) {
  0            
  0            
285 0           die "Can't locate object method \"$call\""
286             }
287              
288 0 0         warn "@command...\n" if $self->{Verbose};
289              
290 0           my $display = 1; # default to "display" commands such as "show" and "seek"
291 0 0 0       if ('fetchall_arrayref' eq $command[0]) {
    0          
292 0           $command[0] = 'show'
293             } elsif ('show' ne $command[0] && 'seek' ne $command[0]) {
294 0           $display = 0
295             }
296              
297 0           my $fhin = new IO::File;
298 0 0         die "IO::File->new failed: $!\n" unless ref($fhin);
299 0           my $fh = new IO::File;
300 0 0         die "IO::File->new failed: $!\n" unless ref($fh);
301 0           my $pid = open2($fh, $fhin, $self->dir . "/@command 2>&1");
302 0           print $fhin "y\n"; # answer 'y'es to "destroy model: are you sure ?", etc.
303              
304 0 0         if (!$display) {
305 0           @{$self->{results}} = <$fh>;
  0            
306 0           waitpid($pid, 0);
307 0           $self->{status} = int($?/256);
308 0 0 0       if (0 != $self->{status} && $self->{verbose}) {
309 0           warn @{$self->{results}}
  0            
310             }
311 0           $fh->close;
312 0           $fhin->close;
313 0           $SIG{PIPE} = $saved_pipe_handler;
314 0 0         return wantarray? ($self->{status}, @{$self->{results}}) : !$self->{status}
  0            
315             }
316              
317             # { parse the output of display commands:
318 0           my @results;
319 0           my $headings = <$fh>;
320 0           @{$self->{results}} = ($headings);
  0            
321 0           chomp $headings;
322 0           my(@headings, @lengths);
323 0           while ($headings) {
324 0           $headings =~ s/^\s*(\S+)\s*//;
325 0           push(@headings, $1);
326 0           push(@lengths, length($&))
327             }
328 0           while (<$fh>) {
329 0           push(@{$self->{results}}, $_);
  0            
330 0           chomp;
331 0           my $index = 1+$#results;
332 0           my $start = 0; # position where this column starts
333 0           my $lcv;
334 0           for ($lcv = 0; $headings[$lcv]; $lcv++) {
335 0           my $val;
336 0 0         if ($lcv < $#headings) {
337 0           $val = substr($_, $start, $lengths[$lcv]);
338 0           $start += $lengths[$lcv]
339             } else {
340 0           $val = substr($_, $start)
341             }
342 0 0         if ($hashref) {
343             # skip columns that the caller didn't request...
344 0 0         next unless $hashref->{$headings[$lcv]}
345             }
346 0           $val =~ s/\s+$//;
347 0           $results[$index]{$headings[$lcv]} = $val
348             }
349             }
350             # }
351 0           waitpid($pid, 0);
352 0           $self->{status} = int($?/256);
353 0           $fh->close;
354 0           $fhin->close;
355 0 0 0       if (0 != $self->{status} && $self->{verbose}) {
356 0           warn @{$self->{results}}
  0            
357             }
358              
359 0           $SIG{PIPE} = $saved_pipe_handler;
360 0 0         wantarray? @results : \@results
361             }
362              
363             sub DESTROY {
364 0     0     my $self = shift;
365 0 0         die unless ref $self;
366 0 0         if ( $^O =~ /^(ms)?(dos|win(32|nt)?)/i ) {
367 0           system($self->dir . "/disconnect >null 2>&1");
368             } else {
369 0           system($self->dir . "/disconnect >/dev/null 2>&1");
370             }
371             }
372              
373             1;
374             __END__