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