5
0

speed_test.py 60 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # Copyright 2012 Matt Martz
  4. # All Rights Reserved.
  5. #
  6. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  7. # not use this file except in compliance with the License. You may obtain
  8. # a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  14. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  15. # License for the specific language governing permissions and limitations
  16. # under the License.
  17. import csv
  18. import datetime
  19. import errno
  20. import math
  21. import os
  22. import platform
  23. import re
  24. import signal
  25. import socket
  26. import sys
  27. import threading
  28. import timeit
  29. import xml.parsers.expat
  30. try:
  31. import gzip
  32. GZIP_BASE = gzip.GzipFile
  33. except ImportError:
  34. gzip = None
  35. GZIP_BASE = object
  36. __version__ = "2.1.4b1"
  37. class FakeShutdownEvent(object):
  38. """Class to fake a threading.Event.isSet so that users of this module
  39. are not required to register their own threading.Event()
  40. """
  41. @staticmethod
  42. def isSet():
  43. "Dummy method to always return false" ""
  44. return False
  45. is_set = isSet
  46. # Some global variables we use
  47. DEBUG = False
  48. _GLOBAL_DEFAULT_TIMEOUT = object()
  49. PY25PLUS = sys.version_info[:2] >= (2, 5)
  50. PY26PLUS = sys.version_info[:2] >= (2, 6)
  51. PY32PLUS = sys.version_info[:2] >= (3, 2)
  52. PY310PLUS = sys.version_info[:2] >= (3, 10)
  53. # Begin import game to handle Python 2 and Python 3
  54. try:
  55. import json
  56. except ImportError:
  57. try:
  58. import simplejson as json
  59. except ImportError:
  60. json = None
  61. try:
  62. import xml.etree.ElementTree as ET
  63. try:
  64. from xml.etree.ElementTree import _Element as ET_Element
  65. except ImportError:
  66. pass
  67. except ImportError:
  68. from xml.dom import minidom as DOM
  69. from xml.parsers.expat import ExpatError
  70. ET = None
  71. try:
  72. from urllib2 import (
  73. AbstractHTTPHandler,
  74. HTTPDefaultErrorHandler,
  75. HTTPError,
  76. HTTPErrorProcessor,
  77. HTTPRedirectHandler,
  78. OpenerDirector,
  79. ProxyHandler,
  80. Request,
  81. URLError,
  82. urlopen,
  83. )
  84. except ImportError:
  85. from urllib.request import (
  86. AbstractHTTPHandler,
  87. HTTPDefaultErrorHandler,
  88. HTTPError,
  89. HTTPErrorProcessor,
  90. HTTPRedirectHandler,
  91. OpenerDirector,
  92. ProxyHandler,
  93. Request,
  94. URLError,
  95. urlopen,
  96. )
  97. try:
  98. from httplib import BadStatusLine, HTTPConnection
  99. except ImportError:
  100. from http.client import BadStatusLine, HTTPConnection
  101. try:
  102. from httplib import HTTPSConnection
  103. except ImportError:
  104. try:
  105. from http.client import HTTPSConnection
  106. except ImportError:
  107. HTTPSConnection = None
  108. try:
  109. from httplib import FakeSocket
  110. except ImportError:
  111. FakeSocket = None
  112. try:
  113. from Queue import Queue
  114. except ImportError:
  115. from queue import Queue
  116. try:
  117. from urlparse import urlparse
  118. except ImportError:
  119. from urllib.parse import urlparse
  120. try:
  121. from urlparse import parse_qs
  122. except ImportError:
  123. try:
  124. from urllib.parse import parse_qs
  125. except ImportError:
  126. from cgi import parse_qs
  127. try:
  128. from hashlib import md5
  129. except ImportError:
  130. from md5 import md5
  131. try:
  132. from argparse import SUPPRESS as ARG_SUPPRESS
  133. from argparse import ArgumentParser as ArgParser
  134. PARSER_TYPE_INT = int
  135. PARSER_TYPE_STR = str
  136. PARSER_TYPE_FLOAT = float
  137. except ImportError:
  138. from optparse import SUPPRESS_HELP as ARG_SUPPRESS
  139. from optparse import OptionParser as ArgParser
  140. PARSER_TYPE_INT = "int"
  141. PARSER_TYPE_STR = "string"
  142. PARSER_TYPE_FLOAT = "float"
  143. try:
  144. from cStringIO import StringIO
  145. BytesIO = None
  146. except ImportError:
  147. try:
  148. from StringIO import StringIO
  149. BytesIO = None
  150. except ImportError:
  151. from io import BytesIO, StringIO
  152. try:
  153. import __builtin__
  154. except ImportError:
  155. import builtins
  156. from io import FileIO, TextIOWrapper
  157. class _Py3Utf8Output(TextIOWrapper):
  158. """UTF-8 encoded wrapper around stdout for py3, to override
  159. ASCII stdout
  160. """
  161. def __init__(self, f, **kwargs):
  162. buf = FileIO(f.fileno(), "w")
  163. super(_Py3Utf8Output, self).__init__(buf, encoding="utf8", errors="strict")
  164. def write(self, s):
  165. super(_Py3Utf8Output, self).write(s)
  166. self.flush()
  167. _py3_print = getattr(builtins, "print")
  168. try:
  169. _py3_utf8_stdout = _Py3Utf8Output(sys.stdout)
  170. _py3_utf8_stderr = _Py3Utf8Output(sys.stderr)
  171. except OSError:
  172. # sys.stdout/sys.stderr is not a compatible stdout/stderr object
  173. # just use it and hope things go ok
  174. _py3_utf8_stdout = sys.stdout
  175. _py3_utf8_stderr = sys.stderr
  176. def to_utf8(v):
  177. """No-op encode to utf-8 for py3"""
  178. return v
  179. def print_(*args, **kwargs):
  180. """Wrapper function for py3 to print, with a utf-8 encoded stdout"""
  181. if kwargs.get("file") == sys.stderr:
  182. kwargs["file"] = _py3_utf8_stderr
  183. else:
  184. kwargs["file"] = kwargs.get("file", _py3_utf8_stdout)
  185. _py3_print(*args, **kwargs)
  186. else:
  187. del __builtin__
  188. def to_utf8(v):
  189. """Encode value to utf-8 if possible for py2"""
  190. try:
  191. return v.encode("utf8", "strict")
  192. except AttributeError:
  193. return v
  194. def print_(*args, **kwargs):
  195. """The new-style print function for Python 2.4 and 2.5.
  196. Taken from https://pypi.python.org/pypi/six/
  197. Modified to set encoding to UTF-8 always, and to flush after write
  198. """
  199. fp = kwargs.pop("file", sys.stdout)
  200. if fp is None:
  201. return
  202. def write(data):
  203. if not isinstance(data, basestring):
  204. data = str(data)
  205. # If the file has an encoding, encode unicode with it.
  206. encoding = "utf8" # Always trust UTF-8 for output
  207. if isinstance(fp, file) and isinstance(data, unicode) and encoding is not None:
  208. errors = getattr(fp, "errors", None)
  209. if errors is None:
  210. errors = "strict"
  211. data = data.encode(encoding, errors)
  212. fp.write(data)
  213. fp.flush()
  214. want_unicode = False
  215. sep = kwargs.pop("sep", None)
  216. if sep is not None:
  217. if isinstance(sep, unicode):
  218. want_unicode = True
  219. elif not isinstance(sep, str):
  220. raise TypeError("sep must be None or a string")
  221. end = kwargs.pop("end", None)
  222. if end is not None:
  223. if isinstance(end, unicode):
  224. want_unicode = True
  225. elif not isinstance(end, str):
  226. raise TypeError("end must be None or a string")
  227. if kwargs:
  228. raise TypeError("invalid keyword arguments to print()")
  229. if not want_unicode:
  230. for arg in args:
  231. if isinstance(arg, unicode):
  232. want_unicode = True
  233. break
  234. if want_unicode:
  235. newline = unicode("\n")
  236. space = unicode(" ")
  237. else:
  238. newline = "\n"
  239. space = " "
  240. if sep is None:
  241. sep = space
  242. if end is None:
  243. end = newline
  244. for i, arg in enumerate(args):
  245. if i:
  246. write(sep)
  247. write(arg)
  248. write(end)
  249. # Exception "constants" to support Python 2 through Python 3
  250. try:
  251. import ssl
  252. try:
  253. CERT_ERROR = (ssl.CertificateError,)
  254. except AttributeError:
  255. CERT_ERROR = tuple()
  256. HTTP_ERRORS = (HTTPError, URLError, socket.error, ssl.SSLError, BadStatusLine) + CERT_ERROR
  257. except ImportError:
  258. ssl = None
  259. HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine)
  260. if PY32PLUS:
  261. etree_iter = ET.Element.iter
  262. elif PY25PLUS:
  263. etree_iter = ET_Element.getiterator
  264. if PY26PLUS:
  265. thread_is_alive = threading.Thread.is_alive
  266. else:
  267. thread_is_alive = threading.Thread.isAlive
  268. def event_is_set(event):
  269. try:
  270. return event.is_set()
  271. except AttributeError:
  272. return event.isSet()
  273. class SpeedtestException(Exception):
  274. """Base exception for this module"""
  275. class SpeedtestCLIError(SpeedtestException):
  276. """Generic exception for raising errors during CLI operation"""
  277. class SpeedtestHTTPError(SpeedtestException):
  278. """Base HTTP exception for this module"""
  279. class SpeedtestConfigError(SpeedtestException):
  280. """Configuration XML is invalid"""
  281. class SpeedtestServersError(SpeedtestException):
  282. """Servers XML is invalid"""
  283. class ConfigRetrievalError(SpeedtestHTTPError):
  284. """Could not retrieve config.php"""
  285. class ServersRetrievalError(SpeedtestHTTPError):
  286. """Could not retrieve speedtest-servers.php"""
  287. class InvalidServerIDType(SpeedtestException):
  288. """Server ID used for filtering was not an integer"""
  289. class NoMatchedServers(SpeedtestException):
  290. """No servers matched when filtering"""
  291. class SpeedtestMiniConnectFailure(SpeedtestException):
  292. """Could not connect to the provided speedtest mini server"""
  293. class InvalidSpeedtestMiniServer(SpeedtestException):
  294. """Server provided as a speedtest mini server does not actually appear
  295. to be a speedtest mini server
  296. """
  297. class ShareResultsConnectFailure(SpeedtestException):
  298. """Could not connect to speedtest.net API to POST results"""
  299. class ShareResultsSubmitFailure(SpeedtestException):
  300. """Unable to successfully POST results to speedtest.net API after
  301. connection
  302. """
  303. class SpeedtestUploadTimeout(SpeedtestException):
  304. """testlength configuration reached during upload
  305. Used to ensure the upload halts when no additional data should be sent
  306. """
  307. class SpeedtestBestServerFailure(SpeedtestException):
  308. """Unable to determine best server"""
  309. class SpeedtestMissingBestServer(SpeedtestException):
  310. """get_best_server not called or not able to determine best server"""
  311. def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None):
  312. """Connect to *address* and return the socket object.
  313. Convenience function. Connect to *address* (a 2-tuple ``(host,
  314. port)``) and return the socket object. Passing the optional
  315. *timeout* parameter will set the timeout on the socket instance
  316. before attempting to connect. If no *timeout* is supplied, the
  317. global default timeout setting returned by :func:`getdefaulttimeout`
  318. is used. If *source_address* is set it must be a tuple of (host, port)
  319. for the socket to bind as a source address before making the connection.
  320. An host of '' or port 0 tells the OS to use the default.
  321. Largely vendored from Python 2.7, modified to work with Python 2.4
  322. """
  323. host, port = address
  324. err = None
  325. for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
  326. af, socktype, proto, canonname, sa = res
  327. sock = None
  328. try:
  329. sock = socket.socket(af, socktype, proto)
  330. if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
  331. sock.settimeout(float(timeout))
  332. if source_address:
  333. sock.bind(source_address)
  334. sock.connect(sa)
  335. return sock
  336. except socket.error:
  337. err = get_exception()
  338. if sock is not None:
  339. sock.close()
  340. if err is not None:
  341. raise err
  342. else:
  343. raise socket.error("getaddrinfo returns an empty list")
  344. class SpeedtestHTTPConnection(HTTPConnection):
  345. """Custom HTTPConnection to support source_address across
  346. Python 2.4 - Python 3
  347. """
  348. def __init__(self, *args, **kwargs):
  349. source_address = kwargs.pop("source_address", None)
  350. timeout = kwargs.pop("timeout", 10)
  351. self._tunnel_host = None
  352. HTTPConnection.__init__(self, *args, **kwargs)
  353. self.source_address = source_address
  354. self.timeout = timeout
  355. def connect(self):
  356. """Connect to the host and port specified in __init__."""
  357. try:
  358. self.sock = socket.create_connection((self.host, self.port), self.timeout, self.source_address)
  359. except (AttributeError, TypeError):
  360. self.sock = create_connection((self.host, self.port), self.timeout, self.source_address)
  361. if self._tunnel_host:
  362. self._tunnel()
  363. if HTTPSConnection:
  364. class SpeedtestHTTPSConnection(HTTPSConnection):
  365. """Custom HTTPSConnection to support source_address across
  366. Python 2.4 - Python 3
  367. """
  368. default_port = 443
  369. def __init__(self, *args, **kwargs):
  370. source_address = kwargs.pop("source_address", None)
  371. timeout = kwargs.pop("timeout", 10)
  372. self._tunnel_host = None
  373. HTTPSConnection.__init__(self, *args, **kwargs)
  374. self.timeout = timeout
  375. self.source_address = source_address
  376. def connect(self):
  377. "Connect to a host on a given (SSL) port."
  378. try:
  379. self.sock = socket.create_connection((self.host, self.port), self.timeout, self.source_address)
  380. except (AttributeError, TypeError):
  381. self.sock = create_connection((self.host, self.port), self.timeout, self.source_address)
  382. if self._tunnel_host:
  383. self._tunnel()
  384. if ssl:
  385. try:
  386. kwargs = {}
  387. if hasattr(ssl, "SSLContext"):
  388. if self._tunnel_host:
  389. kwargs["server_hostname"] = self._tunnel_host
  390. else:
  391. kwargs["server_hostname"] = self.host
  392. self.sock = self._context.wrap_socket(self.sock, **kwargs)
  393. except AttributeError:
  394. self.sock = ssl.wrap_socket(self.sock)
  395. try:
  396. self.sock.server_hostname = self.host
  397. except AttributeError:
  398. pass
  399. elif FakeSocket:
  400. # Python 2.4/2.5 support
  401. try:
  402. self.sock = FakeSocket(self.sock, socket.ssl(self.sock))
  403. except AttributeError:
  404. raise SpeedtestException("This version of Python does not support HTTPS/SSL " "functionality")
  405. else:
  406. raise SpeedtestException("This version of Python does not support HTTPS/SSL " "functionality")
  407. def _build_connection(connection, source_address, timeout, context=None):
  408. """Cross Python 2.4 - Python 3 callable to build an ``HTTPConnection`` or
  409. ``HTTPSConnection`` with the args we need
  410. Called from ``http(s)_open`` methods of ``SpeedtestHTTPHandler`` or
  411. ``SpeedtestHTTPSHandler``
  412. """
  413. def inner(host, **kwargs):
  414. kwargs.update({"source_address": source_address, "timeout": timeout})
  415. if context:
  416. kwargs["context"] = context
  417. return connection(host, **kwargs)
  418. return inner
  419. class SpeedtestHTTPHandler(AbstractHTTPHandler):
  420. """Custom ``HTTPHandler`` that can build a ``HTTPConnection`` with the
  421. args we need for ``source_address`` and ``timeout``
  422. """
  423. def __init__(self, debuglevel=0, source_address=None, timeout=10):
  424. AbstractHTTPHandler.__init__(self, debuglevel)
  425. self.source_address = source_address
  426. self.timeout = timeout
  427. def http_open(self, req):
  428. return self.do_open(_build_connection(SpeedtestHTTPConnection, self.source_address, self.timeout), req)
  429. http_request = AbstractHTTPHandler.do_request_
  430. class SpeedtestHTTPSHandler(AbstractHTTPHandler):
  431. """Custom ``HTTPSHandler`` that can build a ``HTTPSConnection`` with the
  432. args we need for ``source_address`` and ``timeout``
  433. """
  434. def __init__(self, debuglevel=0, context=None, source_address=None, timeout=10):
  435. AbstractHTTPHandler.__init__(self, debuglevel)
  436. self._context = context
  437. self.source_address = source_address
  438. self.timeout = timeout
  439. def https_open(self, req):
  440. return self.do_open(
  441. _build_connection(
  442. SpeedtestHTTPSConnection,
  443. self.source_address,
  444. self.timeout,
  445. context=self._context,
  446. ),
  447. req,
  448. )
  449. https_request = AbstractHTTPHandler.do_request_
  450. def build_opener(source_address=None, timeout=10):
  451. """Function similar to ``urllib2.build_opener`` that will build
  452. an ``OpenerDirector`` with the explicit handlers we want,
  453. ``source_address`` for binding, ``timeout`` and our custom
  454. `User-Agent`
  455. """
  456. printer("Timeout set to %d" % timeout, debug=True)
  457. if source_address:
  458. source_address_tuple = (source_address, 0)
  459. printer("Binding to source address: %r" % (source_address_tuple,), debug=True)
  460. else:
  461. source_address_tuple = None
  462. handlers = [
  463. ProxyHandler(),
  464. SpeedtestHTTPHandler(source_address=source_address_tuple, timeout=timeout),
  465. SpeedtestHTTPSHandler(source_address=source_address_tuple, timeout=timeout),
  466. HTTPDefaultErrorHandler(),
  467. HTTPRedirectHandler(),
  468. HTTPErrorProcessor(),
  469. ]
  470. opener = OpenerDirector()
  471. opener.addheaders = [("User-agent", build_user_agent())]
  472. for handler in handlers:
  473. opener.add_handler(handler)
  474. return opener
  475. class GzipDecodedResponse(GZIP_BASE):
  476. """A file-like object to decode a response encoded with the gzip
  477. method, as described in RFC 1952.
  478. Largely copied from ``xmlrpclib``/``xmlrpc.client`` and modified
  479. to work for py2.4-py3
  480. """
  481. def __init__(self, response):
  482. # response doesn't support tell() and read(), required by
  483. # GzipFile
  484. if not gzip:
  485. raise SpeedtestHTTPError("HTTP response body is gzip encoded, " "but gzip support is not available")
  486. IO = BytesIO or StringIO
  487. self.io = IO()
  488. while 1:
  489. chunk = response.read(1024)
  490. if len(chunk) == 0:
  491. break
  492. self.io.write(chunk)
  493. self.io.seek(0)
  494. gzip.GzipFile.__init__(self, mode="rb", fileobj=self.io)
  495. def close(self):
  496. try:
  497. gzip.GzipFile.close(self)
  498. finally:
  499. self.io.close()
  500. def get_exception():
  501. """Helper function to work with py2.4-py3 for getting the current
  502. exception in a try/except block
  503. """
  504. return sys.exc_info()[1]
  505. def distance(origin, destination):
  506. """Determine distance between 2 sets of [lat,lon] in km"""
  507. lat1, lon1 = origin
  508. lat2, lon2 = destination
  509. radius = 6371 # km
  510. dlat = math.radians(lat2 - lat1)
  511. dlon = math.radians(lon2 - lon1)
  512. a = math.sin(dlat / 2) * math.sin(dlat / 2) + math.cos(math.radians(lat1)) * math.cos(
  513. math.radians(lat2)
  514. ) * math.sin(dlon / 2) * math.sin(dlon / 2)
  515. c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
  516. d = radius * c
  517. return d
  518. def build_user_agent():
  519. """Build a Mozilla/5.0 compatible User-Agent string"""
  520. ua_tuple = (
  521. "Mozilla/5.0",
  522. "(%s; U; %s; en-us)" % (platform.platform(), platform.architecture()[0]),
  523. "Python/%s" % platform.python_version(),
  524. "(KHTML, like Gecko)",
  525. "speedtest-cli/%s" % __version__,
  526. )
  527. user_agent = " ".join(ua_tuple)
  528. printer("User-Agent: %s" % user_agent, debug=True)
  529. return user_agent
  530. def build_request(url, data=None, headers=None, bump="0", secure=False):
  531. """Build a urllib2 request object
  532. This function automatically adds a User-Agent header to all requests
  533. """
  534. if not headers:
  535. headers = {}
  536. if url[0] == ":":
  537. scheme = ("http", "https")[bool(secure)]
  538. schemed_url = "%s%s" % (scheme, url)
  539. else:
  540. schemed_url = url
  541. if "?" in url:
  542. delim = "&"
  543. else:
  544. delim = "?"
  545. # WHO YOU GONNA CALL? CACHE BUSTERS!
  546. final_url = "%s%sx=%s.%s" % (schemed_url, delim, int(timeit.time.time() * 1000), bump)
  547. headers.update(
  548. {
  549. "Cache-Control": "no-cache",
  550. }
  551. )
  552. printer("%s %s" % (("GET", "POST")[bool(data)], final_url), debug=True)
  553. return Request(final_url, data=data, headers=headers)
  554. def catch_request(request, opener=None):
  555. """Helper function to catch common exceptions encountered when
  556. establishing a connection with a HTTP/HTTPS request
  557. """
  558. if opener:
  559. _open = opener.open
  560. else:
  561. _open = urlopen
  562. try:
  563. uh = _open(request)
  564. if request.get_full_url() != uh.geturl():
  565. printer("Redirected to %s" % uh.geturl(), debug=True)
  566. return uh, False
  567. except HTTP_ERRORS:
  568. e = get_exception()
  569. return None, e
  570. def get_response_stream(response):
  571. """Helper function to return either a Gzip reader if
  572. ``Content-Encoding`` is ``gzip`` otherwise the response itself
  573. """
  574. try:
  575. getheader = response.headers.getheader
  576. except AttributeError:
  577. getheader = response.getheader
  578. if getheader("content-encoding") == "gzip":
  579. return GzipDecodedResponse(response)
  580. return response
  581. def get_attributes_by_tag_name(dom, tag_name):
  582. """Retrieve an attribute from an XML document and return it in a
  583. consistent format
  584. Only used with xml.dom.minidom, which is likely only to be used
  585. with python versions older than 2.5
  586. """
  587. elem = dom.getElementsByTagName(tag_name)[0]
  588. return dict(list(elem.attributes.items()))
  589. def print_dots(shutdown_event):
  590. """Built in callback function used by Thread classes for printing
  591. status
  592. """
  593. def inner(current, total, start=False, end=False):
  594. if event_is_set(shutdown_event):
  595. return
  596. sys.stdout.write(".")
  597. if current + 1 == total and end is True:
  598. sys.stdout.write("\n")
  599. sys.stdout.flush()
  600. return inner
  601. def do_nothing(*args, **kwargs):
  602. pass
  603. class HTTPDownloader(threading.Thread):
  604. """Thread class for retrieving a URL"""
  605. def __init__(self, i, request, start, timeout, opener=None, shutdown_event=None):
  606. threading.Thread.__init__(self)
  607. self.request = request
  608. self.result = [0]
  609. self.starttime = start
  610. self.timeout = timeout
  611. self.i = i
  612. if opener:
  613. self._opener = opener.open
  614. else:
  615. self._opener = urlopen
  616. if shutdown_event:
  617. self._shutdown_event = shutdown_event
  618. else:
  619. self._shutdown_event = FakeShutdownEvent()
  620. def run(self):
  621. try:
  622. if (timeit.default_timer() - self.starttime) <= self.timeout:
  623. f = self._opener(self.request)
  624. while (
  625. not event_is_set(self._shutdown_event) and (timeit.default_timer() - self.starttime) <= self.timeout
  626. ):
  627. self.result.append(len(f.read(10240)))
  628. if self.result[-1] == 0:
  629. break
  630. f.close()
  631. except IOError:
  632. pass
  633. except HTTP_ERRORS:
  634. pass
  635. class HTTPUploaderData(object):
  636. """File like object to improve cutting off the upload once the timeout
  637. has been reached
  638. """
  639. def __init__(self, length, start, timeout, shutdown_event=None):
  640. self.length = length
  641. self.start = start
  642. self.timeout = timeout
  643. if shutdown_event:
  644. self._shutdown_event = shutdown_event
  645. else:
  646. self._shutdown_event = FakeShutdownEvent()
  647. self._data = None
  648. self.total = [0]
  649. def pre_allocate(self):
  650. chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  651. multiplier = int(round(int(self.length) / 36.0))
  652. IO = BytesIO or StringIO
  653. try:
  654. self._data = IO(("content1=%s" % (chars * multiplier)[0 : int(self.length) - 9]).encode())
  655. except MemoryError:
  656. raise SpeedtestCLIError("Insufficient memory to pre-allocate upload data. Please " "use --no-pre-allocate")
  657. @property
  658. def data(self):
  659. if not self._data:
  660. self.pre_allocate()
  661. return self._data
  662. def read(self, n=10240):
  663. if (timeit.default_timer() - self.start) <= self.timeout and not event_is_set(self._shutdown_event):
  664. chunk = self.data.read(n)
  665. self.total.append(len(chunk))
  666. return chunk
  667. else:
  668. raise SpeedtestUploadTimeout()
  669. def __len__(self):
  670. return self.length
  671. class HTTPUploader(threading.Thread):
  672. """Thread class for putting a URL"""
  673. def __init__(self, i, request, start, size, timeout, opener=None, shutdown_event=None):
  674. threading.Thread.__init__(self)
  675. self.request = request
  676. self.request.data.start = self.starttime = start
  677. self.size = size
  678. self.result = 0
  679. self.timeout = timeout
  680. self.i = i
  681. if opener:
  682. self._opener = opener.open
  683. else:
  684. self._opener = urlopen
  685. if shutdown_event:
  686. self._shutdown_event = shutdown_event
  687. else:
  688. self._shutdown_event = FakeShutdownEvent()
  689. def run(self):
  690. request = self.request
  691. try:
  692. if (timeit.default_timer() - self.starttime) <= self.timeout and not event_is_set(self._shutdown_event):
  693. try:
  694. f = self._opener(request)
  695. except TypeError:
  696. # PY24 expects a string or buffer
  697. # This also causes issues with Ctrl-C, but we will concede
  698. # for the moment that Ctrl-C on PY24 isn't immediate
  699. request = build_request(self.request.get_full_url(), data=request.data.read(self.size))
  700. f = self._opener(request)
  701. f.read(11)
  702. f.close()
  703. self.result = sum(self.request.data.total)
  704. else:
  705. self.result = 0
  706. except (IOError, SpeedtestUploadTimeout):
  707. self.result = sum(self.request.data.total)
  708. except HTTP_ERRORS:
  709. self.result = 0
  710. class SpeedtestResults(object):
  711. """Class for holding the results of a speedtest, including:
  712. Download speed
  713. Upload speed
  714. Ping/Latency to test server
  715. Data about server that the test was run against
  716. Additionally this class can return a result data as a dictionary or CSV,
  717. as well as submit a POST of the result data to the speedtest.net API
  718. to get a share results image link.
  719. """
  720. def __init__(self, download=0, upload=0, ping=0, server=None, client=None, opener=None, secure=False):
  721. self.download = download
  722. self.upload = upload
  723. self.ping = ping
  724. if server is None:
  725. self.server = {}
  726. else:
  727. self.server = server
  728. self.client = client or {}
  729. self._share = None
  730. self.timestamp = "%sZ" % datetime.datetime.utcnow().isoformat()
  731. self.bytes_received = 0
  732. self.bytes_sent = 0
  733. if opener:
  734. self._opener = opener
  735. else:
  736. self._opener = build_opener()
  737. self._secure = secure
  738. def __repr__(self):
  739. return repr(self.dict())
  740. def share(self):
  741. """POST data to the speedtest.net API to obtain a share results
  742. link
  743. """
  744. if self._share:
  745. return self._share
  746. download = int(round(self.download / 1000.0, 0))
  747. ping = int(round(self.ping, 0))
  748. upload = int(round(self.upload / 1000.0, 0))
  749. # Build the request to send results back to speedtest.net
  750. # We use a list instead of a dict because the API expects parameters
  751. # in a certain order
  752. api_data = [
  753. "recommendedserverid=%s" % self.server["id"],
  754. "ping=%s" % ping,
  755. "screenresolution=",
  756. "promo=",
  757. "download=%s" % download,
  758. "screendpi=",
  759. "upload=%s" % upload,
  760. "testmethod=http",
  761. "hash=%s" % md5(("%s-%s-%s-%s" % (ping, upload, download, "297aae72")).encode()).hexdigest(),
  762. "touchscreen=none",
  763. "startmode=pingselect",
  764. "accuracy=1",
  765. "bytesreceived=%s" % self.bytes_received,
  766. "bytessent=%s" % self.bytes_sent,
  767. "serverid=%s" % self.server["id"],
  768. ]
  769. headers = {"Referer": "http://c.speedtest.net/flash/speedtest.swf"}
  770. request = build_request(
  771. "://www.speedtest.net/api/api.php", data="&".join(api_data).encode(), headers=headers, secure=self._secure
  772. )
  773. f, e = catch_request(request, opener=self._opener)
  774. if e:
  775. raise ShareResultsConnectFailure(e)
  776. response = f.read()
  777. code = f.code
  778. f.close()
  779. if int(code) != 200:
  780. raise ShareResultsSubmitFailure("Could not submit results to " "speedtest.net")
  781. qsargs = parse_qs(response.decode())
  782. resultid = qsargs.get("resultid")
  783. if not resultid or len(resultid) != 1:
  784. raise ShareResultsSubmitFailure("Could not submit results to " "speedtest.net")
  785. self._share = "http://www.speedtest.net/result/%s.png" % resultid[0]
  786. return self._share
  787. def dict(self):
  788. """Return dictionary of result data"""
  789. return {
  790. "download": self.download,
  791. "upload": self.upload,
  792. "ping": self.ping,
  793. "server": self.server,
  794. "timestamp": self.timestamp,
  795. "bytes_sent": self.bytes_sent,
  796. "bytes_received": self.bytes_received,
  797. "share": self._share,
  798. "client": self.client,
  799. }
  800. @staticmethod
  801. def csv_header(delimiter=","):
  802. """Return CSV Headers"""
  803. row = [
  804. "Server ID",
  805. "Sponsor",
  806. "Server Name",
  807. "Timestamp",
  808. "Distance",
  809. "Ping",
  810. "Download",
  811. "Upload",
  812. "Share",
  813. "IP Address",
  814. ]
  815. out = StringIO()
  816. writer = csv.writer(out, delimiter=delimiter, lineterminator="")
  817. writer.writerow([to_utf8(v) for v in row])
  818. return out.getvalue()
  819. def csv(self, delimiter=","):
  820. """Return data in CSV format"""
  821. data = self.dict()
  822. out = StringIO()
  823. writer = csv.writer(out, delimiter=delimiter, lineterminator="")
  824. row = [
  825. data["server"]["id"],
  826. data["server"]["sponsor"],
  827. data["server"]["name"],
  828. data["timestamp"],
  829. data["server"]["d"],
  830. data["ping"],
  831. data["download"],
  832. data["upload"],
  833. self._share or "",
  834. self.client["ip"],
  835. ]
  836. writer.writerow([to_utf8(v) for v in row])
  837. return out.getvalue()
  838. def json(self, pretty=False):
  839. """Return data in JSON format"""
  840. kwargs = {}
  841. if pretty:
  842. kwargs.update({"indent": 4, "sort_keys": True})
  843. return json.dumps(self.dict(), **kwargs)
  844. class Speedtest(object):
  845. """Class for performing standard speedtest.net testing operations"""
  846. def __init__(self, config=None, source_address=None, timeout=10, secure=False, shutdown_event=None):
  847. self.config = {}
  848. self._source_address = source_address
  849. self._timeout = timeout
  850. self._opener = build_opener(source_address, timeout)
  851. self._secure = secure
  852. if shutdown_event:
  853. self._shutdown_event = shutdown_event
  854. else:
  855. self._shutdown_event = FakeShutdownEvent()
  856. self.get_config()
  857. if config is not None:
  858. self.config.update(config)
  859. self.servers = {}
  860. self.closest = []
  861. self._best = {}
  862. self.results = SpeedtestResults(
  863. client=self.config["client"],
  864. opener=self._opener,
  865. secure=secure,
  866. )
  867. @property
  868. def best(self):
  869. if not self._best:
  870. self.get_best_server()
  871. return self._best
  872. def get_config(self):
  873. """Download the speedtest.net configuration and return only the data
  874. we are interested in
  875. """
  876. headers = {}
  877. if gzip:
  878. headers["Accept-Encoding"] = "gzip"
  879. request = build_request("://www.speedtest.net/speedtest-config.php", headers=headers, secure=self._secure)
  880. uh, e = catch_request(request, opener=self._opener)
  881. if e:
  882. raise ConfigRetrievalError(e)
  883. configxml_list = []
  884. stream = get_response_stream(uh)
  885. while 1:
  886. try:
  887. configxml_list.append(stream.read(1024))
  888. except (OSError, EOFError):
  889. raise ConfigRetrievalError(get_exception())
  890. if len(configxml_list[-1]) == 0:
  891. break
  892. stream.close()
  893. uh.close()
  894. if int(uh.code) != 200:
  895. return None
  896. configxml = "".encode().join(configxml_list)
  897. printer("Config XML:\n%s" % configxml, debug=True)
  898. try:
  899. try:
  900. root = ET.fromstring(configxml)
  901. except ET.ParseError:
  902. e = get_exception()
  903. raise SpeedtestConfigError("Malformed speedtest.net configuration: %s" % e)
  904. server_config = root.find("server-config").attrib
  905. download = root.find("download").attrib
  906. upload = root.find("upload").attrib
  907. # times = root.find('times').attrib
  908. client = root.find("client").attrib
  909. except AttributeError:
  910. try:
  911. root = DOM.parseString(configxml)
  912. except ExpatError:
  913. e = get_exception()
  914. raise SpeedtestConfigError("Malformed speedtest.net configuration: %s" % e)
  915. server_config = get_attributes_by_tag_name(root, "server-config")
  916. download = get_attributes_by_tag_name(root, "download")
  917. upload = get_attributes_by_tag_name(root, "upload")
  918. # times = get_attributes_by_tag_name(root, 'times')
  919. client = get_attributes_by_tag_name(root, "client")
  920. ignore_servers = [int(i) for i in server_config["ignoreids"].split(",") if i]
  921. ratio = int(upload["ratio"])
  922. upload_max = int(upload["maxchunkcount"])
  923. up_sizes = [32768, 65536, 131072, 262144, 524288, 1048576, 7340032]
  924. sizes = {"upload": up_sizes[ratio - 1 :], "download": [350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000]}
  925. size_count = len(sizes["upload"])
  926. upload_count = int(math.ceil(upload_max / size_count))
  927. counts = {"upload": upload_count, "download": int(download["threadsperurl"])}
  928. threads = {"upload": int(upload["threads"]), "download": int(server_config["threadcount"]) * 2}
  929. length = {"upload": int(upload["testlength"]), "download": int(download["testlength"])}
  930. self.config.update(
  931. {
  932. "client": client,
  933. "ignore_servers": ignore_servers,
  934. "sizes": sizes,
  935. "counts": counts,
  936. "threads": threads,
  937. "length": length,
  938. "upload_max": upload_count * size_count,
  939. }
  940. )
  941. try:
  942. self.lat_lon = (float(client["lat"]), float(client["lon"]))
  943. except ValueError:
  944. raise SpeedtestConfigError("Unknown location: lat=%r lon=%r" % (client.get("lat"), client.get("lon")))
  945. printer("Config:\n%r" % self.config, debug=True)
  946. return self.config
  947. def get_servers(self, servers=None, exclude=None):
  948. """Retrieve a the list of speedtest.net servers, optionally filtered
  949. to servers matching those specified in the ``servers`` argument
  950. """
  951. if servers is None:
  952. servers = []
  953. if exclude is None:
  954. exclude = []
  955. self.servers.clear()
  956. for server_list in (servers, exclude):
  957. for i, s in enumerate(server_list):
  958. try:
  959. server_list[i] = int(s)
  960. except ValueError:
  961. raise InvalidServerIDType("%s is an invalid server type, must be int" % s)
  962. urls = [
  963. "://www.speedtest.net/speedtest-servers-static.php",
  964. "http://c.speedtest.net/speedtest-servers-static.php",
  965. "://www.speedtest.net/speedtest-servers.php",
  966. "http://c.speedtest.net/speedtest-servers.php",
  967. ]
  968. headers = {}
  969. if gzip:
  970. headers["Accept-Encoding"] = "gzip"
  971. errors = []
  972. for url in urls:
  973. try:
  974. request = build_request(
  975. "%s?threads=%s" % (url, self.config["threads"]["download"]), headers=headers, secure=self._secure
  976. )
  977. uh, e = catch_request(request, opener=self._opener)
  978. if e:
  979. errors.append("%s" % e)
  980. raise ServersRetrievalError()
  981. stream = get_response_stream(uh)
  982. serversxml_list = []
  983. while 1:
  984. try:
  985. serversxml_list.append(stream.read(1024))
  986. except (OSError, EOFError):
  987. raise ServersRetrievalError(get_exception())
  988. if len(serversxml_list[-1]) == 0:
  989. break
  990. stream.close()
  991. uh.close()
  992. if int(uh.code) != 200:
  993. raise ServersRetrievalError()
  994. serversxml = "".encode().join(serversxml_list)
  995. printer("Servers XML:\n%s" % serversxml, debug=True)
  996. try:
  997. try:
  998. try:
  999. root = ET.fromstring(serversxml)
  1000. except ET.ParseError:
  1001. e = get_exception()
  1002. raise SpeedtestServersError("Malformed speedtest.net server list: %s" % e)
  1003. elements = etree_iter(root, "server")
  1004. except AttributeError:
  1005. try:
  1006. root = DOM.parseString(serversxml)
  1007. except ExpatError:
  1008. e = get_exception()
  1009. raise SpeedtestServersError("Malformed speedtest.net server list: %s" % e)
  1010. elements = root.getElementsByTagName("server")
  1011. except (SyntaxError, xml.parsers.expat.ExpatError):
  1012. raise ServersRetrievalError()
  1013. for server in elements:
  1014. try:
  1015. attrib = server.attrib
  1016. except AttributeError:
  1017. attrib = dict(list(server.attributes.items()))
  1018. if servers and int(attrib.get("id")) not in servers:
  1019. continue
  1020. if int(attrib.get("id")) in self.config["ignore_servers"] or int(attrib.get("id")) in exclude:
  1021. continue
  1022. try:
  1023. d = distance(self.lat_lon, (float(attrib.get("lat")), float(attrib.get("lon"))))
  1024. except Exception:
  1025. continue
  1026. attrib["d"] = d
  1027. try:
  1028. self.servers[d].append(attrib)
  1029. except KeyError:
  1030. self.servers[d] = [attrib]
  1031. break
  1032. except ServersRetrievalError:
  1033. continue
  1034. if (servers or exclude) and not self.servers:
  1035. raise NoMatchedServers()
  1036. return self.servers
  1037. def set_mini_server(self, server):
  1038. """Instead of querying for a list of servers, set a link to a
  1039. speedtest mini server
  1040. """
  1041. urlparts = urlparse(server)
  1042. name, ext = os.path.splitext(urlparts[2])
  1043. if ext:
  1044. url = os.path.dirname(server)
  1045. else:
  1046. url = server
  1047. request = build_request(url)
  1048. uh, e = catch_request(request, opener=self._opener)
  1049. if e:
  1050. raise SpeedtestMiniConnectFailure("Failed to connect to %s" % server)
  1051. else:
  1052. text = uh.read()
  1053. uh.close()
  1054. extension = re.findall('upload_?[Ee]xtension: "([^"]+)"', text.decode())
  1055. if not extension:
  1056. for ext in ["php", "asp", "aspx", "jsp"]:
  1057. try:
  1058. f = self._opener.open("%s/speedtest/upload.%s" % (url, ext))
  1059. except Exception:
  1060. pass
  1061. else:
  1062. data = f.read().strip().decode()
  1063. if f.code == 200 and len(data.splitlines()) == 1 and re.match("size=[0-9]", data):
  1064. extension = [ext]
  1065. break
  1066. if not urlparts or not extension:
  1067. raise InvalidSpeedtestMiniServer("Invalid Speedtest Mini Server: " "%s" % server)
  1068. self.servers = [
  1069. {
  1070. "sponsor": "Speedtest Mini",
  1071. "name": urlparts[1],
  1072. "d": 0,
  1073. "url": "%s/speedtest/upload.%s" % (url.rstrip("/"), extension[0]),
  1074. "latency": 0,
  1075. "id": 0,
  1076. }
  1077. ]
  1078. return self.servers
  1079. def get_closest_servers(self, limit=5):
  1080. """Limit servers to the closest speedtest.net servers based on
  1081. geographic distance
  1082. """
  1083. if not self.servers:
  1084. self.get_servers()
  1085. for d in sorted(self.servers.keys()):
  1086. for s in self.servers[d]:
  1087. self.closest.append(s)
  1088. if len(self.closest) == limit:
  1089. break
  1090. else:
  1091. continue
  1092. break
  1093. printer("Closest Servers:\n%r" % self.closest, debug=True)
  1094. return self.closest
  1095. def get_best_server(self, servers=None):
  1096. """Perform a speedtest.net "ping" to determine which speedtest.net
  1097. server has the lowest latency
  1098. """
  1099. if not servers:
  1100. if not self.closest:
  1101. servers = self.get_closest_servers()
  1102. servers = self.closest
  1103. if self._source_address:
  1104. source_address_tuple = (self._source_address, 0)
  1105. else:
  1106. source_address_tuple = None
  1107. user_agent = build_user_agent()
  1108. results = {}
  1109. for server in servers:
  1110. cum = []
  1111. url = os.path.dirname(server["url"])
  1112. stamp = int(timeit.time.time() * 1000)
  1113. latency_url = "%s/latency.txt?x=%s" % (url, stamp)
  1114. for i in range(0, 3):
  1115. this_latency_url = "%s.%s" % (latency_url, i)
  1116. printer("%s %s" % ("GET", this_latency_url), debug=True)
  1117. urlparts = urlparse(latency_url)
  1118. try:
  1119. if urlparts[0] == "https":
  1120. h = SpeedtestHTTPSConnection(urlparts[1], source_address=source_address_tuple)
  1121. else:
  1122. h = SpeedtestHTTPConnection(urlparts[1], source_address=source_address_tuple)
  1123. headers = {"User-Agent": user_agent}
  1124. path = "%s?%s" % (urlparts[2], urlparts[4])
  1125. start = timeit.default_timer()
  1126. h.request("GET", path, headers=headers)
  1127. r = h.getresponse()
  1128. total = timeit.default_timer() - start
  1129. except HTTP_ERRORS:
  1130. e = get_exception()
  1131. printer("ERROR: %r" % e, debug=True)
  1132. cum.append(3600)
  1133. continue
  1134. text = r.read(9)
  1135. if int(r.status) == 200 and text == "test=test".encode():
  1136. cum.append(total)
  1137. else:
  1138. cum.append(3600)
  1139. h.close()
  1140. avg = round((sum(cum) / 6) * 1000.0, 3)
  1141. results[avg] = server
  1142. try:
  1143. fastest = sorted(results.keys())[0]
  1144. except IndexError:
  1145. raise SpeedtestBestServerFailure("Unable to connect to servers to " "test latency.")
  1146. best = results[fastest]
  1147. best["latency"] = fastest
  1148. self.results.ping = fastest
  1149. self.results.server = best
  1150. self._best.update(best)
  1151. printer("Best Server:\n%r" % best, debug=True)
  1152. return best
  1153. def download(self, callback=do_nothing, threads=None):
  1154. """Test download speed against speedtest.net
  1155. A ``threads`` value of ``None`` will fall back to those dictated
  1156. by the speedtest.net configuration
  1157. """
  1158. urls = []
  1159. for size in self.config["sizes"]["download"]:
  1160. for _ in range(0, self.config["counts"]["download"]):
  1161. urls.append("%s/random%sx%s.jpg" % (os.path.dirname(self.best["url"]), size, size))
  1162. request_count = len(urls)
  1163. requests = []
  1164. for i, url in enumerate(urls):
  1165. requests.append(build_request(url, bump=i, secure=self._secure))
  1166. max_threads = threads or self.config["threads"]["download"]
  1167. in_flight = {"threads": 0}
  1168. def producer(q, requests, request_count):
  1169. for i, request in enumerate(requests):
  1170. thread = HTTPDownloader(
  1171. i,
  1172. request,
  1173. start,
  1174. self.config["length"]["download"],
  1175. opener=self._opener,
  1176. shutdown_event=self._shutdown_event,
  1177. )
  1178. while in_flight["threads"] >= max_threads:
  1179. timeit.time.sleep(0.001)
  1180. thread.start()
  1181. q.put(thread, True)
  1182. in_flight["threads"] += 1
  1183. callback(i, request_count, start=True)
  1184. finished = []
  1185. def consumer(q, request_count):
  1186. _is_alive = thread_is_alive
  1187. while len(finished) < request_count:
  1188. thread = q.get(True)
  1189. while _is_alive(thread):
  1190. thread.join(timeout=0.001)
  1191. in_flight["threads"] -= 1
  1192. finished.append(sum(thread.result))
  1193. callback(thread.i, request_count, end=True)
  1194. q = Queue(max_threads)
  1195. prod_thread = threading.Thread(target=producer, args=(q, requests, request_count))
  1196. cons_thread = threading.Thread(target=consumer, args=(q, request_count))
  1197. start = timeit.default_timer()
  1198. prod_thread.start()
  1199. cons_thread.start()
  1200. _is_alive = thread_is_alive
  1201. while _is_alive(prod_thread):
  1202. prod_thread.join(timeout=0.001)
  1203. while _is_alive(cons_thread):
  1204. cons_thread.join(timeout=0.001)
  1205. stop = timeit.default_timer()
  1206. self.results.bytes_received = sum(finished)
  1207. self.results.download = (self.results.bytes_received / (stop - start)) * 8.0
  1208. if self.results.download > 100000:
  1209. self.config["threads"]["upload"] = 8
  1210. return self.results.download
  1211. def upload(self, callback=do_nothing, pre_allocate=True, threads=None):
  1212. """Test upload speed against speedtest.net
  1213. A ``threads`` value of ``None`` will fall back to those dictated
  1214. by the speedtest.net configuration
  1215. """
  1216. sizes = []
  1217. for size in self.config["sizes"]["upload"]:
  1218. for _ in range(0, self.config["counts"]["upload"]):
  1219. sizes.append(size)
  1220. # request_count = len(sizes)
  1221. request_count = self.config["upload_max"]
  1222. requests = []
  1223. for i, size in enumerate(sizes):
  1224. # We set ``0`` for ``start`` and handle setting the actual
  1225. # ``start`` in ``HTTPUploader`` to get better measurements
  1226. data = HTTPUploaderData(size, 0, self.config["length"]["upload"], shutdown_event=self._shutdown_event)
  1227. if pre_allocate:
  1228. data.pre_allocate()
  1229. headers = {"Content-length": size}
  1230. requests.append((build_request(self.best["url"], data, secure=self._secure, headers=headers), size))
  1231. max_threads = threads or self.config["threads"]["upload"]
  1232. in_flight = {"threads": 0}
  1233. def producer(q, requests, request_count):
  1234. for i, request in enumerate(requests[:request_count]):
  1235. thread = HTTPUploader(
  1236. i,
  1237. request[0],
  1238. start,
  1239. request[1],
  1240. self.config["length"]["upload"],
  1241. opener=self._opener,
  1242. shutdown_event=self._shutdown_event,
  1243. )
  1244. while in_flight["threads"] >= max_threads:
  1245. timeit.time.sleep(0.001)
  1246. thread.start()
  1247. q.put(thread, True)
  1248. in_flight["threads"] += 1
  1249. callback(i, request_count, start=True)
  1250. finished = []
  1251. def consumer(q, request_count):
  1252. _is_alive = thread_is_alive
  1253. while len(finished) < request_count:
  1254. thread = q.get(True)
  1255. while _is_alive(thread):
  1256. thread.join(timeout=0.001)
  1257. in_flight["threads"] -= 1
  1258. finished.append(thread.result)
  1259. callback(thread.i, request_count, end=True)
  1260. q = Queue(threads or self.config["threads"]["upload"])
  1261. prod_thread = threading.Thread(target=producer, args=(q, requests, request_count))
  1262. cons_thread = threading.Thread(target=consumer, args=(q, request_count))
  1263. start = timeit.default_timer()
  1264. prod_thread.start()
  1265. cons_thread.start()
  1266. _is_alive = thread_is_alive
  1267. while _is_alive(prod_thread):
  1268. prod_thread.join(timeout=0.1)
  1269. while _is_alive(cons_thread):
  1270. cons_thread.join(timeout=0.1)
  1271. stop = timeit.default_timer()
  1272. self.results.bytes_sent = sum(finished)
  1273. self.results.upload = (self.results.bytes_sent / (stop - start)) * 8.0
  1274. return self.results.upload
  1275. def ctrl_c(shutdown_event):
  1276. """Catch Ctrl-C key sequence and set a SHUTDOWN_EVENT for our threaded
  1277. operations
  1278. """
  1279. def inner(signum, frame):
  1280. shutdown_event.set()
  1281. printer("\nCancelling...", error=True)
  1282. sys.exit(0)
  1283. return inner
  1284. def version():
  1285. """Print the version"""
  1286. printer("speedtest-cli %s" % __version__)
  1287. printer("Python %s" % sys.version.replace("\n", ""))
  1288. sys.exit(0)
  1289. def csv_header(delimiter=","):
  1290. """Print the CSV Headers"""
  1291. printer(SpeedtestResults.csv_header(delimiter=delimiter))
  1292. sys.exit(0)
  1293. def parse_args():
  1294. """Function to handle building and parsing of command line arguments"""
  1295. description = (
  1296. "Command line interface for testing internet bandwidth using "
  1297. "speedtest.net.\n"
  1298. "------------------------------------------------------------"
  1299. "--------------\n"
  1300. "https://github.com/sivel/speedtest-cli"
  1301. )
  1302. parser = ArgParser(description=description)
  1303. # Give optparse.OptionParser an `add_argument` method for
  1304. # compatibility with argparse.ArgumentParser
  1305. try:
  1306. parser.add_argument = parser.add_option
  1307. except AttributeError:
  1308. pass
  1309. parser.add_argument(
  1310. "--no-download",
  1311. dest="download",
  1312. default=True,
  1313. action="store_const",
  1314. const=False,
  1315. help="Do not perform download test",
  1316. )
  1317. parser.add_argument(
  1318. "--no-upload", dest="upload", default=True, action="store_const", const=False, help="Do not perform upload test"
  1319. )
  1320. parser.add_argument(
  1321. "--single",
  1322. default=False,
  1323. action="store_true",
  1324. help="Only use a single connection instead of " "multiple. This simulates a typical file " "transfer.",
  1325. )
  1326. parser.add_argument(
  1327. "--bytes",
  1328. dest="units",
  1329. action="store_const",
  1330. const=("byte", 8),
  1331. default=("bit", 1),
  1332. help="Display values in bytes instead of bits. Does "
  1333. "not affect the image generated by --share, nor "
  1334. "output from --json or --csv",
  1335. )
  1336. parser.add_argument(
  1337. "--share",
  1338. action="store_true",
  1339. help="Generate and provide a URL to the speedtest.net " "share results image, not displayed with --csv",
  1340. )
  1341. parser.add_argument(
  1342. "--simple", action="store_true", default=False, help="Suppress verbose output, only show basic " "information"
  1343. )
  1344. parser.add_argument(
  1345. "--csv",
  1346. action="store_true",
  1347. default=False,
  1348. help="Suppress verbose output, only show basic "
  1349. "information in CSV format. Speeds listed in "
  1350. "bit/s and not affected by --bytes",
  1351. )
  1352. parser.add_argument(
  1353. "--csv-delimiter",
  1354. default=",",
  1355. type=PARSER_TYPE_STR,
  1356. help="Single character delimiter to use in CSV " 'output. Default ","',
  1357. )
  1358. parser.add_argument("--csv-header", action="store_true", default=False, help="Print CSV headers")
  1359. parser.add_argument(
  1360. "--json",
  1361. action="store_true",
  1362. default=False,
  1363. help="Suppress verbose output, only show basic "
  1364. "information in JSON format. Speeds listed in "
  1365. "bit/s and not affected by --bytes",
  1366. )
  1367. parser.add_argument(
  1368. "--list", action="store_true", help="Display a list of speedtest.net servers " "sorted by distance"
  1369. )
  1370. parser.add_argument(
  1371. "--server",
  1372. type=PARSER_TYPE_INT,
  1373. action="append",
  1374. help="Specify a server ID to test against. Can be " "supplied multiple times",
  1375. )
  1376. parser.add_argument(
  1377. "--exclude",
  1378. type=PARSER_TYPE_INT,
  1379. action="append",
  1380. help="Exclude a server from selection. Can be " "supplied multiple times",
  1381. )
  1382. parser.add_argument("--mini", help="URL of the Speedtest Mini server")
  1383. parser.add_argument("--source", help="Source IP address to bind to")
  1384. parser.add_argument("--timeout", default=10, type=PARSER_TYPE_FLOAT, help="HTTP timeout in seconds. Default 10")
  1385. parser.add_argument(
  1386. "--secure",
  1387. action="store_true",
  1388. help="Use HTTPS instead of HTTP when communicating " "with speedtest.net operated servers",
  1389. )
  1390. parser.add_argument(
  1391. "--no-pre-allocate",
  1392. dest="pre_allocate",
  1393. action="store_const",
  1394. default=True,
  1395. const=False,
  1396. help="Do not pre allocate upload data. Pre allocation "
  1397. "is enabled by default to improve upload "
  1398. "performance. To support systems with "
  1399. "insufficient memory, use this option to avoid a "
  1400. "MemoryError",
  1401. )
  1402. parser.add_argument("--version", action="store_true", help="Show the version number and exit")
  1403. parser.add_argument("--debug", action="store_true", help=ARG_SUPPRESS, default=ARG_SUPPRESS)
  1404. options = parser.parse_args()
  1405. if isinstance(options, tuple):
  1406. args = options[0]
  1407. else:
  1408. args = options
  1409. return args
  1410. def validate_optional_args(args):
  1411. """Check if an argument was provided that depends on a module that may
  1412. not be part of the Python standard library.
  1413. If such an argument is supplied, and the module does not exist, exit
  1414. with an error stating which module is missing.
  1415. """
  1416. optional_args = {
  1417. "json": ("json/simplejson python module", json),
  1418. "secure": ("SSL support", HTTPSConnection),
  1419. }
  1420. for arg, info in optional_args.items():
  1421. if getattr(args, arg, False) and info[1] is None:
  1422. raise SystemExit("%s is not installed. --%s is " "unavailable" % (info[0], arg))
  1423. def printer(string, quiet=False, debug=False, error=False, **kwargs):
  1424. """Helper function print a string with various features"""
  1425. if debug and not DEBUG:
  1426. return
  1427. if debug:
  1428. if sys.stdout.isatty():
  1429. out = "\033[1;30mDEBUG: %s\033[0m" % string
  1430. else:
  1431. out = "DEBUG: %s" % string
  1432. else:
  1433. out = string
  1434. if error:
  1435. kwargs["file"] = sys.stderr
  1436. if not quiet:
  1437. print_(out, **kwargs)
  1438. def shell():
  1439. """Run the full speedtest.net test"""
  1440. global DEBUG
  1441. shutdown_event = threading.Event()
  1442. signal.signal(signal.SIGINT, ctrl_c(shutdown_event))
  1443. args = parse_args()
  1444. # Print the version and exit
  1445. if args.version:
  1446. version()
  1447. if not args.download and not args.upload:
  1448. raise SpeedtestCLIError("Cannot supply both --no-download and " "--no-upload")
  1449. if len(args.csv_delimiter) != 1:
  1450. raise SpeedtestCLIError("--csv-delimiter must be a single character")
  1451. if args.csv_header:
  1452. csv_header(args.csv_delimiter)
  1453. validate_optional_args(args)
  1454. debug = getattr(args, "debug", False)
  1455. if debug == "SUPPRESSHELP":
  1456. debug = False
  1457. if debug:
  1458. DEBUG = True
  1459. if args.simple or args.csv or args.json:
  1460. quiet = True
  1461. else:
  1462. quiet = False
  1463. if args.csv or args.json:
  1464. machine_format = True
  1465. else:
  1466. machine_format = False
  1467. # Don't set a callback if we are running quietly
  1468. if quiet or debug:
  1469. callback = do_nothing
  1470. else:
  1471. callback = print_dots(shutdown_event)
  1472. printer("Retrieving speedtest.net configuration...", quiet)
  1473. try:
  1474. speedtest = Speedtest(source_address=args.source, timeout=args.timeout, secure=args.secure)
  1475. except (ConfigRetrievalError,) + HTTP_ERRORS:
  1476. printer("Cannot retrieve speedtest configuration", error=True)
  1477. raise SpeedtestCLIError(get_exception())
  1478. if args.list:
  1479. try:
  1480. speedtest.get_servers()
  1481. except (ServersRetrievalError,) + HTTP_ERRORS:
  1482. printer("Cannot retrieve speedtest server list", error=True)
  1483. raise SpeedtestCLIError(get_exception())
  1484. for _, servers in sorted(speedtest.servers.items()):
  1485. for server in servers:
  1486. line = "%(id)5s) %(sponsor)s (%(name)s, %(country)s) " "[%(d)0.2f km]" % server
  1487. try:
  1488. printer(line)
  1489. except IOError:
  1490. e = get_exception()
  1491. if e.errno != errno.EPIPE:
  1492. raise
  1493. sys.exit(0)
  1494. printer("Testing from %(isp)s (%(ip)s)..." % speedtest.config["client"], quiet)
  1495. if not args.mini:
  1496. printer("Retrieving speedtest.net server list...", quiet)
  1497. try:
  1498. speedtest.get_servers(servers=args.server, exclude=args.exclude)
  1499. except NoMatchedServers:
  1500. raise SpeedtestCLIError("No matched servers: %s" % ", ".join("%s" % s for s in args.server))
  1501. except (ServersRetrievalError,) + HTTP_ERRORS:
  1502. printer("Cannot retrieve speedtest server list", error=True)
  1503. raise SpeedtestCLIError(get_exception())
  1504. except InvalidServerIDType:
  1505. raise SpeedtestCLIError(
  1506. "%s is an invalid server type, must " "be an int" % ", ".join("%s" % s for s in args.server)
  1507. )
  1508. if args.server and len(args.server) == 1:
  1509. printer("Retrieving information for the selected server...", quiet)
  1510. else:
  1511. printer("Selecting best server based on ping...", quiet)
  1512. speedtest.get_best_server()
  1513. elif args.mini:
  1514. speedtest.get_best_server(speedtest.set_mini_server(args.mini))
  1515. results = speedtest.results
  1516. printer("Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: " "%(latency)s ms" % results.server, quiet)
  1517. if args.download:
  1518. printer("Testing download speed", quiet, end=("", "\n")[bool(debug)])
  1519. speedtest.download(callback=callback, threads=(None, 1)[args.single])
  1520. printer("Download: %0.2f M%s/s" % ((results.download / 1000.0 / 1000.0) / args.units[1], args.units[0]), quiet)
  1521. else:
  1522. printer("Skipping download test", quiet)
  1523. if args.upload:
  1524. printer("Testing upload speed", quiet, end=("", "\n")[bool(debug)])
  1525. speedtest.upload(callback=callback, pre_allocate=args.pre_allocate, threads=(None, 1)[args.single])
  1526. printer("Upload: %0.2f M%s/s" % ((results.upload / 1000.0 / 1000.0) / args.units[1], args.units[0]), quiet)
  1527. else:
  1528. printer("Skipping upload test", quiet)
  1529. printer("Results:\n%r" % results.dict(), debug=True)
  1530. if not args.simple and args.share:
  1531. results.share()
  1532. if args.simple:
  1533. printer(
  1534. "Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s"
  1535. % (
  1536. results.ping,
  1537. (results.download / 1000.0 / 1000.0) / args.units[1],
  1538. args.units[0],
  1539. (results.upload / 1000.0 / 1000.0) / args.units[1],
  1540. args.units[0],
  1541. )
  1542. )
  1543. elif args.csv:
  1544. printer(results.csv(delimiter=args.csv_delimiter))
  1545. elif args.json:
  1546. printer(results.json())
  1547. if args.share and not machine_format:
  1548. printer("Share results: %s" % results.share())
  1549. def main():
  1550. try:
  1551. shell()
  1552. except KeyboardInterrupt:
  1553. printer("\nCancelling...", error=True)
  1554. except (SpeedtestException, SystemExit):
  1555. e = get_exception()
  1556. # Ignore a successful exit, or argparse exit
  1557. if getattr(e, "code", 1) not in (0, 2):
  1558. msg = "%s" % e
  1559. if not msg:
  1560. msg = "%r" % e
  1561. raise SystemExit("ERROR: %s" % msg)
  1562. if __name__ == "__main__":
  1563. main()