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