#
# Copyright (c) 1998 Dirk Koopman G1TLH
#
-# $Id$
+#
#
package DXCommandmode;
@ISA = qw(DXChannel);
+require 5.10.1;
+
use POSIX qw(:math_h);
use DXUtil;
use DXChannel;
use Sun;
use Internet;
use Script;
-use Net::Telnet;
use QSL;
use DB_File;
use VE7CC;
use DXXml;
+use AsyncMsg;
+use JSON;
+use Time::HiRes qw(gettimeofday tv_interval);
+
+use Mojo::IOLoop;
+use Mojo::IOLoop::Subprocess;
+use Mojo::UserAgent;
use strict;
-use vars qw(%Cache %cmd_cache $errstr %aliases $scriptbase $maxerrors %nothereslug
- $maxbadcount $msgpolltime $default_pagelth $cmdimportdir);
+use vars qw(%Cache %cmd_cache $errstr %aliases $scriptbase %nothereslug
+ $maxbadcount $msgpolltime $default_pagelth $cmdimportdir $users $maxusers);
%Cache = (); # cache of dynamically loaded routine's mod times
%cmd_cache = (); # cache of short names
$errstr = (); # error string from eval
%aliases = (); # aliases for (parts of) commands
$scriptbase = "$main::root/scripts"; # the place where all users start scripts go
-$maxerrors = 20; # the maximum number of concurrent errors allowed before disconnection
$maxbadcount = 3; # no of bad words allowed before disconnection
$msgpolltime = 3600; # the time between polls for new messages
$cmdimportdir = "$main::root/cmd_import"; # the base directory for importing command scripts
# this does not exist as default, you need to create it manually
- #
+$users = 0; # no of users on this node currently
+$maxusers = 0; # max no users on this node for this run
#
# obtain a new connection this is derived from dxchannel
# routing, this must go out here to prevent race condx
my $pkg = shift;
my $call = shift;
- my @rout = $main::routeroot->add_user($call, Route::here(1));
+# my @rout = $main::routeroot->add_user($call, Route::here(1));
+ DXProt::_add_thingy($main::routeroot, [$call, 0, 0, 1, undef, undef, $self->hostname], );
# ALWAYS output the user
my $ref = Route::User::get($call);
if ($ref) {
$main::me->route_pc16($main::mycall, undef, $main::routeroot, $ref);
- $main::me->route_pc92a($main::mycall, undef, $main::routeroot, $ref);
+ $main::me->route_pc92a($main::mycall, undef, $main::routeroot, $ref) unless $DXProt::pc92_slug_changes;
}
return $self;
my $name = $user->{name};
# log it
- my $host = $self->{conn}->{peerhost};
+ my $host = $self->{conn}->peerhost;
$host ||= "AGW Port #$self->{conn}->{agwport}" if exists $self->{conn}->{agwport};
$host ||= "unknown";
- LogDbg('DXCommand', "$call connected from $host");
+ $self->{hostname} = $host;
$self->{name} = $name ? $name : $call;
$self->send($self->msg('l2',$self->{name}));
$pagelth = $default_pagelth unless defined $pagelth;
$self->{pagelth} = $pagelth;
($self->{width}) = $line =~ /width=(\d+)/; $line =~ s/\s*width=\d+\s*//;
+ if ($line =~ /host=/) {
+ my ($h) = $line =~ /host=(\d+\.\d+\.\d+\.\d+)/;
+ $line =~ s/\s*host=\d+\.\d+\.\d+\.\d+// if $h;
+ unless ($h) {
+ ($h) = $line =~ /host=([\da..fA..F:]+)/;
+ $line =~ s/\s*host=[\da..fA..F:]+// if $h;
+ }
+ $self->{hostname} = $h if $h;
+ }
$self->{width} = 80 unless $self->{width} && $self->{width} > 80;
$self->{consort} = $line; # save the connection type
-
+
+ LogDbg('DXCommand', "$call connected from $self->{hostname}");
+
# set some necessary flags on the user if they are connecting
$self->{beep} = $user->wantbeep;
$self->{ann} = $user->wantann;
$self->{ann_talk} = $user->wantann_talk;
$self->{here} = 1;
$self->{prompt} = $user->prompt if $user->prompt;
+ $self->{lastmsgpoll} = 0;
# sort out new dx spot stuff
$user->wantdxcq(0) unless defined $user->{wantdxcq};
$self->{registered} = 1;
}
-
- # decide which motd to send
- my $motd;
- unless ($self->{registered}) {
- $motd = "${main::motd}_nor_$self->{lang}";
- $motd = "${main::motd}_nor" unless -e $motd;
- }
- $motd = "${main::motd}_$self->{lang}" unless $motd && -e $motd;
- $motd = $main::motd unless $motd && -e $motd;
- $self->send_file($motd) if -e $motd;
+ # send the relevant MOTD
+ $self->send_motd;
# sort out privilege reduction
- $self->{priv} = 0 if $line =~ /^(ax|te)/ && !$self->conn->{usedpasswd};
+ $self->{priv} = 0 unless $self->{hostname} eq '127.0.0.1' || $self->{hostname} eq '::1' || $self->conn->{usedpasswd};
# get the filters
my $nossid = $call;
$script->run($self) if $script;
# send cluster info
- my $info = Route::cluster();
- $self->send("Cluster:$info");
+ $self->send($self->run_cmd("show/cluster"));
# send prompts for qth, name and things
$self->send($self->msg('namee1')) if !$user->name;
foreach my $call (@{$user->buddies}) {
my $ref = Route::User::get($call);
if ($ref) {
- foreach my $node (@{$ref->parent}) {
+ foreach my $node ($ref->parents) {
$self->send($self->msg($node eq $main::mycall ? 'loginb' : 'loginbn', $call, $node));
}
}
$self->conn->{echo} = $self->conn->{decho};
delete $self->conn->{decho};
$self->state('prompt');
- } elsif ($self->{state} eq 'talk') {
+ } elsif ($self->{state} eq 'talk' || $self->{state} eq 'chat') {
if ($cmdline =~ m{^(?:/EX|/ABORT)}i) {
for (@{$self->{talklist}}) {
- $self->send_talks($_, $self->msg('talkend'));
+ if ($self->{state} eq 'talk') {
+ $self->send_talks($_, $self->msg('talkend'));
+ } else {
+ $self->local_send('C', $self->msg('chatend', $_));
+ }
}
$self->state('prompt');
delete $self->{talklist};
LogDbg('DXCommand', "$self->{call} swore: $l with words:" . join(',', @bad) . ")");
} else {
for (@{$self->{talklist}}) {
- $self->send_talks($_, $l);
+ if ($self->{state} eq 'talk') {
+ $self->send_talks($_, $l);
+ } else {
+ send_chats($self, $_, $l)
+ }
}
}
}
}
- $self->send($self->talk_prompt);
+ $self->send($self->{state} eq 'talk' ? $self->talk_prompt : $self->chat_prompt);
} elsif ($self->{talklist} && @{$self->{talklist}}) {
# send what has been said to whoever is in this person's talk list
my @bad;
LogDbg('DXCommand', "$self->{call} swore: $cmdline with words:" . join(',', @bad) . ")");
} else {
for (@{$self->{talklist}}) {
- $self->send_talks($_, $rawline);
+ if ($self->{state} eq 'talk') {
+ $self->send_talks($_, $rawline);
+ } else {
+ send_chats($self, $_, $rawline);
+ }
}
}
$self->send($self->talk_prompt) if $self->{state} eq 'talk';
+ $self->send($self->chat_prompt) if $self->{state} eq 'chat';
} else {
# for safety
$self->state('prompt');
}
}
-sub talk_prompt
+sub send_chats
+{
+ my $self = shift;
+ my $target = shift;
+ my $text = shift;
+
+ my $msgid = DXProt::nextchatmsgid();
+ $text = "#$msgid $text";
+ $main::me->normal(DXProt::pc93($target, $self->{call}, undef, $text));
+}
+
+sub special_prompt
{
my $self = shift;
+ my $prompt = shift;
my @call;
for (@{$self->{talklist}}) {
my ($to, $via) = /(\S+)>(\S+)/;
$to = $_ unless $to;
push @call, $to;
}
- return $self->msg('talkprompt', join(',', @call));
+ return $self->msg($prompt, join(',', @call));
+}
+
+sub talk_prompt
+{
+ my $self = shift;
+ return $self->special_prompt('talkprompt');
+}
+
+sub chat_prompt
+{
+ my $self = shift;
+ return $self->special_prompt('chatprompt');
}
#
}
}
}
+
#
-# this is the thing that runs the command, it is done like this for the
+# this is the thing that preps for running the command, it is done like this for the
# benefit of remote command execution
#
my $cmdline = shift;
my @ans;
-
return () if length $cmdline == 0;
-
# split the command line up into parts, the first part is the command
my ($cmd, $args) = split /\s+/, $cmdline, 2;
$args = "" unless defined $args;
if ($cmd) {
+
+ # check cmd
+ if ($cmd =~ m|^/| || $cmd =~ m|[^-?\w/]|) {
+ LogDbg('DXCommand', "cmd: invalid characters in '$cmd'");
+ return $self->_error_out('e1');
+ }
+
# strip out // on command only
$cmd =~ s|//|/|g;
- $cmd =~ s|^/||g; # no leading / either
- $cmd =~ s|[^-?\w/]||g; # and no funny characters either
my ($path, $fcmd);
if ($acmd) {
($cmd, $args) = split /\s+/, "$acmd $args", 2;
$args = "" unless defined $args;
- dbg("aliased cmd: $cmd $args") if isdbg('command');
+ dbg("cmd: aliased $cmd $args") if isdbg('command');
}
# first expand out the entry to a command
($path, $fcmd) = search($main::cmd, $cmd, "pl") unless $path && $fcmd;
if ($path && $cmd) {
- dbg("path: $cmd cmd: $fcmd") if isdbg('command');
+ dbg("cmd: path $cmd cmd: $fcmd") if isdbg('command');
my $package = find_cmd_name($path, $fcmd);
return ($@) if $@;
- if ($package) {
+ if ($package && $self->can("${package}::handle")) {
no strict 'refs';
- dbg("package: $package") if isdbg('command');
- eval { @ans = &$package($self, $args) };
- return (DXDebug::shortmess($@)) if $@;
+ dbg("cmd: package $package") if isdbg('command');
+ my $t0 = [gettimeofday];
+ eval { @ans = &{"${package}::handle"}($self, $args) };
+ if ($@) {
+ DXDebug::dbgprintring(25);
+ return (DXDebug::shortmess($@));
+ }
+ if (isdbg('progress')) {
+ my $msecs = _diffms($t0);
+ my $s = "CMD: '$cmd $args' by $call ip: $self->{hostname} ${msecs}mS";
+ dbg($s) if $cmd !~ /^(?:echo|blank)/ || isdbg('echo'); # cut down a bit on HRD and other clients' noise
+ }
+ } else {
+ dbg("cmd: $package not present") if isdbg('command');
+ return $self->_error_out('e1');
}
} else {
dbg("cmd: $cmd not found") if isdbg('command');
- if (++$self->{errors} > $maxerrors) {
- $self->send($self->msg('e26'));
- $self->disconnect;
- return ();
- } else {
- return ($self->msg('e1'));
- }
+ return $self->_error_out('e1');
}
}
if ($ok) {
delete $self->{errors};
} else {
- if (++$self->{errors} > $maxerrors) {
+ if (++$self->{errors} > $DXChannel::maxerrors) {
$self->send($self->msg('e26'));
$self->disconnect;
return ();
my $t = time;
my @dxchan = DXChannel::get_all();
my $dxchan;
-
+
+ $users = 0;
foreach $dxchan (@dxchan) {
- next if $dxchan->sort ne 'U';
+ next unless $dxchan->is_user;
# send a outstanding message prompt if required
if ($t >= $dxchan->lastmsgpoll + $msgpolltime) {
$dxchan->prompt() if $dxchan->{state} =~ /^prompt/o;
$dxchan->t($t);
}
+ ++$users;
+ $maxusers = $users if $users > $maxusers;
}
while (my ($k, $v) = each %nothereslug) {
my $uref = Route::User::get($call);
my @rout;
if ($uref) {
- @rout = $main::routeroot->del_user($uref);
+# @rout = $main::routeroot->del_user($uref);
+ @rout = DXProt::_del_thingy($main::routeroot, [$call, 0]);
+
dbg("B/C PC17 on $main::mycall for: $call") if isdbg('route');
# issue a pc17 to everybody interested
$main::me->route_pc17($main::mycall, undef, $main::routeroot, $uref);
- $main::me->route_pc92d($main::mycall, undef, $main::routeroot, $uref);
+ $main::me->route_pc92d($main::mycall, undef, $main::routeroot, $uref) unless $DXProt::pc92_slug_changes;
} else {
confess "trying to disconnect a non existant user $call";
}
my $s = shift; # the line to be rebroadcast
foreach my $dxchan (DXChannel::get_all()) {
- next unless $dxchan->{sort} eq 'U'; # only interested in user channels
+ next unless $dxchan->is_user; # only interested in user channels
next if grep $dxchan == $_, @_;
$dxchan->send($s); # send it
}
# gimme all the users
sub get_all
{
- return grep {$_->{sort} eq 'U'} DXChannel::get_all();
+ goto &DXChannel::get_all_users;
}
# run a script for this user
{
no strict 'refs';
- for (keys %Cache) {
- undef *{$_} unless /cmd_cache/;
- dbg("Undefining cmd $_") if isdbg('command');
+ for my $k (keys %Cache) {
+ unless ($k =~ /cmd_cache/) {
+ dbg("Undefining cmd $k") if isdbg('command');
+ undef $DXCommandmode::{"${k}::"};
+ }
}
%cmd_cache = ();
- %Cache = ();
+ %Cache = ( cmd_clear_cmd_cache => $Cache{cmd_clear_cmd_cache} );
}
#
#
# This has been nicked directly from the perlembed pages
#
-
#require Devel::Symdump;
sub valid_package_name {
- my($string) = @_;
+ my $string = shift;
$string =~ s|([^A-Za-z0-9_/])|sprintf("_%2x",unpack("C",$1))|eg;
$string =~ s|/|_|g;
return undef;
}
- if(defined $Cache{$package}->{mtime} &&$Cache{$package}->{mtime} <= $mtime) {
+ if(exists $Cache{$package} && exists $Cache{$package}->{mtime} && $Cache{$package}->{mtime} <= $mtime) {
#we have compiled this subroutine already,
#it has not been updated on disk, nothing left to do
#print STDERR "already compiled $package->handler\n";
- ;
+ dbg("find_cmd_name: $package cached") if isdbg('command');
} else {
my $sub = readfilestr($filename);
};
#wrap the code into a subroutine inside our unique package
- my $eval = qq( sub $package { $sub } );
+ my $eval = qq(package DXCommandmode::$package; use POSIX qw{:math_h}; use DXLog; use DXDebug; use DXUser; use DXUtil; our \@ISA = qw{DXCommandmode}; );
+
+
+ if ($sub =~ m|\s*sub\s+handle\n|) {
+ $eval .= $sub;
+ } else {
+ $eval .= qq(sub handle { $sub });
+ }
if (isdbg('eval')) {
my @list = split /\n/, $eval;
no strict 'refs';
if (exists $Cache{$package}) {
- dbg("Redefining $package") if isdbg('command');
- undef *$package;
+ dbg("find_cmd_name: Redefining $package") if isdbg('command');
+ undef $DXCommandmode::{"${package}::"};
+ delete $Cache{$package};
} else {
- dbg("Defining $package") if isdbg('command');
+ dbg("find_cmd_name: Defining $package") if isdbg('command');
}
+
eval $eval;
-
- $Cache{$package} = {mtime => $mtime };
-
+
+ $Cache{$package} = {mtime => $mtime } unless $@;
}
- return $package;
+ return "DXCommandmode::$package";
}
sub send
sub local_send
{
my ($self, $let, $buf) = @_;
- if ($self->{state} eq 'prompt' || $self->{state} eq 'talk') {
+ if ($self->{state} eq 'prompt' || $self->{state} eq 'talk' || $self->{state} eq 'chat') {
if ($self->{enhanced}) {
$self->send_later($let, $buf);
} else {
my $comment = substr (($_[3] || ''), 0, $clth);
$comment .= ' ' x ($clth - length($comment));
if ($self->{user}->wantgrid) {
- my $ref = DXUser->get_current($_[4]);
+ my $ref = DXUser::get_current($_[4]);
if ($ref) {
$loc = $ref->qra || '';
$loc = ' ' . substr($loc, 0, 4) if $loc;
} elsif ($self->{gtk}) {
my ($dxloc, $byloc);
- my $ref = DXUser->get_current($_[4]);
+ my $ref = DXUser::get_current($_[4]);
if ($ref) {
$byloc = $ref->qra;
$byloc = substr($byloc, 0, 4) if $byloc;
my $spot = $_[1];
$spot =~ s|/\w{1,4}$||;
- $ref = DXUser->get_current($spot);
+ $ref = DXUser::get_current($spot);
if ($ref) {
$dxloc = $ref->qra;
$dxloc = substr($dxloc, 0, 4) if $dxloc;
{
my $s = shift; # the line to be rebroadcast
- foreach my $dxchan (DXChannel::get_all) {
+ foreach my $dxchan (DXChannel::get_all_users) {
next unless $dxchan->{enhanced} && $dxchan->{senddbg};
if ($dxchan->{gtk}) {
$dxchan->send_later('L', dd(['db', $s]));
my @names = readdir(DIR);
closedir(DIR);
my $name;
+
+ return unless @names;
+
foreach $name (@names) {
next if $name =~ /^\./;
if ($dxchan) {
@out = $s->run($dxchan, 1);
} else {
- my $u = DXUser->get($call);
+ my $u = DXUser::get($call);
if ($u) {
$dxchan = $main::me;
my $old = $dxchan->{call};
$dxchan->{priv} = $u->priv;
$dxchan->{user} = $u;
@out = $s->run($dxchan, 1);
- $dxchan->{call} = $call;
+ $dxchan->{call} = $old;
$dxchan->{priv} = $priv;
$dxchan->{user} = $user;
} else {
my $sort = $flag == 2 ? "External" : "Local";
$self->send("$sort $target found at $node in $ms ms" );
}
+
+# send the most relevant motd
+sub send_motd
+{
+ my $self = shift;
+ my $motd;
+
+ unless ($self->{registered}) {
+ $motd = "${main::motd}_nor_$self->{lang}";
+ $motd = "${main::motd}_nor" unless -e $motd;
+ }
+ $motd = "${main::motd}_$self->{lang}" unless $motd && -e $motd;
+ $motd = $main::motd unless $motd && -e $motd;
+ if ($self->conn->ax25) {
+ if ($motd) {
+ $motd = "${motd}_ax25" if -e "${motd}_ax25";
+ } else {
+ $motd = "${main::motd}_ax25" if -e "${main::motd}_ax25";
+ }
+ }
+ $self->send_file($motd) if -e $motd;
+}
+
+
+# Punt off a long running command into a separate process
+#
+# This is called from commands to run some potentially long running
+# function. The process forks and then runs the function and returns
+# the result back to the cmd.
+#
+# NOTE: this merely forks the current process and then runs the cmd in that (current) context.
+# IT DOES NOT START UP SOME NEW PROGRAM AND RELIES ON THE FACT THAT IT IS RUNNING DXSPIDER
+# THE CURRENT CONTEXT!!
+#
+# call: $self->spawn_cmd($original_cmd_line, \<function>, [cb => sub{...}], [prefix => "cmd> "], [progress => 0|1], [args => [...]]);
+sub spawn_cmd
+{
+ my $self = shift;
+ my $line = shift;
+ my $cmdref = shift;
+ my $call = $self->{call};
+ my %args = @_;
+ my @out;
+
+ my $cb = delete $args{cb};
+ my $prefix = delete $args{prefix};
+ my $progress = delete $args{progress};
+ my $args = delete $args{args} || [];
+ my $t0 = [gettimeofday];
+
+ no strict 'refs';
+
+ # just behave normally if something has set the "one-shot" _nospawn in the channel
+ if ($self->{_nospawn}) {
+ eval { @out = $cmdref->(@$args); };
+ if ($@) {
+ DXDebug::dbgprintring(25);
+ push @out, DXDebug::shortmess($@);
+ }
+ return @out;
+ }
+
+ my $fc = Mojo::IOLoop::Subprocess->new;
+# $fc->serializer(\&encode_json);
+# $fc->deserializer(\&decode_json);
+ $fc->run(
+ sub {
+ my $subpro = shift;
+ if (isdbg('progress')) {
+ my $s = qq{line: "$line"};
+ $s .= ", args: " . join(', ', map { defined $_ ? qq{'$_'} : q{'undef'} } @$args) if $args && @$args;
+ dbg($s);
+ }
+ eval { @out = $cmdref->(@$args); };
+ if ($@) {
+ DXDebug::dbgprintring(25);
+ push @out, DXDebug::shortmess($@);
+ }
+ return @out;
+ },
+# $args,
+ sub {
+ my ($fc, $err, @res) = @_;
+ my $dxchan = DXChannel::get($call);
+ return unless $dxchan;
+
+ if ($err) {
+ my $s = "DXProt::spawn_cmd: call $call error $err";
+ dbg($s) if isdbg('chan');
+ $dxchan->send($s);
+ return;
+ }
+ if ($cb) {
+ # transform output if required
+ @res = $cb->($dxchan, @res);
+ }
+ if (@res) {
+ if (defined $prefix) {
+ $dxchan->send(map {"$prefix$_"} @res);
+ } else {
+ $dxchan->send(@res);
+ }
+ }
+ diffms("by $call", $line, $t0, scalar @res) if isdbg('progress');
+ });
+
+ return @out;
+}
+
+sub user_count
+{
+ return ($users, $maxusers);
+}
1;
__END__