handlers.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. # -*- coding: utf-8 -*-
  2. """
  3. livereload.handlers
  4. ~~~~~~~~~~~~~~~~~~~
  5. HTTP and WebSocket handlers for livereload.
  6. :copyright: (c) 2013 by Hsiaoming Yang
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import datetime
  10. import hashlib
  11. import os
  12. import stat
  13. import time
  14. import logging
  15. from tornado import web
  16. from tornado import ioloop
  17. from tornado import escape
  18. from tornado.log import gen_log
  19. from tornado.websocket import WebSocketHandler
  20. from tornado.util import ObjectDict
  21. logger = logging.getLogger('livereload')
  22. class LiveReloadHandler(WebSocketHandler):
  23. waiters = set()
  24. watcher = None
  25. live_css = None
  26. _last_reload_time = None
  27. def allow_draft76(self):
  28. return True
  29. def check_origin(self, origin):
  30. return True
  31. def on_close(self):
  32. if self in LiveReloadHandler.waiters:
  33. LiveReloadHandler.waiters.remove(self)
  34. def send_message(self, message):
  35. if isinstance(message, dict):
  36. message = escape.json_encode(message)
  37. try:
  38. self.write_message(message)
  39. except:
  40. logger.error('Error sending message', exc_info=True)
  41. @classmethod
  42. def start_tasks(cls):
  43. if cls._last_reload_time:
  44. return
  45. if not cls.watcher._tasks:
  46. logger.info('Watch current working directory')
  47. cls.watcher.watch(os.getcwd())
  48. cls._last_reload_time = time.time()
  49. logger.info('Start watching changes')
  50. if not cls.watcher.start(cls.poll_tasks):
  51. logger.info('Start detecting changes')
  52. ioloop.PeriodicCallback(cls.poll_tasks, 800).start()
  53. @classmethod
  54. def poll_tasks(cls):
  55. filepath, delay = cls.watcher.examine()
  56. if not filepath or delay == 'forever' or not cls.waiters:
  57. return
  58. reload_time = 3
  59. if delay:
  60. reload_time = max(3 - delay, 1)
  61. if filepath == '__livereload__':
  62. reload_time = 0
  63. if time.time() - cls._last_reload_time < reload_time:
  64. # if you changed lot of files in one time
  65. # it will refresh too many times
  66. logger.info('Ignore: %s', filepath)
  67. return
  68. if delay:
  69. loop = ioloop.IOLoop.current()
  70. loop.call_later(delay, cls.reload_waiters)
  71. else:
  72. cls.reload_waiters()
  73. @classmethod
  74. def reload_waiters(cls, path=None):
  75. logger.info(
  76. 'Reload %s waiters: %s',
  77. len(cls.waiters),
  78. cls.watcher.filepath,
  79. )
  80. if path is None:
  81. path = cls.watcher.filepath or '*'
  82. msg = {
  83. 'command': 'reload',
  84. 'path': path,
  85. 'liveCSS': cls.live_css,
  86. 'liveImg': True,
  87. }
  88. cls._last_reload_time = time.time()
  89. for waiter in cls.waiters.copy():
  90. try:
  91. waiter.write_message(msg)
  92. except:
  93. logger.error('Error sending message', exc_info=True)
  94. cls.waiters.remove(waiter)
  95. def on_message(self, message):
  96. """Handshake with livereload.js
  97. 1. client send 'hello'
  98. 2. server reply 'hello'
  99. 3. client send 'info'
  100. """
  101. message = ObjectDict(escape.json_decode(message))
  102. if message.command == 'hello':
  103. handshake = {
  104. 'command': 'hello',
  105. 'protocols': [
  106. 'http://livereload.com/protocols/official-7',
  107. ],
  108. 'serverName': 'livereload-tornado',
  109. }
  110. self.send_message(handshake)
  111. if message.command == 'info' and 'url' in message:
  112. logger.info('Browser Connected: %s' % message.url)
  113. LiveReloadHandler.waiters.add(self)
  114. class MtimeStaticFileHandler(web.StaticFileHandler):
  115. _static_mtimes = {} # type: typing.Dict
  116. @classmethod
  117. def get_content_modified_time(cls, abspath):
  118. """Returns the time that ``abspath`` was last modified.
  119. May be overridden in subclasses. Should return a `~datetime.datetime`
  120. object or None.
  121. """
  122. stat_result = os.stat(abspath)
  123. modified = datetime.datetime.utcfromtimestamp(
  124. stat_result[stat.ST_MTIME])
  125. return modified
  126. @classmethod
  127. def get_content_version(cls, abspath):
  128. """Returns a version string for the resource at the given path.
  129. This class method may be overridden by subclasses. The
  130. default implementation is a hash of the file's contents.
  131. .. versionadded:: 3.1
  132. """
  133. data = cls.get_content(abspath)
  134. hasher = hashlib.md5()
  135. mtime_data = format(cls.get_content_modified_time(abspath), "%Y-%m-%d %H:%M:%S")
  136. hasher.update(mtime_data.encode())
  137. if isinstance(data, bytes):
  138. hasher.update(data)
  139. else:
  140. for chunk in data:
  141. hasher.update(chunk)
  142. return hasher.hexdigest()
  143. @classmethod
  144. def _get_cached_version(cls, abs_path):
  145. def _load_version(abs_path):
  146. try:
  147. hsh = cls.get_content_version(abs_path)
  148. mtm = cls.get_content_modified_time(abs_path)
  149. return mtm, hsh
  150. except Exception:
  151. gen_log.error("Could not open static file %r", abs_path)
  152. return None, None
  153. with cls._lock:
  154. hashes = cls._static_hashes
  155. mtimes = cls._static_mtimes
  156. if abs_path not in hashes:
  157. mtm, hsh = _load_version(abs_path)
  158. hashes[abs_path] = mtm
  159. mtimes[abs_path] = hsh
  160. else:
  161. hsh = hashes.get(abs_path)
  162. mtm = mtimes.get(abs_path)
  163. if mtm != cls.get_content_modified_time(abs_path):
  164. mtm, hsh = _load_version(abs_path)
  165. hashes[abs_path] = mtm
  166. mtimes[abs_path] = hsh
  167. if hsh:
  168. return hsh
  169. return None
  170. class LiveReloadJSHandler(web.RequestHandler):
  171. def get(self):
  172. self.set_header('Content-Type', 'application/javascript')
  173. root = os.path.abspath(os.path.dirname(__file__))
  174. js_file = os.path.join(root, 'vendors/livereload.js')
  175. with open(js_file, 'rb') as f:
  176. self.write(f.read())
  177. class ForceReloadHandler(web.RequestHandler):
  178. def get(self):
  179. path = self.get_argument('path', default=None) or '*'
  180. LiveReloadHandler.reload_waiters(path)
  181. self.write('ok')
  182. class StaticFileHandler(MtimeStaticFileHandler):
  183. def should_return_304(self):
  184. return False