ab8d9573dd4c4f8e446472cd71737f23bd889d67
[spider.git] / perl / DXCommandmode.pm
1 #!/usr/bin/perl
2 #
3 # This module impliments the user facing command mode for a dx cluster
4 #
5 # Copyright (c) 1998 Dirk Koopman G1TLH
6 #
7 # $Id$
8
9
10 package DXCommandmode;
11
12 @ISA = qw(DXChannel);
13
14 use DXUtil;
15 use DXChannel;
16 use DXUser;
17 use DXVars;
18 use DXDebug;
19 use DXM;
20 use DXLog;
21 use CmdAlias;
22 use FileHandle;
23 use Carp;
24
25 use strict;
26 use vars qw(%Cache %cmd_cache $errstr %aliases);
27
28 %Cache = ();                  # cache of dynamically loaded routine's mod times
29 %cmd_cache = ();            # cache of short names
30 $errstr = ();                # error string from eval
31 %aliases = ();              # aliases for (parts of) commands
32
33 #
34 # obtain a new connection this is derived from dxchannel
35 #
36
37 sub new 
38 {
39   my $self = DXChannel::alloc(@_);
40   $self->{sort} = 'U';   # in absence of how to find out what sort of an object I am
41   return $self;
42 }
43
44 # this is how a a connection starts, you get a hello message and the motd with
45 # possibly some other messages asking you to set various things up if you are
46 # new (or nearly new and slacking) user.
47
48 sub start
49
50   my ($self, $line, $sort) = @_;
51   my $user = $self->{user};
52   my $call = $self->{call};
53   my $name = $user->{name};
54   my $info = DXCluster::cluster();
55   
56   $self->{name} = $name ? $name : $call;
57   $self->send($self->msg('l2',$self->{name}));
58   $self->send_file($main::motd) if (-e $main::motd);
59   $self->send("Cluster:$info");
60   $self->send($self->msg('pr', $call));
61   $self->state('prompt');                  # a bit of room for further expansion, passwords etc
62   $self->{priv} = $user->priv;
63   $self->{lang} = $user->lang;
64   $self->{pagelth} = 20;
65   $self->{priv} = 0 if $line =~ /^(ax|te)/;     # set the connection priv to 0 - can be upgraded later
66   $self->{consort} = $line;                # save the connection type
67
68   # set some necessary flags on the user if they are connecting
69   $self->{wwv} = $self->{talk} = $self->{ann} = $self->{here} = $self->{dx} = 1;
70 #  $self->prompt() if $self->{state} =~ /^prompt/o;
71   
72   # add yourself to the database
73   my $node = DXNode->get($main::mycall) or die "$main::mycall not allocated in DXNode database";
74   my $cuser = DXNodeuser->new($self, $node, $call, 0, 1);
75   $node->dxchan($self) if $call eq $main::myalias;       # send all output for mycall to myalias
76   
77   # issue a pc16 to everybody interested
78   my $nchan = DXChannel->get($main::mycall);
79   my @pc16 = DXProt::pc16($nchan, $cuser);
80   DXProt::broadcast_ak1a(@pc16);
81   Log('DXCommand', "$call connected");
82 }
83
84 #
85 # This is the normal command prompt driver
86 #
87
88 sub normal
89 {
90         my $self = shift;
91         my $cmdline = shift;
92         my @ans;
93         
94         # remove leading and trailing spaces
95         $cmdline =~ s/^\s*(.*)\s*$/$1/;
96         
97         if ($self->{state} eq 'prompt') {
98                 @ans = run_cmd($self, $cmdline) if length $cmdline;
99        
100                 if ($self->{pagelth} && @ans > $self->{pagelth}) {
101                         my $i;
102                         for ($i = $self->{pagelth}; $i-- > 0; ) {
103                                 my $line = shift @ans;
104                                 $line =~ s/\s+$//o;            # why am having to do this? 
105                                 $self->send($line);
106                         }
107                         $self->{pagedata} =  \@ans;
108                         $self->state('page');
109                         $self->send($self->msg('page', scalar @ans));
110                 } else {
111                         for (@ans) {
112                                 s/\s+$//o;                     # why ?????????
113                                 $self->send($_);
114                         }
115                 } 
116         } elsif ($self->{state} eq 'page') {
117                 my $i = $self->{pagelth};
118                 my $ref = $self->{pagedata};
119                 my $tot = @$ref;
120                 
121                 # abort if we get a line starting in with a
122                 if ($cmdline =~ /^a/io) {
123                         undef $ref;
124                         $i = 0;
125                 }
126         
127                 # send a tranche of data
128                 while ($i-- > 0 && @$ref) {
129                         my $line = shift @$ref;
130                         $line =~ s/\s+$//o;            # why am having to do this? 
131                         $self->send($line);
132                 }
133
134                 # reset state if none or else chuck out an intermediate prompt
135                 if ($ref && @$ref) {
136                         $tot -= $self->{pagelth};
137                         $self->send($self->msg('page', $tot));
138                 } else {
139                         $self->state('prompt');
140                 }
141         }
142         
143         # send a prompt only if we are in a prompt state
144         $self->prompt() if $self->{state} =~ /^prompt/o;
145 }
146
147
148 # this is the thing that runs the command, it is done like this for the 
149 # benefit of remote command execution
150 #
151
152 sub run_cmd
153 {
154   my $self = shift;
155   my $user = $self->{user};
156   my $call = $self->{call};
157   my $cmdline = shift;
158   my @ans;
159
160   # are we in stored state?
161   if ($self->{func}) {
162     my $c = qq{ \@ans = $self->{func}(\$self, \$cmdline) };
163     dbg('eval', "stored func cmd = $c\n");
164     eval  $c;
165     if ($@) {
166       return (1, "Syserr: Eval err $errstr on stored func $self->{func}");
167     }
168   } else {
169
170     # special case only \n input => " "
171 #    if ($cmdline eq " ") {
172 #         $self->prompt();
173 #         return;
174 #       }
175         
176     # strip out //
177     $cmdline =~ s|//|/|og;
178   
179     # split the command line up into parts, the first part is the command
180     my ($cmd, $args) = $cmdline =~ /^([\w\/]+)\s*(.*)/o;
181
182     if ($cmd) {
183     
184           my ($path, $fcmd);
185           
186           # alias it if possible
187           my $acmd = CmdAlias::get_cmd($cmd);
188           if ($acmd) {
189             ($cmd, $args) = "$acmd $args" =~ /^([\w\/]+)\s*(.*)/o;
190           }
191    
192       # first expand out the entry to a command
193           ($path, $fcmd) = search($main::localcmd, $cmd, "pl");
194           ($path, $fcmd) = search($main::cmd, $cmd, "pl") if !$path || !$fcmd;
195
196       my $package = find_cmd_name($path, $fcmd);
197           @ans = (0) if !$package ;
198
199       if ($package) {
200             my $c = qq{ \@ans = $package(\$self, \$args) };
201             dbg('eval', "cluster cmd = $c\n");
202             eval  $c;
203             if ($@) {
204                   @ans = (0, "Syserr: Eval err cached $package\n$@");
205         }
206           }
207         }
208   }
209         
210   if ($ans[0]) {
211     shift @ans;
212   } else {
213     shift @ans;
214         if (@ans > 0) {
215                 unshift @ans, $self->msg('e2');
216         } else {
217                 @ans = $self->msg('e1');
218         }
219   }
220   return (@ans);
221 }
222
223 #
224 # This is called from inside the main cluster processing loop and is used
225 # for despatching commands that are doing some long processing job
226 #
227 sub process
228 {
229   my $t = time;
230   my @chan = DXChannel->get_all();
231   my $chan;
232   
233   foreach $chan (@chan) {
234     next if $chan->sort ne 'U';  
235
236     # send a prompt if no activity out on this channel
237     if ($t >= $chan->t + $main::user_interval) {
238       $chan->prompt() if $chan->{state} =~ /^prompt/o;
239           $chan->t($t);
240         }
241   }
242 }
243
244 #
245 # finish up a user context
246 #
247 sub finish
248 {
249   my $self = shift;
250   my $call = $self->call;
251
252   if ($call eq $main::myalias) {   # unset the channel if it is us really
253     my $node = DXNode->get($main::mycall);
254         $node->{dxchan} = 0;
255   }
256   my $ref = DXNodeuser->get($call);
257
258   # issue a pc17 to everybody interested
259   my $nchan = DXChannel->get($main::mycall);
260   my $pc17 = $nchan->pc17($self);
261   DXProt::broadcast_ak1a($pc17);
262
263   Log('DXCommand', "$call disconnected");
264   $ref->del() if $ref;
265 }
266
267 #
268 # short cut to output a prompt
269 #
270
271 sub prompt
272 {
273   my $self = shift;
274   my $call = $self->{call};
275   $self->send($self->msg('pr', $call));
276   #DXChannel::msg($self, 'pr', $call);
277 }
278
279 # broadcast a message to all users [except those mentioned after buffer]
280 sub broadcast
281 {
282   my $pkg = shift;                # ignored
283   my $s = shift;                  # the line to be rebroadcast
284   my @except = @_;                # to all channels EXCEPT these (dxchannel refs)
285   my @list = DXChannel->get_all();   # just in case we are called from some funny object
286   my ($chan, $except);
287   
288 L: foreach $chan (@list) {
289      next if !$chan->sort eq 'U';  # only interested in user channels  
290          foreach $except (@except) {
291            next L if $except == $chan;  # ignore channels in the 'except' list
292          }
293          chan->send($s);              # send it
294   }
295 }
296
297 # gimme all the users
298 sub get_all
299 {
300   my @list = DXChannel->get_all();
301   my $ref;
302   my @out;
303   foreach $ref (@list) {
304     push @out, $ref if $ref->sort eq 'U';
305   }
306   return @out;
307 }
308
309 #
310 # search for the command in the cache of short->long form commands
311 #
312
313 sub search
314 {
315   my ($path, $short_cmd, $suffix) = @_;
316   my ($apath, $acmd);
317
318   # commands are lower case
319   $short_cmd = lc $short_cmd;
320   dbg('command', "command: $path $short_cmd\n");
321   
322   # return immediately if we have it
323   my ($apath, $acmd) = split ',', $cmd_cache{$short_cmd};
324   if ($apath && $acmd) {
325     dbg('command', "cached $short_cmd = ($apath, $acmd)\n");
326     return ($apath, $acmd);
327   }
328   
329   # if not guess
330   my @parts = split '/', $short_cmd;
331   my $dirfn;
332   my $curdir = $path;
333   my $p;
334   my $i;
335   my @lparts;
336   
337   for ($i = 0; $i < @parts; $i++) {
338     my  $p = $parts[$i];
339         opendir(D, $curdir) or confess "can't open $curdir $!";
340         my @ls = readdir D;
341         closedir D;
342         my $l;
343         foreach $l (sort @ls) {
344           next if $l =~ /^\./;
345       if ($i < $#parts) {            # we are dealing with directories
346         if ((-d "$curdir/$l") && $p eq substr($l, 0, length $p)) {
347                   dbg('command', "got dir: $curdir/$l\n");
348                   $dirfn .= "$l/";
349                   $curdir .= "/$l";
350                   last;
351                 }
352       } else {                # we are dealing with commands
353             @lparts = split /\./, $l;                  
354                 next if $lparts[$#lparts] ne $suffix;       # only look for .$suffix files
355                 if ($p eq substr($l, 0, length $p)) {
356                   pop @lparts;        #  remove the suffix
357                   $l = join '.', @lparts;
358 #                 chop $dirfn;               # remove trailing /
359                   $cmd_cache{"$short_cmd"} = join(',', ($path, "$dirfn$l"));   # cache it
360           dbg('command', "got path: $path cmd: $dirfn$l\n");
361                   return ($path, "$dirfn$l"); 
362                 }
363           }
364         }
365   }
366   return ();  
367 }  
368
369 # clear the command name cache
370 sub clear_cmd_cache
371 {
372   %cmd_cache = ();
373 }
374
375 #
376 # the persistant execution of things from the command directories
377 #
378 #
379 # This allows perl programs to call functions dynamically
380
381 # This has been nicked directly from the perlembed pages
382 #
383
384 #require Devel::Symdump;  
385
386 sub valid_package_name {
387   my($string) = @_;
388   $string =~ s/([^A-Za-z0-9\/])/sprintf("_%2x",unpack("C",$1))/eg;
389   
390   #second pass only for words starting with a digit
391   $string =~ s|/(\d)|sprintf("/_%2x",unpack("C",$1))|eg;
392         
393   #Dress it up as a real package name
394   $string =~ s/\//_/og;
395   return "Emb_" . $string;
396 }
397
398 #borrowed from Safe.pm
399 sub delete_package {
400   my $pkg = shift;
401   my ($stem, $leaf);
402         
403   no strict 'refs';
404   $pkg = "DXCommandmode::$pkg\::";    # expand to full symbol table name
405   ($stem, $leaf) = $pkg =~ m/(.*::)(\w+::)$/;
406
407   if ($stem && $leaf) {
408     my $stem_symtab = *{$stem}{HASH};
409     delete $stem_symtab->{$leaf};
410   }
411 }
412
413 # find a cmd reference
414 # this is really for use in user written stubs
415 #
416 # use the result as a symbolic reference:-
417 #
418 # no strict 'refs';
419 # @out = &$r($self, $line);
420 #
421 sub find_cmd_ref
422 {
423   my $cmd = shift;
424   my $r;
425   
426   if ($cmd) {
427   
428     # first expand out the entry to a command
429     my ($path, $fcmd) = search($main::localcmd, $cmd, "pl");
430     ($path, $fcmd) = search($main::cmd, $cmd, "pl") if !$path || !$fcmd;
431
432     # make sure it is loaded
433     $r = find_cmd_name($path, $fcmd);
434   }
435   return $r;
436 }
437
438
439 # this bit of magic finds a command in the offered directory
440 sub find_cmd_name {
441   my $path = shift;
442   my $cmdname = shift;
443   my $package = valid_package_name($cmdname);
444   my $filename = "$path/$cmdname.pl";
445   my $mtime = -M $filename;
446   
447   # return if we can't find it
448   $errstr = undef;
449   if (undef $mtime) {
450     $errstr = DXM::msg('e1');
451         return undef;
452   }
453   
454   if(defined $Cache{$package}{mtime} && $Cache{$package}{mtime } <= $mtime) {
455     #we have compiled this subroutine already,
456         #it has not been updated on disk, nothing left to do
457         #print STDERR "already compiled $package->handler\n";
458         ;
459   } else {
460         my $fh = new FileHandle;
461         if (!open $fh, $filename) {
462           $errstr = "Syserr: can't open '$filename' $!";
463           return undef;
464         };
465         local $/ = undef;
466         my $sub = <$fh>;
467         close $fh;
468                 
469     #wrap the code into a subroutine inside our unique package
470         my $eval = qq{ 
471         sub $package 
472         { 
473           $sub 
474         } };
475         
476         if (isdbg('eval')) {
477           my @list = split /\n/, $eval;
478           my $line;
479           for (@list) {
480             dbg('eval', $_, "\n");
481           }
482         }
483         
484         {
485           #hide our variables within this block
486           my($filename,$mtime,$package,$sub);
487           eval $eval;
488         }
489         
490         if ($@) {
491           print "\$\@ = $@";
492           $errstr = $@;
493           delete_package($package);
494         } else {
495       #cache it unless we're cleaning out each time
496           $Cache{$package}{mtime} = $mtime;
497         }
498   }
499   
500   #print Devel::Symdump->rnew($package)->as_string, $/;
501   $package = "DXCommandmode::$package" if $package;
502   $package = undef if $errstr;
503   return $package;
504 }
505
506 1;
507 __END__