File Coverage

blib/lib/AnyEvent/Ping.pm
Criterion Covered Total %
statement 135 141 95.7
branch 27 44 61.3
condition 4 6 66.6
subroutine 24 26 92.3
pod 6 7 85.7
total 196 224 87.5


line stmt bran cond sub pod time code
1             package AnyEvent::Ping;
2              
3 1     1   769 use strict;
  1         2  
  1         25  
4 1     1   5 use warnings;
  1         2  
  1         30  
5 1     1   21 use 5.008_001;
  1         11  
6              
7             our $VERSION = 0.011;
8              
9 1     1   970 use Socket qw/SOCK_RAW/;
  1         4053  
  1         218  
10 1     1   942 use Time::HiRes 'time';
  1         1521  
  1         5  
11 1     1   5108 use IO::Socket::INET qw/sockaddr_in inet_aton/;
  1         22387  
  1         9  
12 1     1   108 use List::Util ();
  1         3  
  1         1674  
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 3370 my ($class, %args) = @_;
26              
27 2         4 my $interval = $args{interval};
28 2 50       10 $interval = 0.2 unless defined $interval;
29              
30 2         4 my $timeout = $args{timeout};
31 2 100       7 $timeout = 5 unless defined $timeout;
32              
33 2         4 my $packet_generator = $args{packet_generator};
34 2 50       5 unless (defined $packet_generator) {
35 2         4 my $packet_size = $args{packet_size};
36 2 50       5 $packet_size = 56 unless defined $packet_size;
37              
38             $packet_generator = sub {
39 18     18   58 &AnyEvent::Ping::generate_data_random($packet_size);
40 2         8 };
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       15 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         490 $self->{_socket} = $socket;
57              
58 2 100       7 if (my $on_prepare = $args{on_prepare}) {
59 1         4 $on_prepare->($socket);
60             }
61              
62             # Create Poll object
63             $self->{_poll_read} = AnyEvent->io(
64             fh => $socket,
65             poll => 'r',
66 48     48   524 cb => sub { $self->_on_read },
67 2         20 );
68              
69             # Ping tasks
70 2         5234 $self->{_tasks} = [];
71 2         5 $self->{_tasks_out} = [];
72 2         6 $self->{_timers} = {};
73              
74 2         9 return $self;
75             }
76              
77 13 50   13 1 111 sub interval { @_ > 1 ? $_[0]->{interval} = $_[1] : $_[0]->{interval} }
78              
79 18 50   18 1 202 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 10389 my ($self, $host, $times, $cb) = @_;
85              
86 6         14 my $socket = $self->{_socket};
87              
88 6         45 my $ip = inet_aton($host);
89              
90 6         72 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         75 push @{$self->{_tasks}}, $request;
  6         17  
100              
101 6         9 push @{$self->{_tasks_out}}, $request;
  6         13  
