This commit is contained in:
Dean Peters 2014-06-26 02:29:49 +00:00
commit 0d33778936
3 changed files with 123 additions and 45 deletions

View File

@ -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. 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 # Supported Python Versions
All code needs to support Python 2.4-3.4 and pypy. All code needs to support Python 2.4-3.4 and pypy.

View File

@ -89,5 +89,26 @@ Usage
--server SERVER Specify a server ID to test against --server SERVER Specify a server ID to test against
--mini MINI URL of the Speedtest Mini server --mini MINI URL of the Speedtest Mini server
--source SOURCE Source IP address to bind to --source SOURCE Source IP address to bind to
--traceroute Runs traceroute against test host after speedtest
--version Show the version number and exit --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.

View File

@ -22,13 +22,14 @@ source = None
shutdown_event = None shutdown_event = None
import math import math
import time import timeit
import os import os
import sys import sys
import threading import threading
import re import re
import signal import signal
import socket import socket
import platform
# Used for bound_interface # Used for bound_interface
socket_socket = socket.socket socket_socket = socket.socket
@ -48,6 +49,11 @@ try:
except ImportError: except ImportError:
from urllib.request import urlopen, Request, HTTPError, URLError from urllib.request import urlopen, Request, HTTPError, URLError
try:
from httplib import HTTPConnection, HTTPSConnection
except ImportError:
from http.client import HTTPConnection, HTTPSConnection
try: try:
from Queue import Queue from Queue import Queue
except ImportError: except ImportError:
@ -172,7 +178,7 @@ class FileGetter(threading.Thread):
def run(self): def run(self):
self.result = [0] self.result = [0]
try: try:
if (time.time() - self.starttime) <= 10: if (timeit.default_timer() - self.starttime) <= 10:
f = urlopen(self.url) f = urlopen(self.url)
while 1 and not shutdown_event.isSet(): while 1 and not shutdown_event.isSet():
self.result.append(len(f.read(10240))) self.result.append(len(f.read(10240)))
@ -186,7 +192,7 @@ class FileGetter(threading.Thread):
def downloadSpeed(files, quiet=False): def downloadSpeed(files, quiet=False):
"""Function to launch FileGetter threads and calculate download speeds""" """Function to launch FileGetter threads and calculate download speeds"""
start = time.time() start = timeit.default_timer()
def producer(q, files): def producer(q, files):
for file in files: for file in files:
@ -210,14 +216,14 @@ def downloadSpeed(files, quiet=False):
q = Queue(6) q = Queue(6)
prod_thread = threading.Thread(target=producer, args=(q, files)) prod_thread = threading.Thread(target=producer, args=(q, files))
cons_thread = threading.Thread(target=consumer, args=(q, len(files))) cons_thread = threading.Thread(target=consumer, args=(q, len(files)))
start = time.time() start = timeit.default_timer()
prod_thread.start() prod_thread.start()
cons_thread.start() cons_thread.start()
while prod_thread.isAlive(): while prod_thread.isAlive():
prod_thread.join(timeout=0.1) prod_thread.join(timeout=0.1)
while cons_thread.isAlive(): while cons_thread.isAlive():
cons_thread.join(timeout=0.1) cons_thread.join(timeout=0.1)
return (sum(finished) / (time.time() - start)) return (sum(finished) / (timeit.default_timer() - start))
class FilePutter(threading.Thread): class FilePutter(threading.Thread):
@ -235,7 +241,7 @@ class FilePutter(threading.Thread):
def run(self): def run(self):
try: try:
if ((time.time() - self.starttime) <= 10 and if ((timeit.default_timer() - self.starttime) <= 10 and
not shutdown_event.isSet()): not shutdown_event.isSet()):
f = urlopen(self.url, self.data) f = urlopen(self.url, self.data)
f.read(11) f.read(11)
@ -250,7 +256,7 @@ class FilePutter(threading.Thread):
def uploadSpeed(url, sizes, quiet=False): def uploadSpeed(url, sizes, quiet=False):
"""Function to launch FilePutter threads and calculate upload speeds""" """Function to launch FilePutter threads and calculate upload speeds"""
start = time.time() start = timeit.default_timer()
def producer(q, sizes): def producer(q, sizes):
for size in sizes: for size in sizes:
@ -274,14 +280,14 @@ def uploadSpeed(url, sizes, quiet=False):
q = Queue(6) q = Queue(6)
prod_thread = threading.Thread(target=producer, args=(q, sizes)) prod_thread = threading.Thread(target=producer, args=(q, sizes))
cons_thread = threading.Thread(target=consumer, args=(q, len(sizes))) cons_thread = threading.Thread(target=consumer, args=(q, len(sizes)))
start = time.time() start = timeit.default_timer()
prod_thread.start() prod_thread.start()
cons_thread.start() cons_thread.start()
while prod_thread.isAlive(): while prod_thread.isAlive():
prod_thread.join(timeout=0.1) prod_thread.join(timeout=0.1)
while cons_thread.isAlive(): while cons_thread.isAlive():
cons_thread.join(timeout=0.1) cons_thread.join(timeout=0.1)
return (sum(finished) / (time.time() - start)) return (sum(finished) / (timeit.default_timer() - start))
def getAttributesByTagName(dom, tagName): def getAttributesByTagName(dom, tagName):
@ -310,19 +316,23 @@ def getConfig():
return None return None
uh.close() uh.close()
try: try:
root = ET.fromstring(''.encode().join(configxml)) try:
config = { root = ET.fromstring(''.encode().join(configxml))
'client': root.find('client').attrib, config = {
'times': root.find('times').attrib, 'client': root.find('client').attrib,
'download': root.find('download').attrib, 'times': root.find('times').attrib,
'upload': root.find('upload').attrib} 'download': root.find('download').attrib,
except AttributeError: 'upload': root.find('upload').attrib}
root = DOM.parseString(''.join(configxml)) except AttributeError:
config = { root = DOM.parseString(''.join(configxml))
'client': getAttributesByTagName(root, 'client'), config = {
'times': getAttributesByTagName(root, 'times'), 'client': getAttributesByTagName(root, 'client'),
'download': getAttributesByTagName(root, 'download'), 'times': getAttributesByTagName(root, 'times'),
'upload': getAttributesByTagName(root, 'upload')} 'download': getAttributesByTagName(root, 'download'),
'upload': getAttributesByTagName(root, 'upload')}
except SyntaxError:
print_('Failed to parse speedtest.net configuration')
sys.exit(1)
del root del root
del configxml del configxml
return config return config
@ -333,7 +343,7 @@ def closestServers(client, all=False):
distance distance
""" """
uh = urlopen('http://www.speedtest.net/speedtest-servers.php') uh = urlopen('http://c.speedtest.net/speedtest-servers-static.php')
serversxml = [] serversxml = []
while 1: while 1:
serversxml.append(uh.read(10240)) serversxml.append(uh.read(10240))
@ -343,11 +353,15 @@ def closestServers(client, all=False):
return None return None
uh.close() uh.close()
try: try:
root = ET.fromstring(''.encode().join(serversxml)) try:
elements = root.getiterator('server') root = ET.fromstring(''.encode().join(serversxml))
except AttributeError: elements = root.getiterator('server')
root = DOM.parseString(''.join(serversxml)) except AttributeError:
elements = root.getElementsByTagName('server') root = DOM.parseString(''.join(serversxml))
elements = root.getElementsByTagName('server')
except SyntaxError:
print_('Failed to parse list of speedtest.net servers')
sys.exit(1)
servers = {} servers = {}
for server in elements: for server in elements:
try: try:
@ -380,31 +394,36 @@ def closestServers(client, all=False):
def getBestServer(servers): def getBestServer(servers):
"""Perform a speedtest.net "ping" to determine which speedtest.net """Perform a speedtest.net latency request to determine which
server has the lowest latency speedtest.net server has the lowest latency
""" """
results = {} results = {}
for server in servers: for server in servers:
cum = [] 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): for i in range(0, 3):
try: 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): except (HTTPError, URLError):
cum.append(3600) cum.append(3600)
continue continue
start = time.time() text = r.read(9)
text = uh.read(9) if int(r.status) == 200 and text == 'test=test'.encode():
total = time.time() - start
if int(uh.code) == 200 and text == 'test=test'.encode():
cum.append(total) cum.append(total)
else: else:
cum.append(3600) cum.append(3600)
uh.close() h.close()
avg = round((sum(cum) / 3) * 1000000, 3) avg = round((sum(cum) / 6) * 1000, 3)
results[avg] = server results[avg] = server
fastest = sorted(results.keys())[0] fastest = sorted(results.keys())[0]
best = results[fastest] best = results[fastest]
best['latency'] = fastest best['latency'] = fastest
@ -423,11 +442,28 @@ def ctrl_c(signum, frame):
def version(): def version():
print getNetworkIp("speedtest-dev.oit.duke.edu")
"""Print the version""" """Print the version"""
raise SystemExit(__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(): def speedtest():
"""Run the full speedtest.net test""" """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('--server', help='Specify a server ID to test against')
parser.add_argument('--mini', help='URL of the Speedtest Mini server') 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('--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', parser.add_argument('--version', action='store_true',
help='Show the version number and exit') help='Show the version number and exit')
@ -573,22 +612,26 @@ def speedtest():
best = servers[0] best = servers[0]
else: else:
if not args.simple: if not args.simple:
print_('Selecting best server based on ping...') print_('Selecting best server based on latency...')
best = getBestServer(servers) best = getBestServer(servers)
if not args.simple: 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 # Python 2.7 and newer seem to be ok with the resultant encoding
# from parsing the XML, but older versions have some issues. # from parsing the XML, but older versions have some issues.
# This block should detect whether we need to encode or not # This block should detect whether we need to encode or not
try: try:
unicode() unicode()
print_(('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: ' print_(hostedby.encode('utf-8', 'ignore'))
'%(latency)s ms' % best).encode('utf-8', 'ignore'))
except NameError: except NameError:
print_('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: ' print_(hostedby)
'%(latency)s ms' % best)
else: else:
print_('Ping: %(latency)s ms' % best) print_('Latency: %(latency)s ms' % best)
sizes = [350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000] sizes = [350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000]
urls = [] urls = []
@ -661,6 +704,12 @@ def speedtest():
print_('Share results: http://www.speedtest.net/result/%s.png' % print_('Share results: http://www.speedtest.net/result/%s.png' %
resultid[0]) 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(): def main():