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