speed_test.py 64 KB

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