Asterisk-AMI
view release on metacpan or search on metacpan
lib/Asterisk/AMI.pm view on Meta::CPAN
This checks the connection to the AMI to ensure it is still functional. It checks at the socket layer and
also sends a 'PING' to the AMI to ensure it is still responding. If no TIMEOUT is given this will block
waiting for a response.
Returns 1 if the connection is good, 0 if it is not.
error ()
Returns 1 if there are currently errors on the socket, 0 if everything is ok.
destroy ()
Destroys the contents of all buffers and removes any current callbacks that are set.
Mostly used internally. Useful if you want to ensure that our IO handle watcher gets removed.
Gets called automatically when our object goes out of scope.
loop ()
Starts an event loop via AnyEvent.
break ()
Breaks/exits the current event loop. The program will continue from where the event loop was invoked.
=head1 See Also
AnyEvent, Asterisk::AMI::Common, Asterisk::AMI::Common::Dev
=head1 AUTHOR
Ryan Bullock (rrb3942@gmail.com)
=head1 BUG REPORTING AND FEEDBACK
Please report any bugs or errors to our github issue tracker at http://github.com/rrb3942/perl-Asterisk-AMI/issues or
the cpan request tracker at https://rt.cpan.org/Public/Bug/Report.html?Queue=perl-Asterisk-AMI
=head1 LICENSE
Copyright (C) 2011 by Ryan Bullock (rrb3942@gmail.com)
This module is free software. You can redistribute it and/or modify it under the terms of the Artistic License 2.0.
This program is distributed in the hope that it will be useful, but without any warranty; without even the implied
warranty of merchantability or fitness for a particular purpose.
=cut
package Asterisk::AMI;
#Register warnings
use warnings::register;
use strict;
use warnings;
use AnyEvent;
use AnyEvent::Handle;
use AnyEvent::Socket;
use Digest::MD5;
use Scalar::Util qw/weaken/;
use Carp qw/carp/;
#Duh
use version 0.77; our $VERSION = version->declare("v0.2.8");
#Used for storing events while reading command responses Events are stored as hashes in the array Example
#$self->{EVETNBUFFER}->{'Event'} = Something
#Buffer for holding action responses and data
# Structure: $self->{RESPONSEBUFFER}->{'ActionID'}->{'Response'} = (Success|Failure|Follows|Goodbye|Pong|Etc..)
# //Reponse Status
# {'Message'} = Message //Message in the response {'EVENTS'} = [%hash1, %hash2, ..] //Arry
# of Hashes of parsed events and data for this actionID {'PARSED'} = { Hashkey => value,
# ...} {'COMPLETED'} = 0 or 1 //If the command is completed {'GOOD'} = 0 or 1 //if this
# responses is good, no error, can only be 1 if also COMPLETED
#Create a new object and return it; If required options are missing, returns undef
sub new {
my ($class, %values) = @_;
my $self = bless {}, $class;
#Configure our new object and connect, else return undef
if ($self->_configure(%values) && $self->_connect()) {
return $self;
}
return;
}
#Used by anyevent to load our read type
sub anyevent_read_type {
my ($hdl, $cb) = @_;
return sub {
if ($hdl->{rbuf} =~ s/^(.+)(?:\015\012\015\012)//sox) {
$cb->($hdl, $1);
}
return 0;
}
}
#Sets variables for this object Also checks for minimum settings Returns 1 if everything was set, 0 if options were
#missing
sub _configure {
my ($self, %config) = @_;
#Required settings
my @required = ( 'USERNAME', 'SECRET' );
#Defaults
my %defaults = ( PEERADDR => '127.0.0.1',
PEERPORT => 5038,
AUTHTYPE => 'plaintext',
EVENTS => 'off',
BUFFERSIZE => 30000,
BLOCKING => 1
);
lib/Asterisk/AMI.pm view on Meta::CPAN
return;
} else {
#lowercase it for consistency
$val = $lval;
}
}
#Ensure all handlers are sub refs
} elsif ($opt eq 'HANDLERS') {
while (my ($event, $handler) = each %{$val}) {
if (ref($handler) ne 'CODE') {
carp "Handler for event type \'$event\' must be an anonymous subroutine or a subroutine reference" if warnings::enabled('Asterisk::AMI');
return;
}
}
}
$self->{CONFIG}->{$opt} = $val;
}
#Check for required options
foreach my $req (@required) {
if (!exists $self->{CONFIG}->{$req}) {
carp "Must supply a username and secret for connecting to asterisk" if warnings::enabled('Asterisk::AMI');
return;
}
}
#Change default port if using ssl
if ($self->{CONFIG}->{USESSL}) {
$defaults{PEERPORT} = 5039;
}
#Assign defaults for any missing options
while (my ($opt, $val) = each(%defaults)) {
if (!defined $self->{CONFIG}->{$opt}) {
$self->{CONFIG}->{$opt} = $val;
}
}
#Make adjustments for Originate Async bullscrap
if ($self->{CONFIG}->{ORIGINATEHACK}) {
#Turn on call events, otherwise we wont get the Async response
if (lc($self->{CONFIG}->{EVENTS}) eq 'off') {
$self->{CONFIG}->{EVENTS} = 'call';
#Fake event type so that we will discard events, else by turning on events our event buffer
#Will just continue to fill up.
$self->{CONFIG}->{HANDLERS} = { 'JUSTMAKETHEHASHNOTEMPTY' => sub {} } unless ($self->{CONFIG}->{HANDLERS});
#They already turned events on, just add call types to it, assume they are doing something with events
#and don't mess with the handlers
} elsif (lc($self->{CONFIG}->{EVENTS}) !~ /on|call/x) {
$self->{CONFIG}->{EVENTS} .= ',call';
}
}
#Initialize the seq number
$self->{idseq} = 1;
#Weaken reference for use in anonsub
weaken($self);
#Set keepalive
$self->{CONFIG}->{KEEPALIVE} = AE::timer($self->{CONFIG}->{KEEPALIVE}, $self->{CONFIG}->{KEEPALIVE}, sub { $self->_send_keepalive }) if ($self->{CONFIG}->{KEEPALIVE});
return 1;
}
#Handles connection failures (includes login failure);
sub _on_connect_err {
my ($self, $message) = @_;
warnings::warnif('Asterisk::AMI', "Failed to connect to asterisk - $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
warnings::warnif('Asterisk::AMI', "Error Message: $message");
#Dispatch all callbacks as if they timed out
$self->_clear_cbs();
if (exists $self->{CONFIG}->{ON_CONNECT_ERR}) {
$self->{CONFIG}->{ON_CONNECT_ERR}->($self, $message);
} elsif (exists $self->{CONFIG}->{ON_ERROR}) {
$self->{CONFIG}->{ON_ERROR}->($self, $message);
}
$self->{SOCKERR} = 1;
$self->destroy();
return;
}
#Handles other errors on the socket
sub _on_error {
my ($self, $message) = @_;
warnings::warnif('Asterisk::AMI', "Received Error on socket - $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
warnings::warnif('Asterisk::AMI', "Error Message: $message");
#Call all cbs as if they had timed out
$self->_clear_cbs();
$self->{CONFIG}->{ON_ERROR}->($self, $message) if (exists $self->{CONFIG}->{ON_ERROR});
$self->{SOCKERR} = 1;
$self->destroy();
return;
}
#Handles the remote end disconnecting
sub _on_disconnect {
my ($self) = @_;
my $message = "Remote end disconnected - $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}";
warnings::warnif('Asterisk::AMI', "Remote Asterisk Server ended connection - $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
#Call all callbacks as if they had timed out
_
$self->_clear_cbs();
if (exists $self->{CONFIG}->{ON_DISCONNECT}) {
$self->{CONFIG}->{ON_DISCONNECT}->($self, $message);
} elsif (exists $self->{CONFIG}->{ON_ERROR}) {
$self->{CONFIG}->{ON_ERROR}->($self, $message);
}
$self->{SOCKERR} = 1;
$self->destroy();
return;
}
#What happens if our keep alive times out
sub _on_timeout {
my ($self, $message) = @_;
warnings::warnif('Asterisk::AMI', $message);
if (exists $self->{CONFIG}->{ON_TIMEOUT}) {
$self->{CONFIG}->{ON_TIMEOUT}->($self, $message);
} elsif (exists $self->{CONFIG}->{ON_ERROR}) {
$self->{CONFIG}->{ON_ERROR}->($self, $message);
}
$self->{SOCKERR} = 1;
return;
}
#Things to do after our initial connect
sub _on_connect {
my ($self, $fh, $line) = @_;
if ($line =~ /^Asterisk\ Call\ Manager\/([0-9]\.[0-9])$/ox) {
$self->{AMIVER} = $1;
} else {
warnings::warnif('Asterisk::AMI', "Unknown Protocol/AMI Version from $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
}
#Weak reference for us in anonysub
weaken($self);
$self->{handle}->push_read( 'Asterisk::AMI' => sub { $self->_handle_packet(@_); } );
return 1;
}
#Connects to the AMI Returns 1 on success, 0 on failure
sub _connect {
my ($self) = @_;
#Weaken ref for use in anonysub
weaken($self);
#Build a hash of our anyevent::handle options
my %hdl = ( connect => [$self->{CONFIG}->{PEERADDR} => $self->{CONFIG}->{PEERPORT}],
on_connect_err => sub { $self->_on_connect_err($_[1]); },
on_error => sub { $self->_on_error($_[2]) },
on_eof => sub { $self->_on_disconnect; },
on_connect => sub { $self->{handle}->push_read( line => sub { $self->_on_connect(@_); } ); });
#TLS stuff
$hdl{'tls'} = 'connect' if ($self->{CONFIG}->{USESSL});
#TCP Keepalive
$hdl{'keeplive'} = 1 if ($self->{CONFIG}->{TCP_KEEPALIVE});
#Make connection/create handle
$self->{handle} = AnyEvent::Handle->new(%hdl);
#Return login status if blocking
return $self->_login if ($self->{CONFIG}->{BLOCKING});
#Queue our login
$self->_login;
#If we have a handle, SUCCESS!
return 1 if (defined $self->{handle});
return;
}
sub _handle_packet {
my ($self, $hdl, $buffer) = @_;
foreach my $packet (split /\015\012\015\012/ox, $buffer) {
my %parsed;
foreach my $line (split /\015\012/ox, $packet) {
#Is this our command output?
if ($line =~ s/--END\ COMMAND--$//ox) {
$parsed{'COMPLETED'} = 1;
push(@{$parsed{'CMD'}},split(/\x20*\x0A/ox, $line));
} else {
#Regular output, split on :\
my ($key, $value) = split /:\ /x, $line, 2;
$parsed{$key} = $value;
}
}
#Dispatch depending on packet type
if (exists $parsed{'ActionID'}) {
$self->_handle_action(\%parsed);
} elsif (exists $parsed{'Event'}) {
$self->_handle_event(\%parsed);
}
}
return 1;
}
lib/Asterisk/AMI.pm view on Meta::CPAN
}
}
}
if ($self->{RESPONSEBUFFER}->{$actionid}->{'COMPLETED'}) {
#This aciton is finished do not accept any more packets for it
delete $self->{EXPECTED}->{$actionid};
#Determine goodness, do callback
$self->_action_complete($actionid);
}
return 1;
}
#Handles proccessing and callbacks for 'Event' packets
sub _handle_event {
my ($self, $event) = @_;
#If handlers were configured just dispatch, don't buffer
if ($self->{CONFIG}->{HANDLERS}) {
if (exists $self->{CONFIG}->{HANDLERS}->{$event->{'Event'}}) {
$self->{CONFIG}->{HANDLERS}->{$event->{'Event'}}->($self, $event);
} elsif (exists $self->{CONFIG}->{HANDLERS}->{'default'}) {
$self->{CONFIG}->{HANDLERS}->{'default'}->($self, $event);
}
} else {
#Someone is waiting on this packet, don't bother buffering
if (exists $self->{CALLBACKS}->{'EVENT'}) {
$self->{CALLBACKS}->{'EVENT'}->{'cb'}->($event);
delete $self->{CALLBACKS}->{'EVENT'};
#Save for later
} else {
push(@{$self->{EVENTBUFFER}}, $event);
}
}
return 1;
}
#This is used to provide blocking behavior for calls It installs callbacks for an action if it is not in the buffer
#and waits for the response before returning it.
sub _wait_response {
my ($self, $id, $timeout) = @_;
#Already got it?
if ($self->{RESPONSEBUFFER}->{$id}->{'COMPLETED'}) {
my $resp = $self->{RESPONSEBUFFER}->{$id};
delete $self->{RESPONSEBUFFER}->{$id};
delete $self->{CALLBACKS}->{$id};
delete $self->{EXPECTED}->{$id};
return $resp;
}
#Don't Have it, wait for it Install some handlers and use a CV to simulate blocking
my $process = AE::cv;
$self->{CALLBACKS}->{$id}->{'cb'} = sub { $process->send($_[1]) };
$timeout = $self->{CONFIG}->{TIMEOUT} unless (defined $timeout);
#Should not need to weaken here because this is a blocking call Only outcomes can be error, timeout, or
#complete, all of which will finish the cb and clear the reference weaken($self)
if ($timeout) {
$self->{CALLBACKS}->{$id}->{'timeout'} = sub {
my $response = $self->{'RESPONSEBUFFER'}->{$id};
delete $self->{RESPONSEBUFFER}->{$id};
delete $self->{CALLBACKS}->{$id};
delete $self->{EXPECTED}->{$id};
$process->send($response);
};
#Make sure event loop is up to date in case of sleeps
AE::now_update;
$self->{CALLBACKS}->{$id}->{'timer'} = AE::timer $timeout, 0, $self->{CALLBACKS}->{$id}->{'timeout'};
}
return $process->recv;
}
sub _build_action {
my ($actionhash, $id) = @_;
my $action;
my $async;
my $callback;
my $timeout;
#Create an action out of a hash
while (my ($key, $value) = each(%{$actionhash})) {
my $lkey = lc($key);
#Callbacks
if ($key eq 'CALLBACK') {
carp "Use of the CALLBACK key in an action is deprecated and will be removed in a future release.\n",
"Please use the syntax that is available." if warnings::enabled('Asterisk::AMI');
$callback = $actionhash->{$key} unless (defined $callback);
next;
#Timeout
} elsif ($key eq 'TIMEOUT') {
carp "Use of the TIMEOUT key in an action is deprecated and will be removed in a future release\n",
"Please use the syntax that is available." if warnings::enabled('Asterisk::AMI');
$timeout = $actionhash->{$key} unless (defined $timeout);
next;
#Exception of Orignate Async
} elsif ($lkey eq 'async' && $value == 1) {
$async = 1;
#Clean out user ActionIDs
} elsif ($lkey eq 'actionid') {
carp "User supplied ActionID being ignored." if warnings::enabled('Asterisk::AMI');
next;
}
#Handle multiple values
if (ref($value) eq 'ARRAY') {
foreach my $var (@{$value}) {
$action .= $key . ': ' . $var . "\015\012";
}
} else {
$action .= $key . ': ' . $value . "\015\012";
}
}
#Append ActionID and End Command
$action .= 'ActionID: ' . $id . "\015\012\015\012";
return ($action, $async, $callback, $timeout);
}
#Sends an action to the AMI Accepts an Array Returns the actionid of the action
sub send_action {
my ($self, $actionhash, $callback, $timeout, $store) = @_;
#No connection
return unless ($self->{handle});
#resets id number
if ($self->{idseq} > $self->{CONFIG}->{BUFFERSIZE}) {
$self->{idseq} = 1;
}
my $id = $self->{idseq}++;
#Store the Action ID
$self->{lastid} = $id;
#Delete anything that might be in the buffer
delete $self->{RESPONSEBUFFER}->{$id};
delete $self->{CALLBACKS}->{$id};
my ($action, $hcb, $htimeout);
($action, $self->{RESPONSEBUFFER}->{$id}->{'ASYNC'}, $hcb, $htimeout) = _build_action($actionhash, $id);
$callback = $hcb unless (defined $callback);
$timeout = $htimeout unless (defined $timeout);
if ($self->{LOGGEDIN} || lc($actionhash->{'Action'}) =~ /login|challenge/x) {
$self->{handle}->push_write($action);
} else {
$self->{PRELOGIN}->{$id} = $action;
}
$self->{RESPONSEBUFFER}->{$id}->{'COMPLETED'} = 0;
$self->{RESPONSEBUFFER}->{$id}->{'GOOD'} = 0;
$self->{EXPECTED}->{$id} = 1;
#Weaken ref of use in anonsub
weaken($self);
#Set default timeout if needed
$timeout = $self->{CONFIG}->{TIMEOUT} unless (defined $timeout);
#Setup callback
if (defined $callback) {
#Set callback if defined
$self->{CALLBACKS}->{$id}->{'cb'} = $callback;
#Variable to return with Callback
$self->{CALLBACKS}->{$id}->{'store'} = $store;
}
#Start timer for timeouts
if ($timeout && defined $self->{CALLBACKS}->{$id}) {
$self->{CALLBACKS}->{$id}->{'timeout'} = sub {
my $response = $self->{RESPONSEBUFFER}->{$id};
my $cb = $self->{CALLBACKS}->{$id}->{'cb'};
my $st = $self->{CALLBACKS}->{$id}->{'store'};
delete $self->{RESPONSEBUFFER}->{$id};
delete $self->{CALLBACKS}->{$id};
delete $self->{EXPECTED}->{$id};
delete $self->{PRELOGIN}->{$id};
$cb->($self, $response, $st);;
};
$self->{CALLBACKS}->{$id}->{'timer'} = AE::timer $timeout, 0, $self->{CALLBACKS}->{$id}->{'timeout'};
}
return $id;
}
#Checks for a response to an action If no actionid is given uses last actionid sent Returns 1 if action success, 0 if
#failure
sub check_response {
my ($self, $actionid, $timeout) = @_;
#Check if an actionid was passed, else us last
$actionid = $self->{lastid} unless (defined $actionid);
my $resp = $self->_wait_response($actionid, $timeout);
if ($resp->{'COMPLETED'}) {
return $resp->{'GOOD'};
}
return;
}
#Returns the Action with all command data and event Actions are hash references If an actionid is specified returns
#that action, otherwise uses last actionid sent Removes the event from the buffer
sub get_response {
my ($self, $actionid, $timeout) = @_;
#Check if an actionid was passed, else us last
$actionid = $self->{lastid} unless (defined $actionid);
#Wait for the action to complete
my $resp = $self->_wait_response($actionid, $timeout);
if ($resp->{'COMPLETED'}) {
return $resp;
lib/Asterisk/AMI.pm view on Meta::CPAN
delete $self->{PRELOGIN};
return 1;
} else {
#Login failed
if ($login->{'COMPLETED'}) {
$self->_on_connect_err("Login Failed to Asterisk (bad auth) at $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
} else {
$self->_on_connect_err("Login Failed to Asterisk due to timeout at $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
}
return;
}
return;
}
#Blocking Login
sub _login_block {
my ($self, $action, $challenge, $timeout) = @_;
my $resp;
#If a challenge exists do handle it first before the login
if (%{$challenge}) {
#Get challenge response
my $chresp = $self->action($challenge,$timeout);
if ($chresp->{'GOOD'}) {
$action->{'Key'} = $self->_md5_resp($chresp->{'PARSED'}->{'Challenge'}, $self->{CONFIG}->{SECRET});
$action->{'AuthType'} = $self->{CONFIG}->{AUTHTYPE};
#Login
$resp = $self->action($action, $timeout);
} else {
#Challenge Failed
if ($chresp->{'COMPLETED'}) {
$self->_on_connect_err("$self->{CONFIG}->{AUTHTYPE} challenge failed");
} else {
$self->_on_connect_err("Timed out waiting for challenge");
}
return;
}
} else {
#Plaintext login
$resp = $self->action($action, $timeout);
}
return $self->_logged_in($resp);
}
#Non-blocking login
sub _login_noblock {
my ($self, $action, $challenge, $timeout) = @_;
#Weaken ref for use in anonsub
weaken($self);
#Callback for login action
my $login_cb = sub { $self->_logged_in($_[1]) };
#Do a md5 challenge
if (%{$challenge}) {
#Create callbacks for the challenge
my $challenge_cb = sub {
if ($_[1]->{'GOOD'}) {
my $md5 = Digest::MD5->new();
$md5->add($_[1]->{'PARSED'}->{'Challenge'});
$md5->add($self->{CONFIG}->{SECRET});
$md5 = $md5->hexdigest;
$action->{'Key'} = $md5;
$action->{'AuthType'} = $self->{CONFIG}->{AUTHTYPE};
$self->send_action($action, $login_cb, $timeout);
} else {
if ($_[1]->{'COMPLETED'}) {
$self->_on_connect_err("$self->{CONFIG}->{AUTHTYPE} challenge failed");
} else {
$self->_on_connect_err("Timed out waiting for challenge");
}
return;
}
};
#Send challenge
$self->send_action($challenge, $challenge_cb, $timeout);
} else {
#Plaintext login
$self->send_action($action, $login_cb, $timeout);
}
return 1;
}
#Disconnect from the AMI If logged in will first issue a logoff
sub disconnect {
my ($self) = @_;
$self->destroy();
#No socket? No Problem.
return 1;
}
#Pops the topmost event out of the buffer and returns it Events are hash references
sub get_event {
my ($self, $timeout) = @_;
#my $timeout = $_[1];
$timeout = $self->{CONFIG}->{TIMEOUT} unless (defined $timeout);
unless (defined $self->{EVENTBUFFER}->[0]) {
my $process = AE::cv;
$self->{CALLBACKS}->{'EVENT'}->{'cb'} = sub { $process->send($_[0]) };
$self->{CALLBACKS}->{'EVENT'}->{'timeout'} = sub { warnings::warnif('Asterisk::AMI', "Timed out waiting for event"); $process->send(undef); };
$timeout = $self->{CONFIG}->{TIMEOUT} unless (defined $timeout);
if ($timeout) {
#Make sure event loop is up to date in case of sleeps
AE::now_update;
$self->{CALLBACKS}->{'EVENT'}->{'timer'} = AE::timer $timeout, 0, $self->{CALLBACKS}->{'EVENT'}->{'timeout'};
}
return $process->recv;
}
return shift @{$self->{EVENTBUFFER}};
}
#Returns server AMI version
sub amiver {
my ($self) = @_;
return $self->{AMIVER};
}
#Checks the connection, returns 1 if the connection is good
sub connected {
my ($self, $timeout) = @_;
if ($self && $self->simple_action({ Action => 'Ping'}, $timeout)) {
return 1;
}
return 0;
}
#Check whether there was an error on the socket
sub error {
my ($self) = @_;
return $self->{SOCKERR};
}
#Sends a keep alive
sub _send_keepalive {
my ($self) = @_;
#Weaken ref for use in anonysub
weaken($self);
my $cb = sub {
unless ($_[1]->{'GOOD'}) {
$self->_on_timeout("Asterisk failed to respond to keepalive - $self->{CONFIG}->{PEERADDR}:$self->{CONFIG}->{PEERPORT}");
};
};
my $timeout = $self->{CONFIG}->{TIMEOUT} || 5;
return $self->send_action({ Action => 'Ping' }, $cb, $timeout);
}
#Calls all callbacks as if they had timed out Used when an error has occured on the socket
sub _clear_cbs {
my ($self) = @_;
foreach my $id (keys %{$self->{CALLBACKS}}) {
my $response = $self->{RESPONSEBUFFER}->{$id};
my $callback = $self->{CALLBACKS}->{$id}->{'cb'};
my $store = $self->{CALLBACKS}->{$id}->{'store'};
delete $self->{RESPONSEBUFFER}->{$id};
delete $self->{CALLBACKS}->{$id};
delete $self->{EXPECTED}->{$id};
$callback->($self, $response, $store);
}
return 1;
}
#Cleans up
sub destroy {
my ($self) = @_;
$self->DESTROY;
bless $self, "Asterisk::AMI::destroyed";
return 1;
}
#Run event loop via anyevent
sub loop {
$_[0]->{tmp_loop} = AnyEvent->condvar;
my $exit = $_[0]->{tmp_loop}->recv;
delete $_[0]->{tmp_loop};
return $exit;
}
#Ends exits loops
sub break {
if (defined $_[0]->{tmp_loop}) {
$_[0]->{tmp_loop}->send(1);
}
}
#Bye bye
sub DESTROY {
my ($self) = @_;
#Logoff if we are not in error
if (!$self->{SOCKERR} && $self->{LOGGEDIN}) {
( run in 1.593 second using v1.01-cache-2.11-cpan-c221a9de4ec )