Skip to content

Commit

Permalink
Merge romana#25, fix close() calls
Browse files Browse the repository at this point in the history
  • Loading branch information
eis-joshua committed Apr 9, 2020
1 parent 687e64a commit 5c9a3bb
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 36 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ Here is an example of how to use MultiPing in your own code:
responses, no_responses = mp.receive(1)

The `receive()` function returns a tuple containing a results dictionary
(addresses and response times) as well as a list of addresses that did not
(addresses with response times and retry) as well as a list of addresses that did not
respond in time. The results may be processed like this:

...

for addr, rtt in responses.items():
print "%s responded in %f seconds" % (addr, rtt)
for addr, result in responses.items():
print "%s responded in %f seconds (retry=%d)" % (addr, result['time'], result['retry'])

if no_responses:
print "These addresses did not respond: %s" % ", ".join(no_responses)
Expand Down Expand Up @@ -102,3 +102,8 @@ surpressed if the `silent_lookup_errors` parameter flag is set. Either as named
parameter for the `multi_ping` function or when a `MultiPing` object is
created.

To avoid burst issues with packet loss on some networks, the `delay` parameter
can be used with the `multi_ping` function or when a `MultiPing` object is
created. This delay in seconds will be applied between every ICMP request.
For milliseconds delay simply use floating number, e.g.: `0.001` for 1 ms.

61 changes: 42 additions & 19 deletions demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,46 +23,69 @@
from multiping import multi_ping

if __name__ == "__main__":

# A list of addresses and names, which should be pinged.
addrs = ["8.8.8.8", "cnn.com", "127.0.0.1", "youtube.com",
"2001:4860:4860::8888"]

print("sending one round of pings and checking twice for responses")
#addrs = ["127.0.0.1", "10.11.1.5","10.12.1.1"]
mp = MultiPing(addrs)

##
## Demo Basic Usage
##
print("Round: 0")
print(" [Ping]")
mp.send()
# First attempt: We probably will only have received response from
# localhost so far. Note: 0.01 seconds is usually a very unrealistic
# timeout value. 1 second, or so, may be more useful in a real world
# example.
responses, no_responses = mp.receive(0.01)

print(" received responses: %s" % list(responses.keys()))
print(" no responses so far: %s" % no_responses)
# First attempt: keep recv time very, to show the staggered responses.
print(" Receive() Waiting Replies.......")
responses, no_responses = mp.receive(0.01)

# By now we should have received responses from the others as well
print(" --- trying another receive ---")
responses, no_responses = mp.receive(0.1)
print(" received responses: %s" % list(responses.keys()))
print(" still no responses: %s" % no_responses)
print("")
complete = len(responses.keys()) == len(addrs)
if complete:
print(" received responses: %s" % list(responses.keys()))
print('--- Round Complete ---')
print('')
else:
pending = list(responses.keys())
print(" received responses: %s" % pending)
print(" no responses so far: %s" % no_responses)
print(" Try Receive() Again.......")
# By now we should have received responses from the others as well
responses, no_responses = mp.receive(0.1)
complete = (len(pending) + len(responses.keys())) == len(addrs)
if complete:
print(" received responses: %s" % list(responses.keys()))
print('--- Round Complete ---')
else:
print(" received responses: %s" % list(responses.keys()))
print(" still no responses: %s" % no_responses)
print("")
pass
print('')

# Sometimes ICMP packets can get lost. It's easy to retry (re-send the
# ping) to those hosts that haven't responded, yet. Send can be called
# multiple times and will result in pings to be resent to those addresses
# from which we have not heard back, yet. Here we try three pings max.
print("sending again, but waiting with retries if no response received")
print("Round 1: (3 Attempts)")
mp = MultiPing(addrs)
pending = []
for i in range(3):
print(" [Ping]")
mp.send()

print(" Receive() Waiting Replies.......")
responses, no_response = mp.receive(0.01)
pending += list(responses.keys())
complete = len(pending) == len(addrs)

print(" received responses: %s" % list(responses.keys()))
if not no_response:
print(" all done, received responses from everyone")
if complete:
print(' [100%]')
break
else:
print(" %d. retry, resending to: %s" % (i + 1, no_response))

if no_response:
print(" no response received in time, even after 3 retries: %s" %
no_response)
Expand Down
57 changes: 43 additions & 14 deletions multiping/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"""

__version__ = "1.1.0"
__version__ = "1.1.3"

import os
import socket
Expand Down Expand Up @@ -71,7 +71,8 @@ class MultiPingSocketError(socket.gaierror):

class MultiPing(object):

def __init__(self, dest_addrs, sock=None, ignore_lookup_errors=False):
def __init__(self, dest_addrs, sock=None, ignore_lookup_errors=False,
delay=0):
"""
Initialize a new multi ping object. This takes the configuration
consisting of the list of destination addresses and an optional socket
Expand All @@ -95,6 +96,7 @@ def __init__(self, dest_addrs, sock=None, ignore_lookup_errors=False):
"65535 addresses at the same time.")

self._ignore_lookup_errors = ignore_lookup_errors
self._xmit_delay = delay # delay between each ICMP request

# Get the IP addresses for every specified target: We allow
# specification of the ping targets by name, so a name lookup needs to
Expand Down Expand Up @@ -145,6 +147,7 @@ def __init__(self, dest_addrs, sock=None, ignore_lookup_errors=False):
self._unprocessed_targets.append(d)

