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 |