PageRenderTime 34ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/runserver.py

https://gitlab.com/HerrTapper/PokemonGoMap
Python | 354 lines | 250 code | 55 blank | 49 comment | 51 complexity | bdb090c5e7683b16b9f76942d717d20b MD5 | raw file
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import os
  4. import sys
  5. import shutil
  6. import logging
  7. import time
  8. import re
  9. import requests
  10. import ssl
  11. import json
  12. from distutils.version import StrictVersion
  13. from threading import Thread, Event
  14. from queue import Queue
  15. from cachetools import LFUCache
  16. from flask_cors import CORS
  17. from flask_cache_bust import init_cache_busting
  18. from pogom import config
  19. from pogom.app import Pogom
  20. from pogom.utils import get_args, now
  21. from pogom.search import search_overseer_thread
  22. from pogom.models import (init_database, create_tables, drop_tables,
  23. Pokemon, db_updater, clean_db_loop)
  24. from pogom.webhook import wh_updater
  25. from pogom.proxy import check_proxies, proxies_refresher
  26. # Currently supported pgoapi.
  27. pgoapi_version = "1.1.7"
  28. # Moved here so logger is configured at load time.
  29. logging.basicConfig(
  30. format='%(asctime)s [%(threadName)16s][%(module)14s][%(levelname)8s] ' +
  31. '%(message)s')
  32. log = logging.getLogger()
  33. # Make sure pogom/pgoapi is actually removed if it is an empty directory.
  34. # This is a leftover directory from the time pgoapi was embedded in
  35. # PokemonGo-Map.
  36. # The empty directory will cause problems with `import pgoapi` so it needs to
  37. # go.
  38. # Now also removes the pogom/libencrypt and pokecrypt-pgoapi folders,
  39. # don't cause issues but aren't needed.
  40. oldpgoapiPath = os.path.join(os.path.dirname(__file__), "pogom/pgoapi")
  41. oldlibPath = os.path.join(os.path.dirname(__file__), "pokecrypt-pgoapi")
  42. oldoldlibPath = os.path.join(os.path.dirname(__file__), "pogom/libencrypt")
  43. if os.path.isdir(oldpgoapiPath):
  44. log.warn("I found a really really old pgoapi thing, but its no longer " +
  45. "used. Going to remove it...", oldpgoapiPath)
  46. shutil.rmtree(oldpgoapiPath)
  47. log.warn("Done!")
  48. if os.path.isdir(oldlibPath):
  49. log.warn("I found the pokecrypt-pgoapi folder/submodule, but its no " +
  50. "longer used. Going to remove it...", oldpgoapiPath)
  51. shutil.rmtree(oldlibPath)
  52. log.warn("Done!")
  53. if os.path.isdir(oldoldlibPath):
  54. log.warn("I found the old libencrypt folder, from when we used to " +
  55. "bundle encrypt libs, but its no longer used. " +
  56. "Going to remove it...", oldpgoapiPath)
  57. shutil.rmtree(oldoldlibPath)
  58. log.warn("Done!")
  59. # Assert pgoapi is installed.
  60. try:
  61. import pgoapi
  62. from pgoapi import utilities as util
  63. except ImportError:
  64. log.critical(
  65. "It seems `pgoapi` is not installed. Try running " +
  66. "pip install --upgrade -r requirements.txt.")
  67. sys.exit(1)
  68. # Assert pgoapi >= pgoapi_version.
  69. if (not hasattr(pgoapi, "__version__") or
  70. StrictVersion(pgoapi.__version__) < StrictVersion(pgoapi_version)):
  71. log.critical(
  72. "It seems `pgoapi` is not up-to-date. Try running " +
  73. "pip install --upgrade -r requirements.txt again.")
  74. sys.exit(1)
  75. # Patch to make exceptions in threads cause an exception.
  76. def install_thread_excepthook():
  77. """
  78. Workaround for sys.excepthook thread bug
  79. (https://sourceforge.net/tracker/?func=detail&atid=105470&aid=1230540&group_id=5470).
  80. Call once from __main__ before creating any threads.
  81. If using psyco, call psycho.cannotcompile(threading.Thread.run)
  82. since this replaces a new-style class method.
  83. """
  84. import sys
  85. run_old = Thread.run
  86. def run(*args, **kwargs):
  87. try:
  88. run_old(*args, **kwargs)
  89. except (KeyboardInterrupt, SystemExit):
  90. raise
  91. except:
  92. sys.excepthook(*sys.exc_info())
  93. Thread.run = run
  94. # Exception handler will log unhandled exceptions.
  95. def handle_exception(exc_type, exc_value, exc_traceback):
  96. if issubclass(exc_type, KeyboardInterrupt):
  97. sys.__excepthook__(exc_type, exc_value, exc_traceback)
  98. return
  99. log.error("Uncaught exception", exc_info=(
  100. exc_type, exc_value, exc_traceback))
  101. def main():
  102. # Patch threading to make exceptions catchable.
  103. install_thread_excepthook()
  104. # Make sure exceptions get logged.
  105. sys.excepthook = handle_exception
  106. args = get_args()
  107. # Add file logging if enabled.
  108. if args.verbose and args.verbose != 'nofile':
  109. filelog = logging.FileHandler(args.verbose)
  110. filelog.setFormatter(logging.Formatter(
  111. '%(asctime)s [%(threadName)16s][%(module)14s][%(levelname)8s] ' +
  112. '%(message)s'))
  113. logging.getLogger('').addHandler(filelog)
  114. if args.very_verbose and args.very_verbose != 'nofile':
  115. filelog = logging.FileHandler(args.very_verbose)
  116. filelog.setFormatter(logging.Formatter(
  117. '%(asctime)s [%(threadName)16s][%(module)14s][%(levelname)8s] ' +
  118. '%(message)s'))
  119. logging.getLogger('').addHandler(filelog)
  120. if args.verbose or args.very_verbose:
  121. log.setLevel(logging.DEBUG)
  122. else:
  123. log.setLevel(logging.INFO)
  124. # Let's not forget to run Grunt / Only needed when running with webserver.
  125. if not args.no_server:
  126. if not os.path.exists(
  127. os.path.join(os.path.dirname(__file__), 'static/dist')):
  128. log.critical(
  129. 'Missing front-end assets (static/dist) -- please run ' +
  130. '"npm install && npm run build" before starting the server.')
  131. sys.exit()
  132. # You need custom image files now.
  133. if not os.path.isfile(
  134. os.path.join(os.path.dirname(__file__),
  135. 'static/icons-sprite.png')):
  136. log.critical('Missing sprite files.')
  137. sys.exit()
  138. # These are very noisy, let's shush them up a bit.
  139. logging.getLogger('peewee').setLevel(logging.INFO)
  140. logging.getLogger('requests').setLevel(logging.WARNING)
  141. logging.getLogger('pgoapi.pgoapi').setLevel(logging.WARNING)
  142. logging.getLogger('pgoapi.rpc_api').setLevel(logging.INFO)
  143. logging.getLogger('werkzeug').setLevel(logging.ERROR)
  144. config['parse_pokemon'] = not args.no_pokemon
  145. config['parse_pokestops'] = not args.no_pokestops
  146. config['parse_gyms'] = not args.no_gyms
  147. # Turn these back up if debugging.
  148. if args.verbose or args.very_verbose:
  149. logging.getLogger('pgoapi').setLevel(logging.DEBUG)
  150. if args.very_verbose:
  151. logging.getLogger('peewee').setLevel(logging.DEBUG)
  152. logging.getLogger('requests').setLevel(logging.DEBUG)
  153. logging.getLogger('pgoapi.pgoapi').setLevel(logging.DEBUG)
  154. logging.getLogger('pgoapi.rpc_api').setLevel(logging.DEBUG)
  155. logging.getLogger('rpc_api').setLevel(logging.DEBUG)
  156. logging.getLogger('werkzeug').setLevel(logging.DEBUG)
  157. # Use lat/lng directly if matches such a pattern.
  158. prog = re.compile("^(\-?\d+\.\d+),?\s?(\-?\d+\.\d+)$")
  159. res = prog.match(args.location)
  160. if res:
  161. log.debug('Using coordinates from CLI directly')
  162. position = (float(res.group(1)), float(res.group(2)), 0)
  163. else:
  164. log.debug('Looking up coordinates in API')
  165. position = util.get_pos_by_name(args.location)
  166. if position is None or not any(position):
  167. log.error("Location not found: '{}'".format(args.location))
  168. sys.exit()
  169. # Use the latitude and longitude to get the local altitude from Google.
  170. try:
  171. url = ('https://maps.googleapis.com/maps/api/elevation/json?' +
  172. 'locations={},{}').format(str(position[0]), str(position[1]))
  173. altitude = requests.get(url).json()[u'results'][0][u'elevation']
  174. log.debug('Local altitude is: %sm', altitude)
  175. position = (position[0], position[1], altitude)
  176. except (requests.exceptions.RequestException, IndexError, KeyError):
  177. log.error('Unable to retrieve altitude from Google APIs; setting to 0')
  178. log.info('Parsed location is: %.4f/%.4f/%.4f (lat/lng/alt)',
  179. position[0], position[1], position[2])
  180. if args.no_pokemon:
  181. log.info('Parsing of Pokemon disabled.')
  182. if args.no_pokestops:
  183. log.info('Parsing of Pokestops disabled.')
  184. if args.no_gyms:
  185. log.info('Parsing of Gyms disabled.')
  186. if args.encounter:
  187. log.info('Encountering pokemon enabled.')
  188. config['LOCALE'] = args.locale
  189. config['CHINA'] = args.china
  190. app = Pogom(__name__)
  191. db = init_database(app)
  192. if args.clear_db:
  193. log.info('Clearing database')
  194. if args.db_type == 'mysql':
  195. drop_tables(db)
  196. elif os.path.isfile(args.db):
  197. os.remove(args.db)
  198. create_tables(db)
  199. app.set_current_location(position)
  200. # Control the search status (running or not) across threads.
  201. pause_bit = Event()
  202. pause_bit.clear()
  203. if args.on_demand_timeout > 0:
  204. pause_bit.set()
  205. heartbeat = [now()]
  206. # Setup the location tracking queue and push the first location on.
  207. new_location_queue = Queue()
  208. new_location_queue.put(position)
  209. # DB Updates
  210. db_updates_queue = Queue()
  211. # Thread(s) to process database updates.
  212. for i in range(args.db_threads):
  213. log.debug('Starting db-updater worker thread %d', i)
  214. t = Thread(target=db_updater, name='db-updater-{}'.format(i),
  215. args=(args, db_updates_queue, db))
  216. t.daemon = True
  217. t.start()
  218. # db cleaner; really only need one ever.
  219. if not args.disable_clean:
  220. t = Thread(target=clean_db_loop, name='db-cleaner', args=(args,))
  221. t.daemon = True
  222. t.start()
  223. # WH updates queue & WH gym/pokéstop unique key LFU cache.
  224. # The LFU cache will stop the server from resending the same data an
  225. # infinite number of times.
  226. # TODO: Rework webhooks entirely so a LFU cache isn't necessary.
  227. wh_updates_queue = Queue()
  228. wh_key_cache = LFUCache(maxsize=args.wh_lfu_size)
  229. # Thread to process webhook updates.
  230. for i in range(args.wh_threads):
  231. log.debug('Starting wh-updater worker thread %d', i)
  232. t = Thread(target=wh_updater, name='wh-updater-{}'.format(i),
  233. args=(args, wh_updates_queue, wh_key_cache))
  234. t.daemon = True
  235. t.start()
  236. if not args.only_server:
  237. # Processing proxies if set (load from file, check and overwrite old
  238. # args.proxy with new working list)
  239. args.proxy = check_proxies(args)
  240. # Run periodical proxy refresh thread
  241. if (args.proxy_file is not None) and (args.proxy_refresh > 0):
  242. t = Thread(target=proxies_refresher,
  243. name='proxy-refresh', args=(args,))
  244. t.daemon = True
  245. t.start()
  246. else:
  247. log.info('Periodical proxies refresh disabled.')
  248. # Gather the Pokemon!
  249. # Attempt to dump the spawn points (do this before starting threads of
  250. # endure the woe).
  251. if (args.spawnpoint_scanning and
  252. args.spawnpoint_scanning != 'nofile' and
  253. args.dump_spawnpoints):
  254. with open(args.spawnpoint_scanning, 'w+') as file:
  255. log.info('Saving spawn points to %s', args.spawnpoint_scanning)
  256. spawns = Pokemon.get_spawnpoints_in_hex(
  257. position, args.step_limit)
  258. file.write(json.dumps(spawns))
  259. log.info('Finished exporting spawn points')
  260. argset = (args, new_location_queue, pause_bit,
  261. heartbeat, db_updates_queue, wh_updates_queue)
  262. log.debug('Starting a %s search thread', args.scheduler)
  263. search_thread = Thread(target=search_overseer_thread,
  264. name='search-overseer', args=argset)
  265. search_thread.daemon = True
  266. search_thread.start()
  267. if args.cors:
  268. CORS(app)
  269. # No more stale JS.
  270. init_cache_busting(app)
  271. app.set_search_control(pause_bit)
  272. app.set_heartbeat_control(heartbeat)
  273. app.set_location_queue(new_location_queue)
  274. config['ROOT_PATH'] = app.root_path
  275. config['GMAPS_KEY'] = args.gmaps_key
  276. if args.no_server:
  277. # This loop allows for ctrl-c interupts to work since flask won't be
  278. # holding the program open.
  279. while search_thread.is_alive():
  280. time.sleep(60)
  281. else:
  282. ssl_context = None
  283. if (args.ssl_certificate and args.ssl_privatekey and
  284. os.path.exists(args.ssl_certificate) and
  285. os.path.exists(args.ssl_privatekey)):
  286. ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
  287. ssl_context.load_cert_chain(
  288. args.ssl_certificate, args.ssl_privatekey)
  289. log.info('Web server in SSL mode.')
  290. if args.verbose or args.very_verbose:
  291. app.run(threaded=True, use_reloader=False, debug=True,
  292. host=args.host, port=args.port, ssl_context=ssl_context)
  293. else:
  294. app.run(threaded=True, use_reloader=False, debug=False,
  295. host=args.host, port=args.port, ssl_context=ssl_context)
  296. if __name__ == '__main__':
  297. main()