AnyEvent-Ping

 view release on metacpan or  search on metacpan

lib/AnyEvent/Ping.pm  view on Meta::CPAN

use List::Util ();
require Carp;

my $ICMP_PING = 'ccnnna*';

my $ICMP_ECHOREPLY     = 0;     # Echo Reply
my $ICMP_DEST_UNREACH  = 3;     # Destination Unreachable
my $ICMP_SOURCE_QUENCH = 4;     # Source Quench
my $ICMP_REDIRECT      = 5;     # Redirect (change route)
my $ICMP_ECHO          = 8;     # Echo Request
my $ICMP_TIME_EXCEEDED = 11;    # Time Exceeded

sub new {
    my ($class, %args) = @_;

    my $interval = $args{interval};
    $interval = 0.2 unless defined $interval;

    my $timeout = $args{timeout};
    $timeout = 5 unless defined $timeout;

    my $packet_generator = $args{packet_generator};
    unless (defined $packet_generator) {
        my $packet_size = $args{packet_size};
        $packet_size = 56 unless defined $packet_size;

        $packet_generator = sub {
            &AnyEvent::Ping::generate_data_random($packet_size);
        };
    }

    my $self = bless {
        interval       => $interval,
        timeout        => $timeout,
        packet_generator => $packet_generator
    }, $class;

    # Create RAW socket
    my $socket = IO::Socket::INET->new(
        Proto    => 'icmp',
        Type     => SOCK_RAW,
        Blocking => 0
    ) or Carp::croak "Unable to create icmp socket : $!";

    $self->{_socket} = $socket;

    if (my $on_prepare = $args{on_prepare}) {
        $on_prepare->($socket);
    }

    # Create Poll object
    $self->{_poll_read} = AnyEvent->io(
        fh   => $socket,
        poll => 'r',
        cb   => sub { $self->_on_read },
    );

    # Ping tasks
    $self->{_tasks}     = [];
    $self->{_tasks_out} = [];
    $self->{_timers}    = {};

    return $self;
}

sub interval { @_ > 1 ? $_[0]->{interval} = $_[1] : $_[0]->{interval} }

sub timeout { @_ > 1 ? $_[0]->{timeout} = $_[1] : $_[0]->{timeout} }

sub error { $_[0]->{error} }

sub ping {
    my ($self, $host, $times, $cb) = @_;

    my $socket = $self->{_socket};

    my $ip = inet_aton($host);

    my $request = {
        host        => $host,
        times       => $times,
        results     => [],
        cb          => $cb,
        identifier  => int(rand 0x10000),
        destination => scalar sockaddr_in(0, $ip),
    };

    push @{$self->{_tasks}}, $request;

    push @{$self->{_tasks_out}}, $request;

    $self->_add_write_poll;

    return $self;
}

sub end {
    my $self = shift;

    delete $self->{_poll_read};
    delete $self->{_poll_write};
    delete $self->{_timers};

    while (my $request = pop @{$self->{_tasks}}) {
        $request->{cb}->($request->{results});
    }

    close delete $self->{_socket}
        if exists $self->{_socket};
}

sub generate_data_random {
    my $length = shift;

    my $data = '';
    while ($length > 0) {
        $data .= pack('C', int(rand(256)));
        $length--;
    }

    $data;
}

sub _add_write_poll {
    my $self = shift;

    return if exists $self->{_poll_write};

    $self->{_poll_write} = AnyEvent->io(
        fh   => $self->{_socket},
        poll => 'w',
        cb   => sub { $self->_send_requests },
    );
}

sub _send_requests {
    my $self = shift;

    foreach my $request (@{$self->{_tasks_out}}) {
        $self->_send_request($request);
    }

    $self->{_tasks_out} = [];
    delete $self->{_poll_write};
}

