first commit.
David Blume

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
+![Single Threaded Pings](http://git.dlma.com/python_pinger.git/blob/master/images/ping_single_threaded.png)
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
+![Long Lived Queue Workers](http://git.dlma.com/python_pinger.git/blob/master/images/ping_long_lived_queue_workers.png)
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
+![Short Lived Workers](http://git.dlma.com/python_pinger.git/blob/master/images/ping_short_lived_workers.png)
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
+![Long Lived Looping Workers](http://git.dlma.com/python_pinger.git/blob/master/images/ping_long_lived_looping_workers.png)
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