File Coverage

blib/lib/AnyEvent/Ping.pm
Criterion Covered Total %
statement 124 146 84.9
branch 19 46 41.3
condition 3 6 50.0
subroutine 25 28 89.2
pod 6 7 85.7
total 177 233 75.9


line stmt bran cond sub pod time code
1             package AnyEvent::Ping;
2              
3 1     1   703 use strict;
  1         2  
  1         25  
4 1     1   5 use warnings;
  1         1  
  1         28  
5 1     1   20 use 5.008_001;
  1         11  
6              
7             our $VERSION = 0.010;
8              
9 1     1   1007 use Socket qw/SOCK_RAW/;
  1         3780  
  1         213  
10 1     1   13173 use Time::HiRes 'time';
  1         1663  
  1         5  
11 1     1   8563 use IO::Socket::INET qw/sockaddr_in inet_aton/;
  1         25232  
  1         10  
12 1     1   93 use List::Util ();
  1         2  
  1         1650  
13             require Carp;
14              
15             my $ICMP_PING = 'ccnnna*';
16              
17             my $ICMP_ECHOREPLY = 0; # Echo Reply
18             my $ICMP_DEST_UNREACH = 3; # Destination Unreachable
19             my $ICMP_SOURCE_QUENCH = 4; # Source Quench
20             my $ICMP_REDIRECT = 5; # Redirect (change route)
21             my $ICMP_ECHO = 8; # Echo Request
22             my $ICMP_TIME_EXCEEDED = 11; # Time Exceeded
23              
24             sub new {
25 2     2 1 2579 my ($class, %args) = @_;
26              
27 2         5 my $interval = $args{interval};
28 2 50       10 $interval = 0.2 unless defined $interval;
29              
30 2         3 my $timeout = $args{timeout};
31 2 100       6 $timeout = 5 unless defined $timeout;
32              
33 2         4 my $packet_generator = $args{packet_generator};
34 2 50       6 unless (defined $packet_generator) {
35 2         3 my $packet_size = $args{packet_size};
36 2 50       6 $packet_size = 56 unless defined $packet_size;
37              
38             $packet_generator = sub {
39 19     19   65 &AnyEvent::Ping::generate_data_random($packet_size);
40 2         7 };
41             }
42              
43 2         10 my $self = bless {
44             interval => $interval,
45             timeout => $timeout,
46             packet_generator => $packet_generator
47             }, $class;
48              
49 2         6 my $socket = $self->_create_socket();
50              
51 2         1028 $self->{_socket} = $socket;
52              
53 2 100       8 if (my $on_prepare = $args{on_prepare}) {
54 1         4 $on_prepare->($socket);
55             }
56              
57             # Create Poll object
58             $self->{_poll_read} = AnyEvent->io(
59             fh => $socket,
60             poll => 'r',
61 52     52   4411 cb => sub { $self->_on_read },
62 2         20 );
63              
64             # Ping tasks
65 2         5051 $self->{_tasks} = [];
66 2         5 $self->{_tasks_out} = [];
67 2         7 $self->{_timers} = {};
68              
69 2         7 return $self;
70             }
71              
72             sub _create_socket {
73 2     2   3 my $self = shift;
74              
75 2 50       16 IO::Socket::INET->new(
76             Proto => 'icmp',
77             Type => SOCK_RAW,
78             Blocking => 0
79             ) or Carp::croak "Unable to create icmp socket : $!";
80             }
81              
82 13 50   13 1 280 sub interval { @_ > 1 ? $_[0]->{interval} = $_[1] : $_[0]->{interval} }
83              
84 19 50   19 1 217 sub timeout { @_ > 1 ? $_[0]->{timeout} = $_[1] : $_[0]->{timeout} }
85              
86 0     0 1 0 sub error { $_[0]->{error} }
87              
88             sub ping {
89 6     6 1 15053 my ($self, $host, $times, $cb) = @_;
90              
91 6         18 my $socket = $self->{_socket};
92              
93 6         53 my $ip = inet_aton($host);
94              
95 6         83 my $request = {
96             host => $host,
97             times => $times,
98             results => [],
99             cb => $cb,
100             identifier => int(rand 0x10000),
101             destination => scalar sockaddr_in(0, $ip),
102             };
103              
104 6         99 push @{$self->{_tasks}}, $request;
  6         21  
105              
106 6         12 push @{$self->{_tasks_out}}, $request;
  6         17  
107              
108 6         23 $self->_add_write_poll;
109              
110 6         79 return $self;
111             }
112              
113             sub end {
114 2     2 1 2214 my $self = shift;
115              
116 2         24 delete $self->{_poll_read};
117 2         5 delete $self->{_poll_write};
118 2         14 delete $self->{_timers};
119              
120 2         6 while (my $request = pop @{$self->{_tasks}}) {
  3         27  
121 1         8 $request->{cb}->($request->{results});
122             }
123              
124             close delete $self->{_socket}
125 2 50       105 if exists $self->{_socket};
126             }
127              
128             sub generate_data_random {
129 20     20 0 7571 my $length = shift;
130              
131 20         69 my $data = '';
132 20         85 while ($length > 0) {
133 1084         2689 $data .= pack('C', int(rand(256)));
134 1084         2650 $length--;
135             }
136              
137 20         82 $data;
138             }
139              
140             sub _add_write_poll {
141 19     19   61 my $self = shift;
142              
143 19 100       191 return if exists $self->{_poll_write};
144              
145             $self->{_poll_write} = AnyEvent->io(
146             fh => $self->{_socket},
147             poll => 'w',
148 11     11   389 cb => sub { $self->_send_requests },
149 11         215 );
150             }
151              
152             sub _send_requests {
153 11     11   35 my $self = shift;
154              
155 11         25 foreach my $request (@{$self->{_tasks_out}}) {
  11         66  
156 19         1254 $self->_send_request($request);
157             }
158              
159 11         795 $self->{_tasks_out} = [];
160 11         157 delete $self->{_poll_write};
161             }
162              
163             sub _on_read {
164 52     52   124 my $self = shift;
165              
166 52         128 my $socket = $self->{_socket};
167 52         270 $socket->sysread(my $chunk, 4194304, 0);
168              
169 52         1674 my ($request, $type, $data) = $self->_process_chunk($chunk);
170 0 0       0 return unless $request;
171              
172 0 0       0 if ($type == $ICMP_ECHOREPLY) {
    0          
    0          
173              
174             # Check data
175 0 0       0 if ($data eq $request->{data}) {
176 0         0 $self->_store_result($request, 'OK');
177             }
178             else {
179 0         0 $self->_store_result($request, 'MALFORMED');
180             }
181             }
182             elsif ($type == $ICMP_DEST_UNREACH) {
183 0         0 $self->_store_result($request, 'DEST_UNREACH');
184             }
185             elsif ($type == $ICMP_TIME_EXCEEDED) {
186 0         0 $self->_store_result($request, 'TIMEOUT');
187             }
188             }
189              
190             sub _process_chunk_to_request {
191 0     0   0 my ($self, $chunk) = @_;
192              
193 0         0 my $icmp_msg = substr $chunk, 20;
194              
195 0         0 my ($type, $identifier, $sequence, $data);
196              
197 0         0 $type = unpack 'c', $icmp_msg;
198              
199 0 0 0     0 if ($type == $ICMP_ECHOREPLY) {
    0          
200 0         0 ($type, $identifier, $sequence, $data) =
201             (unpack $ICMP_PING, $icmp_msg)[0, 3, 4, 5];
202             }
203             elsif ($type == $ICMP_DEST_UNREACH || $type == $ICMP_TIME_EXCEEDED) {
204 0         0 ($identifier, $sequence) = unpack('nn', substr($chunk, 52));
205             }
206             else {
207              
208             # Don't mind
209 0         0 return;
210             }
211              
212             # Find our task
213             my $request =
214 0     0   0 List::Util::first { $identifier == $_->{identifier} }
215 0         0 @{$self->{_tasks}};
  0         0  
216              
217 0 0       0 return unless $request;
218              
219             # Is it response to our latest message?
220 0 0       0 return unless $sequence == @{$request->{results}} + 1;
  0         0  
221              
222             }
223              
224             sub _store_result {
225 18     18   142 my ($self, $request, $result) = @_;
226              
227 18         86 my $results = $request->{results};
228              
229             # Clear request specific data
230 18         196 delete $self->{_timers}->{$request};
231              
232 18         245 push @$results, [$result, time - $request->{start}];
233              
234 18 100 100     254 if (@$results == $request->{times} || $result eq 'ERROR') {
235              
236             # Cleanup
237 5         20 my $tasks = $self->{_tasks};
238 5         30 for my $i (0 .. scalar @$tasks) {
239 5 50       28 if ($tasks->[$i] == $request) {
240 5         17 splice @$tasks, $i, 1;
241 5         19 last;
242             }
243             }
244              
245             # Testing done
246 5         36 $request->{cb}->($results);
247              
248 5         226 undef $request;
249             }
250              
251             # Perform another check
252             else {
253              
254             # Setup interval timer before next request
255             $self->{_timers}{$request} = AnyEvent->timer(
256             after => $self->interval,
257             cb => sub {
258 13     13   1404358 delete $self->{_timers}{$request};
259 13         50 push @{$self->{_tasks_out}}, $request;
  13         93  
260 13         94 $self->_add_write_poll;
261             }
262 13         79 );
263             }
264             }
265              
266             sub _send_request {
267 19     19   66 my ($self, $request) = @_;
268              
269 19         44 my $checksum = 0x0000;
270 19         61 my $identifier = $request->{identifier};
271 19         41 my $sequence = @{$request->{results}} + 1;
  19         62  
272 19         81 my $data = $self->{packet_generator}->();
273              
274 19         122 my $msg = pack $ICMP_PING,
275             $ICMP_ECHO, 0x00, $checksum,
276             $identifier, $sequence, $data;
277              
278 19         75 $checksum = $self->_icmp_checksum($msg);
279              
280 19         101 $msg = pack $ICMP_PING,
281             0x08, 0x00, $checksum,
282             $identifier, $sequence, $data;
283              
284 19         80 $request->{data} = $data;
285              
286 19         102 $request->{start} = time;
287              
288             $self->{_timers}->{$request}->{timer} = AnyEvent->timer(
289             after => $self->timeout,
290             cb => sub {
291 17     17   26001161 $self->_store_result($request, 'TIMEOUT');
292             }
293 19         87 );
294              
295 19         424 my $socket = $self->{_socket};
296              
297 19 100       141 $socket->send($msg, 0, $request->{destination}) or
298             $self->_store_result($request, 'ERROR');
299             }
300              
301             sub _icmp_checksum {
302 19     19   51 my ($self, $msg) = @_;
303              
304 19         34 my $res = 0;
305 19         132 foreach my $int (unpack "n*", $msg) {
306 608         1008 $res += $int;
307             }
308              
309             # Add possible odd byte
310 19 50       146 $res += unpack('C', substr($msg, -1, 1)) << 8
311             if length($msg) % 2;
312              
313             # Fold high into low
314 19         50 $res = ($res >> 16) + ($res & 0xffff);
315              
316             # Two times
317 19         38 $res = ($res >> 16) + ($res & 0xffff);
318              
319 19         63 return ~$res;
320             }
321              
322             1;
323             __END__