File Coverage

blib/lib/AnyEvent/Ping.pm
Criterion Covered Total %
statement 136 142 95.7
branch 27 44 61.3
condition 4 6 66.6
subroutine 24 26 92.3
pod 6 7 85.7
total 197 225 87.5


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