PageRenderTime 49ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/scripts/template_verifier.py

https://github.com/balp/NaCl-main
Python | 311 lines | 283 code | 1 blank | 27 comment | 0 complexity | 141cd785ed8620436b9a90c2fbce2781 MD5 | raw file
  1. #!/usr/bin/python
  2. """\
  3. @file template_verifier.py
  4. @brief Message template compatibility verifier.
  5. $LicenseInfo:firstyear=2007&license=viewerlgpl$
  6. Second Life Viewer Source Code
  7. Copyright (C) 2010, Linden Research, Inc.
  8. This library is free software; you can redistribute it and/or
  9. modify it under the terms of the GNU Lesser General Public
  10. License as published by the Free Software Foundation;
  11. version 2.1 of the License only.
  12. This library is distributed in the hope that it will be useful,
  13. but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  15. Lesser General Public License for more details.
  16. You should have received a copy of the GNU Lesser General Public
  17. License along with this library; if not, write to the Free Software
  18. Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  19. Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
  20. $/LicenseInfo$
  21. """
  22. """template_verifier is a script which will compare the
  23. current repository message template with the "master" message template, accessible
  24. via http://secondlife.com/app/message_template/master_message_template.msg
  25. If [FILE] is specified, it will be checked against the master template.
  26. If [FILE] [FILE] is specified, two local files will be checked against
  27. each other.
  28. """
  29. import sys
  30. import os.path
  31. # Look for indra/lib/python in all possible parent directories ...
  32. # This is an improvement over the setup-path.py method used previously:
  33. # * the script may blocated anywhere inside the source tree
  34. # * it doesn't depend on the current directory
  35. # * it doesn't depend on another file being present.
  36. def add_indra_lib_path():
  37. root = os.path.realpath(__file__)
  38. # always insert the directory of the script in the search path
  39. dir = os.path.dirname(root)
  40. if dir not in sys.path:
  41. sys.path.insert(0, dir)
  42. # Now go look for indra/lib/python in the parent dies
  43. while root != os.path.sep:
  44. root = os.path.dirname(root)
  45. dir = os.path.join(root, 'indra', 'lib', 'python')
  46. if os.path.isdir(dir):
  47. if dir not in sys.path:
  48. sys.path.insert(0, dir)
  49. break
  50. else:
  51. print >>sys.stderr, "This script is not inside a valid installation."
  52. sys.exit(1)
  53. add_indra_lib_path()
  54. import optparse
  55. import os
  56. import urllib
  57. from indra.ipc import compatibility
  58. from indra.ipc import tokenstream
  59. from indra.ipc import llmessage
  60. def getstatusall(command):
  61. """ Like commands.getstatusoutput, but returns stdout and
  62. stderr separately(to get around "killed by signal 15" getting
  63. included as part of the file). Also, works on Windows."""
  64. (input, out, err) = os.popen3(command, 't')
  65. status = input.close() # send no input to the command
  66. output = out.read()
  67. error = err.read()
  68. status = out.close()
  69. status = err.close() # the status comes from the *last* pipe that is closed
  70. return status, output, error
  71. def getstatusoutput(command):
  72. status, output, error = getstatusall(command)
  73. return status, output
  74. def die(msg):
  75. print >>sys.stderr, msg
  76. sys.exit(1)
  77. MESSAGE_TEMPLATE = 'message_template.msg'
  78. PRODUCTION_ACCEPTABLE = (compatibility.Same, compatibility.Newer)
  79. DEVELOPMENT_ACCEPTABLE = (
  80. compatibility.Same, compatibility.Newer,
  81. compatibility.Older, compatibility.Mixed)
  82. MAX_MASTER_AGE = 60 * 60 * 4 # refresh master cache every 4 hours
  83. def retry(times, function, *args, **kwargs):
  84. for i in range(times):
  85. try:
  86. return function(*args, **kwargs)
  87. except Exception, e:
  88. if i == times - 1:
  89. raise e # we retried all the times we could
  90. def compare(base_parsed, current_parsed, mode):
  91. """Compare the current template against the base template using the given
  92. 'mode' strictness:
  93. development: Allows Same, Newer, Older, and Mixed
  94. production: Allows only Same or Newer
  95. Print out information about whether the current template is compatible
  96. with the base template.
  97. Returns a tuple of (bool, Compatibility)
  98. Return True if they are compatible in this mode, False if not.
  99. """
  100. compat = current_parsed.compatibleWithBase(base_parsed)
  101. if mode == 'production':
  102. acceptable = PRODUCTION_ACCEPTABLE
  103. else:
  104. acceptable = DEVELOPMENT_ACCEPTABLE
  105. if type(compat) in acceptable:
  106. return True, compat
  107. return False, compat
  108. def fetch(url):
  109. if url.startswith('file://'):
  110. # just open the file directly because urllib is dumb about these things
  111. file_name = url[len('file://'):]
  112. return open(file_name).read()
  113. else:
  114. # *FIX: this doesn't throw an exception for a 404, and oddly enough the sl.com 404 page actually gets parsed successfully
  115. return ''.join(urllib.urlopen(url).readlines())
  116. def cache_master(master_url):
  117. """Using the url for the master, updates the local cache, and returns an url to the local cache."""
  118. master_cache = local_master_cache_filename()
  119. master_cache_url = 'file://' + master_cache
  120. # decide whether to refresh the master cache based on its age
  121. import time
  122. if (os.path.exists(master_cache)
  123. and time.time() - os.path.getmtime(master_cache) < MAX_MASTER_AGE):
  124. return master_cache_url # our cache is fresh
  125. # new master doesn't exist or isn't fresh
  126. print "Refreshing master cache from %s" % master_url
  127. def get_and_test_master():
  128. new_master_contents = fetch(master_url)
  129. llmessage.parseTemplateString(new_master_contents)
  130. return new_master_contents
  131. try:
  132. new_master_contents = retry(3, get_and_test_master)
  133. except IOError, e:
  134. # the refresh failed, so we should just soldier on
  135. print "WARNING: unable to download new master, probably due to network error. Your message template compatibility may be suspect."
  136. print "Cause: %s" % e
  137. return master_cache_url
  138. try:
  139. tmpname = '%s.%d' % (master_cache, os.getpid())
  140. mc = open(tmpname, 'wb')
  141. mc.write(new_master_contents)
  142. mc.close()
  143. try:
  144. os.rename(tmpname, master_cache)
  145. except OSError:
  146. # We can't rename atomically on top of an existing file on
  147. # Windows. Unlinking the existing file will fail if the
  148. # file is being held open by a process, but there's only
  149. # so much working around a lame I/O API one can take in
  150. # a single day.
  151. os.unlink(master_cache)
  152. os.rename(tmpname, master_cache)
  153. except IOError, e:
  154. print "WARNING: Unable to write master message template to %s, proceeding without cache." % master_cache
  155. print "Cause: %s" % e
  156. return master_url
  157. return master_cache_url
  158. def local_template_filename():
  159. """Returns the message template's default location relative to template_verifier.py:
  160. ./messages/message_template.msg."""
  161. d = os.path.dirname(os.path.realpath(__file__))
  162. return os.path.join(d, 'messages', MESSAGE_TEMPLATE)
  163. def getuser():
  164. try:
  165. # Unix-only.
  166. import getpass
  167. return getpass.getuser()
  168. except ImportError:
  169. import ctypes
  170. MAX_PATH = 260 # according to a recent WinDef.h
  171. name = ctypes.create_unicode_buffer(MAX_PATH)
  172. namelen = ctypes.c_int(len(name)) # len in chars, NOT bytes
  173. if not ctypes.windll.advapi32.GetUserNameW(name, ctypes.byref(namelen)):
  174. raise ctypes.WinError()
  175. return name.value
  176. def local_master_cache_filename():
  177. """Returns the location of the master template cache (which is in the system tempdir)
  178. <temp_dir>/master_message_template_cache.msg"""
  179. import tempfile
  180. d = tempfile.gettempdir()
  181. user = getuser()
  182. return os.path.join(d, 'master_message_template_cache.%s.msg' % user)
  183. def run(sysargs):
  184. parser = optparse.OptionParser(
  185. usage="usage: %prog [FILE] [FILE]",
  186. description=__doc__)
  187. parser.add_option(
  188. '-m', '--mode', type='string', dest='mode',
  189. default='development',
  190. help="""[development|production] The strictness mode to use
  191. while checking the template; see the wiki page for details about
  192. what is allowed and disallowed by each mode:
  193. http://wiki.secondlife.com/wiki/Template_verifier.py
  194. """)
  195. parser.add_option(
  196. '-u', '--master_url', type='string', dest='master_url',
  197. default='http://secondlife.com/app/message_template/master_message_template.msg',
  198. help="""The url of the master message template.""")
  199. parser.add_option(
  200. '-c', '--cache_master', action='store_true', dest='cache_master',
  201. default=False, help="""Set to true to attempt use local cached copy of the master template.""")
  202. options, args = parser.parse_args(sysargs)
  203. if options.mode == 'production':
  204. options.cache_master = False
  205. # both current and master supplied in positional params
  206. if len(args) == 2:
  207. master_filename, current_filename = args
  208. print "master:", master_filename
  209. print "current:", current_filename
  210. master_url = 'file://%s' % master_filename
  211. current_url = 'file://%s' % current_filename
  212. # only current supplied in positional param
  213. elif len(args) == 1:
  214. master_url = None
  215. current_filename = args[0]
  216. print "master:", options.master_url
  217. print "current:", current_filename
  218. current_url = 'file://%s' % current_filename
  219. # nothing specified, use defaults for everything
  220. elif len(args) == 0:
  221. master_url = None
  222. current_url = None
  223. else:
  224. die("Too many arguments")
  225. if master_url is None:
  226. master_url = options.master_url
  227. if current_url is None:
  228. current_filename = local_template_filename()
  229. print "master:", options.master_url
  230. print "current:", current_filename
  231. current_url = 'file://%s' % current_filename
  232. # retrieve the contents of the local template and check for syntax
  233. current = fetch(current_url)
  234. current_parsed = llmessage.parseTemplateString(current)
  235. if options.cache_master:
  236. # optionally return a url to a locally-cached master so we don't hit the network all the time
  237. master_url = cache_master(master_url)
  238. def parse_master_url():
  239. master = fetch(master_url)
  240. return llmessage.parseTemplateString(master)
  241. try:
  242. master_parsed = retry(3, parse_master_url)
  243. except (IOError, tokenstream.ParseError), e:
  244. if options.mode == 'production':
  245. raise e
  246. else:
  247. print "WARNING: problems retrieving the master from %s." % master_url
  248. print "Syntax-checking the local template ONLY, no compatibility check is being run."
  249. print "Cause: %s\n\n" % e
  250. return 0
  251. acceptable, compat = compare(
  252. master_parsed, current_parsed, options.mode)
  253. def explain(header, compat):
  254. print header
  255. # indent compatibility explanation
  256. print '\n\t'.join(compat.explain().split('\n'))
  257. if acceptable:
  258. explain("--- PASS ---", compat)
  259. else:
  260. explain("*** FAIL ***", compat)
  261. return 1
  262. if __name__ == '__main__':
  263. sys.exit(run(sys.argv[1:]))