diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d73a580..f40e03a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,14 @@ In general, I follow strict pep8 and pyflakes. All code must pass these tests. Since we support python 2.4-3.4 and pypy, pyflakes reports unknown names in python 3. pyflakes is run in python 2.7 only in my tests. +## Some other points + +1. Do not use `\` for line continuations, long strings should be wrapped in `()`. Imports should start a brand new line in the form of `from foo import...` +1. String quoting should be done with single quotes `'`, except for situations where you would otherwise have to escape an internal single quote +1. Docstrings should use three double quotes `"""` +1. All functions, classes and modules should have docstrings following both the PEP257 and PEP8 standards +1. Inline comments should only be used on code where it is not immediately obvious what the code achieves + # Supported Python Versions All code needs to support Python 2.4-3.4 and pypy. diff --git a/README.rst b/README.rst index e068d2c..36c1a6e 100644 --- a/README.rst +++ b/README.rst @@ -89,5 +89,26 @@ Usage --server SERVER Specify a server ID to test against --mini MINI URL of the Speedtest Mini server --source SOURCE Source IP address to bind to + --traceroute Runs traceroute against test host after speedtest --version Show the version number and exit +Inconsistency +------------- + +It is not a goal of this application to be a reliable latency reporting tool. + +Latency reported by this tool should not be relied on as a value indicative of ICMP +style latency. It is a relative value used for determining the lowest latency server +for performing the actual speed test against. + +There is the potential for this tool to report results inconsistent with Speedtest.net. +There are several concepts to be aware of that factor into the potential inconsistency: + +1. Speedtest.net has migrated to using pure socket tests instead of HTTP based tests +2. This application is written in Python +3. Different versions of Python will execute certain parts of the code faster than others +4. CPU and Memory capacity and speed will play a large part in inconsistency between + Speedtest.net and even other machines on the same network + +Issues relating to inconsistencies will be closed as wontfix and without +additional reason or context. diff --git a/speedtest_cli.py b/speedtest_cli.py index 6c89b1d..4884f3b 100755 --- a/speedtest_cli.py +++ b/speedtest_cli.py @@ -22,13 +22,14 @@ source = None shutdown_event = None import math -import time +import timeit import os import sys import threading import re import signal import socket +import platform # Used for bound_interface socket_socket = socket.socket @@ -48,6 +49,11 @@ try: except ImportError: from urllib.request import urlopen, Request, HTTPError, URLError +try: + from httplib import HTTPConnection, HTTPSConnection +except ImportError: + from http.client import HTTPConnection, HTTPSConnection + try: from Queue import Queue except ImportError: @@ -172,7 +178,7 @@ class FileGetter(threading.Thread): def run(self): self.result = [0] try: - if (time.time() - self.starttime) <= 10: + if (timeit.default_timer() - self.starttime) <= 10: f = urlopen(self.url) while 1 and not shutdown_event.isSet(): self.result.append(len(f.read(10240))) @@ -186,7 +192,7 @@ class FileGetter(threading.Thread): def downloadSpeed(files, quiet=False): """Function to launch FileGetter threads and calculate download speeds""" - start = time.time() + start = timeit.default_timer() def producer(q, files): for file in files: @@ -210,14 +216,14 @@ def downloadSpeed(files, quiet=False): q = Queue(6) prod_thread = threading.Thread(target=producer, args=(q, files)) cons_thread = threading.Thread(target=consumer, args=(q, len(files))) - start = time.time() + start = timeit.default_timer() prod_thread.start() cons_thread.start() while prod_thread.isAlive(): prod_thread.join(timeout=0.1) while cons_thread.isAlive(): cons_thread.join(timeout=0.1) - return (sum(finished) / (time.time() - start)) + return (sum(finished) / (timeit.default_timer() - start)) class FilePutter(threading.Thread): @@ -235,7 +241,7 @@ class FilePutter(threading.Thread): def run(self): try: - if ((time.time() - self.starttime) <= 10 and + if ((timeit.default_timer() - self.starttime) <= 10 and not shutdown_event.isSet()): f = urlopen(self.url, self.data) f.read(11) @@ -250,7 +256,7 @@ class FilePutter(threading.Thread): def uploadSpeed(url, sizes, quiet=False): """Function to launch FilePutter threads and calculate upload speeds""" - start = time.time() + start = timeit.default_timer() def producer(q, sizes): for size in sizes: @@ -274,14 +280,14 @@ def uploadSpeed(url, sizes, quiet=False): q = Queue(6) prod_thread = threading.Thread(target=producer, args=(q, sizes)) cons_thread = threading.Thread(target=consumer, args=(q, len(sizes))) - start = time.time() + start = timeit.default_timer() prod_thread.start() cons_thread.start() while prod_thread.isAlive(): prod_thread.join(timeout=0.1) while cons_thread.isAlive(): cons_thread.join(timeout=0.1) - return (sum(finished) / (time.time() - start)) + return (sum(finished) / (timeit.default_timer() - start)) def getAttributesByTagName(dom, tagName): @@ -310,19 +316,23 @@ def getConfig(): return None uh.close() try: - root = ET.fromstring(''.encode().join(configxml)) - config = { - 'client': root.find('client').attrib, - 'times': root.find('times').attrib, - 'download': root.find('download').attrib, - 'upload': root.find('upload').attrib} - except AttributeError: - root = DOM.parseString(''.join(configxml)) - config = { - 'client': getAttributesByTagName(root, 'client'), - 'times': getAttributesByTagName(root, 'times'), - 'download': getAttributesByTagName(root, 'download'), - 'upload': getAttributesByTagName(root, 'upload')} + try: + root = ET.fromstring(''.encode().join(configxml)) + config = { + 'client': root.find('client').attrib, + 'times': root.find('times').attrib, + 'download': root.find('download').attrib, + 'upload': root.find('upload').attrib} + except AttributeError: + root = DOM.parseString(''.join(configxml)) + config = { + 'client': getAttributesByTagName(root, 'client'), + 'times': getAttributesByTagName(root, 'times'), + 'download': getAttributesByTagName(root, 'download'), + 'upload': getAttributesByTagName(root, 'upload')} + except SyntaxError: + print_('Failed to parse speedtest.net configuration') + sys.exit(1) del root del configxml return config @@ -333,7 +343,7 @@ def closestServers(client, all=False): distance """ - uh = urlopen('http://www.speedtest.net/speedtest-servers.php') + uh = urlopen('http://c.speedtest.net/speedtest-servers-static.php') serversxml = [] while 1: serversxml.append(uh.read(10240)) @@ -343,11 +353,15 @@ def closestServers(client, all=False): return None uh.close() try: - root = ET.fromstring(''.encode().join(serversxml)) - elements = root.getiterator('server') - except AttributeError: - root = DOM.parseString(''.join(serversxml)) - elements = root.getElementsByTagName('server') + try: + root = ET.fromstring(''.encode().join(serversxml)) + elements = root.getiterator('server') + except AttributeError: + root = DOM.parseString(''.join(serversxml)) + elements = root.getElementsByTagName('server') + except SyntaxError: + print_('Failed to parse list of speedtest.net servers') + sys.exit(1) servers = {} for server in elements: try: @@ -380,31 +394,36 @@ def closestServers(client, all=False): def getBestServer(servers): - """Perform a speedtest.net "ping" to determine which speedtest.net - server has the lowest latency + """Perform a speedtest.net latency request to determine which + speedtest.net server has the lowest latency """ results = {} for server in servers: cum = [] - url = os.path.dirname(server['url']) + url = '%s/latency.txt' % os.path.dirname(server['url']) + urlparts = urlparse(url) for i in range(0, 3): try: - uh = urlopen('%s/latency.txt' % url) + if urlparts[0] == 'https': + h = HTTPSConnection(urlparts[1]) + else: + h = HTTPConnection(urlparts[1]) + start = timeit.default_timer() + h.request("GET", urlparts[2]) + r = h.getresponse() + total = (timeit.default_timer() - start) except (HTTPError, URLError): cum.append(3600) continue - start = time.time() - text = uh.read(9) - total = time.time() - start - if int(uh.code) == 200 and text == 'test=test'.encode(): + text = r.read(9) + if int(r.status) == 200 and text == 'test=test'.encode(): cum.append(total) else: cum.append(3600) - uh.close() - avg = round((sum(cum) / 3) * 1000000, 3) + h.close() + avg = round((sum(cum) / 6) * 1000, 3) results[avg] = server - fastest = sorted(results.keys())[0] best = results[fastest] best['latency'] = fastest @@ -423,11 +442,28 @@ def ctrl_c(signum, frame): def version(): + print getNetworkIp("speedtest-dev.oit.duke.edu") """Print the version""" raise SystemExit(__version__) +def tracerouter(url): + host = '{uri.netloc}'.format(uri=urlparse(url)) + tracer = 'tracert' if (platform.system() == 'Windows') else 'traceroute' + print_('Running ', tracer, ' against ', host) + + from subprocess import Popen, PIPE + p = Popen([tracer, '-d', '-w', '3', host], stdout=PIPE) + while True: + line = p.stdout.readline() + if not line: + break + print '\t', line, + p.wait() + return true + + def speedtest(): """Run the full speedtest.net test""" @@ -466,6 +502,9 @@ def speedtest(): parser.add_argument('--server', help='Specify a server ID to test against') parser.add_argument('--mini', help='URL of the Speedtest Mini server') parser.add_argument('--source', help='Source IP address to bind to') + parser.add_argument('--traceroute', action='store_true', + help='Runs traceroute against test host ' + 'after speedtest') parser.add_argument('--version', action='store_true', help='Show the version number and exit') @@ -573,22 +612,26 @@ def speedtest(): best = servers[0] else: if not args.simple: - print_('Selecting best server based on ping...') + print_('Selecting best server based on latency...') best = getBestServer(servers) if not args.simple: + host = '{uri.scheme}://{uri.netloc}/'.format(uri=urlparse(best['url'])) + print_(best['url']) + hostedby = ('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: ' + '%(latency)s ms' % best) + if args.traceroute: + hostedby += (' via %s' % host) # Python 2.7 and newer seem to be ok with the resultant encoding # from parsing the XML, but older versions have some issues. # This block should detect whether we need to encode or not try: unicode() - print_(('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: ' - '%(latency)s ms' % best).encode('utf-8', 'ignore')) + print_(hostedby.encode('utf-8', 'ignore')) except NameError: - print_('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: ' - '%(latency)s ms' % best) + print_(hostedby) else: - print_('Ping: %(latency)s ms' % best) + print_('Latency: %(latency)s ms' % best) sizes = [350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000] urls = [] @@ -661,6 +704,12 @@ def speedtest(): print_('Share results: http://www.speedtest.net/result/%s.png' % resultid[0]) + if args.traceroute: + try: + tracerouter(best['url']) + except: + print_('Unable to run Traceroute against ', + '{uri.netloc}'.format(uri=urlparse(best['url']))) def main():