aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/linden/scripts/template_verifier.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xlinden/scripts/template_verifier.py159
1 files changed, 122 insertions, 37 deletions
diff --git a/linden/scripts/template_verifier.py b/linden/scripts/template_verifier.py
index 1bc03e7..68d82ca 100755
--- a/linden/scripts/template_verifier.py
+++ b/linden/scripts/template_verifier.py
@@ -42,37 +42,49 @@ import os
42import sys 42import sys
43import urllib 43import urllib
44 44
45from indra import compatibility 45from indra.ipc import compatibility
46from indra import llmessage 46from indra.ipc import tokenstream
47 47from indra.ipc import llmessage
48def die(msg):
49 print >>sys.stderr, msg
50 sys.exit(1)
51
52MESSAGE_TEMPLATE = 'message_template.msg'
53
54PRODUCTION_ACCEPTABLE = (compatibility.Same, compatibility.Newer)
55DEVELOPMENT_ACCEPTABLE = (
56 compatibility.Same, compatibility.Newer,
57 compatibility.Older, compatibility.Mixed)
58 48
59def getstatusall(command): 49def getstatusall(command):
60 """ Like commands.getstatusoutput, but returns stdout and 50 """ Like commands.getstatusoutput, but returns stdout and
61 stderr separately(to get around "killed by signal 15" getting 51 stderr separately(to get around "killed by signal 15" getting
62 included as part of the file). Also, works on Windows.""" 52 included as part of the file). Also, works on Windows."""
63 (input, out, err) = os.popen3(command, 't') 53 (input, out, err) = os.popen3(command, 't')
64 input.close() # send no input to the command 54 status = input.close() # send no input to the command
65 output = out.read() 55 output = out.read()
66 error = err.read() 56 error = err.read()
67 out.close() 57 status = out.close()
68 status = err.close() # the status comes from the *last* pipe you close 58 status = err.close() # the status comes from the *last* pipe that is closed
69 return status, output, error 59 return status, output, error
70 60
71def getstatusoutput(command): 61def getstatusoutput(command):
72 status, output, error = getstatusall(command) 62 status, output, error = getstatusall(command)
73 return status, output 63 return status, output
74 64
75def compare(base, current, mode): 65
66def die(msg):
67 print >>sys.stderr, msg
68 sys.exit(1)
69
70MESSAGE_TEMPLATE = 'message_template.msg'
71
72PRODUCTION_ACCEPTABLE = (compatibility.Same, compatibility.Newer)
73DEVELOPMENT_ACCEPTABLE = (
74 compatibility.Same, compatibility.Newer,
75 compatibility.Older, compatibility.Mixed)
76
77MAX_MASTER_AGE = 60 * 60 * 4 # refresh master cache every 4 hours
78
79def retry(times, function, *args, **kwargs):
80 for i in range(times):
81 try:
82 return function(*args, **kwargs)
83 except Exception, e:
84 if i == times - 1:
85 raise e # we retried all the times we could
86
87def compare(base_parsed, current_parsed, mode):
76 """Compare the current template against the base template using the given 88 """Compare the current template against the base template using the given
77 'mode' strictness: 89 'mode' strictness:
78 90
@@ -85,10 +97,8 @@ def compare(base, current, mode):
85 Returns a tuple of (bool, Compatibility) 97 Returns a tuple of (bool, Compatibility)
86 Return True if they are compatible in this mode, False if not. 98 Return True if they are compatible in this mode, False if not.
87 """ 99 """
88 base = llmessage.parseTemplateString(base)
89 current = llmessage.parseTemplateString(current)
90 100
91 compat = current.compatibleWithBase(base) 101 compat = current_parsed.compatibleWithBase(base_parsed)
92 if mode == 'production': 102 if mode == 'production':
93 acceptable = PRODUCTION_ACCEPTABLE 103 acceptable = PRODUCTION_ACCEPTABLE
94 else: 104 else:
@@ -98,12 +108,61 @@ def compare(base, current, mode):
98 return True, compat 108 return True, compat
99 return False, compat 109 return False, compat
100 110
111def fetch(url):
112 if url.startswith('file://'):
113 # just open the file directly because urllib is dumb about these things
114 file_name = url[len('file://'):]
115 return open(file_name).read()
116 else:
117 # *FIX: this doesn't throw an exception for a 404, and oddly enough the sl.com 404 page actually gets parsed successfully
118 return ''.join(urllib.urlopen(url).readlines())
119
120def cache_master(master_url):
121 """Using the url for the master, updates the local cache, and returns an url to the local cache."""
122 master_cache = local_master_cache_filename()
123 master_cache_url = 'file://' + master_cache
124 # decide whether to refresh the master cache based on its age
125 import time
126 if (os.path.exists(master_cache)
127 and time.time() - os.path.getmtime(master_cache) < MAX_MASTER_AGE):
128 return master_cache_url # our cache is fresh
129 # new master doesn't exist or isn't fresh
130 print "Refreshing master cache from %s" % master_url
131 def get_and_test_master():
132 new_master_contents = fetch(master_url)
133 llmessage.parseTemplateString(new_master_contents)
134 return new_master_contents
135 try:
136 new_master_contents = retry(3, get_and_test_master)
137 except IOError, e:
138 # the refresh failed, so we should just soldier on
139 print "WARNING: unable to download new master, probably due to network error. Your message template compatibility may be suspect."
140 print "Cause: %s" % e
141 return master_cache_url
142 try:
143 mc = open(master_cache, 'wb')
144 mc.write(new_master_contents)
145 mc.close()
146 except IOError, e:
147 print "WARNING: Unable to write master message template to %s, proceeding without cache." % master_cache
148 print "Cause: %s" % e
149 return master_url
150 return master_cache_url
151
101def local_template_filename(): 152def local_template_filename():
102 """Returns the message template's default location relative to template_verifier.py: 153 """Returns the message template's default location relative to template_verifier.py:
103 ./messages/message_template.msg.""" 154 ./messages/message_template.msg."""
104 d = os.path.dirname(os.path.realpath(__file__)) 155 d = os.path.dirname(os.path.realpath(__file__))
105 return os.path.join(d, 'messages', MESSAGE_TEMPLATE) 156 return os.path.join(d, 'messages', MESSAGE_TEMPLATE)
106 157
158def local_master_cache_filename():
159 """Returns the location of the master template cache (which is in the system tempdir)
160 <temp_dir>/master_message_template_cache.msg"""
161 import tempfile
162 d = tempfile.gettempdir()
163 return os.path.join(d, 'master_message_template_cache.msg')
164
165
107def run(sysargs): 166def run(sysargs):
108 parser = optparse.OptionParser( 167 parser = optparse.OptionParser(
109 usage="usage: %prog [FILE] [FILE]", 168 usage="usage: %prog [FILE] [FILE]",
@@ -120,43 +179,69 @@ http://wiki.secondlife.com/wiki/Template_verifier.py
120 '-u', '--master_url', type='string', dest='master_url', 179 '-u', '--master_url', type='string', dest='master_url',
121 default='http://secondlife.com/app/message_template/master_message_template.msg', 180 default='http://secondlife.com/app/message_template/master_message_template.msg',
122 help="""The url of the master message template.""") 181 help="""The url of the master message template.""")
182 parser.add_option(
183 '-c', '--cache_master', action='store_true', dest='cache_master',
184 default=False, help="""Set to true to attempt use local cached copy of the master template.""")
123 185
124 options, args = parser.parse_args(sysargs) 186 options, args = parser.parse_args(sysargs)
125 187
188 if options.mode == 'production':
189 options.cache_master = False
190
126 # both current and master supplied in positional params 191 # both current and master supplied in positional params
127 if len(args) == 2: 192 if len(args) == 2:
128 master_filename, current_filename = args 193 master_filename, current_filename = args
129 print "base:", master_filename 194 print "master:", master_filename
130 print "current:", current_filename 195 print "current:", current_filename
131 master = file(master_filename).read() 196 master_url = 'file://%s' % master_filename
132 current = file(current_filename).read() 197 current_url = 'file://%s' % current_filename
133 # only current supplied in positional param 198 # only current supplied in positional param
134 elif len(args) == 1: 199 elif len(args) == 1:
135 master = None 200 master_url = None
136 current_filename = args[0] 201 current_filename = args[0]
137 print "base: <master template from repository>" 202 print "master:", options.master_url
138 print "current:", current_filename 203 print "current:", current_filename
139 current = file(current_filename).read() 204 current_url = 'file://%s' % current_filename
140 # nothing specified, use defaults for everything 205 # nothing specified, use defaults for everything
141 elif len(args) == 0: 206 elif len(args) == 0:
142 master = None 207 master_url = None
143 current = None 208 current_url = None
144 else: 209 else:
145 die("Too many arguments") 210 die("Too many arguments")
146 211
147 # fetch the master from the url (default or supplied) 212 if master_url is None:
148 if master is None: 213 master_url = options.master_url
149 master = urllib.urlopen(options.master_url).read() 214
150 215 if current_url is None:
151 # fetch the template for this build
152 if current is None:
153 current_filename = local_template_filename() 216 current_filename = local_template_filename()
154 print "base: <master template from repository>" 217 print "master:", options.master_url
155 print "current:", current_filename 218 print "current:", current_filename
156 current = file(current_filename).read() 219 current_url = 'file://%s' % current_filename
220
221 # retrieve the contents of the local template and check for syntax
222 current = fetch(current_url)
223 current_parsed = llmessage.parseTemplateString(current)
224
225 if options.cache_master:
226 # optionally return a url to a locally-cached master so we don't hit the network all the time
227 master_url = cache_master(master_url)
157 228
229 def parse_master_url():
230 master = fetch(master_url)
231 return llmessage.parseTemplateString(master)
232 try:
233 master_parsed = retry(3, parse_master_url)
234 except (IOError, tokenstream.ParseError), e:
235 if options.mode == 'production':
236 raise e
237 else:
238 print "WARNING: problems retrieving the master from %s." % master_url
239 print "Syntax-checking the local template ONLY, no compatibility check is being run."
240 print "Cause: %s\n\n" % e
241 return 0
242
158 acceptable, compat = compare( 243 acceptable, compat = compare(
159 master, current, options.mode) 244 master_parsed, current_parsed, options.mode)
160 245
161 def explain(header, compat): 246 def explain(header, compat):
162 print header 247 print header