David Blume commited on 2019-07-05 20:08:07
Showing 9 changed files, with 339 additions and 0 deletions.
| ... | ... |
@@ -0,0 +1,21 @@ |
| 1 |
+The MIT License (MIT) |
|
| 2 |
+ |
|
| 3 |
+Copyright (c) 2019 David Blume |
|
| 4 |
+ |
|
| 5 |
+Permission is hereby granted, free of charge, to any person obtaining a copy |
|
| 6 |
+of this software and associated documentation files (the "Software"), to deal |
|
| 7 |
+in the Software without restriction, including without limitation the rights |
|
| 8 |
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
| 9 |
+copies of the Software, and to permit persons to whom the Software is |
|
| 10 |
+furnished to do so, subject to the following conditions: |
|
| 11 |
+ |
|
| 12 |
+The above copyright notice and this permission notice shall be included in all |
|
| 13 |
+copies or substantial portions of the Software. |
|
| 14 |
+ |
|
| 15 |
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
| 16 |
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
| 17 |
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
| 18 |
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
| 19 |
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
| 20 |
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
| 21 |
+SOFTWARE. |
| ... | ... |
@@ -0,0 +1,73 @@ |
| 1 |
+# python_pinger |
|
| 2 |
+ |
|
| 3 |
+These are different approaches to how one might implement a daemon to |
|
| 4 |
+repeatedly ping multiple hosts to see if the network is up. |
|
| 5 |
+ |
|
| 6 |
+## Single Threaded |
|
| 7 |
+ |
|
| 8 |
+This is the most naive implementation. For each ping host, the main |
|
| 9 |
+process thread pings them one at a time, and prints any changes. |
|
| 10 |
+ |
|
| 11 |
+ |
|
| 12 |
+ |
|
| 13 |
+### Upsides |
|
| 14 |
+ |
|
| 15 |
+Really simple code. |
|
| 16 |
+ |
|
| 17 |
+### Downsides |
|
| 18 |
+ |
|
| 19 |
+Since the pings are serialized, one long timeout from one host could |
|
| 20 |
+affect detecting a problem at another host. |
|
| 21 |
+ |
|
| 22 |
+## Long Lived Workers Consuming from Queue |
|
| 23 |
+ |
|
| 24 |
+Raymond Hettinger's [PyBay 2018 Keynote](https://pybay.com/site_media/slides/raymond2017-keynote/threading.html) |
|
| 25 |
+uses the queue module to send data between threads, so I thought I'd make a version of the pinger that did the same. |
|
| 26 |
+ |
|
| 27 |
+[long\_lived\_worker\_queue.py](http://git.dlma.com/python_pinger.git/blob/master/long_lived_worker_queue.py) |
|
| 28 |
+ |
|
| 29 |
+ |
|
| 30 |
+ |
|
| 31 |
+### Upsides |
|
| 32 |
+ |
|
| 33 |
+Multi-threaded ping calls won't block each other. |
|
| 34 |
+ |
|
| 35 |
+### Downsides |
|
| 36 |
+ |
|
| 37 |
+The ping tasks read from, and write to a shared dictionary. (Reading and updating the last status.) |
|
| 38 |
+ |
|
| 39 |
+## Short Lived Workers |
|
| 40 |
+ |
|
| 41 |
+ |
|
| 42 |
+ |
|
| 43 |
+[short\_lived\_workers.py](http://git.dlma.com/python_pinger.git/blob/master/short_lived_workers.py) |
|
| 44 |
+ |
|
| 45 |
+### Upsides |
|
| 46 |
+ |
|
| 47 |
+The worker tasks aren't in memory if they're not doing anything. So usually a smaller memory profile. |
|
| 48 |
+ |
|
| 49 |
+### Downsides |
|
| 50 |
+ |
|
| 51 |
+The ping tasks still read from, and write to a shared dictionary. (Reading and updating the last status.) |
|
| 52 |
+ |
|
| 53 |
+## Long Lived Looping Workers |
|
| 54 |
+ |
|
| 55 |
+ |
|
| 56 |
+ |
|
| 57 |
+[long\_lived\_looping\_workers.py](http://git.dlma.com/python_pinger.git/blob/master/long_lived_looping_workers.py) |
|
| 58 |
+ |
|
| 59 |
+### Upsides |
|
| 60 |
+ |
|
| 61 |
+No more race conditions! The worker threads mind their own business. |
|
| 62 |
+ |
|
| 63 |
+### Downsides |
|
| 64 |
+ |
|
| 65 |
+The worker threads remain in memory. |
|
| 66 |
+ |
|
| 67 |
+## Is it any good? |
|
| 68 |
+ |
|
| 69 |
+[Yes](https://news.ycombinator.com/item?id=3067434). |
|
| 70 |
+ |
|
| 71 |
+## Licence |
|
| 72 |
+ |
|
| 73 |
+This software uses the [MIT license](http://git.dlma.com/python_pinger.git/blob/master/LICENSE.txt). |
| ... | ... |
@@ -0,0 +1,77 @@ |
| 1 |
+#!/usr/bin/python3 |
|
| 2 |
+# |
|
| 3 |
+# Since this is meant to be run as a daemon, do not use #!/usr/bin/env python3, |
|
| 4 |
+# because the process name would then be python3, not based on __FILE__. |
|
| 5 |
+# |
|
| 6 |
+# [filename] -d 3 192.168.1.1 192.168.1.12 & |
|
| 7 |
+# disown [jobid] |
|
| 8 |
+import os |
|
| 9 |
+import sys |
|
| 10 |
+import subprocess |
|
| 11 |
+import time |
|
| 12 |
+from argparse import ArgumentParser |
|
| 13 |
+import threading |
|
| 14 |
+import queue |
|
| 15 |
+ |
|
| 16 |
+ |
|
| 17 |
+def pinger(host, delay): |
|
| 18 |
+ """Independent worker thread: repeatedly ping, check, print and sleep.""" |
|
| 19 |
+ last_results = None |
|
| 20 |
+ while True: |
|
| 21 |
+ now = time.localtime() |
|
| 22 |
+ success = ping(host) |
|
| 23 |
+ if success != last_results: |
|
| 24 |
+ status = "UP" if success else "DOWN" |
|
| 25 |
+ _print_queue.put([f'{time.strftime("%Y-%m-%d %H:%M:%S", now)} {host} {status}'])
|
|
| 26 |
+ last_results = success |
|
| 27 |
+ time.sleep(delay) |
|
| 28 |
+ |
|
| 29 |
+ |
|
| 30 |
+def ping(host): |
|
| 31 |
+ """Returns True if host (str) responds to a ping request.""" |
|
| 32 |
+ return subprocess.call([*_ping_cmd, host], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0 |
|
| 33 |
+ |
|
| 34 |
+ |
|
| 35 |
+def print_manager(): |
|
| 36 |
+ """The only thread allowed to write output.""" |
|
| 37 |
+ while True: |
|
| 38 |
+ job = _print_queue.get() |
|
| 39 |
+ for line in job: |
|
| 40 |
+ log(line) |
|
| 41 |
+ _print_queue.task_done() |
|
| 42 |
+ |
|
| 43 |
+ |
|
| 44 |
+def log(*args, **kwargs): |
|
| 45 |
+ """Opens the logfile, writes the log, and closes the logfile.""" |
|
| 46 |
+ # I don't leave the log file open for writing because another process needs access too. |
|
| 47 |
+ with open(_logname, 'a') as fp: |
|
| 48 |
+ return print(*args, **kwargs, file=fp) |
|
| 49 |
+ |
|
| 50 |
+ |
|
| 51 |
+if __name__ == '__main__': |
|
| 52 |
+ parser = ArgumentParser(description="Repeatedly ping sites. Ex: %(prog)s -d 3 192.168.1.1 192.168.1.12") |
|
| 53 |
+ parser.add_argument('-d', '--delay', type=float, default=1.0, help='Delay between pings')
|
|
| 54 |
+ parser.add_argument('addresses', nargs='+', help='Addresses to ping')
|
|
| 55 |
+ parser_args = parser.parse_args() |
|
| 56 |
+ |
|
| 57 |
+ _logname = os.path.join(os.path.expanduser('~'), 'log', os.path.basename(sys.argv[0]).replace('py', 'txt'))
|
|
| 58 |
+ |
|
| 59 |
+ # Choose the ping parameters appropriate for the platform |
|
| 60 |
+ if sys.platform == 'cygwin' or sys.platform == 'win32': |
|
| 61 |
+ _ping_cmd = ('ping', '-n', '1', '-w', '2000')
|
|
| 62 |
+ else: |
|
| 63 |
+ _ping_cmd = ('ping', '-c', '1', '-W', '2', '-q')
|
|
| 64 |
+ |
|
| 65 |
+ _print_queue = queue.Queue() |
|
| 66 |
+ print_thread = threading.Thread(target=print_manager, daemon=True) |
|
| 67 |
+ print_thread.start() |
|
| 68 |
+ |
|
| 69 |
+ _print_queue.put([f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}'
|
|
| 70 |
+ f' PID={os.getpid()} repeat={parser_args.delay}s Starting'])
|
|
| 71 |
+ for address in parser_args.addresses: |
|
| 72 |
+ t = threading.Thread(target=pinger, args=(address, parser_args.delay), daemon=True) |
|
| 73 |
+ t.start() |
|
| 74 |
+ del t |
|
| 75 |
+ |
|
| 76 |
+ # Stay alive forever. Join the infinitely looping print thread. |
|
| 77 |
+ print_thread.join() |
| ... | ... |
@@ -0,0 +1,90 @@ |
| 1 |
+#!/usr/bin/python3 |
|
| 2 |
+# |
|
| 3 |
+# Since this is meant to be run as a daemon, do not use #!/usr/bin/env python3, |
|
| 4 |
+# because the process name would then be python3, not based on __FILE__. |
|
| 5 |
+# |
|
| 6 |
+# [filename] -d 3 192.168.1.1 192.168.1.12 & |
|
| 7 |
+# disown [jobid] |
|
| 8 |
+import os |
|
| 9 |
+import sys |
|
| 10 |
+import subprocess |
|
| 11 |
+import time |
|
| 12 |
+from argparse import ArgumentParser |
|
| 13 |
+import threading |
|
| 14 |
+import queue |
|
| 15 |
+ |
|
| 16 |
+ |
|
| 17 |
+def pinger(): |
|
| 18 |
+ """Each queue item is a new address to ping. Ping and compare and print. |
|
| 19 |
+ Issue: Each of these threads reads from and writes to the global _results dict. |
|
| 20 |
+ """ |
|
| 21 |
+ while True: |
|
| 22 |
+ host = _ping_queue.get() |
|
| 23 |
+ now = time.localtime() |
|
| 24 |
+ success = ping(host) |
|
| 25 |
+ if success != _results[host]: |
|
| 26 |
+ status = "UP" if success else "DOWN" |
|
| 27 |
+ _print_queue.put([f'{time.strftime("%Y-%m-%d %H:%M:%S", now)} {host} {status}'])
|
|
| 28 |
+ _results[host] = success |
|
| 29 |
+ _ping_queue.task_done() |
|
| 30 |
+ |
|
| 31 |
+ |
|
| 32 |
+def ping(host): |
|
| 33 |
+ """Returns True if host (str) responds to a ping request.""" |
|
| 34 |
+ return subprocess.call([*_ping_cmd, host], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0 |
|
| 35 |
+ |
|
| 36 |
+ |
|
| 37 |
+def print_manager(): |
|
| 38 |
+ """The only thread allowed to write output.""" |
|
| 39 |
+ while True: |
|
| 40 |
+ job = _print_queue.get() |
|
| 41 |
+ for line in job: |
|
| 42 |
+ log(line) |
|
| 43 |
+ _print_queue.task_done() |
|
| 44 |
+ |
|
| 45 |
+ |
|
| 46 |
+def log(*args, **kwargs): |
|
| 47 |
+ """Opens the logfile, writes the log, and closes the logfile.""" |
|
| 48 |
+ # I don't leave the log file open for writing because another process needs access too. |
|
| 49 |
+ with open(_logname, 'a') as fp: |
|
| 50 |
+ return print(*args, **kwargs, file=fp) |
|
| 51 |
+ |
|
| 52 |
+ |
|
| 53 |
+if __name__ == '__main__': |
|
| 54 |
+ parser = ArgumentParser(description="Repeatedly ping sites. Ex: %(prog)s -d 3 192.168.1.1 192.168.1.12") |
|
| 55 |
+ parser.add_argument('-d', '--delay', type=float, default=1.0, help='Delay between pings')
|
|
| 56 |
+ parser.add_argument('addresses', nargs='+', help='Addresses to ping')
|
|
| 57 |
+ parser_args = parser.parse_args() |
|
| 58 |
+ |
|
| 59 |
+ _results = dict() |
|
| 60 |
+ for addr in parser_args.addresses: |
|
| 61 |
+ _results[addr] = None |
|
| 62 |
+ delay = parser_args.delay |
|
| 63 |
+ |
|
| 64 |
+ _ping_queue = queue.Queue(2 * len(parser_args.addresses)) # No more than 2 pending pings per address |
|
| 65 |
+ _print_queue = queue.Queue() |
|
| 66 |
+ |
|
| 67 |
+ _logname = os.path.join(os.path.expanduser('~'), 'log', os.path.basename(sys.argv[0]).replace('py', 'txt'))
|
|
| 68 |
+ _pid = os.getpid() |
|
| 69 |
+ |
|
| 70 |
+ # Choose the ping parameters appropriate for the platform |
|
| 71 |
+ if sys.platform == 'cygwin' or sys.platform == 'win32': |
|
| 72 |
+ _ping_cmd = ('ping', '-n', '1', '-w', '2000')
|
|
| 73 |
+ else: |
|
| 74 |
+ _ping_cmd = ('ping', '-c', '1', '-W', '2', '-q')
|
|
| 75 |
+ |
|
| 76 |
+ for i in range(len(parser_args.addresses)): |
|
| 77 |
+ t = threading.Thread(target=pinger, daemon=True) |
|
| 78 |
+ t.start() |
|
| 79 |
+ del t |
|
| 80 |
+ |
|
| 81 |
+ t = threading.Thread(target=print_manager, daemon=True) |
|
| 82 |
+ t.start() |
|
| 83 |
+ del t |
|
| 84 |
+ |
|
| 85 |
+ _print_queue.put([f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}'
|
|
| 86 |
+ f' PID={os.getpid()} repeat={delay}s Starting'])
|
|
| 87 |
+ while True: |
|
| 88 |
+ for address in _results.keys(): |
|
| 89 |
+ _ping_queue.put(address) |
|
| 90 |
+ time.sleep(delay) |
| ... | ... |
@@ -0,0 +1,78 @@ |
| 1 |
+#!/usr/bin/python3 |
|
| 2 |
+# |
|
| 3 |
+# Since this is meant to be run as a daemon, do not use #!/usr/bin/env python3, |
|
| 4 |
+# because the process name would then be python3, not based on __FILE__. |
|
| 5 |
+# |
|
| 6 |
+# [filename] -d 3 192.168.1.1 192.168.1.12 & |
|
| 7 |
+# disown [jobid] |
|
| 8 |
+import os |
|
| 9 |
+import sys |
|
| 10 |
+import subprocess |
|
| 11 |
+import time |
|
| 12 |
+from argparse import ArgumentParser |
|
| 13 |
+import threading |
|
| 14 |
+import queue |
|
| 15 |
+ |
|
| 16 |
+ |
|
| 17 |
+def pinger(host): |
|
| 18 |
+ """Executes one ping, prints if there was a change, exits thread.""" |
|
| 19 |
+ now = time.localtime() |
|
| 20 |
+ success = ping(host) |
|
| 21 |
+ if success != _results[host]: |
|
| 22 |
+ status = "UP" if success else "DOWN" |
|
| 23 |
+ _print_queue.put([f'{time.strftime("%Y-%m-%d %H:%M:%S", now)} {host} {status}'])
|
|
| 24 |
+ _results[host] = success |
|
| 25 |
+ |
|
| 26 |
+ |
|
| 27 |
+def ping(host): |
|
| 28 |
+ """Returns True if host (str) responds to a ping request.""" |
|
| 29 |
+ return subprocess.call([*_ping_cmd, host], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0 |
|
| 30 |
+ |
|
| 31 |
+ |
|
| 32 |
+def print_manager(): |
|
| 33 |
+ """ The only thread allowed to write output. """ |
|
| 34 |
+ while True: |
|
| 35 |
+ job = _print_queue.get() |
|
| 36 |
+ for line in job: |
|
| 37 |
+ log(line) |
|
| 38 |
+ _print_queue.task_done() |
|
| 39 |
+ |
|
| 40 |
+ |
|
| 41 |
+def log(*args, **kwargs): |
|
| 42 |
+ """Opens the logfile, writes the log, and closes the logfile.""" |
|
| 43 |
+ # I don't leave the log file open for writing because another process needs access too. |
|
| 44 |
+ with open(_logname, 'a') as fp: |
|
| 45 |
+ return print(*args, **kwargs, file=fp) |
|
| 46 |
+ |
|
| 47 |
+ |
|
| 48 |
+if __name__ == '__main__': |
|
| 49 |
+ parser = ArgumentParser(description="Repeatedly ping sites. Ex: %(prog)s -d 3 192.168.1.1 192.168.1.12") |
|
| 50 |
+ parser.add_argument('-d', '--delay', type=float, default=1.0, help='Delay between pings')
|
|
| 51 |
+ parser.add_argument('addresses', nargs='+', help='Addresses to ping')
|
|
| 52 |
+ parser_args = parser.parse_args() |
|
| 53 |
+ |
|
| 54 |
+ _results = dict() |
|
| 55 |
+ for addr in parser_args.addresses: |
|
| 56 |
+ _results[addr] = None |
|
| 57 |
+ delay = parser_args.delay |
|
| 58 |
+ _logname = os.path.join(os.path.expanduser('~'), 'log', os.path.basename(sys.argv[0]).replace('py', 'txt'))
|
|
| 59 |
+ |
|
| 60 |
+ # Choose the ping parameters appropriate for the platform |
|
| 61 |
+ if sys.platform == 'cygwin' or sys.platform == 'win32': |
|
| 62 |
+ _ping_cmd = ('ping', '-n', '1', '-w', '2000')
|
|
| 63 |
+ else: |
|
| 64 |
+ _ping_cmd = ('ping', '-c', '1', '-W', '2', '-q')
|
|
| 65 |
+ |
|
| 66 |
+ _print_queue = queue.Queue() |
|
| 67 |
+ t = threading.Thread(target=print_manager, daemon=True) |
|
| 68 |
+ t.start() |
|
| 69 |
+ del t |
|
| 70 |
+ |
|
| 71 |
+ _print_queue.put([f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}'
|
|
| 72 |
+ f' PID={os.getpid()} repeat={delay}s Starting'])
|
|
| 73 |
+ while True: |
|
| 74 |
+ for address in _results.keys(): |
|
| 75 |
+ t = threading.Thread(target=pinger, args=(address,), daemon=True) |
|
| 76 |
+ t.start() |
|
| 77 |
+ del t |
|
| 78 |
+ time.sleep(delay) |
|
| 0 | 79 |