diff --git a/control/sys.txt b/control/sys.txt index c9e3ee91de..f84591ea77 100644 --- a/control/sys.txt +++ b/control/sys.txt @@ -49,7 +49,7 @@ loadPlugins 2 # loadPlugins_list # if loadPlugins is set to 2, this comma-separated list of plugin names (filename without the extension) # specifies which plugin files to load at startup or when the "plugin load all" command is used. -loadPlugins_list macro,profiles,breakTime,raiseStat,raiseSkill,map,reconnect,eventMacro,item_weight_recorder,xconf +loadPlugins_list macro,profiles,breakTime,raiseStat,raiseSkill,map,reconnect,eventMacro,item_weight_recorder,xconf,bus_hook,bus_party,bus_command # skipPlugins_list # if loadPlugins is set to 3, this comma-separated list of plugin names (filename without the extension) diff --git a/plugins/bus_command/bus_command.pl b/plugins/bus_command/bus_command.pl new file mode 100644 index 0000000000..6f930f25cb --- /dev/null +++ b/plugins/bus_command/bus_command.pl @@ -0,0 +1,59 @@ +package OpenKore::Plugins::BusCommand; +############################################################################### +# Plugin to allow console commands to be sent via bus. +# Depends on the bus_hook plugin. + +use strict; + +use Globals qw( $char %config $field $net ); +use Utils qw( &existsInList ); + +our $name = 'bus_command'; + +Plugins::register( $name, "$name plugin", \&Unload, \&Unload ); + +my $hooks = Plugins::addHooks( # + [ 'bus/recv/RUN_COMMAND' => \&onBusRecvRunCommand ], +); + +sub Unload { + Plugins::delHooks( $hooks ); +} + +sub onBusRecvRunCommand { + my ( undef, $args ) = @_; + + my $allow = 1; + $allow = 0 if $args->{group} && !check_group( $args->{group} ); + $allow = 0 if $args->{party} && !( $char && $char->{party} && $char->{party}->{name} eq $args->{party} ); + if ( !$allow ) { + Log::debug( "[$name] Received and ignored command: $args->{command}\n", $name ); + return; + } + + Log::debug( "[$name] Received command: $args->{command}\n", $name ); + Commands::run( $args->{command} ); +} + +sub check_group { + my ( $to ) = @_; + + # Support comma-separated groups, like ONLINE,LEADERS. All checks must match. + return !grep { !check_group( $_ ) } split /\s*,\s*/, $to if index( $to, ',' ) > -1; + + return 1 if $to eq 'ALL'; + return 1 if $to eq 'AI=auto' && AI::state == AI::AUTO; + return 1 if $to eq 'AI=manual' && AI::state == AI::MANUAL; + return 1 if $to eq 'AI=off' && AI::state == AI::OFF; + return 1 if $to eq 'ONLINE' && $net->getState == Network::IN_GAME; + return 1 if $to eq 'OFFLINE' && $net->getState != Network::IN_GAME; + return 1 if $to eq 'LEADERS' && !$config{follow}; + return 1 if $to eq 'FOLLOWERS' && $config{follow} && $config{followTarget}; + return 1 if existsInList( $config{bus_command_groups}, $to ); + return 1 if $to =~ /^MAP=(.*)$/o && $field && $1 eq $field->baseName; + return 1 if $to =~ /^(\w+)=(.*)$/o && $config{$1} eq $2; + + return; +} + +1; diff --git a/plugins/bus_hook/bus_hook.pl b/plugins/bus_hook/bus_hook.pl new file mode 100644 index 0000000000..7ad75d45e7 --- /dev/null +++ b/plugins/bus_hook/bus_hook.pl @@ -0,0 +1,63 @@ +package OpenKore::Plugins::BusHook; +############################################################################### +# Plugin to connect to a Bus server and proxy Bus messages through hooks. + +use strict; + +use Bus::Client; +use Log qw( &debug ); + +our $name = 'bus_hook'; +our $bus; + +Plugins::register( $name, "$name plugin", \&Unload, \&Unload ); + +my $hooks = Plugins::addHooks( # + [ 'start3' => \&onStart ], + [ 'mainLoop_pre' => \&onMainLoopPre ], + [ 'bus/send' => \&onBusSend ], +); + +sub Unload { + $bus = undef; + Plugins::delHooks( $hooks ); +} + +sub onStart { + return if $bus; + + $bus = $Globals::bus || Bus::Client->new( userAgent => "$name plugin" ); + $bus->onConnected->add( undef, \&onConnected ); + $bus->onMessageReceived->add( undef, \&onMessageReceived ); +} + +sub onMainLoopPre { + onStart() if !$bus; + $bus->iterate; +} + +sub onBusSend { + my ( undef, $msg ) = @_; + Plugins::callHook( 'bus/sent' => $msg ); + debug "[$name] >> sent $msg->{messageID}\n", $name; + $bus->send( $msg->{messageID}, $msg->{args} ); +} + +# Ask for a list of other clients. +sub onConnected { + debug "[$name] >> connected\n", $name; + onBusSend( undef, { messageID => 'LIST_CLIENTS2' } ); + Plugins::callHook( 'bus/connect' ); +} + +sub onMessageReceived { + my ( undef, undef, $msg ) = @_; + + my $mid = $msg->{messageID}; + my $args = $msg->{args}; + debug "[$name] << received $mid\n", $name; + Plugins::callHook( 'bus/recv' => $msg ); + Plugins::callHook( "bus/recv/$mid" => $args ); +} + +1; diff --git a/plugins/bus_party/bus_party.pl b/plugins/bus_party/bus_party.pl new file mode 100644 index 0000000000..5a1153efa0 --- /dev/null +++ b/plugins/bus_party/bus_party.pl @@ -0,0 +1,150 @@ +package OpenKore::Plugins::BusParty; +############################################################################### +# Plugin to update party information via bus. +# +# This plugin is always loaded, but only sends data if the "bus_party" config +# option is enabled. + +use strict; + +use Globals qw( $accountID $char %config $field $net @partyUsersID ); +use Log qw( &debug &message ); +use Utils qw( &binAdd &binFind &binRemove &calcPosition &timeOut ); +use Time::HiRes qw( &time ); + +our $name = 'bus_party'; +our $timeout = { time => 0, timeout => 0.2 }; +our $bus_party ||= {}; + +Plugins::register( $name, "$name plugin", \&Unload, \&Unload ); + +my $hooks = Plugins::addHooks( # + [ 'mainLoop_pre' => \&onMainLoopPre ], + [ 'bus/connect' => \&onBusConnect ], + [ 'bus/recv/PARTY_REQUEST' => \&onBusRecvPartyRequest ], + [ 'bus/recv/PARTY_UPDATE' => \&onBusRecvPartyUpdate ], + [ 'bus/recv/LEAVE' => \&onBusRecvLeave ], +); + +sub Unload { + Plugins::delHooks( $hooks ); +} + +sub onMainLoopPre { + return if !$config{bus_party}; + + return if !timeOut($timeout); + $timeout->{time} = time; + + my $party_update = partial_party_update(); + return if !%$party_update; + Plugins::callHook( 'bus/send', { messageID => 'PARTY_UPDATE', args => $party_update } ); +} + +sub onBusConnect{ + Plugins::callHook( 'bus/send', { messageID => 'PARTY_REQUEST' } ); + Plugins::callHook( 'bus/send', { messageID => 'PARTY_UPDATE', args => full_party_update() } ); +} + +sub onBusRecvPartyRequest { + my ( undef, $args ) = @_; + Plugins::callHook( 'bus/send', { messageID => 'PARTY_UPDATE', TO => $args->{FROM}, args => full_party_update() } ); +} + +sub onBusRecvPartyUpdate { + my ( undef, $args ) = @_; + my $actor = $bus_party->{ $args->{FROM} } ||= {}; + $actor->{$_} = $args->{$_} foreach keys %$args; + + # Update the party. + my $id = pack 'V', $actor->{id}; + return if !$actor->{id} || $id eq $accountID; + return if !$actor->{name}; + return if !$char; + return if !$char->{party}; + + my $party_user = $char->{party}{users}{$id}; + if ( binFind( \@partyUsersID, $id ) eq '' ) { + binAdd( \@partyUsersID, $id ); + } + if ( !$party_user ) { + $party_user = $char->{party}{users}{$id} = Actor::Party->new; + $party_user->{ID} = $id; + message "[bot_party] Party Member: $args->{name}\n"; + } + + foreach ( qw( name online hp hp_max ) ) { + $party_user->{$_} = $actor->{$_}; + } + $party_user->{bus_party} = $actor->{party}; + $party_user->{map} = "$actor->{map}.gat"; + $party_user->{pos}->{x} = $actor->{x}; + $party_user->{pos}->{y} = $actor->{y}; +} + +sub onBusRecvLeave { + my ( undef, $args ) = @_; + + my $actor = $bus_party->{ $args->{clientID} }; + return if !$actor; + + # Remove the character from $char->{party} if they're not in our actual party. + my $id = pack 'V', $actor->{id}; + if ( $char && $char->{party} && ( !$char->{party}->{name} || $actor->{party} ne $char->{party}->{name} ) && binFind( \@partyUsersID, $id ) ne '' ) { + delete $char->{party}->{users}->{$id}; + binRemove( \@partyUsersID, $id ); + } + + delete $bus_party->{ $args->{clientID} }; +} + +sub full_party_update { + my $update = {}; + $update->{followTarget} = $config{follow} && $config{followTarget} || ''; + if ( $char ) { + my $pos = calcPosition( $char ); + $update->{id} = unpack 'V', $accountID; + $update->{name} = $char->{name}; + $update->{hp} = $char->{hp}; + $update->{hp_max} = $char->{hp_max}; + $update->{sp} = $char->{sp}; + $update->{sp_max} = $char->{sp_max}; + $update->{lv} = $char->{lv}; + $update->{xp} = $char->{exp}; + $update->{xp_max} = $char->{exp_max}; + $update->{jl} = $char->{lv_job}; + $update->{jp} = $char->{exp_job}; + $update->{jp_max} = $char->{exp_job_max}; + $update->{zeny} = $char->{zeny}; + $update->{status} = $char->{statuses} && %{ $char->{statuses} } ? join ', ', keys %{ $char->{statuses} } : ''; + $update->{x} = $pos->{x}; + $update->{y} = $pos->{y}; + $update->{weight} = $char->{weight}; + $update->{weight_max} = $char->{weight_max}; + if ( $char->{party} ) { + $update->{party} = $char->{party}->{name} || ''; + $update->{admin} = $char->{party}->{users}->{$accountID}->{admin} ? 1 : 0; + } + } + if ( $field ) { + $update->{map} = $field->baseName; + } + if ( $net ) { + $update->{online} = $net->getState == Network::IN_GAME ? 1 : 0; + } + $update; +} + +sub partial_party_update { + our $last_update ||= {}; + my $update = full_party_update(); + my $partial = {}; + foreach ( keys %$update ) { + next if $last_update->{$_} eq $update->{$_}; + $partial->{$_} = $update->{$_}; + } + $last_update = $update; + $partial; +} + +1; diff --git a/src/Bus/Client.pm b/src/Bus/Client.pm index a8eb409863..72ebf66935 100644 --- a/src/Bus/Client.pm +++ b/src/Bus/Client.pm @@ -68,6 +68,7 @@ sub new { my %args = @_; my $self = bless {}, $class; + $self->{verbose} = $args{verbose}; $self->{host} = $args{host}; $self->{port} = $args{port}; $self->{userAgent} = $args{userAgent} || "OpenKore"; @@ -80,6 +81,7 @@ sub new { # connected to the bus. $self->{sendQueue} = []; $self->{seq} = 0; + $self->{onConnected} = new CallbackList(); $self->{onMessageReceived} = new CallbackList(); $self->{onDialogRequested} = new CallbackList(); @@ -104,38 +106,39 @@ sub iterate { } elsif ($state == STARTING_SERVER) { if (time - $self->{startTime} > RESTART_INTERVAL) { - #print "Starting\n"; + print "Starting\n" if $self->{verbose}; my $starter = $self->{starter}; my $state = $starter->iterate(); if ($state == Bus::Server::Starter::STARTED) { $self->{state} = HANDSHAKING; $self->{host} = $starter->getHost(); $self->{port} = $starter->getPort(); - #print "Bus server started at $self->{host}:$self->{port}\n"; + print "Bus server started at $self->{host}:$self->{port}\n" if $self->{verbose}; $self->reconnect(); $self->{startTime} = time; } elsif ($state == Bus::Server::Starter::FAILED) { # Cannot start; try again. - #print "Start failed.\n"; + print "Start failed.\n" if $self->{verbose}; $self->{starter} = new Bus::Server::Starter(); $self->{startTime} = time; } } } elsif ($state == HANDSHAKING) { - #print "Handshaking\n"; + print "Handshaking\n" if $self->{verbose}; my $ID; my $args = $self->readNext(\$ID); if ($args) { - #print "Sending HELLO\n"; + print "Sending HELLO\n" if $self->{verbose}; $self->{ID} = $args->{yourID}; $self->{client}->send("HELLO", { userAgent => $self->{userAgent}, privateOnly => $self->{privateOnly} }); $self->{state} = CONNECTED; - #print "Connected\n"; + print "Connected\n" if $self->{verbose}; + $self->onConnected->call($self); } } elsif ($state == CONNECTED) { @@ -201,12 +204,12 @@ sub ID { sub reconnect { my ($self) = @_; eval { - #print "(Re)connecting\n"; + print "(Re)connecting\n" if $self->{verbose}; $self->{client} = new Bus::SimpleClient($self->{host}, $self->{port}); $self->{state} = HANDSHAKING; }; if (caught('SocketException')) { - #print "Cannot connect: $@\n"; + print "Cannot connect: $@\n" if $self->{verbose}; $self->{state} = NOT_CONNECTED; $self->{connectTime} = time; } elsif ($@) { @@ -243,7 +246,7 @@ sub readNext { $args = $self->{client}->readNext($MID); }; if (caught('IOException')) { - #print "Disconnected from IPC server.\n"; + print "Disconnected from IPC server.\n" if $self->{verbose}; $self->handleIOException(); return undef; } elsif ($@) { @@ -273,6 +276,7 @@ sub send { $self->{client}->send($MID, $args); }; if (caught('IOException')) { + print "Failed to send $MID: " . $self->handleIOException . "\n"; $self->handleIOException(); push @{$self->{sendQueue}}, [$MID, $args]; return 0; @@ -396,6 +400,16 @@ sub onMessageReceived { return $_[0]->{onMessageReceived}; } +## +# CallbackList $Bus_Client->onConnected() +# +# This event is triggered when the client connects. It may happen more than +# once, if the client has to reconnect for any reason. The event argument +# is undef. +sub onConnected { + return $_[0]->{onConnected}; +} + sub onDialogRequested { return $_[0]->{onDialogRequested}; } diff --git a/src/Bus/MessageParser.pm b/src/Bus/MessageParser.pm index 7f23bb0572..61524e7658 100644 --- a/src/Bus/MessageParser.pm +++ b/src/Bus/MessageParser.pm @@ -20,7 +20,7 @@ sub readNext { my ($self, $ID) = @_; my $processed; my $args = unserialize($self->{buffer}, $ID, \$processed); - if ($args) { + if ($$ID) { substr($self->{buffer}, 0, $processed, ''); } return $args; diff --git a/src/Bus/Messages.pm b/src/Bus/Messages.pm index 4189f77b95..bd9e343f9f 100644 --- a/src/Bus/Messages.pm +++ b/src/Bus/Messages.pm @@ -90,10 +90,10 @@ use Exporter; use base qw(Exporter); use Encode; use Utils::Exceptions; +use JSON::Tiny qw( &decode_json &encode_json ); our @EXPORT_OK = qw(serialize unserialize); - ## # Bytes Bus::Messages::serialize(String ID, arguments) # ID: The message ID. @@ -104,50 +104,10 @@ our @EXPORT_OK = qw(serialize unserialize); # # This symbol is exportable. sub serialize { - my ($ID, $arguments) = @_; - - # Header - my $options = (!$arguments || ref($arguments) eq 'HASH') ? 0 : 1; - my $ID_bytes = toBytes(\$ID); - my $data = pack("N C C a*", - 0, # Message length - $options, # Options - length($$ID_bytes), # ID length - $$ID_bytes); # ID - - if ($options == 0 && $arguments) { - # Key-value map arguments. - my ($key, $value); - while (($key, $value) = each %{$arguments}) { - my $key_bytes = toBytes(\$key); - my ($type, $value_bytes); - $value_bytes = valueToData(\$type, \$value); - - $data .= pack("C a* C a3 a*", - length($$key_bytes), - $$key_bytes, - - $type, - toInt24(length($$value_bytes)), - $$value_bytes - ); - } - - } elsif ($options == 1) { - # Array arguments. - foreach my $entry (@{$arguments}) { - my ($type, $value_bytes); - $value_bytes = valueToData(\$type, \$entry); - $data .= pack("C a3 a*", - $type, - toInt24(length($$value_bytes)), - $$value_bytes - ); - } - } - - substr($data, 0, 4, pack("N", length($data))); - return $data; + my ( $ID, $arguments ) = @_; + my $data = eval { encode_json( { ID => $ID, args => $arguments } ) }; + $data = 'null' if !defined $data; + pack( 'V', 4 + length $data ) . $data; } ## @@ -167,144 +127,18 @@ sub serialize { # This symbol is exportable. sub unserialize { my ($data, $r_ID, $processed) = @_; - my $dataLen = length($data); - return undef if ($dataLen < 4); + my $dataLen = length $data; + return if $dataLen < 4; # Header - my $messageLen = unpack("N", $data); - return undef if ($dataLen < $messageLen); - my ($options, $ID) = unpack("x[N] C C/a", $data); - Encode::_utf8_on($ID); - if (!Encode::is_utf8($ID, 1)) { - UTF8MalformedException->throw("Malformed UTF-8 data in message ID."); - } - - my $offset = 6 + length($ID); - - my $args; - if ($options == 0) { - # Key-value map arguments. - $args = {}; - while ($offset < $messageLen) { - # Key and type. - my ($key, $type) = unpack("x[$offset] C/a C", $data); - Encode::_utf8_on($key); - if (!Encode::_utf8_on($key)) { - UTF8MalformedException->throw("Malformed UTF-8 data in key."); - } - $offset += 2 + length($key); - - # Value length. - my ($valueLen) = substr($data, $offset, 3); - $valueLen = fromInt24($valueLen); - $offset += 3; - - # Value. - my ($value) = substr($data, $offset, $valueLen); - dataToValue($type, \$value); - - $args->{$key} = $value; - $offset += $valueLen; - } - - } else { - # Array arguments. - $args = []; - while ($offset < $messageLen) { - # Type and length. - my ($type, $len) = unpack("x[$offset] C a3", $data); - $len = fromInt24($len); - $offset += 4; - - # Value. - my ($value) = substr($data, $offset, $len); - dataToValue($type, \$value); + my $messageLen = unpack 'V', $data; + return if $dataLen < $messageLen; - push @{$args}, $value; - $offset += $len; - } - } - - $$r_ID = $ID; - $$processed = $messageLen if ($processed); - return $args; -} - -# Converts a String to Bytes, with as little copying as possible. -# -# r_string: A reference to a String. -# Returns: A reference to the UTF-8 data as Bytes. -sub toBytes { - my ($r_string) = @_; - if (Encode::is_utf8($$r_string)) { - my $data = Encode::encode_utf8($$r_string); - return \$data; - } else { - return $r_string; - } -} - -# Bytes toInt24(int i) -# Ensures: length(result) == 3 -# -# Converts a Perl scalar to a 24-bit unsigned big-endian integer. -sub toInt24 { - my ($i) = @_; - return substr(pack("N", $i), 1, 3); -} - -# int fromInt24(Bytes data) -# Requires: length($data) == 3 -# -# Convert a 24-bit unsigned big-endian integer to a Perl scalar. -sub fromInt24 { - my ($data) = @_; - return unpack("N", "\0" . $data); -} - -# Bytes* valueToData(int* type, Scalar* value) -# -# Autodetect the format of $data, and return a reference to a byte -# string, to be used in serializing a message. The data type is -# returned in $type. -sub valueToData { - my ($type, $value) = @_; - if (!defined $$value) { - my $data = ''; - $$type = 0; - return \$data; - } elsif ($$value =~ /^\d+$/) { - # Integer. - $$type = 2; - my $data = pack("N", $$value); - return \$data; - } elsif (Encode::is_utf8($$value)) { - # UTF-8 string. - $$type = 1; - my $data = Encode::encode_utf8($$value); - return \$data; - } else { - # Binary string. - $$type = 0; - return $value; - } -} + my $msg = decode_json( substr $data, 4, $messageLen - 4 ); -sub dataToValue { - my ($type, $r_value) = @_; - if ($type == 1) { - Encode::_utf8_on($$r_value); - if (!Encode::_utf8_on($$r_value)) { - UTF8MalformedException->throw("Malformed UTF-8 data in value."); - } - } elsif ($type == 2) { - if (length($$r_value) == 4) { - $$r_value = unpack("N", $$r_value); - } else { - DataFormatException->throw("Integer value with invalid length (" . - length($$r_value) . ") found."); - } - } + $$r_ID = $msg->{ID}; + $$processed = $messageLen; + $msg->{args}; } # sub testPerformance { diff --git a/src/Bus/Server/AbstractServer.pm b/src/Bus/Server/AbstractServer.pm index 770d14b948..f42032cf47 100644 --- a/src/Bus/Server/AbstractServer.pm +++ b/src/Bus/Server/AbstractServer.pm @@ -123,8 +123,10 @@ sub onClientData { my $parser = $client->{BAS_parser}; $parser->add($data); - my $ID; - while (my $args = $parser->readNext(\$ID)) { + while () { + my $ID; + my $args = $parser->readNext(\$ID); + last if !$ID; $self->messageReceived($client, $ID, $args); } } diff --git a/src/Bus/Server/MainServer.pm b/src/Bus/Server/MainServer.pm index df2c146837..1fe54b0849 100644 --- a/src/Bus/Server/MainServer.pm +++ b/src/Bus/Server/MainServer.pm @@ -131,35 +131,37 @@ sub broadcast { ########### Internal message processors ########### sub processHELLO { - my ($self, $client, $args) = @_; - - if (ref($args) ne 'HASH') { - # Arguments must be a hash. - $self->log("Client $client->{ID} didn't sent HELLO arguments as map."); - $client->close(); + my ( $self, $client, $args ) = @_; - } elsif ($client->{state} == NOT_IDENTIFIED) { - # A new client just connected. - $client->{userAgent} = $args->{userAgent} || "Unknown"; - $client->{privateOnly} = $args->{privateOnly}; - $client->{name} = $args->{userAgent} . ":" . $client->{ID}; - $client->{state} = IDENTIFIED; - - # Broadcast a JOIN message about this client. - $self->log("Client identified as $client->{name}; broadcasting JOIN\n"); - my %args = ( - clientID => $client->{ID}, - name => $client->{name}, - userAgent => $client->{userAgent}, - host => $client->getIP() - ); - $self->broadcast("JOIN", \%args, { exclude => $client->{ID} }); + # Arguments must be a hash. + if ( ref( $args ) ne 'HASH' ) { + $self->log( "Client $client->{ID} didn't sent HELLO arguments as map." ); + $client->close; + return; + } - } else { - # The client sent HELLO even though it has already done that. - $self->log("Client $client->{ID} sent invalid HELLO.\n"); - $client->close(); + # The client sent HELLO even though it has already done that. + if ( $client->{state} != NOT_IDENTIFIED ) { + $self->log( "Client $client->{ID} sent invalid HELLO.\n" ); + $client->close; + return; } + + # A new client just connected. + $client->{userAgent} = $args->{userAgent} || "Unknown"; + $client->{privateOnly} = $args->{privateOnly}; + $client->{name} = $args->{userAgent} . ":" . $client->{ID}; + $client->{state} = IDENTIFIED; + + # Broadcast a JOIN message about this client. + $self->log( "Client identified as $client->{name}; broadcasting JOIN\n" ); + my %args = ( + clientID => $client->{ID}, + name => $client->{name}, + userAgent => $client->{userAgent}, + host => $client->getIP() + ); + $self->broadcast( JOIN => \%args, { exclude => $client->{ID} } ); } sub processLIST_CLIENTS { @@ -187,4 +189,25 @@ sub processLIST_CLIENTS { } } +sub processLIST_CLIENTS2 { + my ( $self, $client, $args ) = @_; + + # Arguments must be undef or a hash. + if ( defined $args && ref( $args ) ne 'HASH' ) { + $self->log( "Client $client->{ID} didn't send LIST_CLIENTS2 arguments as map." ); + $client->close; + return; + } + + my $clients = []; + foreach my $client ( @{ $self->clients } ) { + next if $client->{state} != IDENTIFIED; + next if $args && !$args->{ $client->{ID} }; + push @$clients, { clientID => $client->{ID}, name => "$client->{ID}:$client->{userAgent}", userAgent => $client->{userAgent}, host => $client->getIP }; + } + my $msg = { IRY => 1, clients => $clients }; + $msg->{SEQ} = $args->{SEQ} if exists $args->{SEQ}; + $self->send( $client->{ID}, LIST_CLIENTS2 => $msg ); +} + 1; diff --git a/src/Bus/bus-command.pl b/src/Bus/bus-command.pl new file mode 100755 index 0000000000..2c8f81b7a7 --- /dev/null +++ b/src/Bus/bus-command.pl @@ -0,0 +1,100 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/.."; +use lib "$RealBin/../.."; +use lib "$RealBin/../deps"; + +use Bus::Client; +use Getopt::Long; +use Time::HiRes qw( &sleep &time ); + +my $opt = get_options( + { + timeout => 10, + }, { + 'group|g=s' => 'only the given GROUP will accept the command', + 'party|p=s' => 'only the given PARTY will accept the command', + 'timeout=s' => 'wait at most TIMEOUT seconds for the commands to be sent', + } +); + +my @commands = @ARGV; +usage() if $opt->{help} || !@commands; + +my $bus = Bus::Client->new( userAgent => 'bus-command' ); +$bus->iterate; + +foreach my $command ( @commands ) { + $bus->send( RUN_COMMAND => { command => $command, group => $opt->{group}, party => $opt->{party} } ); + $bus->iterate; +} + +# Wait up to $opt->{timeout} seconds to send the command(s). +my $timeout = { time => time, timeout => $opt->{timeout} }; +while ( !Utils::timeOut( $timeout ) ) { + $bus->iterate; + exit if !@{ $bus->{sendQueue} }; + sleep 0.01; +} + +sub get_options { + my ( $opt_def, $opt_str ) = @_; + + # Add some default options. + $opt_str = { + 'help|h' => 'this help', + %$opt_str, + }; + + # Auto-convert underscored long names to dashed long names. + foreach ( keys %$opt_str ) { + my ( $name, $type ) = split '='; + my @opts = split /\|/, $name; + my ( $underscored ) = grep {/_/} @opts; + my ( $dashed ) = grep {/-/} @opts; + if ( $underscored && !$dashed ) { + $dashed = $underscored; + $dashed =~ s/_/-/g; + splice @opts, ( length( $opts[-1] ) == 1 ? $#opts : @opts ), 0, $dashed; + my $key = join '|', @opts; + $key .= "=$type" if $type; + $opt_str->{$key} = $opt_str->{$_}; + delete $opt_str->{$_}; + } + } + + my $opt = {%$opt_def}; + my $success = GetOptions( $opt, keys %$opt_str ); + usage( $opt_def, $opt_str ) if $opt->{help} || !$success; + + $opt; +} + +sub usage { + my ( $opt_def, $opt_str ) = @_; + my $maxlen = 0; + my $opt = {}; + foreach ( keys %$opt_str ) { + my ( $name, $type ) = split '='; + my ( $var ) = split /\|/, $name; + my ( $long ) = reverse grep { length $_ != 1 } split /\|/, $name; + my ( $short ) = grep { length $_ == 1 } split /\|/, $name; + $maxlen = length $long if $long && $maxlen < length $long; + $opt->{ $long || $short || '' } = { + short => $short, + long => $long, + desc => $opt_str->{$_}, + default => $opt_def->{$var} + }; + } + print "Usage: $0 [options]\n"; + foreach ( map { $opt->{$_} } sort keys %$opt ) { + printf " %2s %-*s %s%s\n", # + $_->{short} ? "-$_->{short}" : '', $maxlen + 2, $_->{long} ? "--$_->{long}" : '', $_->{desc}, $_->{default} ? " (default: $_->{default})" : ""; + } + exit; +}