self._id_to_addr = {}
self._addr_retry = {}
self._remaining_ids = None
self._last_used_id = None
self._time_stamp_size = struct.calcsize("d")
Expand Down Expand Up @@ -242,7 +245,7 @@ def _send_ping(self, dest_addr, payload):
# - ICMP code = 0 (unsigned byte)
# - checksum = 0 (unsigned short)
# - packet id (unsigned short)
# - sequence = 0 (unsigned short) This doesn't have to be 0.
# - sequence = pid (unsigned short)
dummy_header = bytearray(
struct.pack(_ICMP_HDR_PACK_FORMAT,
icmp_echo_request, 0, 0,
Expand Down Expand Up @@ -292,6 +295,7 @@ def send(self):
# Collect all the addresses for which we have not seen responses yet.
if not self._receive_has_been_called:
all_addrs = self._dest_addrs
self._addr_retry = {addr: -1 for addr in all_addrs}
else:
all_addrs = [a for (i, a) in list(self._id_to_addr.items())
if i in self._remaining_ids]
Expand All @@ -303,8 +307,13 @@ def send(self):
# need to trim it down.
self._last_used_id = int(time.time()) & 0xffff

# Reset the _id_to_addr, we are now retrying to send new request with
# new ID. Reply of a request that have been retried will be ignored.
self._id_to_addr = {}

# Send ICMPecho to all addresses...
for addr in all_addrs:
self._addr_retry[addr] += 1
# Make a unique ID, wrapping around at 65535.
self._last_used_id = (self._last_used_id + 1) & 0xffff
# Remember the address for each ID so we can produce meaningful
Expand All @@ -314,6 +323,14 @@ def send(self):
# of the current time stamp. This is returned to us in the
# response and allows us to calculate the 'ping time'.
self._send_ping(addr, payload=struct.pack("d", time.time()))
# Some system/network doesn't support the bombarding of ICMP
# request and lead to a lot of packet loss and retry, therefore
# introcude a small delay between each request.
if self._xmit_delay > 0:
time.sleep(self._xmit_delay)

# Keep track of the current request IDs to be used in the receive
self._remaining_ids = list(self._id_to_addr.keys())

def _read_all_from_socket(self, timeout):
"""
Expand All @@ -337,9 +354,9 @@ def _read_all_from_socket(self, timeout):
try:
self._sock.settimeout(timeout)
while True:
p = self._sock.recv(64)
p, src_addr = self._sock.recvfrom(128)
# Store the packet and the current time
pkts.append((bytearray(p), time.time()))
pkts.append((src_addr, bytearray(p), time.time()))
# Continue the loop to receive any additional packets that
# may have arrived at this point. Changing the socket to
# non-blocking (by setting the timeout to 0), so that we'll
Expand All @@ -366,8 +383,8 @@ def _read_all_from_socket(self, timeout):
try:
self._sock6.settimeout(timeout)
while True:
p = self._sock6.recv(128)
pkts.append((bytearray(p), time.time()))
p, src_addr = self._sock6.recvfrom(128)
pkts.append((src_addr, bytearray(p), time.time()))
self._sock6.settimeout(0)
except socket.timeout:
pass
Expand Down Expand Up @@ -415,7 +432,7 @@ def receive(self, timeout):
start_time = time.time()
pkts = self._read_all_from_socket(remaining_time)

for pkt, resp_receive_time in pkts:
for src_addr, pkt, resp_receive_time in pkts:
# Extract the ICMP ID of the response

try:
Expand All @@ -438,7 +455,8 @@ def receive(self, timeout):
payload = pkt[_ICMP_PAYLOAD_OFFSET:]

if pkt_ident == self.ident and \
pkt_id in self._remaining_ids:
pkt_id in self._remaining_ids and \
src_addr[0] == self._id_to_addr[pkt_id]:
# The sending timestamp was encoded in the echo request
# body and is now returned to us in the response. Note
# that network byte order doesn't matter here, since we
Expand All @@ -447,7 +465,8 @@ def receive(self, timeout):
req_sent_time = struct.unpack(
"d", payload[:self._time_stamp_size])[0]
results[self._id_to_addr[pkt_id]] = \
resp_receive_time - req_sent_time
{'time': resp_receive_time - req_sent_time,
'retry': self._addr_retry[src_addr[0]]}

self._remaining_ids.remove(pkt_id)
except IndexError:
Expand All @@ -470,11 +489,15 @@ def __del__(self):
"""
Close sockets descriptors.
"""
self._sock.close() # TODO: probably need add some verifications.
self._sock6.close()
local = (
getattr(self, '_sock', None),
getattr(self, '_sock6', None),
)
[ item.close() for item in local if item != None ]
pass


def multi_ping(dest_addrs, timeout, retry=0, ignore_lookup_errors=False):
def multi_ping(dest_addrs, timeout, retry=0, ignore_lookup_errors=False, delay=0):
"""
Combine send and receive measurement into single function.
Expand All @@ -492,11 +515,15 @@ def multi_ping(dest_addrs, timeout, retry=0, ignore_lookup_errors=False):
names or looking up their address information will silently be ignored.
Those targets simply appear in the 'no_results' return list.
The 'delay' parameter can be used to introduced a small delay between
each requests.
"""
retry = int(retry)
if retry < 0:
retry = 0


timeout = float(timeout)
if timeout < 0.1:
raise MultiPingError("Timeout < 0.1 seconds not allowed")
Expand All @@ -505,7 +532,9 @@ def multi_ping(dest_addrs, timeout, retry=0, ignore_lookup_errors=False):
if retry_timeout < 0.1:
raise MultiPingError("Time between ping retries < 0.1 seconds")

mp = MultiPing(dest_addrs, ignore_lookup_errors=ignore_lookup_errors)
print(f"Dest: {dest_addrs}")
mp = MultiPing(dest_addrs, ignore_lookup_errors=ignore_lookup_errors,
delay=delay)

results = {}
retry_count = 0
Expand Down

0 comments on commit 5c9a3bb

Please sign in to comment.