├── README.md └── ping.py /README.md: -------------------------------------------------------------------------------- 1 | ping-python 2 | =========== 3 | 4 | A minimal ping example 5 | 6 | requires: python2 7 | tested on: Arch Linux (as of feb 2014) 8 | 9 | ping.py - the PingService class 10 | 11 | -------------------------------------------------------------------------------- /ping.py: -------------------------------------------------------------------------------- 1 | # 2 | # Ping - bounce ICMP identify packets off remote hosts 3 | # 4 | # The author of this code (a) does not expect anything from you, and 5 | # (b) is not responsible for any problems you may have using this code. 6 | # 7 | # requires: python2 and root/administrator privs to use icmp port 22 8 | # tested on: Arch Linux (as of feb 2014), Windows 8 9 | # 10 | # Linux: 11 | # 12 | # On Linux capabilities can be used to grant 'cap_net_raw+ep' to 13 | # python scripts by copying the python *binary* to a separate 14 | # directory and setting the copy's capabilities with setcap: 15 | # 16 | # $ cp /usr/bin/python2.7 python_net_raw 17 | # # setcap cap_net_raw+ep python_net_raw 18 | # $ ./python_net_raw ping.py ... 19 | # 20 | # Windows 8: 21 | # 22 | # Right click icon in lower left of screen, use Command Prompt (Admin) 23 | 24 | __date__ = "2014/02/27" 25 | __version__ = "v0.93" 26 | 27 | import sys 28 | import time 29 | 30 | # ----------------------------------------------------------------------- 31 | # A thread based polling service with pause and kill. 32 | # Since it polls the function passed in, the function 33 | # needs to return as soon as possible. 34 | 35 | import threading 36 | 37 | ST_KILLED = 0 38 | ST_PAUSED = 1 39 | ST_RUNNING = 2 40 | ST_names = { 0:"killed", 1:"paused", 2:"running" } 41 | 42 | class Poll(threading.Thread): 43 | def __init__(self, func, args=(), name=None, period=0.1): 44 | # we need a tuple here 45 | if type(args) != type((1,)): 46 | args = (args,) 47 | self._function = func 48 | self._args = args 49 | self.period = period 50 | threading.Thread.__init__(self, target=func, name=name, args=()) 51 | self._uptime = time.time() 52 | self._state = ST_RUNNING 53 | self.start() 54 | 55 | def run(self): 56 | while self._state != ST_KILLED: 57 | if self._state == ST_RUNNING: 58 | self._function(self._args) 59 | time.sleep(self.period) 60 | 61 | # note: all threads must be killed before python will exit! 62 | def kill(self): 63 | self._state = ST_KILLED 64 | 65 | def pause(self): 66 | if self._state == ST_RUNNING: 67 | self._state = ST_PAUSED 68 | 69 | def resume(self): 70 | if self._state == ST_PAUSED: 71 | self._state = ST_RUNNING 72 | 73 | def uptime(self): 74 | return time.time() - self._uptime 75 | 76 | def state(self): 77 | return ST_names[self._state] 78 | 79 | def __str__(self): 80 | return self.getName() 81 | 82 | @staticmethod 83 | def thread_list(): 84 | return [x for x in threading.enumerate() 85 | if x.getName() != "MainThread"] 86 | 87 | @staticmethod 88 | def tlist(): 89 | """Human readable version of thread_list()""" 90 | for t in Poll.thread_list(): 91 | if isinstance(t, Poll): 92 | print "%-16s %-8s %4.3f" % (t, t.state(), t.uptime()) 93 | 94 | @staticmethod 95 | def killall(): 96 | for t in Poll.thread_list(): 97 | t.kill() 98 | 99 | # ----------------------------------------------------------------------- 100 | # ping from scratch 101 | 102 | import os 103 | import types 104 | import struct 105 | import socket 106 | 107 | class PingService(object): 108 | """Send out icmp ping requests at 'delay' intervals and 109 | watch for replies. The isup() method can be used by 110 | other threads to check the status of the remote host. 111 | 112 | @host - (string) ip of host to ping 113 | @delay - (float) delay in seconds between pings 114 | @its_dead_jim - (int) seconds to wait before running offline() 115 | @verbose - (bool) print round trip stats for every reply 116 | @persistent - (bool) thread continues to run even if no reply 117 | 118 | usage: p = PingService('192.168.1.1') 119 | p.start() - begin ping loop 120 | p.isup() - True if host has replied recently 121 | p.stop() - stop ping loop 122 | 123 | online() and offline() methods can be overloaded: 124 | 125 | def my_online(self): 126 | self.log(self.host + " is up") 127 | 128 | p.online = types.MethodType(my_online, p) 129 | 130 | """ 131 | 132 | # provide a class-wide thread-safe message queue 133 | msgs = [] 134 | 135 | def __init__(self, host, delay=1.0, its_dead_jim=4, 136 | verbose=True, persistent=False): 137 | self.host = host 138 | self.delay = delay 139 | self.verbose = verbose 140 | self.persistent = persistent 141 | self.obituary_delay = its_dead_jim * delay 142 | self.pad = "getoff my lawn" # should be 14 chars or more 143 | socket.setdefaulttimeout(0.01) 144 | self.started = 0 145 | self.thread = None 146 | self._isup = False 147 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, 148 | socket.getprotobyname('icmp')) 149 | try: 150 | self.sock.connect((host, 22)) 151 | except socket.gaierror, ex: 152 | self.log("ping socket cannot connect to %s: %s" % (host, ex[1])) 153 | self.sock.close() 154 | return 155 | 156 | def log(self, msg): 157 | if not self.verbose: 158 | msg = time.strftime("%H:%M:%S ") + msg 159 | self.msgs.append(msg) 160 | 161 | def start(self): 162 | self.seq = 0 163 | self.pid = os.getpid() 164 | self.last_heartbeat = 0 165 | # send a ping right away 166 | self.time_to_send = self.started = time.time() 167 | self.thread = Poll(self._ping, (None), name=self.host) 168 | #retry = int(round(self.obituary_delay / 0.2)) 169 | ## retry, before letting caller deal with a down state 170 | #while retry > 0 and not self._isup: 171 | # time.sleep(0.2) 172 | # retry -= 1 173 | 174 | def stop(self): 175 | if self.thread: 176 | self.thread.kill() 177 | self.thread = None 178 | 179 | def _icmp_checksum(self, pkt): 180 | n = len(pkt) 181 | two_bytes = struct.unpack("!%sH" % (n/2), pkt) 182 | chksum = sum(two_bytes) 183 | if n & 1 == 1: 184 | chksum += pkt[-1] 185 | chksum = (chksum >> 16) + (chksum & 0xffff) 186 | chksum += chksum >> 16 187 | return ~chksum & 0xffff 188 | 189 | def _icmp_create(self, data): 190 | fmt = "!BBH" 191 | args = [8, 0, 0] 192 | if data and len(data) > 0: 193 | fmt += "%ss" % len(data) 194 | args.append(data) 195 | args[2] = self._icmp_checksum(struct.pack(fmt, *args)) 196 | return struct.pack(fmt, *args) 197 | 198 | def _icmp_parse(self, pkt): 199 | """Parse ICMP packet""" 200 | string_len = len(pkt) - 4 # Ignore IP header 201 | fmt = "!BBH" 202 | if string_len: 203 | fmt += "%ss" % string_len 204 | unpacked_packet = struct.unpack(fmt, pkt) 205 | typ, code, chksum = unpacked_packet[:3] 206 | if self._icmp_checksum(pkt) != 0: 207 | self.log("%s reply checksum is not zero" % self.host) 208 | try: 209 | data = unpacked_packet[3] 210 | except IndexError: 211 | data = None 212 | return typ, code, data 213 | 214 | 215 | def _ping(self, args): 216 | pdatafmt = "!HHd%ds" % len(self.pad) 217 | now = time.time() 218 | if now >= self.time_to_send: 219 | # send ping packet 220 | self.seq += 1 221 | self.seq &= 0xffff 222 | pdata = struct.pack(pdatafmt, self.pid, self.seq, now, self.pad) 223 | self.sock.send(self._icmp_create(pdata)) 224 | self.time_to_send = now + self.delay 225 | 226 | if self._isup and now - self.last_heartbeat > self.obituary_delay: 227 | self._isup = False 228 | self.offline() 229 | 230 | if self.last_heartbeat == 0 \ 231 | and not self.persistent \ 232 | and now - self.started > self.obituary_delay: 233 | if self.verbose: 234 | self.log("no response from " + self.host) 235 | self.thread.kill() 236 | self.thread = None 237 | return 238 | 239 | try: 240 | rbuf = self.sock.recv(10000) 241 | now = time.time() # refresh 'now' to make rtt more accurate 242 | except socket.timeout: 243 | return 244 | 245 | if len(rbuf) <= 20: 246 | self.log("%s truncated reply" % self.host) 247 | return 248 | 249 | # parse ICMP packet; ignore IP header 250 | typ, code, rdata = self._icmp_parse(rbuf[20:]) 251 | 252 | if typ == 8: 253 | self.log("%s is pinging us" % self.host) 254 | self.last_heartbeat = now 255 | if not self._isup: 256 | self._isup = True 257 | self.online() 258 | return 259 | 260 | if typ == 3: 261 | self.log("%s dest unreachable (code=%d)" % (self.host, code)); 262 | return 263 | 264 | if typ != 0: 265 | self.log("%s packet not an echo reply (%d) " % (self.host, typ)) 266 | return 267 | 268 | if not rdata: 269 | self.log("%s packet contains no data" % (self.host)) 270 | return 271 | 272 | if len(rdata) != 12 + len(self.pad): 273 | # other ping programs can cause this 274 | # self.log("%s not our ping (len=%d)" % (self.host, len(rdata))) 275 | return 276 | 277 | # parse ping data 278 | (ident, seqno, timestamp, pad) = struct.unpack(pdatafmt, rdata) 279 | 280 | if ident != self.pid: 281 | # other instances of PingService can cause this 282 | #self.log("%s not our ping (ident=%d)" % (self.host, ident)) 283 | return 284 | 285 | if seqno != self.seq: 286 | self.log("%s sequence out of order " % self.host + 287 | "got(%d) expected(%d)" % (seqno, self.seq)) 288 | return 289 | 290 | if rdata and len(rdata) >= 8: 291 | self.last_heartbeat = now 292 | 293 | if not self._isup: 294 | self._isup = True 295 | self.online() 296 | 297 | if self.verbose: 298 | str = "%d bytes from %s: seq=%u" % ( 299 | len(rbuf), 300 | # inet_ntop not available on windows 301 | '.'.join([('%d' % ord(c)) for c in list(rbuf[12:16])]), 302 | self.seq) 303 | 304 | # calculate rounttrip time 305 | rtt = now - timestamp 306 | rtt *= 1000 307 | # note that some boxes that run python 308 | # can't resolve milisecond time deltas ... 309 | if rtt > 0: 310 | str += ", rtt=%.1f ms" % rtt 311 | 312 | self.log(str) 313 | 314 | def online(self): 315 | if not self.verbose: 316 | self.log("%s is up" % self.host) 317 | 318 | def offline(self): 319 | if not self.verbose: 320 | self.log("%s is down" % self.host) 321 | 322 | def isup(self): 323 | return self._isup 324 | 325 | # ---------------------------------------------------------------------------- 326 | # demonstrate PingService 327 | 328 | if __name__ == "__main__": 329 | import traceback 330 | import types 331 | 332 | if len(sys.argv) < 2: 333 | print "usage: python2 ping.py " 334 | sys.exit(1) 335 | 336 | ping_svc = PingService(sys.argv[1]) 337 | 338 | try: 339 | ping_svc.start() 340 | 341 | while len(Poll.thread_list()) > 0: 342 | time.sleep(0.2) 343 | 344 | # print log messages 345 | while len(PingService.msgs) > 0: 346 | print PingService.msgs.pop(0) 347 | 348 | except KeyboardInterrupt: 349 | pass 350 | 351 | except: 352 | t, v, tb = sys.exc_info() 353 | traceback.print_exception(t, v, tb) 354 | 355 | # note: all threads must be stopped before python will exit! 356 | ping_svc.stop() 357 | 358 | sys.exit(0) 359 | 360 | 361 | # ex: set tabstop=8 expandtab softtabstop=4 shiftwidth=4: 362 | --------------------------------------------------------------------------------