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