311 lines
11 KiB
Python
Executable File
311 lines
11 KiB
Python
Executable File
#!/usr/bin/python
|
|
"""\
|
|
@file template_verifier.py
|
|
@brief Message template compatibility verifier.
|
|
|
|
$LicenseInfo:firstyear=2007&license=viewergpl$
|
|
|
|
Copyright (c) 2007-2009, Linden Research, Inc.
|
|
|
|
Second Life Viewer Source Code
|
|
The source code in this file ("Source Code") is provided by Linden Lab
|
|
to you under the terms of the GNU General Public License, version 2.0
|
|
("GPL"), unless you have obtained a separate licensing agreement
|
|
("Other License"), formally executed by you and Linden Lab. Terms of
|
|
the GPL can be found in doc/GPL-license.txt in this distribution, or
|
|
online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
|
|
|
|
There are special exceptions to the terms and conditions of the GPL as
|
|
it is applied to this Source Code. View the full text of the exception
|
|
in the file doc/FLOSS-exception.txt in this software distribution, or
|
|
online at
|
|
http://secondlifegrid.net/programs/open_source/licensing/flossexception
|
|
|
|
By copying, modifying or distributing this software, you acknowledge
|
|
that you have read and understood your obligations described above,
|
|
and agree to abide by those obligations.
|
|
|
|
ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
|
|
WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
|
|
COMPLETENESS OR PERFORMANCE.
|
|
$/LicenseInfo$
|
|
"""
|
|
|
|
"""template_verifier is a script which will compare the
|
|
current repository message template with the "master" message template, accessible
|
|
via http://secondlife.com/app/message_template/master_message_template.msg
|
|
If [FILE] is specified, it will be checked against the master template.
|
|
If [FILE] [FILE] is specified, two local files will be checked against
|
|
each other.
|
|
"""
|
|
|
|
import sys
|
|
import os.path
|
|
|
|
# Look for indra/lib/python in all possible parent directories ...
|
|
# This is an improvement over the setup-path.py method used previously:
|
|
# * the script may blocated anywhere inside the source tree
|
|
# * it doesn't depend on the current directory
|
|
# * it doesn't depend on another file being present.
|
|
|
|
def add_indra_lib_path():
|
|
root = os.path.realpath(__file__)
|
|
# always insert the directory of the script in the search path
|
|
dir = os.path.dirname(root)
|
|
if dir not in sys.path:
|
|
sys.path.insert(0, dir)
|
|
|
|
# Now go look for indra/lib/python in the parent dies
|
|
while root != os.path.sep:
|
|
root = os.path.dirname(root)
|
|
dir = os.path.join(root, 'indra', 'lib', 'python')
|
|
if os.path.isdir(dir):
|
|
if dir not in sys.path:
|
|
sys.path.insert(0, dir)
|
|
break
|
|
else:
|
|
print >>sys.stderr, "This script is not inside a valid installation."
|
|
sys.exit(1)
|
|
|
|
add_indra_lib_path()
|
|
|
|
import optparse
|
|
import os
|
|
import urllib
|
|
|
|
from indra.ipc import compatibility
|
|
from indra.ipc import tokenstream
|
|
from indra.ipc import llmessage
|
|
|
|
def getstatusall(command):
|
|
""" Like commands.getstatusoutput, but returns stdout and
|
|
stderr separately(to get around "killed by signal 15" getting
|
|
included as part of the file). Also, works on Windows."""
|
|
(input, out, err) = os.popen3(command, 't')
|
|
status = input.close() # send no input to the command
|
|
output = out.read()
|
|
error = err.read()
|
|
status = out.close()
|
|
status = err.close() # the status comes from the *last* pipe that is closed
|
|
return status, output, error
|
|
|
|
def getstatusoutput(command):
|
|
status, output, error = getstatusall(command)
|
|
return status, output
|
|
|
|
|
|
def die(msg):
|
|
print >>sys.stderr, msg
|
|
sys.exit(1)
|
|
|
|
MESSAGE_TEMPLATE = 'message_template.msg'
|
|
|
|
PRODUCTION_ACCEPTABLE = (compatibility.Same, compatibility.Newer)
|
|
DEVELOPMENT_ACCEPTABLE = (
|
|
compatibility.Same, compatibility.Newer,
|
|
compatibility.Older, compatibility.Mixed)
|
|
|
|
MAX_MASTER_AGE = 60 * 60 * 4 # refresh master cache every 4 hours
|
|
|
|
def retry(times, function, *args, **kwargs):
|
|
for i in range(times):
|
|
try:
|
|
return function(*args, **kwargs)
|
|
except Exception, e:
|
|
if i == times - 1:
|
|
raise e # we retried all the times we could
|
|
|
|
def compare(base_parsed, current_parsed, mode):
|
|
"""Compare the current template against the base template using the given
|
|
'mode' strictness:
|
|
|
|
development: Allows Same, Newer, Older, and Mixed
|
|
production: Allows only Same or Newer
|
|
|
|
Print out information about whether the current template is compatible
|
|
with the base template.
|
|
|
|
Returns a tuple of (bool, Compatibility)
|
|
Return True if they are compatible in this mode, False if not.
|
|
"""
|
|
|
|
compat = current_parsed.compatibleWithBase(base_parsed)
|
|
if mode == 'production':
|
|
acceptable = PRODUCTION_ACCEPTABLE
|
|
else:
|
|
acceptable = DEVELOPMENT_ACCEPTABLE
|
|
|
|
if type(compat) in acceptable:
|
|
return True, compat
|
|
return False, compat
|
|
|
|
def fetch(url):
|
|
if url.startswith('file://'):
|
|
# just open the file directly because urllib is dumb about these things
|
|
file_name = url[len('file://'):]
|
|
return open(file_name).read()
|
|
else:
|
|
# *FIX: this doesn't throw an exception for a 404, and oddly enough the sl.com 404 page actually gets parsed successfully
|
|
return ''.join(urllib.urlopen(url).readlines())
|
|
|
|
def cache_master(master_url):
|
|
"""Using the url for the master, updates the local cache, and returns an url to the local cache."""
|
|
master_cache = local_master_cache_filename()
|
|
master_cache_url = 'file://' + master_cache
|
|
# decide whether to refresh the master cache based on its age
|
|
import time
|
|
if (os.path.exists(master_cache)
|
|
and time.time() - os.path.getmtime(master_cache) < MAX_MASTER_AGE):
|
|
return master_cache_url # our cache is fresh
|
|
# new master doesn't exist or isn't fresh
|
|
print "Refreshing master cache from %s" % master_url
|
|
def get_and_test_master():
|
|
new_master_contents = fetch(master_url)
|
|
llmessage.parseTemplateString(new_master_contents)
|
|
return new_master_contents
|
|
try:
|
|
new_master_contents = retry(3, get_and_test_master)
|
|
except IOError, e:
|
|
# the refresh failed, so we should just soldier on
|
|
print "WARNING: unable to download new master, probably due to network error. Your message template compatibility may be suspect."
|
|
print "Cause: %s" % e
|
|
return master_cache_url
|
|
try:
|
|
tmpname = '%s.%d' % (master_cache, os.getpid())
|
|
mc = open(tmpname, 'wb')
|
|
mc.write(new_master_contents)
|
|
mc.close()
|
|
try:
|
|
os.rename(tmpname, master_cache)
|
|
except OSError:
|
|
# We can't rename atomically on top of an existing file on
|
|
# Windows. Unlinking the existing file will fail if the
|
|
# file is being held open by a process, but there's only
|
|
# so much working around a lame I/O API one can take in
|
|
# a single day.
|
|
os.unlink(master_cache)
|
|
os.rename(tmpname, master_cache)
|
|
except IOError, e:
|
|
print "WARNING: Unable to write master message template to %s, proceeding without cache." % master_cache
|
|
print "Cause: %s" % e
|
|
return master_url
|
|
return master_cache_url
|
|
|
|
def local_template_filename():
|
|
"""Returns the message template's default location relative to template_verifier.py:
|
|
./messages/message_template.msg."""
|
|
d = os.path.dirname(os.path.realpath(__file__))
|
|
return os.path.join(d, 'messages', MESSAGE_TEMPLATE)
|
|
|
|
def getuser():
|
|
try:
|
|
# Unix-only.
|
|
import getpass
|
|
return getpass.getuser()
|
|
except ImportError:
|
|
import win32api
|
|
return win32api.GetUserName()
|
|
|
|
def local_master_cache_filename():
|
|
"""Returns the location of the master template cache (which is in the system tempdir)
|
|
<temp_dir>/master_message_template_cache.msg"""
|
|
import tempfile
|
|
d = tempfile.gettempdir()
|
|
user = getuser()
|
|
return os.path.join(d, 'master_message_template_cache.%s.msg' % user)
|
|
|
|
|
|
def run(sysargs):
|
|
parser = optparse.OptionParser(
|
|
usage="usage: %prog [FILE] [FILE]",
|
|
description=__doc__)
|
|
parser.add_option(
|
|
'-m', '--mode', type='string', dest='mode',
|
|
default='development',
|
|
help="""[development|production] The strictness mode to use
|
|
while checking the template; see the wiki page for details about
|
|
what is allowed and disallowed by each mode:
|
|
http://wiki.secondlife.com/wiki/Template_verifier.py
|
|
""")
|
|
parser.add_option(
|
|
'-u', '--master_url', type='string', dest='master_url',
|
|
default='http://secondlife.com/app/message_template/master_message_template.msg',
|
|
help="""The url of the master message template.""")
|
|
parser.add_option(
|
|
'-c', '--cache_master', action='store_true', dest='cache_master',
|
|
default=False, help="""Set to true to attempt use local cached copy of the master template.""")
|
|
|
|
options, args = parser.parse_args(sysargs)
|
|
|
|
if options.mode == 'production':
|
|
options.cache_master = False
|
|
|
|
# both current and master supplied in positional params
|
|
if len(args) == 2:
|
|
master_filename, current_filename = args
|
|
print "master:", master_filename
|
|
print "current:", current_filename
|
|
master_url = 'file://%s' % master_filename
|
|
current_url = 'file://%s' % current_filename
|
|
# only current supplied in positional param
|
|
elif len(args) == 1:
|
|
master_url = None
|
|
current_filename = args[0]
|
|
print "master:", options.master_url
|
|
print "current:", current_filename
|
|
current_url = 'file://%s' % current_filename
|
|
# nothing specified, use defaults for everything
|
|
elif len(args) == 0:
|
|
master_url = None
|
|
current_url = None
|
|
else:
|
|
die("Too many arguments")
|
|
|
|
if master_url is None:
|
|
master_url = options.master_url
|
|
|
|
if current_url is None:
|
|
current_filename = local_template_filename()
|
|
print "master:", options.master_url
|
|
print "current:", current_filename
|
|
current_url = 'file://%s' % current_filename
|
|
|
|
# retrieve the contents of the local template and check for syntax
|
|
current = fetch(current_url)
|
|
current_parsed = llmessage.parseTemplateString(current)
|
|
|
|
if options.cache_master:
|
|
# optionally return a url to a locally-cached master so we don't hit the network all the time
|
|
master_url = cache_master(master_url)
|
|
|
|
def parse_master_url():
|
|
master = fetch(master_url)
|
|
return llmessage.parseTemplateString(master)
|
|
try:
|
|
master_parsed = retry(3, parse_master_url)
|
|
except (IOError, tokenstream.ParseError), e:
|
|
if options.mode == 'production':
|
|
raise e
|
|
else:
|
|
print "WARNING: problems retrieving the master from %s." % master_url
|
|
print "Syntax-checking the local template ONLY, no compatibility check is being run."
|
|
print "Cause: %s\n\n" % e
|
|
return 0
|
|
|
|
acceptable, compat = compare(
|
|
master_parsed, current_parsed, options.mode)
|
|
|
|
def explain(header, compat):
|
|
print header
|
|
# indent compatibility explanation
|
|
print '\n\t'.join(compat.explain().split('\n'))
|
|
|
|
if acceptable:
|
|
explain("--- PASS ---", compat)
|
|
else:
|
|
explain("*** FAIL ***", compat)
|
|
return 1
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(run(sys.argv[1:]))
|