sub _on_read {
    my $self = shift;

    my $socket = $self->{_socket};
    $socket->sysread(my $chunk, 4194304, 0);

    my $icmp_msg = substr $chunk, 20;

    my ($type, $identifier, $sequence, $data);

    $type = unpack 'c', $icmp_msg;

    if ($type == $ICMP_ECHOREPLY) {
        ($type, $identifier, $sequence, $data) =
          (unpack $ICMP_PING, $icmp_msg)[0, 3, 4, 5];
    }
    elsif ($type == $ICMP_DEST_UNREACH || $type == $ICMP_TIME_EXCEEDED) {
        ($identifier, $sequence) = unpack('nn', substr($chunk, 52));
    }
    else {

        # Don't mind
        return;
    }

    # Find our task
    my $request =
      List::Util::first { $identifier == $_->{identifier} }
    @{$self->{_tasks}};

    return unless $request;

    # Is it response to our latest message?
    return unless $sequence == @{$request->{results}} + 1;

    if ($type == $ICMP_ECHOREPLY) {

        # Check data
        if ($data eq $request->{data}) {
            $self->_store_result($request, 'OK');
        }
        else {
            $self->_store_result($request, 'MALFORMED');
        }
    }
    elsif ($type == $ICMP_DEST_UNREACH) {
        $self->_store_result($request, 'DEST_UNREACH');
    }
    elsif ($type == $ICMP_TIME_EXCEEDED) {
        $self->_store_result($request, 'TIMEOUT');
    }
}

sub _store_result {
    my ($self, $request, $result) = @_;

    my $results = $request->{results};

    # Clear request specific data
    delete $self->{_timers}->{$request};

    push @$results, [$result, time - $request->{start}];

    if (@$results == $request->{times} || $result eq 'ERROR') {

        # Cleanup
        my $tasks = $self->{_tasks};
        for my $i (0 .. scalar @$tasks) {
            if ($tasks->[$i] == $request) {
                splice @$tasks, $i, 1;
                last;
            }
        }

        # Testing done
        $request->{cb}->($results);

        undef $request;
    }

    # Perform another check
    else {

        # Setup interval timer before next request
        $self->{_timers}{$request} = AnyEvent->timer(
            after => $self->interval,
            cb    => sub {
                delete $self->{_timers}{$request};
                push @{$self->{_tasks_out}}, $request;
                $self->_add_write_poll;
            }
        );
    }
}

sub _send_request {
    my ($self, $request) = @_;

    my $checksum   = 0x0000;
    my $identifier = $request->{identifier};
    my $sequence   = @{$request->{results}} + 1;
    my $data       = $self->{packet_generator}->();

    my $msg = pack $ICMP_PING,
      $ICMP_ECHO, 0x00, $checksum,
      $identifier, $sequence, $data;

    $checksum = $self->_icmp_checksum($msg);

    $msg = pack $ICMP_PING,
      0x08, 0x00, $checksum,
      $identifier, $sequence, $data;

    $request->{data} = $data;

    $request->{start} = time;

    $self->{_timers}->{$request}->{timer} = AnyEvent->timer(
        after => $self->timeout,
        cb    => sub {
            $self->_store_result($request, 'TIMEOUT');
        }
    );

    my $socket = $self->{_socket};

    $socket->send($msg, 0, $request->{destination}) or
        $self->_store_result($request, 'ERROR');
}

sub _icmp_checksum {
    my ($self, $msg) = @_;

    my $res = 0;
    foreach my $int (unpack "n*", $msg) {
        $res += $int;
    }

    # Add possible odd byte
    $res += unpack('C', substr($msg, -1, 1)) << 8
      if length($msg) % 2;

    # Fold high into low
    $res = ($res >> 16) + ($res & 0xffff);

    # Two times
    $res = ($res >> 16) + ($res & 0xffff);

    return ~$res;
}

1;
__END__

=head1 NAME

AnyEvent::Ping - ping hosts with AnyEvent

=head1 SYNOPSIS

    use AnyEvent;
    use AnyEvent::Ping;

    my $host  = shift || '4.2.2.2';
    my $times = shift || 4;
    my $package_s = shift || 56;
    my $c = AnyEvent->condvar;

    my $ping = AnyEvent::Ping->new;

    print "PING $host $package_s(@{[$package_s+8]}) bytes of data\n";
    $ping->ping($host, $times, sub {
        my $results = shift;
        foreach my $result (@$results) {
            my $status = $result->[0];
            my $time   = $result->[1];
            printf "%s from %s: time=%f ms\n", 
                $status, $host, $time * 1000;



( run in 1.728 second using v1.01-cache-2.11-cpan-39bf76dae61 )