mirror of
https://github.com/golang/go
synced 2024-11-12 07:40:23 -07:00
328aac3a49
The changes were not tested for real in an App Engine environment, so extra care should be taken. That said, some static testing was done with pyflakes, and a few existent problems were fixed on the way. R=adg CC=golang-dev https://golang.org/cl/4378053
745 lines
24 KiB
Python
745 lines
24 KiB
Python
# Copyright 2009 The Go Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style
|
|
# license that can be found in the LICENSE file.
|
|
|
|
# This is the server part of the continuous build system for Go. It must be run
|
|
# by AppEngine.
|
|
|
|
from google.appengine.api import mail
|
|
from google.appengine.api import memcache
|
|
from google.appengine.ext import db
|
|
from google.appengine.ext import webapp
|
|
from google.appengine.ext.webapp import template
|
|
from google.appengine.ext.webapp.util import run_wsgi_app
|
|
import binascii
|
|
import datetime
|
|
import hashlib
|
|
import hmac
|
|
import logging
|
|
import os
|
|
import re
|
|
import struct
|
|
import time
|
|
import bz2
|
|
|
|
# local imports
|
|
import key
|
|
import const
|
|
|
|
# The majority of our state are commit objects. One of these exists for each of
|
|
# the commits known to the build system. Their key names are of the form
|
|
# <commit number (%08x)> "-" <hg hash>. This means that a sorting by the key
|
|
# name is sufficient to order the commits.
|
|
#
|
|
# The commit numbers are purely local. They need not match up to the commit
|
|
# numbers in an hg repo. When inserting a new commit, the parent commit must be
|
|
# given and this is used to generate the new commit number. In order to create
|
|
# the first Commit object, a special command (/init) is used.
|
|
class Commit(db.Model):
|
|
num = db.IntegerProperty() # internal, monotonic counter.
|
|
node = db.StringProperty() # Hg hash
|
|
parentnode = db.StringProperty() # Hg hash
|
|
user = db.StringProperty()
|
|
date = db.DateTimeProperty()
|
|
desc = db.BlobProperty()
|
|
|
|
# This is the list of builds. Each element is a string of the form <builder
|
|
# name> "`" <log hash>. If the log hash is empty, then the build was
|
|
# successful.
|
|
builds = db.StringListProperty()
|
|
|
|
fail_notification_sent = db.BooleanProperty()
|
|
|
|
class Benchmark(db.Model):
|
|
name = db.StringProperty()
|
|
version = db.IntegerProperty()
|
|
|
|
class BenchmarkResults(db.Model):
|
|
builder = db.StringProperty()
|
|
benchmark = db.StringProperty()
|
|
data = db.ListProperty(long) # encoded as [-1, num, iterations, nsperop]*
|
|
|
|
class Cache(db.Model):
|
|
data = db.BlobProperty()
|
|
expire = db.IntegerProperty()
|
|
|
|
# A CompressedLog contains the textual build log of a failed build.
|
|
# The key name is the hex digest of the SHA256 hash of the contents.
|
|
# The contents is bz2 compressed.
|
|
class CompressedLog(db.Model):
|
|
log = db.BlobProperty()
|
|
|
|
# For each builder, we store the last revision that it built. So, if it
|
|
# crashes, it knows where to start up from. The key names for these objects are
|
|
# "hw-" <builder name>
|
|
class Highwater(db.Model):
|
|
commit = db.StringProperty()
|
|
|
|
N = 30
|
|
|
|
def cache_get(key):
|
|
c = Cache.get_by_key_name(key)
|
|
if c is None or c.expire < time.time():
|
|
return None
|
|
return c.data
|
|
|
|
def cache_set(key, val, timeout):
|
|
c = Cache(key_name = key)
|
|
c.data = val
|
|
c.expire = int(time.time() + timeout)
|
|
c.put()
|
|
|
|
def cache_del(key):
|
|
c = Cache.get_by_key_name(key)
|
|
if c is not None:
|
|
c.delete()
|
|
|
|
def builderInfo(b):
|
|
f = b.split('-', 3)
|
|
goos = f[0]
|
|
goarch = f[1]
|
|
note = ""
|
|
if len(f) > 2:
|
|
note = f[2]
|
|
return {'name': b, 'goos': goos, 'goarch': goarch, 'note': note}
|
|
|
|
def builderset():
|
|
q = Commit.all()
|
|
q.order('-__key__')
|
|
results = q.fetch(N)
|
|
builders = set()
|
|
for c in results:
|
|
builders.update(set(parseBuild(build)['builder'] for build in c.builds))
|
|
return builders
|
|
|
|
class MainPage(webapp.RequestHandler):
|
|
def get(self):
|
|
self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
|
|
|
|
try:
|
|
page = int(self.request.get('p', 1))
|
|
if not page > 0:
|
|
raise
|
|
except:
|
|
page = 1
|
|
|
|
try:
|
|
num = int(self.request.get('n', N))
|
|
if num <= 0 or num > 200:
|
|
raise
|
|
except:
|
|
num = N
|
|
|
|
offset = (page-1) * num
|
|
|
|
q = Commit.all()
|
|
q.order('-__key__')
|
|
results = q.fetch(num, offset)
|
|
|
|
revs = [toRev(r) for r in results]
|
|
builders = {}
|
|
|
|
for r in revs:
|
|
for b in r['builds']:
|
|
builders[b['builder']] = builderInfo(b['builder'])
|
|
|
|
for r in revs:
|
|
have = set(x['builder'] for x in r['builds'])
|
|
need = set(builders.keys()).difference(have)
|
|
for n in need:
|
|
r['builds'].append({'builder': n, 'log':'', 'ok': False})
|
|
r['builds'].sort(cmp = byBuilder)
|
|
|
|
builders = list(builders.items())
|
|
builders.sort()
|
|
values = {"revs": revs, "builders": [v for k,v in builders]}
|
|
|
|
values['num'] = num
|
|
values['prev'] = page - 1
|
|
if len(results) == num:
|
|
values['next'] = page + 1
|
|
|
|
path = os.path.join(os.path.dirname(__file__), 'main.html')
|
|
self.response.out.write(template.render(path, values))
|
|
|
|
class GetHighwater(webapp.RequestHandler):
|
|
def get(self):
|
|
builder = self.request.get('builder')
|
|
|
|
key = 'hw-%s' % builder
|
|
node = memcache.get(key)
|
|
if node is None:
|
|
hw = Highwater.get_by_key_name('hw-%s' % builder)
|
|
if hw is None:
|
|
# If no highwater has been recorded for this builder,
|
|
# we go back N+1 commits and return that.
|
|
q = Commit.all()
|
|
q.order('-__key__')
|
|
c = q.fetch(N+1)[-1]
|
|
node = c.node
|
|
else:
|
|
# if the proposed hw is too old, bump it forward
|
|
node = hw.commit
|
|
found = False
|
|
q = Commit.all()
|
|
q.order('-__key__')
|
|
recent = q.fetch(N+1)
|
|
for c in recent:
|
|
if c.node == node:
|
|
found = True
|
|
break
|
|
if not found:
|
|
node = recent[-1].node
|
|
memcache.set(key, node, 3600)
|
|
self.response.set_status(200)
|
|
self.response.out.write(node)
|
|
|
|
def auth(req):
|
|
k = req.get('key')
|
|
return k == hmac.new(key.accessKey, req.get('builder')).hexdigest() or k == key.accessKey
|
|
|
|
class SetHighwater(webapp.RequestHandler):
|
|
def post(self):
|
|
if not auth(self.request):
|
|
self.response.set_status(403)
|
|
return
|
|
|
|
builder = self.request.get('builder')
|
|
newhw = self.request.get('hw')
|
|
q = Commit.all()
|
|
q.filter('node =', newhw)
|
|
c = q.get()
|
|
if c is None:
|
|
self.response.set_status(404)
|
|
return
|
|
|
|
# if the proposed hw is too old, bump it forward
|
|
found = False
|
|
q = Commit.all()
|
|
q.order('-__key__')
|
|
recent = q.fetch(N+1)
|
|
for c in recent:
|
|
if c.node == newhw:
|
|
found = True
|
|
break
|
|
if not found:
|
|
c = recent[-1]
|
|
|
|
key = 'hw-%s' % builder
|
|
memcache.delete(key)
|
|
hw = Highwater(key_name = key)
|
|
hw.commit = c.node
|
|
hw.put()
|
|
|
|
class LogHandler(webapp.RequestHandler):
|
|
def get(self):
|
|
self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
|
|
hash = self.request.path[5:]
|
|
l = CompressedLog.get_by_key_name(hash)
|
|
if l is None:
|
|
self.response.set_status(404)
|
|
return
|
|
log = bz2.decompress(l.log)
|
|
self.response.set_status(200)
|
|
self.response.out.write(log)
|
|
|
|
# Init creates the commit with id 0. Since this commit doesn't have a parent,
|
|
# it cannot be created by Build.
|
|
class Init(webapp.RequestHandler):
|
|
def post(self):
|
|
if not auth(self.request):
|
|
self.response.set_status(403)
|
|
return
|
|
|
|
date = parseDate(self.request.get('date'))
|
|
node = self.request.get('node')
|
|
if not validNode(node) or date is None:
|
|
logging.error("Not valid node ('%s') or bad date (%s %s)", node, date, self.request.get('date'))
|
|
self.response.set_status(500)
|
|
return
|
|
|
|
commit = Commit(key_name = '00000000-%s' % node)
|
|
commit.num = 0
|
|
commit.node = node
|
|
commit.parentnode = ''
|
|
commit.user = self.request.get('user').encode('utf8')
|
|
commit.date = date
|
|
commit.desc = self.request.get('desc').encode('utf8')
|
|
|
|
commit.put()
|
|
|
|
self.response.set_status(200)
|
|
|
|
# Build is the main command: it records the result of a new build.
|
|
class Build(webapp.RequestHandler):
|
|
def post(self):
|
|
if not auth(self.request):
|
|
self.response.set_status(403)
|
|
return
|
|
|
|
builder = self.request.get('builder')
|
|
log = self.request.get('log').encode('utf-8')
|
|
|
|
loghash = ''
|
|
if len(log) > 0:
|
|
loghash = hashlib.sha256(log).hexdigest()
|
|
l = CompressedLog(key_name=loghash)
|
|
l.log = bz2.compress(log)
|
|
l.put()
|
|
|
|
date = parseDate(self.request.get('date'))
|
|
user = self.request.get('user').encode('utf8')
|
|
desc = self.request.get('desc').encode('utf8')
|
|
node = self.request.get('node')
|
|
parenthash = self.request.get('parent')
|
|
if not validNode(node) or not validNode(parenthash) or date is None:
|
|
logging.error("Not valid node ('%s') or bad date (%s %s)", node, date, self.request.get('date'))
|
|
self.response.set_status(500)
|
|
return
|
|
|
|
q = Commit.all()
|
|
q.filter('node =', parenthash)
|
|
parent = q.get()
|
|
if parent is None:
|
|
logging.error('Cannot find parent %s of node %s' % (parenthash, node))
|
|
self.response.set_status(404)
|
|
return
|
|
parentnum, _ = parent.key().name().split('-', 1)
|
|
nodenum = int(parentnum, 16) + 1
|
|
|
|
key_name = '%08x-%s' % (nodenum, node)
|
|
|
|
def add_build():
|
|
n = Commit.get_by_key_name(key_name)
|
|
if n is None:
|
|
n = Commit(key_name = key_name)
|
|
n.num = nodenum
|
|
n.node = node
|
|
n.parentnode = parenthash
|
|
n.user = user
|
|
n.date = date
|
|
n.desc = desc
|
|
s = '%s`%s' % (builder, loghash)
|
|
for i, b in enumerate(n.builds):
|
|
if b.split('`', 1)[0] == builder:
|
|
n.builds[i] = s
|
|
break
|
|
else:
|
|
n.builds.append(s)
|
|
n.put()
|
|
|
|
db.run_in_transaction(add_build)
|
|
|
|
key = 'hw-%s' % builder
|
|
hw = Highwater.get_by_key_name(key)
|
|
if hw is None:
|
|
hw = Highwater(key_name = key)
|
|
hw.commit = node
|
|
hw.put()
|
|
memcache.delete(key)
|
|
memcache.delete('hw')
|
|
|
|
def mark_sent():
|
|
n = Commit.get_by_key_name(key_name)
|
|
n.fail_notification_sent = True
|
|
n.put()
|
|
|
|
n = Commit.get_by_key_name(key_name)
|
|
if loghash and not failed(parent, builder) and not n.fail_notification_sent:
|
|
subject = const.mail_fail_subject % (builder, desc.split("\n")[0])
|
|
path = os.path.join(os.path.dirname(__file__), 'fail-notify.txt')
|
|
body = template.render(path, {
|
|
"builder": builder,
|
|
"node": node[:12],
|
|
"user": user,
|
|
"desc": desc,
|
|
"loghash": loghash
|
|
})
|
|
mail.send_mail(
|
|
sender=const.mail_from,
|
|
reply_to=const.mail_fail_reply_to,
|
|
to=const.mail_fail_to,
|
|
subject=subject,
|
|
body=body
|
|
)
|
|
db.run_in_transaction(mark_sent)
|
|
|
|
self.response.set_status(200)
|
|
|
|
def failed(c, builder):
|
|
for i, b in enumerate(c.builds):
|
|
p = b.split('`', 1)
|
|
if p[0] == builder:
|
|
return len(p[1]) > 0
|
|
return False
|
|
|
|
class Benchmarks(webapp.RequestHandler):
|
|
def json(self):
|
|
q = Benchmark.all()
|
|
q.filter('__key__ >', Benchmark.get_or_insert('v002.').key())
|
|
bs = q.fetch(10000)
|
|
|
|
self.response.set_status(200)
|
|
self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
|
|
self.response.out.write('{"benchmarks": [')
|
|
|
|
sep = "\n\t"
|
|
for b in bs:
|
|
self.response.out.write('%s"%s"' % (sep, b.name))
|
|
sep = ",\n\t"
|
|
self.response.out.write('\n]}\n')
|
|
|
|
def get(self):
|
|
if self.request.get('fmt') == 'json':
|
|
return self.json()
|
|
|
|
self.response.set_status(200)
|
|
self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
|
|
page = memcache.get('bench')
|
|
if not page:
|
|
# use datastore as cache to avoid computation even
|
|
# if memcache starts dropping things on the floor
|
|
logging.error("memcache dropped bench")
|
|
page = cache_get('bench')
|
|
if not page:
|
|
logging.error("cache dropped bench")
|
|
num = memcache.get('hw')
|
|
if num is None:
|
|
q = Commit.all()
|
|
q.order('-__key__')
|
|
n = q.fetch(1)[0]
|
|
num = n.num
|
|
memcache.set('hw', num)
|
|
page = self.compute(num)
|
|
cache_set('bench', page, 600)
|
|
memcache.set('bench', page, 600)
|
|
self.response.out.write(page)
|
|
|
|
def compute(self, num):
|
|
benchmarks, builders = benchmark_list()
|
|
|
|
rows = []
|
|
for bm in benchmarks:
|
|
row = {'name':bm, 'builders': []}
|
|
for bl in builders:
|
|
key = "single-%s-%s" % (bm, bl)
|
|
url = memcache.get(key)
|
|
row['builders'].append({'name': bl, 'url': url})
|
|
rows.append(row)
|
|
|
|
path = os.path.join(os.path.dirname(__file__), 'benchmarks.html')
|
|
data = {
|
|
"builders": [builderInfo(b) for b in builders],
|
|
"rows": rows,
|
|
}
|
|
return template.render(path, data)
|
|
|
|
def post(self):
|
|
if not auth(self.request):
|
|
self.response.set_status(403)
|
|
return
|
|
|
|
builder = self.request.get('builder')
|
|
node = self.request.get('node')
|
|
if not validNode(node):
|
|
logging.error("Not valid node ('%s')", node)
|
|
self.response.set_status(500)
|
|
return
|
|
|
|
benchmarkdata = self.request.get('benchmarkdata')
|
|
benchmarkdata = binascii.a2b_base64(benchmarkdata)
|
|
|
|
def get_string(i):
|
|
l, = struct.unpack('>H', i[:2])
|
|
s = i[2:2+l]
|
|
if len(s) != l:
|
|
return None, None
|
|
return s, i[2+l:]
|
|
|
|
benchmarks = {}
|
|
while len(benchmarkdata) > 0:
|
|
name, benchmarkdata = get_string(benchmarkdata)
|
|
iterations_str, benchmarkdata = get_string(benchmarkdata)
|
|
time_str, benchmarkdata = get_string(benchmarkdata)
|
|
iterations = int(iterations_str)
|
|
time = int(time_str)
|
|
|
|
benchmarks[name] = (iterations, time)
|
|
|
|
q = Commit.all()
|
|
q.filter('node =', node)
|
|
n = q.get()
|
|
if n is None:
|
|
logging.error('Client asked for unknown commit while uploading benchmarks')
|
|
self.response.set_status(404)
|
|
return
|
|
|
|
for (benchmark, (iterations, time)) in benchmarks.items():
|
|
b = Benchmark.get_or_insert('v002.' + benchmark.encode('base64'), name = benchmark, version = 2)
|
|
key = '%s;%s' % (builder, benchmark)
|
|
r1 = BenchmarkResults.get_by_key_name(key)
|
|
if r1 is not None and (len(r1.data) < 4 or r1.data[-4] != -1 or r1.data[-3] != n.num):
|
|
r1.data += [-1L, long(n.num), long(iterations), long(time)]
|
|
r1.put()
|
|
key = "bench(%s,%s,%d)" % (benchmark, builder, n.num)
|
|
memcache.delete(key)
|
|
|
|
self.response.set_status(200)
|
|
|
|
class SingleBenchmark(webapp.RequestHandler):
|
|
"""
|
|
Fetch data for single benchmark/builder combination
|
|
and return sparkline url as HTTP redirect, also set memcache entry.
|
|
"""
|
|
def get(self):
|
|
benchmark = self.request.get('benchmark')
|
|
builder = self.request.get('builder')
|
|
key = "single-%s-%s" % (benchmark, builder)
|
|
|
|
url = memcache.get(key)
|
|
|
|
if url is None:
|
|
minr, maxr, bybuilder = benchmark_data(benchmark)
|
|
for bb in bybuilder:
|
|
if bb[0] != builder:
|
|
continue
|
|
url = benchmark_sparkline(bb[2])
|
|
|
|
if url is None:
|
|
self.response.set_status(500, "No data found")
|
|
return
|
|
|
|
memcache.set(key, url, 700) # slightly longer than bench timeout
|
|
|
|
self.response.set_status(302)
|
|
self.response.headers.add_header("Location", url)
|
|
|
|
def node(num):
|
|
q = Commit.all()
|
|
q.filter('num =', num)
|
|
n = q.get()
|
|
return n
|
|
|
|
def benchmark_data(benchmark):
|
|
q = BenchmarkResults.all()
|
|
q.order('__key__')
|
|
q.filter('benchmark =', benchmark)
|
|
results = q.fetch(100)
|
|
|
|
minr = 100000000
|
|
maxr = 0
|
|
for r in results:
|
|
if r.benchmark != benchmark:
|
|
continue
|
|
# data is [-1, num, iters, nsperop, -1, num, iters, nsperop, ...]
|
|
d = r.data
|
|
if not d:
|
|
continue
|
|
if [x for x in d[::4] if x != -1]:
|
|
# unexpected data framing
|
|
logging.error("bad framing for data in %s;%s" % (r.builder, r.benchmark))
|
|
continue
|
|
revs = d[1::4]
|
|
minr = min(minr, min(revs))
|
|
maxr = max(maxr, max(revs))
|
|
if minr > maxr:
|
|
return 0, 0, []
|
|
|
|
bybuilder = []
|
|
for r in results:
|
|
if r.benchmark != benchmark:
|
|
continue
|
|
d = r.data
|
|
if not d:
|
|
continue
|
|
nsbyrev = [-1 for x in range(minr, maxr+1)]
|
|
iterbyrev = [-1 for x in range(minr, maxr+1)]
|
|
for num, iter, ns in zip(d[1::4], d[2::4], d[3::4]):
|
|
iterbyrev[num - minr] = iter
|
|
nsbyrev[num - minr] = ns
|
|
bybuilder.append((r.builder, iterbyrev, nsbyrev))
|
|
|
|
return minr, maxr, bybuilder
|
|
|
|
def benchmark_graph(builder, minhash, maxhash, ns):
|
|
valid = [x for x in ns if x >= 0]
|
|
if not valid:
|
|
return ""
|
|
m = max(max(valid), 2*sum(valid)/len(valid))
|
|
s = ""
|
|
encoding = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-"
|
|
for val in ns:
|
|
if val < 0:
|
|
s += "__"
|
|
continue
|
|
val = int(val*4095.0/m)
|
|
s += encoding[val/64] + encoding[val%64]
|
|
return ("http://chart.apis.google.com/chart?cht=lc&chxt=x,y&chxl=0:|%s|%s|1:|0|%g ns|%g ns&chd=e:%s" %
|
|
(minhash[0:12], maxhash[0:12], m/2, m, s))
|
|
|
|
def benchmark_sparkline(ns):
|
|
valid = [x for x in ns if x >= 0]
|
|
if not valid:
|
|
return ""
|
|
m = max(max(valid), 2*sum(valid)/len(valid))
|
|
# Encoding is 0-61, which is fine enough granularity for our tiny graphs. _ means missing.
|
|
encoding = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
s = ''.join([x < 0 and "_" or encoding[int((len(encoding)-1)*x/m)] for x in ns])
|
|
url = "http://chart.apis.google.com/chart?cht=ls&chd=s:"+s+"&chs=80x20&chf=bg,s,00000000&chco=000000ff&chls=1,1,0"
|
|
return url
|
|
|
|
def benchmark_list():
|
|
q = BenchmarkResults.all()
|
|
q.order('__key__')
|
|
q.filter('builder = ', u'darwin-amd64')
|
|
benchmarks = [r.benchmark for r in q]
|
|
|
|
q = BenchmarkResults.all()
|
|
q.order('__key__')
|
|
q.filter('benchmark =', u'math_test.BenchmarkSqrt')
|
|
builders = [r.builder for r in q.fetch(20)]
|
|
|
|
return benchmarks, builders
|
|
|
|
class GetBenchmarks(webapp.RequestHandler):
|
|
def get(self):
|
|
benchmark = self.request.path[12:]
|
|
minr, maxr, bybuilder = benchmark_data(benchmark)
|
|
minhash = node(minr).node
|
|
maxhash = node(maxr).node
|
|
|
|
if self.request.get('fmt') == 'json':
|
|
self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
|
|
self.response.out.write('{ "min": "%s", "max": "%s", "data": {' % (minhash, maxhash))
|
|
sep = "\n\t"
|
|
for builder, iter, ns in bybuilder:
|
|
self.response.out.write('%s{ "builder": "%s", "iterations": %s, "nsperop": %s }' %
|
|
(sep, builder, str(iter).replace("L", ""), str(ns).replace("L", "")))
|
|
sep = ",\n\t"
|
|
self.response.out.write('\n}\n')
|
|
return
|
|
|
|
graphs = []
|
|
for builder, iter, ns in bybuilder:
|
|
graphs.append({"builder": builder, "url": benchmark_graph(builder, minhash, maxhash, ns)})
|
|
|
|
revs = []
|
|
for i in range(minr, maxr+1):
|
|
r = nodeInfo(node(i))
|
|
x = []
|
|
for _, _, ns in bybuilder:
|
|
t = ns[i - minr]
|
|
if t < 0:
|
|
t = None
|
|
x.append(t)
|
|
r["ns_by_builder"] = x
|
|
revs.append(r)
|
|
revs.reverse() # same order as front page
|
|
|
|
path = os.path.join(os.path.dirname(__file__), 'benchmark1.html')
|
|
data = {
|
|
"benchmark": benchmark,
|
|
"builders": [builderInfo(b) for b,_,_ in bybuilder],
|
|
"graphs": graphs,
|
|
"revs": revs,
|
|
}
|
|
self.response.out.write(template.render(path, data))
|
|
|
|
|
|
class FixedOffset(datetime.tzinfo):
|
|
"""Fixed offset in minutes east from UTC."""
|
|
|
|
def __init__(self, offset):
|
|
self.__offset = datetime.timedelta(seconds = offset)
|
|
|
|
def utcoffset(self, dt):
|
|
return self.__offset
|
|
|
|
def tzname(self, dt):
|
|
return None
|
|
|
|
def dst(self, dt):
|
|
return datetime.timedelta(0)
|
|
|
|
def validNode(node):
|
|
if len(node) != 40:
|
|
return False
|
|
for x in node:
|
|
o = ord(x)
|
|
if (o < ord('0') or o > ord('9')) and (o < ord('a') or o > ord('f')):
|
|
return False
|
|
return True
|
|
|
|
def parseDate(date):
|
|
if '-' in date:
|
|
(a, offset) = date.split('-', 1)
|
|
try:
|
|
return datetime.datetime.fromtimestamp(float(a), FixedOffset(0-int(offset)))
|
|
except ValueError:
|
|
return None
|
|
if '+' in date:
|
|
(a, offset) = date.split('+', 1)
|
|
try:
|
|
return datetime.datetime.fromtimestamp(float(a), FixedOffset(int(offset)))
|
|
except ValueError:
|
|
return None
|
|
try:
|
|
return datetime.datetime.utcfromtimestamp(float(date))
|
|
except ValueError:
|
|
return None
|
|
|
|
email_re = re.compile('^[^<]+<([^>]*)>$')
|
|
|
|
def toUsername(user):
|
|
r = email_re.match(user)
|
|
if r is None:
|
|
return user
|
|
email = r.groups()[0]
|
|
return email.replace('@golang.org', '')
|
|
|
|
def dateToShortStr(d):
|
|
return d.strftime('%a %b %d %H:%M')
|
|
|
|
def parseBuild(build):
|
|
[builder, logblob] = build.split('`')
|
|
return {'builder': builder, 'log': logblob, 'ok': len(logblob) == 0}
|
|
|
|
def nodeInfo(c):
|
|
return {
|
|
"node": c.node,
|
|
"user": toUsername(c.user),
|
|
"date": dateToShortStr(c.date),
|
|
"desc": c.desc,
|
|
"shortdesc": c.desc.split('\n', 2)[0]
|
|
}
|
|
|
|
def toRev(c):
|
|
b = nodeInfo(c)
|
|
b['builds'] = [parseBuild(build) for build in c.builds]
|
|
return b
|
|
|
|
def byBuilder(x, y):
|
|
return cmp(x['builder'], y['builder'])
|
|
|
|
# This is the URL map for the server. The first three entries are public, the
|
|
# rest are only used by the builders.
|
|
application = webapp.WSGIApplication(
|
|
[('/', MainPage),
|
|
('/log/.*', LogHandler),
|
|
('/hw-get', GetHighwater),
|
|
('/hw-set', SetHighwater),
|
|
|
|
('/init', Init),
|
|
('/build', Build),
|
|
('/benchmarks', Benchmarks),
|
|
('/benchmarks/single', SingleBenchmark),
|
|
('/benchmarks/.*', GetBenchmarks),
|
|
], debug=True)
|
|
|
|
def main():
|
|
run_wsgi_app(application)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|