102              
103 6         14 $self->_add_write_poll;
104              
105 6         54 return $self;
106             }
107              
108             sub end {
109 2     2 1 2942 my $self = shift;
110              
111 2         16 delete $self->{_poll_read};
112 2         9 delete $self->{_poll_write};
113 2         9 delete $self->{_timers};
114              
115 2         3 while (my $request = pop @{$self->{_tasks}}) {
  3         21  
116 1         4 $request->{cb}->($request->{results});
117             }
118              
119             close delete $self->{_socket}
120 2 50       71 if exists $self->{_socket};
121             }
122              
123             sub generate_data_random {
124 19     19 0 6404 my $length = shift;
125              
126 19         40 my $data = '';
127 19         69 while ($length > 0) {
128 1028         2096 $data .= pack('C', int(rand(256)));
129 1028         2338 $length--;
130             }
131              
132 19         70 $data;
133             }
134              
135             sub _add_write_poll {
136 19     19   43 my $self = shift;
137              
138 19 100       134 return if exists $self->{_poll_write};
139              
140             $self->{_poll_write} = AnyEvent->io(
141             fh => $self->{_socket},
142             poll => 'w',
143 12     12   436 cb => sub { $self->_send_requests },
144 13         201 );
145             }
146              
147             sub _send_requests {
148 12     12   34 my $self = shift;
149              
150 12         21 foreach my $request (@{$self->{_tasks_out}}) {
  12         56  
151 18         766 $self->_send_request($request);
152             }
153              
154 12         973 $self->{_tasks_out} = [];
155 12         162 delete $self->{_poll_write};
156             }
157              
158             sub _on_read {
159 48     48   101 my $self = shift;
160              
161 48         98 my $socket = $self->{_socket};
162 48         210 $socket->sysread(my $chunk, 4194304, 0);
163              
164 48         608 my $icmp_msg = substr $chunk, 20;
165              
166 48         81 my ($type, $identifier, $sequence, $data);
167              
168 48         119 $type = unpack 'c', $icmp_msg;
169              
170 48 100 33     252 if ($type == $ICMP_ECHOREPLY) {
    50          
171 24         249 ($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         227 return;
181             }
182              
183             # Find our task
184             my $request =
185 23     23   66 List::Util::first { $identifier == $_->{identifier} }
186 24         147 @{$self->{_tasks}};
  24         273  
187              
188 24 100       151 return unless $request;
189              
190             # Is it response to our latest message?
191 17 50       28 return unless $sequence == @{$request->{results}} + 1;
  17         65  
192              
193 17 50       51 if ($type == $ICMP_ECHOREPLY) {
    0          
    0          
194              
195             # Check data
196 17 50       47 if ($data eq $request->{data}) {
197 17         59 $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   67 my ($self, $request, $result) = @_;
213              
214 18         40 my $results = $request->{results};
215              
216             # Clear request specific data
217 18         148 delete $self->{_timers}->{$request};
218              
219 18         108 push @$results, [$result, time - $request->{start}];
220              
221 18 100 100     112 if (@$results == $request->{times} || $result eq 'ERROR') {
222              
223             # Cleanup
224 5         15 my $tasks = $self->{_tasks};
225 5         18 for my $i (0 .. scalar @$tasks) {
226 5 50       25 if ($tasks->[$i] == $request) {
227 5         17 splice @$tasks, $i, 1;
228 5         17 last;
229             }
230             }
231              
232             # Testing done
233 5         28 $request->{cb}->($results);
234              
235 5         164 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   1401261 delete $self->{_timers}{$request};
246 13         38 push @{$self->{_tasks_out}}, $request;
  13         81  
247 13         77 $self->_add_write_poll;
248             }
249 13         46 );
250             }
251             }
252              
253             sub _send_request {
254 18     18   44 my ($self, $request) = @_;
255              
256 18         46 my $checksum = 0x0000;
257 18         57 my $identifier = $request->{identifier};
258 18         30 my $sequence = @{$request->{results}} + 1;
  18         61  
259 18         66 my $data = $self->{packet_generator}->();
260              
261 18         101 my $msg = pack $ICMP_PING,
262             $ICMP_ECHO, 0x00, $checksum,
263             $identifier, $sequence, $data;
264              
265 18         63 $checksum = $self->_icmp_checksum($msg);
266              
267 18         69 $msg = pack $ICMP_PING,
268             0x08, 0x00, $checksum,
269             $identifier, $sequence, $data;
270              
271 18         46 $request->{data} = $data;
272              
273 18         95 $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 18         73 );
281              
282 18         366 my $socket = $self->{_socket};
283              
284 18 100       114 $socket->send($msg, 0, $request->{destination}) or
285             $self->_store_result($request, 'ERROR');
286             }
287              
288             sub _icmp_checksum {
289 18     18   38 my ($self, $msg) = @_;
290              
291 18         33 my $res = 0;
292 18         110 foreach my $int (unpack "n*", $msg) {
293 576         829 $res += $int;
294             }
295              
296             # Add possible odd byte
297 18 50       88 $res += unpack('C', substr($msg, -1, 1)) << 8
298             if length($msg) % 2;
299              
300             # Fold high into low
301 18         49 $res = ($res >> 16) + ($res & 0xffff);
302              
303             # Two times
304 18         35 $res = ($res >> 16) + ($res & 0xffff);
305              
306 18         46 return ~$res;
307             }
308              
309             1;
310             __END__