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