diff options
Diffstat (limited to '')
-rwxr-xr-x | linden/scripts/template_verifier.py | 159 |
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 | |||
42 | import sys | 42 | import sys |
43 | import urllib | 43 | import urllib |
44 | 44 | ||
45 | from indra import compatibility | 45 | from indra.ipc import compatibility |
46 | from indra import llmessage | 46 | from indra.ipc import tokenstream |
47 | 47 | from indra.ipc import llmessage | |
48 | def die(msg): | ||
49 | print >>sys.stderr, msg | ||
50 | sys.exit(1) | ||
51 | |||
52 | MESSAGE_TEMPLATE = 'message_template.msg' | ||
53 | |||
54 | PRODUCTION_ACCEPTABLE = (compatibility.Same, compatibility.Newer) | ||
55 | DEVELOPMENT_ACCEPTABLE = ( | ||
56 | compatibility.Same, compatibility.Newer, | ||
57 | compatibility.Older, compatibility.Mixed) | ||
58 | 48 | ||
59 | def getstatusall(command): | 49 | def 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 | ||
71 | def getstatusoutput(command): | 61 | def getstatusoutput(command): |
72 | status, output, error = getstatusall(command) | 62 | status, output, error = getstatusall(command) |
73 | return status, output | 63 | return status, output |
74 | 64 | ||
75 | def compare(base, current, mode): | 65 | |
66 | def die(msg): | ||
67 | print >>sys.stderr, msg | ||
68 | sys.exit(1) | ||
69 | |||
70 | MESSAGE_TEMPLATE = 'message_template.msg' | ||
71 | |||
72 | PRODUCTION_ACCEPTABLE = (compatibility.Same, compatibility.Newer) | ||
73 | DEVELOPMENT_ACCEPTABLE = ( | ||
74 | compatibility.Same, compatibility.Newer, | ||
75 | compatibility.Older, compatibility.Mixed) | ||
76 | |||
77 | MAX_MASTER_AGE = 60 * 60 * 4 # refresh master cache every 4 hours | ||
78 | |||
79 | def 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 | |||
87 | def 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 | ||
111 | def 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 | |||
120 | def 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 | |||
101 | def local_template_filename(): | 152 | def 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 | ||
158 | def 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 | |||
107 | def run(sysargs): | 166 | def 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 |