diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f10532f --- /dev/null +++ b/.gitignore @@ -0,0 +1,97 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# PyCharm +.idea + +# Private +data/ +p_* + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..838e3a9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2017 Youfou + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..64ad321 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.md LICENSE diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b7bc2e --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# mping + +Ping hosts concurrently and find the fastest to you. + +## Installation + + pip3 install -U mping + +## Usages + +Just tell which host is the fastest: + + mping host1.com host2.com host3.com + +Get hosts from a file, and ping them: + + mping -p PATH/TO/THE/FILE.txt + +> Read **Input File** section below for more details. + +The results will be like this: + + host | count, loss%, min/avg/max + -----------|-------------------------- + shuame.com | 433, 0.0%, 5.4/6.8/14.1 + qq.com | 90, 0.0%, 31.8/33.5/39.5 + baidu.com | 77, 0.0%, 37.4/39.1/43.6 + +> The `count` number represents that how many pings returned to the each host. + +Also check out the help stuff for more instructions: + + mping -h + +## Input File + +You can use either a plain text file or a Json file as the `-p` / `--path` argument. + + +**Plain Text File** + +When use a plain text file, just place each host in a line. + +For example: + + host1.com + host2.com + host3.com + +**Json File** + +You can also use a json file as the input hosts, and below is the 2 modes: + + Put hosts in a list: + + ```json +[ + "host1.com", + "host2.com", + "host3.com" +] +``` + +Or in an object (dict) with names: + + ```json +{ + "S1": "host1.com", + "S2": "host3.com", + "S3": "host3.com" +} +``` + +> The names will be printed in the results. diff --git a/mping/__init__.py b/mping/__init__.py new file mode 100755 index 0000000..9d32a41 --- /dev/null +++ b/mping/__init__.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +""" + +mping +~~~~~ +Ping hosts concurrently and find the fastest to you. + + + >>> results = mping() + + +---- + +GitHub: https://github.com/youfou/mping + +---- + +:copyright: (c) 2017 by Youfou. +:license: Apache 2.0, see LICENSE for more details. + +""" + +from .mping import mping, results_string + +__title__ = 'mping' +__version__ = '0.1.0' +__author__ = 'Youfou' +__license__ = 'Apache 2.0' +__copyright__ = 'Copyright 2017 Youfou' diff --git a/mping/mping.py b/mping/mping.py new file mode 100755 index 0000000..f1d014a --- /dev/null +++ b/mping/mping.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +import argparse +import ctypes +import json +import os +import random +import re +import select +import socket +import struct +import sys +import threading +import time + +if sys.platform.startswith('win32'): + clock = time.clock + run_as_root = ctypes.windll.shell32.IsUserAnAdmin() != 0 +else: + clock = time.time + run_as_root = os.getuid() == 0 + +DEFAULT_DURATION = 3 + +EXIT_CODE_BY_USER = 1 +EXIT_CODE_DNS_ERR = 2 +EXIT_CODE_IO_ERR = 3 + + +# Credit: https://gist.github.com/pyos/10980172 +def chk(data): + x = sum(x << 8 if i % 2 else x for i, x in enumerate(data)) & 0xFFFFFFFF + x = (x >> 16) + (x & 0xFFFF) + x = (x >> 16) + (x & 0xFFFF) + return struct.pack(' 0: + doing_msg += ' within {} seconds'.format(duration) + doing_msg += '...' + + if quiet: + print(doing_msg) + + for task in tasks: + task.start() + + start = clock() + remain = duration + + try: + while remain > 0 or duration <= 0: + time.sleep(min(remain, 1)) + if not quiet: + print('\n{}\n{}'.format( + results_string(ret(tasks, True)[:10]), + doing_msg)) + remain = duration + start - clock() + except KeyboardInterrupt: + print() + finally: + for task in tasks: + task.stop() + for task in tasks: + task.join() + + return ret(tasks, sort) + + +def table_string(rows): + rows = list(map(lambda x: list(map(str, x)), rows)) + widths = list(map(lambda x: max(map(len, x)), zip(*rows))) + rows = list(map(lambda y: ' | '.join(map(lambda x: '{:{w}}'.format(x[0], w=x[1]), zip(y, widths))), rows)) + rows.insert(1, '-|-'.join(list(map(lambda x: '-' * x, widths)))) + return '\n'.join(rows) + + +def results_string(prs): + named = True if isinstance(prs[0][0], tuple) else False + rows = [['host', 'count, loss%, min/avg/max']] + if named: + rows[0].insert(0, 'name') + + for head, pr in prs: + row = list() + if named: + row.extend(head) + else: + row.append(head) + row.append(pr.form_text) + rows.append(row) + return table_string(rows) + + +def main(): + ap = argparse.ArgumentParser( + description='Ping hosts concurrently and find the fastest to you.', + epilog='A plain text file or a json can be used as the -p/--path argument: ' + '1. Plain text file: hosts in lines; ' + '2. Json file: hosts in a list or a object (dict) with names.') + + ap.add_argument( + 'hosts', type=str, nargs='*', + help='a list of hosts, separated by space') + + ap.add_argument( + '-p', '--path', type=str, metavar='path', + help='specify a file path to get the hosts from') + + ap.add_argument( + '-d', '--duration', type=int, default=DEFAULT_DURATION, metavar='secs', + help='the duration how long the progress lasts (default: {})'.format(DEFAULT_DURATION)) + + ap.add_argument( + '-i', '--interval', type=float, default=0.0, metavar='secs', + help='the max time to wait between pings in each thread (default: 0)') + + ap.add_argument( + '-t', '--timeout', type=float, default=1.0, metavar='secs', + help='the timeout for each single ping in each thread (default: 1.0)') + + ap.add_argument( + '-a', '--all', action='store_true', + help='show all results (default: top 10 results)' + ', and note this option can be overridden by -S/--no_sort') + + ap.add_argument( + '-q', '--quiet', action='store_true', + help='do not print results while processing (default: print the top 10 hosts)' + ) + + ap.add_argument( + '-S', '--do_not_sort', action='store_false', dest='sort', + help='do not sort the results (default: sort by ping count in descending order)') + + args = ap.parse_args() + + hosts = None + if not args.path and not args.hosts: + ap.print_help() + elif args.path: + try: + with open(args.path) as fp: + hosts = json.load(fp) + except IOError as e: + print('Unable open file:\n{}'.format(e)) + exit(EXIT_CODE_IO_ERR) + except json.JSONDecodeError: + with open(args.path) as fp: + hosts = re.findall(r'^\s*([a-z0-9\-.]+)\s*$', fp.read(), re.M) + else: + hosts = args.hosts + + if not hosts: + exit() + + results = mping( + hosts=hosts, + duration=args.duration, + timeout=args.timeout, + interval=args.interval, + quiet=args.quiet, + sort=args.sort + ) + + if not args.all and args.sort: + results = results[:10] + + if not args.quiet: + print('\nFinal Results:\n') + print(results_string(results)) + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..83dc84f --- /dev/null +++ b/setup.py @@ -0,0 +1,51 @@ +import logging + +from setuptools import setup, find_packages + +readme_file = 'README.md' + +try: + import pypandoc + + long_description = pypandoc.convert(readme_file, to='rst') +except ImportError: + logging.warning('pypandoc module not found, long_description will be the raw text instead.') + with open(readme_file, encoding='utf-8') as fp: + long_description = fp.read() + +setup( + name='mping', + version='0.1.0', + packages=find_packages(), + package_data={ + '': ['*.md'], + }, + include_package_data=True, + + entry_points={ + 'console_scripts': [ + 'mping = mping.mping:main' + ] + }, + + url='https://github.com/youfou/mping', + license='Apache 2.0', + author='Youfou', + author_email='youfou@qq.com', + description='Ping hosts concurrently and find the fastest to you.', + long_description=long_description, + keywords=[ + 'ping', + 'host', + 'monitor' + ], + classifiers=[ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python :: 3', + 'Operating System :: OS Independent', + 'Intended Audience :: System Administrators', + 'Topic :: System :: Networking :: Monitoring', + 'Topic :: Utilities', + ] +)