staging commt for badword and badip
[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 #
8
9
10 package DXCommandmode;
11
12 #use POSIX;
13
14 @ISA = qw(DXChannel);
15
16 use 5.10.1;
17
18 use POSIX qw(:math_h);
19 use DXUtil;
20 use DXChannel;
21 use DXUser;
22 use DXVars;
23 use DXDebug;
24 use DXM;
25 use DXLog;
26 use DXLogPrint;
27 use DXBearing;
28 use CmdAlias;
29 use Filter;
30 use Minimuf;
31 use DXDb;
32 use AnnTalk;
33 use WCY;
34 use Sun;
35 use Internet;
36 use Script;
37 use QSL;
38 use DB_File;
39 use VE7CC;
40 use DXXml;
41 use AsyncMsg;
42 use JSON;
43 use Time::HiRes qw(gettimeofday tv_interval);
44
45 use Mojo::IOLoop;
46 use DXSubprocess;
47 use Mojo::UserAgent;
48 use DXCIDR;
49
50 use strict;
51 use vars qw(%Cache %cmd_cache $errstr %aliases $scriptbase %nothereslug
52         $maxbadcount $msgpolltime $default_pagelth $cmdimportdir $users $maxusers);
53
54 %Cache = ();                                    # cache of dynamically loaded routine's mod times
55 %cmd_cache = ();                                # cache of short names
56 $errstr = ();                                   # error string from eval
57 %aliases = ();                                  # aliases for (parts of) commands
58 $scriptbase = "$main::root/scripts"; # the place where all users start scripts go
59 $maxbadcount = 3;                               # no of bad words allowed before disconnection
60 $msgpolltime = 3600;                    # the time between polls for new messages 
61 $cmdimportdir = "$main::root/cmd_import"; # the base directory for importing command scripts 
62                                           # this does not exist as default, you need to create it manually
63 $users = 0;                                       # no of users on this node currently
64 $maxusers = 0;                            # max no users on this node for this run
65
66 #
67 # obtain a new connection this is derived from dxchannel
68 #
69
70 sub new 
71 {
72         my $self = DXChannel::alloc(@_);
73
74         # routing, this must go out here to prevent race condx
75         my $pkg = shift;
76         my $call = shift;
77 #       my @rout = $main::routeroot->add_user($call, Route::here(1));
78         DXProt::_add_thingy($main::routeroot, [$call, 0, 0, 1, undef, undef, $self->hostname], );
79
80         # ALWAYS output the user
81         my $ref = Route::User::get($call);
82         if ($ref) {
83                 $main::me->route_pc16($main::mycall, undef, $main::routeroot, $ref);
84                 $main::me->route_pc92a($main::mycall, undef, $main::routeroot, $ref) unless $DXProt::pc92_slug_changes;
85         }
86
87         return $self;
88 }
89
90 # this is how a a connection starts, you get a hello message and the motd with
91 # possibly some other messages asking you to set various things up if you are
92 # new (or nearly new and slacking) user.
93
94 sub start
95
96         my ($self, $line, $sort) = @_;
97         my $user = $self->{user};
98         my $call = $self->{call};
99         my $name = $user->{name};
100         
101         # log it
102         my $host = $self->{conn}->peerhost;
103         $host ||= "AGW Port #$self->{conn}->{agwport}" if exists $self->{conn}->{agwport};
104         $host ||= "unknown";
105         $self->{hostname} = $host;
106
107         $self->{name} = $name ? $name : $call;
108         $self->send($self->msg('l2',$self->{name}));
109         $self->send("Capabilities: ve7cc rbn");
110         $self->state('prompt');         # a bit of room for further expansion, passwords etc
111         $self->{priv} = $user->priv || 0;
112         $self->{lang} = $user->lang || $main::lang || 'en';
113         my $pagelth = $user->pagelth;
114         $pagelth = $default_pagelth unless defined $pagelth;
115         $self->{pagelth} = $pagelth;
116         ($self->{width}) = $line =~ /\s*width=(\d+)/; $line =~ s/\s*width=\d+//;
117         $self->{enhanced} = $line =~ /\s+enhanced/; $line =~ s/\s*enhanced//;
118         if ($line =~ /host=/) {
119                 my ($h) = $line =~ /host=(\d+\.\d+\.\d+\.\d+)/;
120                 $line =~ s/\s*host=\d+\.\d+\.\d+\.\d+// if $h;
121                 unless ($h) {
122                         ($h) = $line =~ /host=([\da..fA..F:]+)/;
123                         $line =~ s/\s*host=[\da..fA..F:]+// if $h;
124                 }
125                 $self->{hostname} = $h if $h;
126         }
127         $self->{width} = 80 unless $self->{width} && $self->{width} > 80;
128         $self->{consort} = $line;       # save the connection type
129
130         LogDbg('DXCommand', "$call connected from $self->{hostname} cols $self->{width}" . ($self->{enhanced}?" enhanced":''));
131
132         # set some necessary flags on the user if they are connecting
133         $self->{beep} = $user->wantbeep;
134         $self->{ann} = $user->wantann;
135         $self->{wwv} = $user->wantwwv;
136         $self->{wcy} = $user->wantwcy;
137         $self->{talk} = $user->wanttalk;
138         $self->{wx} = $user->wantwx;
139         $self->{dx} = $user->wantdx;
140         $self->{logininfo} = $user->wantlogininfo;
141         $self->{ann_talk} = $user->wantann_talk;
142         $self->{wantrbn} = $user->wantrbn;
143         $self->{here} = 1;
144         $self->{prompt} = $user->prompt if $user->prompt;
145         $self->{lastmsgpoll} = 0;
146         $self->{rbnseeme} = $user->rbnseeme;
147         RBN::add_seeme($call) if $self->{rbnseeme};
148         
149         # sort out new dx spot stuff
150         $user->wantdxcq(0) unless defined $user->{wantdxcq};
151         $user->wantdxitu(0) unless defined $user->{wantdxitu};  
152         $user->wantusstate(0) unless defined $user->{wantusstate};
153         
154         # sort out registration
155         if ($main::reqreg == 2) {
156                 $self->{registered} = !$user->registered;
157         } else {
158                 $self->{registered} = $user->registered;
159         } 
160
161         # establish slug queue, if required
162         $self->{sluggedpcs} = [];
163         $self->{isslugged} = $DXProt::pc92_slug_changes + $DXProt::last_pc92_slug + 5 if $DXProt::pc92_slug_changes;
164         $self->{isslugged} = 0 if $self->{priv} || $user->registered || ($user->homenode && $user->homenode eq $main::mycall);
165
166         # send the relevant MOTD
167         $self->send_motd;
168
169         # sort out privilege reduction
170         $self->{priv} = 0 unless $self->{hostname} eq '127.0.0.1' || $self->conn->peerhost eq '127.0.0.1' || $self->{hostname} eq '::1' || $self->conn->{usedpasswd};
171
172         # get the filters
173         my $nossid = $call;
174         $nossid =~ s/-\d+$//;
175         
176         $self->{spotsfilter} = Filter::read_in('spots', $call, 0) 
177                 || Filter::read_in('spots', $nossid, 0)
178                         || Filter::read_in('spots', 'user_default', 0);
179         $self->{wwvfilter} = Filter::read_in('wwv', $call, 0) 
180                 || Filter::read_in('wwv', $nossid, 0) 
181                         || Filter::read_in('wwv', 'user_default', 0);
182         $self->{wcyfilter} = Filter::read_in('wcy', $call, 0) 
183                 || Filter::read_in('wcy', $nossid, 0) 
184                         || Filter::read_in('wcy', 'user_default', 0);
185         $self->{annfilter} = Filter::read_in('ann', $call, 0) 
186                 || Filter::read_in('ann', $nossid, 0) 
187                         || Filter::read_in('ann', 'user_default', 0) ;
188         $self->{rbnfilter} = Filter::read_in('rbn', $call, 0) 
189                 || Filter::read_in('rbn', $nossid, 0)
190                 || Filter::read_in('rbn', 'user_default', 0);
191         
192         # clean up qra locators
193         my $qra = $user->qra;
194         $qra = undef if ($qra && !DXBearing::is_qra($qra));
195         unless ($qra) {
196                 my $lat = $user->lat;
197                 my $long = $user->long;
198                 $user->qra(DXBearing::lltoqra($lat, $long)) if (defined $lat && defined $long);  
199         }
200
201         # decide on echo
202         my $echo = $user->wantecho;
203         unless ($echo) {
204                 $self->send_now('E', "0");
205                 $self->send($self->msg('echow'));
206                 $self->conn->echo($echo) if $self->conn->can('echo');
207         }
208         
209         $self->tell_login('loginu');
210         $self->tell_buddies('loginb');
211
212         # is this a bad ip address?
213         if (is_ipaddr($self->{hostname})) {
214                 $self->{badip} = DXCIDR::find($self->{hostname});
215         }
216         
217         # do we need to send a forward/opernam?
218         my $lastoper = $user->lastoper || 0;
219         my $homenode = $user->homenode || ""; 
220         if ($homenode eq $main::mycall && $main::systime >= $lastoper + $DXUser::lastoperinterval) {
221                 run_cmd($main::me, "forward/opernam $call");
222                 $user->lastoper($main::systime + ((int rand(10)) * 86400));
223         }
224
225         # run a script send the output to the punter
226         my $script = new Script(lc $call) || new Script('user_default');
227         $script->run($self) if $script;
228
229         # send cluster info
230         $self->send($self->run_cmd("show/cluster"));
231
232         # send prompts for qth, name and things
233         $self->send($self->msg('namee1')) if !$user->name;
234         $self->send($self->msg('qthe1')) if !$user->qth;
235         $self->send($self->msg('qll')) if !$user->qra || (!$user->lat && !$user->long);
236         $self->send($self->msg('hnodee1')) if !$user->qth;
237         $self->send($self->msg('m9')) if DXMsg::for_me($call);
238
239         # send out any buddy messages for other people that are online
240         foreach my $call (@{$user->buddies}) {
241                 my $ref = Route::User::get($call);
242                 if ($ref) {
243                         foreach my $node ($ref->parents) {
244                                 $self->send($self->msg($node eq $main::mycall ? 'loginb' : 'loginbn', $call, $node));
245                         } 
246                 }
247         }
248
249         $self->lastmsgpoll($main::systime);
250         $self->prompt;
251 }
252
253 #
254 # This is the normal command prompt driver
255 #
256
257 sub normal
258 {
259         my $self = shift;
260         my $cmdline = shift;
261         my @ans;
262         my @bad;
263
264         # save this for them's that need it
265         my $rawline = $cmdline;
266         
267         # remove leading and trailing spaces
268         $cmdline =~ s/^\s*(.*)\s*$/$1/;
269         
270         if ($self->{state} eq 'page') {
271                 my $i = $self->{pagelth}-5;
272                 my $ref = $self->{pagedata};
273                 my $tot = @$ref;
274                 
275                 # abort if we get a line starting in with a
276                 if ($cmdline =~ /^a/io) {
277                         undef $ref;
278                         $i = 0;
279                 }
280         
281                 # send a tranche of data
282                 for (; $i > 0 && @$ref; --$i) {
283                         my $line = shift @$ref;
284                         $line =~ s/\s+$//o;     # why am having to do this? 
285                         $self->send($line);
286                 }
287                 
288                 # reset state if none or else chuck out an intermediate prompt
289                 if ($ref && @$ref) {
290                         $tot -= $self->{pagelth};
291                         $self->send($self->msg('page', $tot));
292                 } else {
293                         $self->state('prompt');
294                 }
295         } elsif ($self->{state} eq 'sysop') {
296                 my $passwd = $self->{user}->passwd;
297                 if ($passwd) {
298                         my @pw = grep {$_ !~ /\s/} split //, $passwd;
299                         my @l = @{$self->{passwd}};
300                         my $str = "$pw[$l[0]].*$pw[$l[1]].*$pw[$l[2]].*$pw[$l[3]].*$pw[$l[4]]";
301                         if ($cmdline =~ /$str/) {
302                                 $self->{priv} = $self->{user}->priv;
303                         } else {
304                                 $self->send($self->msg('sorry'));
305                         }
306                 } else {
307                         $self->send($self->msg('sorry'));
308                 }
309                 $self->state('prompt');
310         } elsif ($self->{state} eq 'passwd') {
311                 my $passwd = $self->{user}->passwd;
312                 if ($passwd && $cmdline eq $passwd) {
313                         $self->send($self->msg('pw1'));
314                         $self->state('passwd1');
315                 } else {
316                         $self->conn->{echo} = $self->conn->{decho};
317                         delete $self->conn->{decho};
318                         $self->send($self->msg('sorry'));
319                         $self->state('prompt');
320                 }
321         } elsif ($self->{state} eq 'passwd1') {
322                 $self->{passwd} = $cmdline;
323                 $self->send($self->msg('pw2'));
324                 $self->state('passwd2');
325         } elsif ($self->{state} eq 'passwd2') {
326                 if ($cmdline eq $self->{passwd}) {
327                         $self->{user}->passwd($cmdline);
328                         $self->send($self->msg('pw3'));
329                 } else {
330                         $self->send($self->msg('pw4'));
331                 }
332                 $self->conn->{echo} = $self->conn->{decho};
333                 delete $self->conn->{decho};
334                 $self->state('prompt');
335         } elsif ($self->{state} eq 'talk' || $self->{state} eq 'chat') {
336                 if ($cmdline =~ m{^(?:/EX|/ABORT)}i) {
337                         for (@{$self->{talklist}}) {
338                                 if ($self->{state} eq 'talk') {
339                                         $self->send_talks($_,  $self->msg('talkend'));
340                                 } else {
341                                         $self->local_send('C', $self->msg('chatend', $_));
342                                 }
343                         }
344                         $self->state('prompt');
345                         delete $self->{talklist};
346                 } elsif ($cmdline =~ m|^/+\w+|) {
347                         $cmdline =~ s|^/||;
348                         my $sendit = $cmdline =~ s|^/+||;
349                         if (@bad = BadWords::check($cmdline)) {
350                                 $self->badcount(($self->badcount||0) + @bad);
351                                 LogDbg('DXCommand', "$self->{call} swore: '$cmdline' with badwords: '" . join(',', @bad) . "'");
352                         } else {
353                                 my @in = $self->run_cmd($cmdline);
354                                 $self->send_ans(@in);
355                                 if ($sendit && $self->{talklist} && @{$self->{talklist}}) {
356                                         foreach my $l (@in) {
357                                                 for (@{$self->{talklist}}) {
358                                                         if ($self->{state} eq 'talk') {
359                                                                 $self->send_talks($_, $l);
360                                                         } else {
361                                                                 send_chats($self, $_, $l)
362                                                         }
363                                                 }
364                                         }
365                                 }
366                         }
367                         $self->send($self->{state} eq 'talk' ? $self->talk_prompt : $self->chat_prompt);
368                 } elsif ($self->{talklist} && @{$self->{talklist}}) {
369                         # send what has been said to whoever is in this person's talk list
370                         if (@bad = BadWords::check($cmdline)) {
371                                 $self->badcount(($self->badcount||0) + @bad);
372                                 LogDbg('DXCommand', "$self->{call} swore: '$cmdline' with badwords: '" . join(',', @bad) . "'");
373                         } else {
374                                 for (@{$self->{talklist}}) {
375                                         if ($self->{state} eq 'talk') {
376                                                 $self->send_talks($_, $rawline);
377                                         } else {
378                                                 send_chats($self, $_, $rawline);
379                                         }
380                                 }
381                         }
382                         $self->send($self->talk_prompt) if $self->{state} eq 'talk';
383                         $self->send($self->chat_prompt) if $self->{state} eq 'chat';
384                 } else {
385                         # for safety
386                         $self->state('prompt');
387                 }
388         } elsif (my $func = $self->{func}) {
389                 no strict 'refs';
390                 my @ans;
391                 if (ref $self->{edit}) {
392                         eval { @ans = $self->{edit}->$func($self, $rawline)};
393                 } else {
394                         eval {  @ans = &{$self->{func}}($self, $rawline) };
395                 }
396                 if ($@) {
397                         $self->send_ans("Syserr: on stored func $self->{func}", $@);
398                         delete $self->{func};
399                         $self->state('prompt');
400                         undef $@;
401                 }
402                 $self->send_ans(@ans);
403         } else {
404 #               if (@bad = BadWords::check($cmdline)) {
405 #                       $self->badcount(($self->badcount||0) + @bad);
406 #                       LogDbg('DXCommand', "$self->{call} swore: '$cmdline' with badwords: '" . join(',', @bad) . "'");
407 #               } else {
408                         $self->send_ans(run_cmd($self, $cmdline));
409 #               }
410         } 
411
412         # check for excessive swearing
413         if ($maxbadcount && $self->{badcount} && $self->{badcount} >= $maxbadcount) {
414                 LogDbg('DXCommand', "$self->{call} logged out for excessive swearing");
415                 $self->disconnect;
416                 return;
417         }
418
419         # send a prompt only if we are in a prompt state
420         $self->prompt() if $self->{state} =~ /^prompt/o;
421 }
422
423 # send out the talk messages taking into account vias and connectivity
424 sub send_talks
425 {
426         my ($self, $ent, $line) = @_;
427         
428         my ($to, $via) = $ent =~ /(\S+)>(\S+)/;
429         $to = $ent unless $to;
430         my $call = $via && $via ne '*' ? $via : $to;
431         my $clref = Route::get($call);
432         my $dxchan = $clref->dxchan if $clref;
433         if ($dxchan) {
434                 $dxchan->talk($self->{call}, $to, undef, $line);
435         } else {
436                 $self->send($self->msg('disc2', $via ? $via : $to));
437                 my @l = grep { $_ ne $ent } @{$self->{talklist}};
438                 if (@l) {
439                         $self->{talklist} = \@l;
440                 } else {
441                         delete $self->{talklist};
442                         $self->state('prompt');
443                 }
444         }
445 }
446
447 sub send_chats
448 {
449         my $self = shift;
450         my $target = shift;
451         my $text = shift;
452
453         my $msgid = DXProt::nextchatmsgid();
454         $text = "#$msgid $text";
455         $main::me->normal(DXProt::pc93($target, $self->{call}, undef, $text));
456 }
457
458 sub special_prompt
459 {
460         my $self = shift;
461         my $prompt = shift;
462         my @call;
463         for (@{$self->{talklist}}) {
464                 my ($to, $via) = /(\S+)>(\S+)/;
465                 $to = $_ unless $to;
466                 push @call, $to;
467         }
468         return $self->msg($prompt, join(',', @call));
469 }
470
471 sub talk_prompt
472 {
473         my $self = shift;
474         return $self->special_prompt('talkprompt');
475 }
476
477 sub chat_prompt
478 {
479         my $self = shift;
480         return $self->special_prompt('chatprompt');
481 }
482
483 #
484 # send a load of stuff to a command user with page prompting
485 # and stuff
486 #
487
488 sub send_ans
489 {
490         my $self = shift;
491         
492         if ($self->{pagelth} && @_ > $self->{pagelth}) {
493                 my $i;
494                 for ($i = $self->{pagelth}; $i-- > 0; ) {
495                         my $line = shift @_;
496                         $line =~ s/\s+$//o;     # why am having to do this? 
497                         $self->send($line);
498                 }
499                 $self->{pagedata} =  [ @_ ];
500                 $self->state('page');
501                 $self->send($self->msg('page', scalar @_));
502         } else {
503                 for (@_) {
504                         if (defined $_) {
505                                 $self->send($_);
506                         } else {
507                                 $self->send('');
508                         }
509                 }
510         } 
511 }
512
513
514 # this is the thing that preps for running the command, it is done like this for the 
515 # benefit of remote command execution
516 #
517
518 sub run_cmd
519 {
520         my $self = shift;
521         my $user = $self->{user};
522         my $call = $self->{call};
523         my $cmdline = shift;
524         my @ans;
525         
526         return () if length $cmdline == 0;
527         
528         # split the command line up into parts, the first part is the command
529         my ($cmd, $args) = split /\s+/, $cmdline, 2;
530         $args = "" unless defined $args;
531                 
532         if ($cmd) {
533
534                 # check cmd
535                 if ($cmd =~ m|^/| || $cmd =~ m|[^-?\w/]|) {
536                         LogDbg('DXCommand', "cmd: $self->{call} - invalid characters in '$cmd'");
537                         return $self->_error_out('e1');
538                 }
539
540                 # strip out // on command only
541                 $cmd =~ s|//|/|g;
542                                         
543                 my ($path, $fcmd);
544                         
545                 dbg("cmd: $cmd") if isdbg('command');
546                         
547                 # alias it if possible
548                 my $acmd = CmdAlias::get_cmd($cmd);
549                 if ($acmd) {
550                         ($cmd, $args) = split /\s+/, "$acmd $args", 2;
551                         $args = "" unless defined $args;
552                         dbg("cmd: aliased $cmd $args") if isdbg('command');
553                 }
554                         
555                 # first expand out the entry to a command
556                 ($path, $fcmd) = search($main::localcmd, $cmd, "pl");
557                 ($path, $fcmd) = search($main::cmd, $cmd, "pl") unless $path && $fcmd;
558
559                 if ($path && $cmd) {
560                         dbg("cmd: path $cmd cmd: $fcmd") if isdbg('command');
561                         
562                         my $package = find_cmd_name($path, $fcmd);
563                         return ($@) if $@;
564                                 
565                         if ($package && $self->can("${package}::handle")) {
566                                 no strict 'refs';
567                                 dbg("cmd: package $package") if isdbg('command');
568 #                               Log('cmd', "$self->{call} on $self->{hostname} : '$cmd $args'");
569                                 my $t0 = [gettimeofday];
570                                 eval { @ans = &{"${package}::handle"}($self, $args) };
571                                 if ($@) {
572                                         DXDebug::dbgprintring(25);
573                                         return (DXDebug::shortmess($@));
574                                 }
575                                 if (isdbg('progress')) {
576                                         my $msecs = _diffms($t0);
577                                         my $s = "CMD: '$cmd $args' by $call ip: $self->{hostname} ${msecs}mS";
578                                         dbg($s) if $cmd !~ /^(?:echo|blank)/ || isdbg('echo');     # cut down a bit on HRD and other clients' noise
579                                 }
580                         } else {
581                                 dbg("cmd: $package not present") if isdbg('command');
582                                 return $self->_error_out('e1');
583                         }
584                 } else {
585                         dbg("cmd: $cmd not found") if isdbg('command');
586                         return $self->_error_out('e1');
587                 }
588         }
589         
590         my $ok = shift @ans;
591         if ($ok) {
592                 delete $self->{errors};
593         } else {
594                 if (++$self->{errors} > $DXChannel::maxerrors) {
595                         $self->send($self->msg('e26'));
596                         $self->disconnect;
597                         return ();
598                 }
599         }
600         return map {s/([^\s])\s+$/$1/; $_} @ans;
601 }
602
603 #
604 # This is called from inside the main cluster processing loop and is used
605 # for despatching commands that are doing some long processing job
606 #
607 sub process
608 {
609         my $t = time;
610         my @dxchan = DXChannel::get_all();
611         my $dxchan;
612
613         $users = 0;
614         foreach $dxchan (@dxchan) {
615                 next unless $dxchan->is_user;  
616         
617                 # send a outstanding message prompt if required
618                 if ($t >= $dxchan->lastmsgpoll + $msgpolltime) {
619                         $dxchan->send($dxchan->msg('m9')) if DXMsg::for_me($dxchan->call);
620                         $dxchan->lastmsgpoll($t);
621                 }
622                 
623                 # send a prompt if no activity out on this channel
624                 if ($t >= $dxchan->t + $main::user_interval) {
625                         $dxchan->prompt() if $dxchan->{state} =~ /^prompt/o;
626                         $dxchan->t($t);
627                 }
628                 ++$users;
629                 $maxusers = $users if $users > $maxusers;
630
631                 if ($dxchan->{isslugged} && $main::systime > $dxchan->{isslugged}) {
632                         foreach my $ref (@{$dxchan->{sluggedpcs}}) {
633                                 if ($ref->[0] == 61) {
634                                         Spot::add(@{$ref->[2]});
635                                         DXProt::send_dx_spot($dxchan, $ref->[1], @{$ref->[2]});
636                                 }
637                         }
638
639                         $dxchan->{isslugged} = 0;
640                         $dxchan->{sluggedpcs} = [];
641                 }
642         }
643
644         import_cmd();
645 }
646
647 #
648 # finish up a user context
649 #
650 sub disconnect
651 {
652         my $self = shift;
653         my $call = $self->call;
654
655         return if $self->{disconnecting}++;
656
657         delete $self->{senddbg};
658         RBN::del_seeme($call);
659
660         my $uref = Route::User::get($call);
661         my @rout;
662         if ($uref) {
663 #               @rout = $main::routeroot->del_user($uref);
664                 @rout = DXProt::_del_thingy($main::routeroot, [$call, 0]);
665
666                 # dbg("B/C PC17 on $main::mycall for: $call") if isdbg('route');
667
668                 # issue a pc17 to everybody interested
669                 $main::me->route_pc17($main::mycall, undef, $main::routeroot, $uref);
670                 $main::me->route_pc92d($main::mycall, undef, $main::routeroot, $uref) unless $DXProt::pc92_slug_changes;
671         } else {
672                 confess "trying to disconnect a non existant user $call";
673         }
674
675         # I was the last node visited
676     $self->user->node($main::mycall);
677                 
678         # send info to all logged in thingies
679         $self->tell_login('logoutu');
680         $self->tell_buddies('logoutb');
681
682         LogDbg('DXCommand', "$call disconnected");
683
684         $self->SUPER::disconnect;
685 }
686
687 #
688 # short cut to output a prompt
689 #
690
691 sub prompt
692 {
693         my $self = shift;
694
695         return if $self->{gtk};         # 'cos prompts are not a concept that applies here
696         
697         my $call = $self->call;
698         my $date = cldate($main::systime);
699         my $time = ztime($main::systime);
700         my $prompt = $self->{prompt} || $self->msg('pr');
701
702         $call = "($call)" unless $self->here;
703         $prompt =~ s/\%C/$call/g;
704         $prompt =~ s/\%D/$date/g;
705         $prompt =~ s/\%T/$time/g;
706         $prompt =~ s/\%M/$main::mycall/g;
707         
708         $self->send($prompt);
709 }
710
711 # broadcast a message to all users [except those mentioned after buffer]
712 sub broadcast
713 {
714         my $pkg = shift;                        # ignored
715         my $s = shift;                          # the line to be rebroadcast
716         
717     foreach my $dxchan (DXChannel::get_all()) {
718                 next unless $dxchan->is_user; # only interested in user channels  
719                 next if grep $dxchan == $_, @_;
720                 $dxchan->send($s);                      # send it
721         }
722 }
723
724 # gimme all the users
725 sub get_all
726 {
727         goto &DXChannel::get_all_users;
728 }
729
730 # run a script for this user
731 sub run_script
732 {
733         my $self = shift;
734         my $silent = shift || 0;
735         
736 }
737
738 #
739 # search for the command in the cache of short->long form commands
740 #
741
742 sub search
743 {
744         my ($path, $short_cmd, $suffix) = @_;
745         my ($apath, $acmd);
746         
747         # commands are lower case
748         $short_cmd = lc $short_cmd;
749         dbg("command: $path $short_cmd\n") if isdbg('command');
750
751         # do some checking for funny characters
752         return () if $short_cmd =~ /\/$/;
753
754         # return immediately if we have it
755         ($apath, $acmd) = split ',', $cmd_cache{$short_cmd} if $cmd_cache{$short_cmd};
756         if ($apath && $acmd) {
757                 dbg("cached $short_cmd = ($apath, $acmd)\n") if isdbg('command');
758                 return ($apath, $acmd);
759         }
760         
761         # if not guess
762         my @parts = split '/', $short_cmd;
763         my $dirfn;
764         my $curdir = $path;
765         
766         while (my $p = shift @parts) {
767                 opendir(D, $curdir) or confess "can't open $curdir $!";
768                 my @ls = readdir D;
769                 closedir D;
770
771                 # if this isn't the last part
772                 if (@parts) {
773                         my $found;
774                         foreach my $l (sort @ls) {
775                                 next if $l =~ /^\./;
776                                 if ((-d "$curdir/$l") && $p eq substr($l, 0, length $p)) {
777                                         dbg("got dir: $curdir/$l\n") if isdbg('command');
778                                         $dirfn .= "$l/";
779                                         $curdir .= "/$l";
780                                         $found++;
781                                         last;
782                                 }
783                         }
784                         # only proceed if we find the directory asked for
785                         return () unless $found;
786                 } else {
787                         foreach my $l (sort @ls) {
788                                 next if $l =~ /^\./;
789                                 next unless $l =~ /\.$suffix$/;
790                                 if ($p eq substr($l, 0, length $p)) {
791                                         $l =~ s/\.$suffix$//;
792                                         $dirfn = "" unless $dirfn;
793                                         $cmd_cache{$short_cmd} = join(',', ($path, "$dirfn$l")); # cache it
794                                         dbg("got path: $path cmd: $dirfn$l\n") if isdbg('command');
795                                         return ($path, "$dirfn$l");
796                                 }
797                         }
798                 }
799         }
800
801         return ();  
802 }  
803
804 # clear the command name cache
805 sub clear_cmd_cache
806 {
807         no strict 'refs';
808         
809         for my $k (keys %Cache) {
810                 unless ($k =~ /cmd_cache/) {
811                         dbg("Undefining cmd $k") if isdbg('command');
812                         undef $DXCommandmode::{"${k}::"};
813                 }
814         }
815         %cmd_cache = ();
816         %Cache = ( cmd_clear_cmd_cache  => $Cache{cmd_clear_cmd_cache} );
817 }
818
819 #
820 # the persistant execution of things from the command directories
821 #
822 #
823 # This allows perl programs to call functions dynamically
824
825 # This has been nicked directly from the perlembed pages
826 #
827 #require Devel::Symdump;  
828
829 sub valid_package_name {
830         my $string = shift;
831         $string =~ s|([^A-Za-z0-9_/])|sprintf("_%2x",unpack("C",$1))|eg;
832         
833         $string =~ s|/|_|g;
834         return "cmd_$string";
835 }
836
837
838 # this bit of magic finds a command in the offered directory
839 sub find_cmd_name {
840         my $path = shift;
841         my $cmdname = shift;
842         my $package = valid_package_name($cmdname);
843         my $filename = "$path/$cmdname.pl";
844         my $mtime = -M $filename;
845         
846         # return if we can't find it
847         $errstr = undef;
848         unless (defined $mtime) {
849                 $errstr = DXM::msg('e1');
850                 return undef;
851         }
852         
853         if(exists $Cache{$package} && exists $Cache{$package}->{mtime} && $Cache{$package}->{mtime} <= $mtime) {
854                 #we have compiled this subroutine already,
855                 #it has not been updated on disk, nothing left to do
856                 #print STDERR "already compiled $package->handler\n";
857                 dbg("find_cmd_name: $package cached") if isdbg('command');
858         } else {
859
860                 my $sub = readfilestr($filename);
861                 unless ($sub) {
862                         $errstr = "Syserr: can't open '$filename' $!";
863                         return undef;
864                 };
865                 
866                 #wrap the code into a subroutine inside our unique package
867                 my $eval = qq(package DXCommandmode::$package; use 5.10.1; use POSIX qw{:math_h}; use DXLog; use DXDebug; use DXUser; use DXUtil; our \@ISA = qw{DXCommandmode}; );
868
869
870                 if ($sub =~ m|\s*sub\s+handle\n|) {
871                         $eval .= $sub;
872                 } else {
873                         $eval .= qq(sub handle { $sub });
874                 }
875                 
876                 if (isdbg('eval')) {
877                         my @list = split /\n/, $eval;
878                         my $line;
879                         for (@list) {
880                                 dbg($_ . "\n") if isdbg('eval');
881                         }
882                 }
883                 
884                 # get rid of any existing sub and try to compile the new one
885                 no strict 'refs';
886
887                 if (exists $Cache{$package}) {
888                         dbg("find_cmd_name: Redefining $package") if isdbg('command');
889                         undef $DXCommandmode::{"${package}::"};
890                         delete $Cache{$package};
891                 } else {
892                         dbg("find_cmd_name: Defining $package") if isdbg('command');
893                 }
894
895                 eval $eval;
896
897                 $Cache{$package} = {mtime => $mtime } unless $@;
898         }
899
900         return "DXCommandmode::$package";
901 }
902
903 sub send
904 {
905         my $self = shift;
906         if ($self->{gtk}) {
907                 for (@_) {
908                         $self->SUPER::send(dd(['cmd',$_]));
909                 }
910         } else {
911                 $self->SUPER::send(@_);
912         }
913 }
914
915 sub local_send
916 {
917         my ($self, $let, $buf) = @_;
918         if ($self->{state} eq 'prompt' || $self->{state} eq 'talk' || $self->{state} eq 'chat') {
919                 if ($self->{enhanced}) {
920                         $self->send_later($let, $buf);
921                 } else {
922                         $self->send($buf);
923                 }
924         } else {
925                 $self->delay($buf);
926         }
927 }
928
929 # send a talk message here
930 sub talk
931 {
932         my ($self, $from, $to, $via, $line, $onode) = @_;
933         $line =~ s/\\5E/\^/g;
934         if ($self->{talk}) {
935                 if ($self->{gtk}) {
936                         $self->local_send('T', dd(['talk',$to,$from,$via,$line]));
937                 } else {
938                         $self->local_send('T', "$to de $from: $line");
939                 }
940         }
941         Log('talk', $to, $from, '<' . ($onode || '*'), $line);
942         # send a 'not here' message if required
943         unless ($self->{here} && $from ne $to) {
944                 my $key = "$to$from";
945                 unless (exists $nothereslug{$key}) {
946                         my ($ref, $dxchan);
947                         if (($ref = Route::get($from)) && ($dxchan = $ref->dxchan)) {
948                                 my $name = $self->user->name || $to;
949                                 my $s = $self->user->nothere || $dxchan->msg('nothere', $name);
950                                 $nothereslug{$key} = $main::systime;
951                                 $dxchan->talk($to, $from, undef, $s);
952                         }
953                 }
954         }
955 }
956
957 # send an announce
958 sub announce
959 {
960         my $self = shift;
961         my $line = shift;
962         my $isolate = shift;
963         my $to = shift;
964         my $target = shift;
965         my $text = shift;
966         my ($filter, $hops);
967
968         if (!$self->{ann_talk} && $to ne $self->{call}) {
969                 my $call = AnnTalk::is_talk_candidate($_[0], $text);
970                 return if $call;
971         }
972
973         if ($self->{annfilter}) {
974                 ($filter, $hops) = $self->{annfilter}->it(@_ );
975                 return unless $filter;
976         }
977
978         unless ($self->{ann}) {
979                 return if $_[0] ne $main::myalias && $_[0] ne $main::mycall;
980         }
981         return if $target eq 'SYSOP' && $self->{priv} < 5;
982         my $buf;
983         if ($self->{gtk}) {
984                 $buf = dd(['ann', $to, $target, $text, @_])
985         } else {
986                 $buf = "$to$target de $_[0]: $text";
987                 #$buf =~ s/\%5E/^/g;
988                 $buf .= "\a\a" if $self->{beep};
989         }
990         $self->local_send($target eq 'WX' ? 'W' : 'N', $buf);
991 }
992
993 # send a chat
994 sub chat
995 {
996         my $self = shift;
997         my $line = shift;
998         my $isolate = shift;
999         my $target = shift;
1000         my $to = shift;
1001         my $text = shift;
1002         my ($filter, $hops);
1003
1004         return unless grep uc $_ eq $target, @{$self->{user}->{group}};
1005         
1006         $text =~ s/^\#\d+ //;
1007         my $buf;
1008         if ($self->{gtk}) {
1009                 $buf = dd(['chat', $to, $target, $text, @_])
1010         } else {
1011                 $buf = "$target de $_[0]: $text";
1012                 #$buf =~ s/\%5E/^/g;
1013                 $buf .= "\a\a" if $self->{beep};
1014         }
1015         $self->local_send('C', $buf);
1016 }
1017
1018 sub format_dx_spot
1019 {
1020         my $self = shift;
1021
1022         my $t = ztime($_[2]);
1023         my ($slot1, $slot2) = ('', '');
1024         
1025         my $clth = 30 + $self->{width} - 80;    # allow comment to grow according the screen width 
1026         my $c = $_[3];
1027         $c =~ s/\t/ /g;
1028         my $comment = substr (($c || ''), 0, $clth);
1029         $comment .= ' ' x ($clth - (length($comment)));
1030         
1031     if (!$slot1 && $self->{user}->wantgrid) {
1032                 my $ref = DXUser::get_current($_[1]);
1033                 if ($ref && $ref->qra) {
1034                         $slot1 = ' ' . substr($ref->qra, 0, 4);
1035                 }
1036         }
1037         if (!$slot1 && $self->{user}->wantusstate) {
1038                 $slot1 = " $_[12]" if $_[12];
1039         }
1040         unless ($slot1) {
1041                 if ($self->{user}->wantdxitu) {
1042                         $slot1 = sprintf(" %2d", $_[8]) if defined $_[8]; 
1043                 } elsif ($self->{user}->wantdxcq) {
1044                         $slot1 = sprintf(" %2d", $_[9]) if defined $_[9];
1045                 }
1046         }
1047         $comment = substr($comment, 0,  $clth-length($slot1)) . $slot1 if $slot1;
1048         
1049     if (!$slot2 && $self->{user}->wantgrid) {
1050                 my $origin = $_[4];
1051                 $origin =~ s/-#$//;                     # sigh......
1052                 my $ref = DXUser::get_current($origin);
1053                 if ($ref && $ref->qra) {
1054                         $slot2 = ' ' . substr($ref->qra, 0, 4);
1055                 }
1056         }
1057         if (!$slot2 && $self->{user}->wantusstate) {
1058                 $slot2 = " $_[13]" if $_[13];
1059         }
1060         unless ($slot2) {
1061                 if ($self->{user}->wantdxitu) {
1062                         $slot2 = sprintf(" %2d", $_[10]) if defined $_[10]; 
1063                 } elsif ($self->{user}->wantdxcq) {
1064                         $slot2 = sprintf(" %2d", $_[11]) if defined $_[11]; 
1065                 }
1066         }
1067
1068         my $o = sprintf("%-9s", $_[4] . ':');
1069         my $qrg = sprintf "%8.1f", $_[0];
1070         if (length $qrg >= 9) {
1071                 while (length($o)+length($qrg) > 17 && $o =~ / $/) {
1072                         chop $o;
1073                 }
1074         }
1075         my $spot = sprintf "%-12s", $_[1];
1076         my $front = "DX de $o $qrg  $spot";
1077         while (length($front) > 38 && $front =~ /  $/) {
1078                 chop $front;
1079         }
1080
1081         
1082         return sprintf "$front %-s $t$slot2", $comment;
1083 }
1084
1085
1086 # send a dx spot
1087 sub dx_spot
1088 {
1089         my $self = shift;
1090         my $line = shift;
1091         my $isolate = shift;
1092         return unless $self->{dx};
1093
1094         my ($filter, $hops);
1095
1096         if ($self->{spotsfilter}) {
1097                 ($filter, $hops) = $self->{spotsfilter}->it(@_ );
1098                 return unless $filter;
1099         }
1100
1101         dbg('spot: "' . join('","', @_) . '"') if isdbg('dxspot');
1102
1103         my $buf;
1104         if ($self->{ve7cc}) {
1105                 $buf = VE7CC::dx_spot($self, @_);
1106         } elsif ($self->{gtk}) {
1107                 my ($dxloc, $byloc);
1108
1109                 my $ref = DXUser::get_current($_[4]);
1110                 if ($ref) {
1111                         $byloc = $ref->qra;
1112                         $byloc = substr($byloc, 0, 4) if $byloc;
1113                 }
1114
1115                 my $spot = $_[1];
1116                 $spot =~ s|/\w{1,4}$||;
1117                 $ref = DXUser::get_current($spot);
1118                 if ($ref) {
1119                         $dxloc = $ref->qra;
1120                         $dxloc = substr($dxloc, 0, 4) if $dxloc;
1121                 }
1122                 $buf = dd(['dx', @_, ($dxloc||''), ($byloc||'')]);
1123                 
1124         } else {
1125                 $buf = $self->format_dx_spot(@_);
1126                 $buf .= "\a\a" if $self->{beep};
1127                 #$buf =~ s/\%5E/^/g;
1128         }
1129
1130         $self->local_send('X', $buf);
1131 }
1132
1133 sub wwv
1134 {
1135         my $self = shift;
1136         my $line = shift;
1137         my $isolate = shift;
1138         my ($filter, $hops);
1139
1140         return unless $self->{wwv};
1141         
1142         if ($self->{wwvfilter}) {
1143                 ($filter, $hops) = $self->{wwvfilter}->it(@_[7..$#_] );
1144                 return unless $filter;
1145         }
1146
1147         my $buf;
1148         if ($self->{gtk}) {
1149                 $buf = dd(['wwv', @_])
1150         } else {
1151                 $buf = "WWV de $_[6] <$_[1]>:   SFI=$_[2], A=$_[3], K=$_[4], $_[5]";
1152                 $buf .= "\a\a" if $self->{beep};
1153         }
1154         
1155         $self->local_send('V', $buf);
1156 }
1157
1158 sub wcy
1159 {
1160         my $self = shift;
1161         my $line = shift;
1162         my $isolate = shift;
1163         my ($filter, $hops);
1164
1165         return unless $self->{wcy};
1166         
1167         if ($self->{wcyfilter}) {
1168                 ($filter, $hops) = $self->{wcyfilter}->it(@_ );
1169                 return unless $filter;
1170         }
1171
1172         my $buf;
1173         if ($self->{gtk}) {
1174                 $buf = dd(['wcy', @_])
1175         } else {
1176                 $buf = "WCY de $_[10] <$_[1]> : K=$_[4] expK=$_[5] A=$_[3] R=$_[6] SFI=$_[2] SA=$_[7] GMF=$_[8] Au=$_[9]";
1177                 $buf .= "\a\a" if $self->{beep};
1178         }
1179         $self->local_send('Y', $buf);
1180 }
1181
1182 # broadcast debug stuff to all interested parties
1183 sub broadcast_debug
1184 {
1185         my $s = shift;                          # the line to be rebroadcast
1186         
1187         foreach my $dxchan (DXChannel::get_all_users) {
1188                 next unless $dxchan->{enhanced} && $dxchan->{senddbg};
1189                 if ($dxchan->{gtk}) {
1190                         $dxchan->send_later('L', dd(['db', $s]));
1191                 } else {
1192                         $dxchan->send_later('L', $s);
1193                 }
1194         }
1195 }
1196
1197 sub do_entry_stuff
1198 {
1199         my $self = shift;
1200         my $line = shift;
1201         my @out;
1202         
1203         if ($self->state eq 'enterbody') {
1204                 my $loc = $self->{loc} || confess "local var gone missing" ;
1205                 if ($line eq "\032" || $line eq '%1A' || uc $line eq "/EX") {
1206                         no strict 'refs';
1207                         push @out, &{$loc->{endaction}}($self);          # like this for < 5.8.0
1208                         $self->func(undef);
1209                         $self->state('prompt');
1210                 } elsif ($line eq "\031" || uc $line eq "/ABORT" || uc $line eq "/QUIT") {
1211                         push @out, $self->msg('m10');
1212                         delete $loc->{lines};
1213                         delete $self->{loc};
1214                         $self->func(undef);
1215                         $self->state('prompt');
1216                 } else {
1217                         push @{$loc->{lines}}, length($line) > 0 ? $line : " ";
1218                         # i.e. it ain't and end or abort, therefore store the line
1219                 }
1220         } else {
1221                 confess "Invalid state $self->{state}";
1222         }
1223         return @out;
1224 }
1225
1226 sub store_startup_script
1227 {
1228         my $self = shift;
1229         my $loc = $self->{loc} || confess "local var gone missing" ;
1230         my @out;
1231         my $call = $loc->{call} || confess "callsign gone missing";
1232         confess "lines array gone missing" unless ref $loc->{lines};
1233         my $r = Script::store($call, $loc->{lines});
1234         if (defined $r) {
1235                 if ($r) {
1236                         push @out, $self->msg('m19', $call, $r);
1237                 } else {
1238                         push @out, $self->msg('m20', $call);
1239                 }
1240         } else {
1241                 push @out, "error opening startup script $call $!";
1242         } 
1243         return @out;
1244 }
1245
1246 # Import any commands contained in any files in import_cmd directory
1247 #
1248 # If the filename has a recogisable callsign as some delimited part
1249 # of it, then this is the user the command will be run as. 
1250 #
1251 sub import_cmd
1252 {
1253         # are there any to do in this directory?
1254         return unless -d $cmdimportdir;
1255         unless (opendir(DIR, $cmdimportdir)) {
1256                 LogDbg('err', "can\'t open $cmdimportdir $!");
1257                 return;
1258         } 
1259
1260         my @names = readdir(DIR);
1261         closedir(DIR);
1262         my $name;
1263
1264         return unless @names;
1265         
1266         foreach $name (@names) {
1267                 next if $name =~ /^\./;
1268
1269                 my $s = Script->new($name, $cmdimportdir);
1270                 if ($s) {
1271                         LogDbg('DXCommand', "Run import cmd file $name");
1272                         my @cat = split /[^A-Za-z0-9]+/, $name;
1273                         my ($call) = grep {is_callsign(uc $_)} @cat;
1274                         $call ||= $main::mycall;
1275                         $call = uc $call;
1276                         my @out;
1277                         
1278                         
1279                         $s->inscript(0);        # switch off script checks
1280                         
1281                         if ($call eq $main::mycall) {
1282                                 @out = $s->run($main::me, 1);
1283                         } else {
1284                                 my $dxchan = DXChannel::get($call);
1285                             if ($dxchan) {
1286                                         @out = $s->run($dxchan, 1);
1287                                 } else {
1288                                         my $u = DXUser::get($call);
1289                                         if ($u) {
1290                                                 $dxchan = $main::me;
1291                                                 my $old = $dxchan->{call};
1292                                                 my $priv = $dxchan->{priv};
1293                                                 my $user = $dxchan->{user};
1294                                                 $dxchan->{call} = $call;
1295                                                 $dxchan->{priv} = $u->priv;
1296                                                 $dxchan->{user} = $u;
1297                                                 @out = $s->run($dxchan, 1);
1298                                                 $dxchan->{call} = $old;
1299                                                 $dxchan->{priv} = $priv;
1300                                                 $dxchan->{user} = $user;
1301                                         } else {
1302                                                 LogDbg('err', "Trying to run import cmd for non-existant user $call");
1303                                         }
1304                                 }
1305                         }
1306                         $s->erase;
1307                         for (@out) {
1308                                 LogDbg('DXCommand', "Import cmd $name/$call: $_");
1309                         }
1310                 } else {
1311                         LogDbg('err', "Failed to open $cmdimportdir/$name $!");
1312                         unlink "$cmdimportdir/$name";
1313                 }
1314         }
1315 }
1316
1317 sub print_find_reply
1318 {
1319         my ($self, $node, $target, $flag, $ms) = @_;
1320         my $sort = $flag == 2 ? "External" : "Local";
1321         $self->send("$sort $target found at $node in $ms ms" );
1322 }
1323
1324 # send the most relevant motd
1325 sub send_motd
1326 {
1327         my $self = shift;
1328         my $motd;
1329
1330         unless ($self->isregistered) {
1331                 $motd = "${main::motd}_nor_$self->{lang}";
1332                 $motd = "${main::motd}_nor" unless -e $motd;
1333         }
1334         $motd = "${main::motd}_$self->{lang}" unless $motd && -e $motd;
1335         $motd = $main::motd unless $motd && -e $motd;
1336         if ($self->conn->ax25) {
1337                 if ($motd) {
1338                         $motd = "${motd}_ax25" if -e "${motd}_ax25";
1339                 } else {
1340                         $motd = "${main::motd}_ax25" if -e "${main::motd}_ax25";
1341                 }
1342         }
1343         $self->send_file($motd) if -e $motd;
1344 }
1345
1346 # Punt off a long running command into a separate process
1347 #
1348 # This is called from commands to run some potentially long running
1349 # function. The process forks and then runs the function and returns
1350 # the result back to the cmd. 
1351 #
1352 # NOTE: this merely forks the current process and then runs the cmd in that (current) context.
1353 #       IT DOES NOT START UP SOME NEW PROGRAM AND RELIES ON THE FACT THAT IT IS RUNNING DXSPIDER 
1354 #       THE CURRENT CONTEXT!!
1355
1356 # call: $self->spawn_cmd($original_cmd_line, \<function>, [cb => sub{...}], [prefix => "cmd> "], [progress => 0|1], [args => [...]]);
1357 sub spawn_cmd
1358 {
1359         my $self = shift;
1360         my $line = shift;
1361         my $cmdref = shift;
1362         my $call = $self->{call};
1363         my %args = @_;
1364         my @out;
1365         
1366         my $cb = delete $args{cb};
1367         my $prefix = delete $args{prefix};
1368         my $progress = delete $args{progress};
1369         my $args = delete $args{args} || [];
1370         my $t0 = [gettimeofday];
1371
1372         no strict 'refs';
1373
1374         # just behave normally if something has set the "one-shot" _nospawn in the channel
1375         if ($self->{_nospawn}) {
1376                 eval { @out = $cmdref->(@$args); };
1377                 if ($@) {
1378                         DXDebug::dbgprintring(25);
1379                         push @out, DXDebug::shortmess($@);
1380                 }
1381                 return @out;
1382         }
1383         
1384         my $fc = DXSubprocess->new;
1385 #       $fc->serializer(\&encode_json);
1386 #       $fc->deserializer(\&decode_json);
1387         $fc->run(
1388                          sub {
1389                                  my $subpro = shift;
1390                                  if (isdbg('progress')) {
1391                                          my $s = qq{$call line: "$line"};
1392                                          $s .= ", args: " . join(', ', map { defined $_ ? qq{'$_'} : q{'undef'} } @$args) if $args && @$args;
1393                                          dbg($s);
1394                                  }
1395                                  eval {
1396                                          ++$self->{_in_sub_process};
1397                                          dbg "\$self->{_in_sub_process} = $self->{_in_sub_process}";
1398                                          @out = $cmdref->(@$args);
1399                                          --$self->{_in_sub_process} if $self->{_in_sub_process} > 0;
1400                                  };
1401                                  if ($@) {
1402                                          DXDebug::dbgprintring(25);
1403                                          push @out, DXDebug::shortmess($@);
1404                                  }
1405                                  return @out;
1406                          },
1407 #                        $args,
1408                          sub {
1409                                  my ($fc, $err, @res) = @_; 
1410                                  my $dxchan = DXChannel::get($call);
1411                                  return unless $dxchan;
1412
1413                                  if ($err) {
1414                                          my $s = "DXProt::spawn_cmd: call $call error $err";
1415                                          dbg($s) if isdbg('chan');
1416                                          $dxchan->send($s);
1417                                          return;
1418                                  }
1419                                  if ($cb) {
1420                                          # transform output if required
1421                                          @res = $cb->($dxchan, @res);
1422                                  }
1423                                  if (@res) {
1424                                          if (defined $prefix) {
1425                                                  $dxchan->send(map {"$prefix$_"} @res);
1426                                          } else {
1427                                                  $dxchan->send(@res);
1428                                          }
1429                                  }
1430                                  diffms("by $call", $line, $t0, scalar @res) if isdbg('progress');
1431                          });
1432         
1433         return @out;
1434 }
1435
1436 sub user_count
1437 {
1438     return ($users, $maxusers);
1439 }
1440
1441 1;
1442 __END__