| # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from __future__ import with_statement |
| |
| import datetime |
| import json |
| import logging |
| import os |
| import random |
| import re |
| import string |
| import urllib |
| |
| import cloudstorage |
| |
| from google.appengine.api import app_identity, memcache, urlfetch |
| from google.appengine.ext import blobstore, db, deferred |
| # F0401: 16,0: Unable to import 'webapp2_extras' |
| # W0611: 16,0: Unused import jinja2 |
| # pylint: disable=F0401, W0611 |
| from webapp2_extras import jinja2 |
| # F0401:22,0: Unable to import 'jinja2' |
| # pylint: disable=F0401 |
| from jinja2 import Environment, FileSystemLoader |
| |
| from third_party.BeautifulSoup.BeautifulSoup import BeautifulSoup, Tag |
| |
| |
| # pylint: disable=no-value-for-parameter |
| |
| |
| # Deadline for fetching URLs (in seconds). |
| URLFETCH_DEADLINE = 60*5 # 5 mins |
| |
| # Default masters to merge together. |
| DEFAULT_MASTERS_TO_MERGE = [ |
| 'chromium.main', |
| 'chromium.win', |
| 'chromium.mac', |
| 'chromium.linux', |
| 'chromium.chromiumos', |
| 'chromium.chrome', |
| 'chromium.memory', |
| 'chromium.webkit', |
| ] |
| |
| |
| # Perform initial bootstrap for this module. |
| console_template = '' |
| def bootstrap(): |
| global console_template |
| merger_file = os.path.join(os.path.dirname(__file__), 'templates/merger.html') |
| with open(merger_file, 'r') as fh: |
| console_template = fh.read() |
| |
| |
| ########## |
| # Page class definition and related functions. |
| ########## |
| class Page(db.Model): |
| fetch_timestamp = db.DateTimeProperty(required=True) |
| localpath = db.StringProperty(required=True) |
| content = db.TextProperty() |
| title = db.StringProperty() |
| offsite_base = db.StringProperty() |
| body_class = db.StringProperty() |
| remoteurl = db.TextProperty() |
| # Data updated separately, after creation. |
| content_blob = blobstore.BlobReferenceProperty() # DEPRECATED |
| content_gcs_key = db.IntegerProperty() |
| |
| |
| def get_or_create_page(localpath, remoteurl, maxage): |
| return Page.get_or_insert( |
| key_name=localpath, |
| localpath=localpath, |
| remoteurl=remoteurl, |
| maxage=maxage, |
| # The real timestamp and content will be filled when the page is saved. |
| fetch_timestamp=datetime.datetime.min, |
| content=None, |
| content_blob=None, # DEPRECATED |
| content_gcs_key=None) |
| |
| |
| def get_and_cache_pagedata(localpath): |
| """Returns a page_data dict, optionally caching and looking up a blob. |
| |
| get_and_cache_pagedata takes a localpath which is used to fetch data |
| from the cache. If the data is present and there's no content blob, |
| then we have all of the data we need to return a page view to the user |
| and we return early. |
| |
| Otherwise, we need to fetch the page object and set up the page data |
| for the page view. If the page has a blob associated with it, then we |
| mark the page data as having a blob and cache it as-is without the blob. |
| If there's no blob, we associate the content with the page data and |
| cache that. This is so the next time get_and_cache_pagedata is called |
| for either case, we'll get the same behavior (a page-lookup for blobful |
| content and a page cache hit for blobless content). |
| |
| Here we assume localpath is already unquoted. |
| """ |
| page_data = get_data_from_cache(localpath) |
| if page_data and not page_data.get('content_blob'): |
| logging.debug('content for %s found in cache' % localpath) |
| return page_data |
| page = Page.all().filter('localpath =', localpath).get() |
| if not page: |
| logging.debug('get_and_cache_pagedata(\'%s\'): no matching localpath in ' |
| 'datastore' % localpath) |
| return {'content': None} |
| page_data = { |
| 'body_class': page.body_class, |
| 'offsite_base': page.offsite_base, |
| 'title': page.title, |
| 'fetch_timestamp': page.fetch_timestamp, |
| } |
| if page.content_gcs_key: |
| # Get the blob. |
| logging.debug('content for %s found in gcs' % localpath) |
| page_data['content_blob'] = True |
| page_data['content'] = read_blob(page.content_gcs_key) |
| else: |
| logging.debug('content for %s found in datastore' % localpath) |
| page_data['content'] = page.content |
| put_data_into_cache(localpath, page_data) |
| return page_data |
| |
| |
| def save_page(page, localpath, fetch_timestamp, page_data): |
| body_class = page_data.get('body_class', '') |
| content = page_data.get('content') |
| offsite_base = page_data.get('offsite_base', '') |
| title = page_data.get('title', '') |
| |
| content_gcs_key = None |
| try: |
| content = content.decode('utf-8', 'replace') |
| except UnicodeEncodeError: |
| logging.debug('save_page: content was already in unicode') |
| logging.debug('save_page: content size is %d' % len(content)) |
| # Save to blobstore if content + metadata is too big. |
| json_data = json.dumps(content.encode('utf-8'), default=dtdumper) |
| if len(json_data) >= 10**6 - 10**5: |
| logging.debug('save_page: saving to blob') |
| content_gcs_key = write_blob(localpath, content) |
| content = None |
| def tx_page(page_key): |
| page = Page.get(page_key) |
| # E1103:225,7:fetch_page.tx_page: Instance of 'list' has no |
| # 'fetch_timestamp' member (but some types could not be inferred) |
| # pylint: disable=E1103 |
| if page.fetch_timestamp > fetch_timestamp: |
| return |
| page.content = content |
| page.content_gcs_key = content_gcs_key |
| page.fetch_timestamp = fetch_timestamp |
| # title, offsite_base, body_class can all be empty strings for some |
| # content. Where that's true, they're not used for displaying a console- |
| # like resource, and the content alone is returned to the web user. |
| page.title = title |
| page.offsite_base = offsite_base |
| page.body_class = body_class |
| # E1103:231,4:fetch_page.tx_page: Instance of 'list' has no 'put' member |
| # (but some types could not be inferred) |
| # pylint: disable=E1103 |
| page.put() |
| db.run_in_transaction(tx_page, page.key()) |
| page_data = { |
| 'body_class': body_class, |
| 'content': content, |
| 'offsite_base': offsite_base, |
| 'title': title, |
| 'fetch_timestamp': fetch_timestamp, |
| } |
| if content_gcs_key: |
| page_data['content_blob'] = True |
| put_data_into_cache(localpath, page_data) |
| logging.info('Saved and cached page with localpath %s' % localpath) |
| |
| |
| ########## |
| # Row class definition and related functions. |
| ########## |
| class Row(db.Model): |
| fetch_timestamp = db.DateTimeProperty(required=True) |
| rev_number = db.StringProperty(required=True) |
| localpath = db.StringProperty(required=True) |
| revision = db.TextProperty() |
| name = db.TextProperty() |
| status = db.TextProperty() |
| comment = db.TextProperty() |
| details = db.TextProperty() |
| |
| |
| def get_or_create_row(localpath, revision): |
| return Row.get_or_insert( |
| key_name=localpath, |
| rev_number=revision, |
| localpath=localpath, |
| fetch_timestamp=datetime.datetime.min) |
| |
| |
| def get_and_cache_rowdata(localpath): |
| """Returns a row_data dict. |
| |
| get_and_cache_rowdata takes a localpath which is used to fetch data from the |
| cache. If the data is present, then we have all of the data we need and we |
| return early. |
| |
| Otherwise, we need to fetch the row object and set up the row data. |
| |
| Here we assume localpath is already unquoted. |
| """ |
| row_data = get_data_from_cache(localpath) |
| if row_data and isinstance(row_data, dict): |
| return row_data |
| row = Row.get_by_key_name(localpath) |
| if not row: |
| logging.debug('get_and_cache_rowdata(\'%s\'): no matching localpath in ' |
| 'datastore' % localpath) |
| return {} |
| row_data = {} |
| row_data['rev'] = row.revision |
| row_data['name'] = row.name |
| row_data['status'] = row.status |
| row_data['comment'] = row.comment |
| row_data['details'] = row.details |
| row_data['rev_number'] = row.rev_number |
| row_data['fetch_timestamp'] = row.fetch_timestamp |
| logging.debug('content for %s found in datastore' % localpath) |
| put_data_into_cache(localpath, row_data) |
| return row_data |
| |
| |
| def save_row(row_data, localpath): |
| rev_number = row_data['rev_number'] |
| row = get_or_create_row(localpath, rev_number) |
| row_key = row.key() |
| def tx_row(row_key): |
| row = Row.get(row_key) |
| # E1103:959,7:save_row.tx_row: Instance of 'list' has no |
| # 'fetch_timestamp' member (but some types could not be inferred) |
| # pylint: disable=E1103 |
| # if row.fetch_timestamp > timestamp: |
| # return |
| row.fetch_timestamp = row_data['fetch_timestamp'] |
| row.revision = row_data['rev'] |
| row.name = row_data['name'] |
| row.status = row_data['status'] |
| row.comment = row_data['comment'] |
| row.details = row_data['details'] |
| # E1103:967,4:save_row.tx_row: Instance of 'list' has no 'put' member |
| # (but some types could not be inferred) |
| # pylint: disable=E1103 |
| row.put() |
| db.run_in_transaction(tx_row, row_key) |
| put_data_into_cache(localpath, row_data) |
| logging.info('Saved and cached row with localpath %s' % localpath) |
| # Update latest_rev in datastore & cache, or create it if it doesn't exist. |
| prev_rev = get_and_cache_rowdata('latest_rev') |
| if not prev_rev or rev_number > prev_rev['rev_number']: |
| latest_rev_row = { |
| 'rev_number': rev_number, |
| 'fetch_timestamp': datetime.datetime.now(), |
| 'rev': None, |
| 'name': None, |
| 'status': None, |
| 'comment': None, |
| 'details': None, |
| } |
| prev_rev_db = get_or_create_row('latest_rev', rev_number) |
| prev_rev_db.fetch_timestamp = datetime.datetime.now() |
| prev_rev_db.rev_number = rev_number |
| prev_rev_db.put() |
| put_data_into_cache('latest_rev', latest_rev_row) |
| |
| |
| def utf8_convert(beautiful_soup_tag): |
| # cmp also investigated: |
| # beautiful_soup_tag.__str__(encoding='utf-8').decode('utf-8') |
| # He found that the BeautifulSoup() __str__ method when used with a 'utf-8' |
| # encoding returned effectively the same thing as str(), a Python built-in. |
| # After a handful of tests, he switched to using str() to avoid the add'l |
| # complexity of another BeautifulSoup method. |
| return str(beautiful_soup_tag).decode('utf-8') |
| |
| |
| ########## |
| # ConsoleData class definition and related functions. |
| ########## |
| class ConsoleData(object): |
| def __init__(self): |
| self.row_orderedkeys = [] |
| self.row_data = {} |
| |
| # Retain order of observed masters. |
| self.masters = [] |
| |
| # Map(k,v): k=Master, v=List of categories |
| self.category_order = {} |
| # Map(k,v): k=Master, v=Dict of category data |
| self.category_data = {} |
| |
| self.category_count = 0 |
| self.master = '' |
| self.lastRevisionSeen = None |
| self.lastMasterSeen = None |
| |
| @staticmethod |
| def ContentsToHtml(contents): |
| return ''.join(utf8_convert(content) for content in contents) |
| |
| @property |
| def last_row(self): |
| return self.row_data[self.lastRevisionSeen] |
| |
| def SawMaster(self, master): |
| self.lastMasterSeen = master |
| assert(self.lastMasterSeen not in self.category_order) |
| self.masters.append(self.lastMasterSeen) |
| self.category_order.setdefault(self.lastMasterSeen, []) |
| self.category_data.setdefault(self.lastMasterSeen, {}) |
| |
| def SawRevision(self, revision, rev_number): |
| self.lastRevisionSeen = rev_number |
| # TODO(cmp): Fix the order of the revision data in self.row_orderedkeys |
| if self.lastRevisionSeen not in self.row_orderedkeys: |
| logging.debug('SawRevision: guessing at row ordering') |
| self.row_orderedkeys.append(self.lastRevisionSeen) |
| self.row_data.setdefault(self.lastRevisionSeen, {}) |
| self.last_row['revision'] = revision |
| self.last_row.setdefault('status', {}) |
| self.last_row['status'].setdefault(self.lastMasterSeen, {}) |
| |
| def SetLink(self, revlink): |
| self.last_row['revlink'] = revlink |
| |
| def SetName(self, who): |
| self.last_row['who'] = who |
| |
| def SetStatus(self, category, status): |
| self.last_row['status'][self.lastMasterSeen][category] = status |
| |
| def SetComment(self, comment): |
| self.last_row['comment'] = comment |
| |
| def SetDetail(self, detail): |
| self.last_row['detail'] = detail |
| |
| def AddCategory(self, category, builder_status): |
| self.category_order[self.lastMasterSeen].append(category) |
| # Map(k,v): k=Master/category, v=Dict of category data (last build status) |
| self.category_data[self.lastMasterSeen].setdefault(category, {}) |
| self.category_data[self.lastMasterSeen][category] = builder_status |
| self.category_count += 1 |
| |
| def AddRow(self, row): |
| self.SawRevision(row['rev'], row['rev_number']) |
| revlink = BeautifulSoup(row['rev']).a['href'] |
| self.SetLink(revlink) |
| name = BeautifulSoup(row['name']) |
| self.SetName(self.ContentsToHtml(name)) |
| status = BeautifulSoup(row['status']).findAll('table') |
| for i, stat in enumerate(status): |
| self.SetStatus(self.category_order[self.lastMasterSeen][i], |
| unicode(stat)) |
| comment = BeautifulSoup(row['comment']) |
| self.SetComment(self.ContentsToHtml(comment)) |
| if row['details']: |
| details = BeautifulSoup(row['details']) |
| self.SetDetail(self.ContentsToHtml(details)) |
| |
| def ParseRow(self, row): |
| cells = row.findAll('td', recursive=False) |
| # Figure out which row this is. |
| for attrname, attrvalue in cells[0].attrs: |
| if attrname != 'class': |
| continue |
| attrvalue = re.sub(r'^(\S+).*', r'\1', attrvalue) |
| if attrvalue == 'DevRev': |
| revision = cells[0] |
| self.SawRevision(self.ContentsToHtml( |
| revision.findAll('a')[0].contents[0])) |
| self.SetLink(self.ContentsToHtml(revision.findAll('a')[0].attrs[0][1])) |
| nameparts = cells[1].contents |
| self.SetName(re.sub(r'^\s+(.*)\s*$', |
| r'\1', |
| self.ContentsToHtml(nameparts))) |
| for i, bs in enumerate(cells[2:]): |
| self.SetStatus(self.category_order[self.lastMasterSeen][i], |
| self.ContentsToHtml(bs.findAll('table', |
| recursive=False)[0])) |
| if attrvalue == 'DevComment': |
| self.SetComment(comment=self.ContentsToHtml(cells[0].contents)) |
| if attrvalue == 'DevDetails': |
| self.SetDetail(detail=self.ContentsToHtml(cells[0].contents)) |
| |
| def Finish(self): |
| self.row_orderedkeys = sorted(self.row_orderedkeys, key=int, reverse=True) |
| # TODO(cmp): Look for row/master/categories that are unset. If they are |
| # at the latest revisions, leave them unset. If they are at |
| # the earliest revisions, set them to ''. |
| |
| |
| ########## |
| # Heavy-lifting functions that do most of the console processing. |
| # AKA postfetch and postsave functions/handlers. |
| ########## |
| def console_merger(localpath, remoteurl, page_data, |
| masters_to_merge=None, num_rows_to_merge=None): |
| masters_to_merge = masters_to_merge or DEFAULT_MASTERS_TO_MERGE |
| num_rows_to_merge = num_rows_to_merge or 25 |
| console_data = ConsoleData() |
| surroundings = get_and_cache_pagedata( |
| '%s/console/surroundings' % masters_to_merge[0]) |
| merged_page = BeautifulSoup(surroundings['content']) |
| merged_tag = merged_page.find('table', 'ConsoleData') |
| if merged_tag is None: |
| msg = 'console_merger("%s", "%s", "%s"): merged_tag cannot be None.' % ( |
| localpath, remoteurl, page_data) |
| logging.error(msg) |
| raise Exception(msg) |
| latest_rev = int(get_and_cache_rowdata('latest_rev')['rev_number']) |
| if not latest_rev: |
| logging.error('console_merger(\'%s\', \'%s\', \'%s\'): cannot get latest ' |
| 'revision number.' % ( |
| localpath, remoteurl, page_data)) |
| return |
| fetch_timestamp = datetime.datetime.now() |
| for master in masters_to_merge: |
| # Fetch the summary one-box-per-builder for the master. |
| # If we don't get it, something is wrong, skip the master entirely. |
| master_summary = get_and_cache_pagedata('%s/console/summary' % master) |
| if not master_summary['content']: |
| continue |
| console_data.SawMaster(master) |
| # Get the categories for this builder. If the builder doesn't have any |
| # categories, just use the default empty-string category. |
| category_list = [] |
| master_categories = get_and_cache_pagedata('%s/console/categories' % master) |
| if not master_categories['content']: |
| category_list.append('') |
| else: |
| category_row = BeautifulSoup(master_categories['content']) |
| category_list = [c.text for c in category_row.findAll('td', 'DevStatus')] |
| # Get the corresponding summary box(es). |
| summary_row = BeautifulSoup(master_summary['content']) |
| summary_list = summary_row.findAll('table') |
| for category, summary in zip(category_list, summary_list): |
| console_data.AddCategory(category, summary) |
| |
| # Fetch all of the rows that we need. |
| rows_fetched = 0 |
| revs_skipped = 0 |
| current_rev = latest_rev |
| while rows_fetched < num_rows_to_merge and current_rev >= 0: |
| # Don't get stuck looping backwards forever into data we don't have. |
| # How hard we try scales with how many rows the person wants. |
| if revs_skipped > max(num_rows_to_merge, 10): |
| break |
| row_data = get_and_cache_rowdata('%s/console/%s' % (master, current_rev)) |
| if not row_data: |
| current_rev -= 1 |
| revs_skipped += 1 |
| continue |
| console_data.AddRow(row_data) |
| current_rev -= 1 |
| revs_skipped = 0 |
| rows_fetched += 1 |
| |
| # Convert the merged content into console content. |
| console_data.Finish() |
| template_environment = Environment() |
| template_environment.loader = FileSystemLoader('.') |
| def notstarted(builder_status): |
| """Convert a BeautifulSoup Tag from builder status to a notstarted line.""" |
| builder_status = re.sub(r'DevSlaveBox', 'DevStatusBox', str(builder_status)) |
| builder_status = re.sub(r'class=\'([^\']*)\' target=', |
| 'class=\'DevStatusBox notstarted\' target=', |
| builder_status) |
| builder_status = re.sub(r'class="([^"]*)" target=', |
| 'class="DevStatusBox notstarted" target=', |
| builder_status) |
| return builder_status |
| template_environment.filters['notstarted'] = notstarted |
| merged_template = template_environment.from_string(console_template) |
| merged_console = merged_template.render(data=console_data) |
| # For debugging: |
| # logging.info('%r' % merged_console) |
| # import code |
| # code.interact(local=locals()) |
| |
| # Place merged console at |merged_tag|'s location in |merged_page|, and put |
| # the result in |merged_content|. |
| merged_tag.replaceWith(merged_console) |
| merged_content = utf8_convert(merged_page) |
| merged_content = re.sub( |
| r'\'\<a href="\'', '\'<a \' + attributes + \' href="\'', merged_content) |
| merged_content = re.sub( |
| r'\'\<table\>\'', r"'<table ' + attributes + '>'", merged_content) |
| merged_content = re.sub( |
| r'\'\<div\>\'', r"'<div ' + attributes + '>'", merged_content) |
| merged_content = re.sub( |
| r'\'\<td\>\'', r"'<td ' + attributes + '>'", merged_content) |
| merged_content = re.sub( |
| r'\<iframe\>\</iframe\>', |
| '<iframe \' + attributes + \' src="\' + url + \'"></iframe>', |
| merged_content) |
| |
| # Update the merged console page. |
| merged_page = get_or_create_page(localpath, None, maxage=30) |
| logging.info('console_merger: saving merged console') |
| page_data = get_and_cache_pagedata(localpath) |
| page_data['title'] = 'BuildBot: Chromium' |
| page_data['offsite_base'] = 'http://build.chromium.org/p/chromium' |
| page_data['body_class'] = 'interface' |
| page_data['content'] = merged_content |
| save_page(merged_page, localpath, fetch_timestamp, page_data) |
| return |
| |
| |
| def console_handler(unquoted_localpath, remoteurl, page_data=None): |
| page_data = page_data or {} |
| content = page_data.get('content') |
| if not content: |
| logging.error('console_handler(\'%s\', \'%s\', \'%s\'): cannot get site ' |
| 'from local path' % ( |
| unquoted_localpath, remoteurl, page_data)) |
| return page_data |
| |
| # Decode content from utf-8 to unicode, replacing bad characters. |
| content = content.decode('utf-8', 'replace') |
| |
| # Scrub in sheriff file content to console. |
| sheriff_files = [ |
| 'sheriff', |
| 'sheriff_android', |
| 'sheriff_angle', |
| 'sheriff_cr_cros_gardeners', |
| 'sheriff_cros_mtv', |
| 'sheriff_cros_nonmtv', |
| 'sheriff_gpu', |
| 'sheriff_ios', |
| 'sheriff_memory', |
| 'sheriff_nacl', |
| 'sheriff_perf', |
| 'sheriff_perfbot', |
| 'sheriff_v8', |
| 'sheriff_webkit', |
| ] |
| for sheriff_file in sheriff_files: |
| sheriff_page_data = get_and_cache_pagedata('chromium/%s.js' % sheriff_file) |
| sheriff_content = sheriff_page_data['content'] |
| console_re = (r'<script src=\'http://chromium-build.appspot.com/' |
| 'p/chromium/%s.js\'></script>') |
| content = re.sub(console_re % sheriff_file, |
| '<script>%s</script>' % sheriff_content, content) |
| |
| # Replace showBuildBox with direct links. |
| content = re.sub(r'<a href=\'#\' onclick=\'showBuildBox\(\"./(.+)\", event\);' |
| ' return false;\'', |
| r"<a href='\1'", content) |
| |
| # Create a string representing the parent of remoteurl. If remoteurl looks |
| # like 'http://somehost/somepath/foo', then remoteurl_parent would be |
| # 'http://somehost/somepath/'. |
| remoteurl_parent = re.sub(r'^(.*?)[^/]*$', r'\1', remoteurl) |
| |
| # JavaScript can bring about text that looks like <a href="' + ..., so our |
| # regex needs to avoid introducing a new base URL in those cases. Hence we |
| # exclude single and double quotes in both cases. |
| content = re.sub(r'<a href="([^:\"\'\$]+)"', |
| r'<a href="%s\1"' % remoteurl_parent, |
| content) |
| content = re.sub(r'<a href=\'([^:\'\"\$]+)\'', |
| r"<a href='%s\1'" % remoteurl_parent, |
| content) |
| |
| # Convert any occurrences of ['"]./ and ['"]../ to prepend b.c.o. |
| content = re.sub(r'"\./', r'"%s' % remoteurl_parent, content) |
| content = re.sub(r"'\./", r"'%s" % remoteurl_parent, content) |
| content = re.sub(r'"\.\./', r'"%s../' % remoteurl_parent, content) |
| content = re.sub(r"'\.\./", r"'%s../" % remoteurl_parent, content) |
| |
| # Convert the webkit waterfall reference to reuse the local instance. |
| content = re.sub(r"c.webkit = '([^\']+)'", r"c.webkit = ''", content) |
| content = re.sub("'http://build\.chromium.org/p/chromium\.webkit/" |
| "horizontal_one_box_per_builder'", |
| "'https://chromium-build.appspot.com/p/" |
| "chromium.webkit/horizontal_one_box_per_builder" |
| "?chromiumconsole'", content) |
| |
| # Convert the chromium-status reference to reuse the local instance. |
| content = re.sub(r"http://chromium-status\.appspot\.com/current", |
| "https://chromium-build.appspot.com/p/" |
| "chromium-status/current", content) |
| |
| # TODO(hinoka): Enable these when the app is done. |
| # Convert direct build.chromium.org links to local links. |
| # content = re.sub("http://build.chromium.org/p/", "/buildbot/", content) |
| # content = re.sub(r"/buildbot/(.*)/buildstatus\?builder=(.*)&number=(\d+)", |
| # r"/buildbot/\1/builders/\2/builds/\3", content) |
| |
| # Disable the personalized for box for now. |
| content = re.sub(r"<input id='namebox'[^>]+>", '', content) |
| content = re.sub(r"<input.*onclick='reload_page\(\)'/>", '', content) |
| |
| # Replace lkgrPath with a URL to chromium-build. |
| content = re.sub( |
| "var lkgrPath = c.status_lkgr", |
| "var lkgrPath = '/p/chromium.lkgr'", |
| content) |
| content = string.replace(content, |
| "'/json/builders/Linux%20x64/builds/-1?as_text=1';", |
| "'/json/builders/Linux%20x64/builds/-1/as_text=1.json';") |
| |
| # Fix up a reference to http chromium-build in BarUrl(). |
| content = string.replace(content, |
| "return 'http://chromium-build.appspot.com/p/'", |
| "return 'https://chromium-build.appspot.com/p/'") |
| |
| # Encode content from unicode to utf-8. |
| page_data['content'] = content.encode('utf-8') |
| |
| # Last tweaks to HTML, plus extracting metadata about the page itself. |
| page_data['offsite_base'] = remoteurl + '/../' |
| |
| # Extract the title from the page. |
| md = re.search( |
| r'^.*<title>([^\<]+)</title>', |
| page_data['content'], |
| re.MULTILINE|re.DOTALL) |
| if not md: |
| raise Exception('failed to locate title in page') |
| page_data['title'] = md.group(1) |
| |
| # Remove the leading text up to the end of the opening body tag. While |
| # there, extract the body_class from the page. |
| md = re.search( |
| r'^.*<body class="(\w+)\">(.*)$', |
| page_data['content'], |
| re.MULTILINE|re.DOTALL) |
| if not md: |
| raise Exception('failed to locate leading text up to body tag') |
| page_data['body_class'] = md.group(1) |
| page_data['content'] = md.group(2) |
| |
| # Remove the leading div and hr tags. |
| md = re.search( |
| r'^.*?<hr/>(.*)$', |
| page_data['content'], |
| re.MULTILINE|re.DOTALL) |
| if not md: |
| raise Exception('failed to locate leading div and hr tags') |
| page_data['content'] = md.group(1) |
| |
| # Strip the trailing body and html tags. |
| md = re.search( |
| r'^(.*)</body>.*$', |
| page_data['content'], |
| re.MULTILINE|re.DOTALL) |
| if not md: |
| raise Exception('failed to locate trailing body and html tags') |
| page_data['content'] = md.group(1) |
| |
| return page_data |
| |
| |
| def get_position_number(commit_msg): |
| # Cr-Commit-Position should exist at least once in the commit message, but can |
| # exist more than once. Reverts are examples of commits which can contain |
| # multiple Cr-Commit-Position instances. In those cases, only the last one |
| # is correct, so split on the break tag and reverse the result to find the |
| # last occurrence of Cr-Commit-Position. |
| for line in reversed(commit_msg.split('<br />')): |
| if line.startswith('Cr-Commit-Position: '): |
| return filter(str.isdigit, str(line.split('@')[-1])) |
| return '0' |
| |
| |
| # W0613:600,28:parse_master: Unused argument 'remoteurl' |
| # pylint: disable=W0613 |
| def parse_master(localpath, remoteurl, page_data=None): |
| """Part of the new pipeline to store individual rows rather than |
| whole pages of html. Parses the master data into a set of rows, |
| and writes them out to the datastore in an easily retrievable format. |
| |
| Doesn't modify page_data dict. |
| """ |
| ts = datetime.datetime.now() |
| page_data = page_data or {} |
| content = page_data.get('content') |
| if not content: |
| return page_data |
| content = content.decode('utf-8', 'replace') |
| |
| # Split page into surroundings (announce, legend, footer) and data (rows). |
| surroundings = BeautifulSoup(content) |
| data = surroundings.find('table', 'ConsoleData') |
| if data is None: |
| raise Exception('parse_master: data can not be None') |
| new_data = Tag(surroundings, 'table', [('class', 'ConsoleData'), |
| ('width', '96%')]) |
| data.replaceWith(new_data) |
| |
| surroundings_page = get_or_create_page(localpath + '/surroundings', |
| None, maxage=30) |
| surroundings_data = {} |
| surroundings_data['title'] = 'Surroundings for ' + localpath |
| surroundings_data['content'] = utf8_convert(surroundings) |
| save_page(surroundings_page, localpath + '/surroundings', ts, |
| surroundings_data) |
| |
| rows = data.findAll('tr', recursive=False) |
| # The first table row can be special: the list of categories. |
| categories = None |
| # If the first row contains a DevStatus cell... |
| if rows[0].find('td', 'DevStatus') != None: |
| # ...extract it into the categories... |
| categories = rows[0] |
| # ...and get rid of the next (spacer) row too. |
| rows = rows[2:] |
| |
| if categories: |
| category_page = get_or_create_page(localpath + '/categories', |
| None, maxage=30) |
| category_data = {} |
| category_data['title'] = 'Categories for ' + localpath |
| category_data['content'] = utf8_convert(categories) |
| save_page(category_page, localpath + '/categories', ts, category_data) |
| |
| # The next table row is special, it's the summary one-box-per-builder. |
| summary = rows[0] |
| rows = rows[1:] |
| |
| summary_page = get_or_create_page(localpath + '/summary', None, maxage=30) |
| summary_data = {} |
| summary_data['title'] = 'Summary for ' + localpath |
| summary_data['content'] = utf8_convert(summary) |
| save_page(summary_page, localpath + '/summary', ts, summary_data) |
| |
| curr_row = {} |
| # Each table row is either a status row with a revision, name, and status, |
| # a comment row with the commit message, a details row with flakiness info, |
| # or a spacer row (in which case we finalize the row and save it). |
| for row in rows: |
| if row.find('td', 'DevComment'): |
| curr_row['comment'] = ''.join(utf8_convert(tag).strip() |
| for tag in row.td.contents) |
| elif row.find('td', 'DevDetails'): |
| curr_row['details'] = ''.join(utf8_convert(tag).strip() |
| for tag in row.td.contents) |
| elif row.find('td', 'DevStatus'): |
| curr_row['rev'] = ''.join(utf8_convert(tag).strip() |
| for tag in row.find('td', 'DevRev').contents) |
| curr_row['name'] = ''.join(utf8_convert(tag).strip() |
| for tag in row.find('td', 'DevName').contents) |
| curr_row['status'] = ''.join(utf8_convert(box.table).strip() |
| for box in row.findAll('td', 'DevStatus')) |
| else: |
| if 'details' not in curr_row: |
| curr_row['details'] = '' |
| curr_row['fetch_timestamp'] = ts |
| curr_row['rev_number'] = get_position_number(curr_row['comment']) |
| save_row(curr_row, localpath + '/' + curr_row['rev_number']) |
| curr_row = {} |
| |
| return page_data |
| |
| |
| def one_box_handler(unquoted_localpath, remoteurl, page_data=None): |
| page_data = page_data or {} |
| content = page_data.get('content') |
| if content is None: |
| return page_data |
| # Get the site name from the local path. |
| md = re.match('^([^\/]+)/.*$', unquoted_localpath) |
| if not md: |
| logging.error('one_box_handler(\'%s\', \'%s\', \'%s\'): cannot get site ' |
| 'from local path' % ( |
| unquoted_localpath, remoteurl, page_data)) |
| return page_data |
| site = md.group(1) |
| new_waterfall_url = 'http://build.chromium.org/p/%s/waterfall' % site |
| page_data['content'] = re.sub( |
| r'waterfall', |
| new_waterfall_url, |
| page_data['content']) |
| return page_data |
| |
| |
| ########## |
| # Utility functions for blobstore and memcache. |
| ########## |
| def get_data_from_cache(localpath): |
| memcache_data = memcache.get(localpath) |
| if not memcache_data: |
| return None |
| logging.debug('content for %s found in memcache' % localpath) |
| return json.loads(memcache_data) |
| |
| |
| def dtdumper(obj): |
| if hasattr(obj, 'isoformat'): |
| return obj.isoformat() |
| else: |
| raise TypeError(repr(obj) + "is not JSON serializable") |
| |
| |
| def put_data_into_cache(localpath, data): |
| memcache_data = json.dumps(data, default=dtdumper) |
| if not memcache.set(key=localpath, value=memcache_data, time=2*60): |
| logging.error('put_data_into_cache(\'%s\'): memcache.set() failed' % ( |
| localpath)) |
| |
| |
| def write_blob(name, data): |
| """Saves a Unicode string as a new blob, returns the blob's key.""" |
| bucket = app_identity.get_default_gcs_bucket_name() |
| key = hash(data) |
| path = '/%s/%d' % (bucket, key) |
| mime = path_to_mime_type(name) |
| blob = data.encode('utf-8') |
| with cloudstorage.open(path, 'w', content_type=mime) as f: |
| f.write(blob) |
| return key |
| |
| |
| def read_blob(key): |
| """Reads a Unicode string back from a given blob.""" |
| bucket = app_identity.get_default_gcs_bucket_name() |
| path = '/%s/%d' % (bucket, key) |
| with cloudstorage.open(path, 'r') as f: |
| blob = f.read() |
| data = blob.decode('utf-8', 'replace') |
| return data |
| |
| |
| def path_to_mime_type(path): |
| return EXT_TO_MIME.get(os.path.splitext(path)[1], 'text/html') |
| |
| |
| EXT_TO_MIME = { |
| '.css': 'text/css', |
| '.js': 'text/javascript', |
| '.json': 'application/json', |
| '.html': 'text/html', |
| } |
| |
| |
| ########## |
| # Functions for actually fetching original pages. |
| ########## |
| def fetch_pages(): |
| """Starts a background fetch operation for pages that need it.""" |
| logging.debug('fetch_pages()') |
| for url in URLS: |
| deferred.defer(fetch_page, **url) |
| |
| |
| def nonfatal_fetch_url(url, *args, **kwargs): |
| # Temporary workaround to disable AppEngine global cache of these pages. |
| if '?' in url: |
| url += '&' + str(random.random()) |
| else: |
| url += '?' + str(random.random()) |
| |
| try: |
| return urlfetch.fetch(url, deadline=URLFETCH_DEADLINE, *args, **kwargs) |
| except urlfetch.DownloadError: |
| logging.warn('urlfetch failed: %s' % url, exc_info=1) |
| return None |
| |
| |
| def fetch_page(localpath, remoteurl, maxage, postfetch=None, postsave=None, |
| fetch_url=nonfatal_fetch_url): |
| """Fetches data about a set of pages.""" |
| if not isinstance(localpath, (str, unicode)): |
| logging.error('fetch_page: localpath is %r, expected a string' % ( |
| repr(localpath))) |
| return |
| unquoted_localpath = urllib.unquote(localpath) |
| logging.debug('fetch_page("%s", "%s", "%s")' % ( |
| unquoted_localpath, remoteurl, maxage)) |
| page = get_or_create_page(unquoted_localpath, remoteurl, maxage) |
| |
| # Check if our copy of the page is younger than maxage. If it is, we'll |
| # skip the fetch. |
| oldest_acceptable_timestamp = datetime.datetime.now() - datetime.timedelta( |
| seconds=maxage) |
| if (page.fetch_timestamp and |
| page.fetch_timestamp > oldest_acceptable_timestamp): |
| logging.debug('fetch_page: too recent, skipping') |
| return |
| |
| # Perform the actual page fetch. |
| fetch_timestamp = datetime.datetime.now() |
| response = fetch_url(remoteurl) |
| if not response: |
| logging.warning('fetch_page: got empty response') |
| return |
| if response.status_code != 200: |
| logging.warning('fetch_page: got non-empty response but code ' |
| '%d' % response.status_code) |
| return |
| |
| # We have actual content. If there's one or more handlers, call them. |
| page_data = {} |
| page_data['content'] = response.content |
| if postfetch: |
| if not isinstance(postfetch, list): |
| postfetch = [postfetch] |
| for handler in postfetch: |
| logging.debug('fetch_page: calling postfetch handler ' |
| '%s' % handler.__name__) |
| page_data = handler(unquoted_localpath, remoteurl, page_data) |
| |
| # Save the returned content into the DB and caching layers. |
| logging.debug('fetch_page: saving page') |
| save_page(page, unquoted_localpath, fetch_timestamp, page_data) |
| if postsave: |
| if not isinstance(postsave, list): |
| postsave = [postsave] |
| for handler in postsave: |
| logging.debug('fetch_page: calling postsave handler ' |
| '%s' % handler.__name__) |
| handler(unquoted_localpath, remoteurl, page_data) |
| |
| |
| # List of URLs to fetch. |
| # localpath is relative to the http://build.chromium.org/p directory. |
| URLS = [ |
| # Console URLs. |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.chrome/console', |
| 'localpath': 'chromium.chrome/console', |
| 'postfetch': [console_handler, parse_master], |
| 'postsave': console_merger, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.chromiumos/console', |
| 'localpath': 'chromium.chromiumos/console', |
| 'postfetch': [console_handler, parse_master], |
| 'postsave': console_merger, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.linux/console', |
| 'localpath': 'chromium.linux/console', |
| 'postfetch': [console_handler, parse_master], |
| 'postsave': console_merger, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.mac/console', |
| 'localpath': 'chromium.mac/console', |
| 'postfetch': [console_handler, parse_master], |
| 'postsave': console_merger, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/console', |
| 'localpath': 'chromium.main/console', |
| 'postfetch': [console_handler, parse_master], |
| 'postsave': console_merger, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.memory/console', |
| 'localpath': 'chromium.memory/console', |
| 'postfetch': [console_handler, parse_master], |
| 'postsave': console_merger, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.webkit/console', |
| 'localpath': 'chromium.webkit/console', |
| 'postfetch': [console_handler, parse_master], |
| 'postsave': console_merger, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.win/console', |
| 'localpath': 'chromium.win/console', |
| 'postfetch': [console_handler, parse_master], |
| 'postsave': console_merger, |
| 'maxage': 30, # 30 secs |
| }, |
| |
| # Tree status URL. |
| { |
| 'remoteurl': 'http://chromium-status.appspot.com/current', |
| 'localpath': 'chromium-status/current', |
| 'maxage': 30, # 30 secs |
| }, |
| |
| # Static resources. |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/default.css', |
| 'localpath': 'chromium/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.chrome/default.css', |
| 'localpath': 'chromium.chrome/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.chromiumos/default.css', |
| 'localpath': 'chromium.chromiumos/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.gpu/default.css', |
| 'localpath': 'chromium.gpu/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.gpu.fyi/default.css', |
| 'localpath': 'chromium.gpu.fyi/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.linux/default.css', |
| 'localpath': 'chromium.linux/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.lkgr/default.css', |
| 'localpath': 'chromium.lkgr/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.mac/default.css', |
| 'localpath': 'chromium.mac/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.memory/default.css', |
| 'localpath': 'chromium.memory/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.webkit/default.css', |
| 'localpath': 'chromium.webkit/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.memory.full/default.css', |
| 'localpath': 'chromium.memory.full/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.perf/default.css', |
| 'localpath': 'chromium.perf/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium.win/default.css', |
| 'localpath': 'chromium.win/default.css', |
| 'maxage': 15*60, # 15 mins |
| }, |
| |
| # Sheriff URLs. |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/all_rotations.js', |
| 'localpath': 'chromium/all_rotations.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/trooper.js', |
| 'localpath': 'chromium/trooper.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/current_trooper.txt', |
| 'localpath': 'chromium/current_trooper.txt', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff.js', |
| 'localpath': 'chromium/sheriff.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff_android.js', |
| 'localpath': 'chromium/sheriff_android.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': ( |
| 'http://build.chromium.org/p/chromium/sheriff_android_gardeners.js'), |
| 'localpath': 'chromium/sheriff_android_gardeners.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': |
| 'http://build.chromium.org/p/chromium/sheriff_cr_cros_gardeners.js', |
| 'localpath': 'chromium/sheriff_cr_cros_gardeners.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff_angle.js', |
| 'localpath': 'chromium/sheriff_angle.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff_cros_mtv.js', |
| 'localpath': 'chromium/sheriff_cros_mtv.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff_cros_nonmtv.js', |
| 'localpath': 'chromium/sheriff_cros_nonmtv.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff_gpu.js', |
| 'localpath': 'chromium/sheriff_gpu.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff_ios.js', |
| 'localpath': 'chromium/sheriff_ios_us.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff_memory.js', |
| 'localpath': 'chromium/sheriff_memory.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff_nacl.js', |
| 'localpath': 'chromium/sheriff_nacl.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff_perf.js', |
| 'localpath': 'chromium/sheriff_perf.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff_perfbot.js', |
| 'localpath': 'chromium/sheriff_perfbot.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff_v8.js', |
| 'localpath': 'chromium/sheriff_v8.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| { |
| 'remoteurl': 'http://build.chromium.org/p/chromium/sheriff_webkit.js', |
| 'localpath': 'chromium/sheriff_webkit.js', |
| 'maxage': 15*60, # 15 mins |
| }, |
| |
| # Buildbot "One Boxes". |
| { |
| 'remoteurl': |
| 'http://build.chromium.org/p/chromium/horizontal_one_box_per_builder', |
| 'localpath': 'chromium/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.chrome/' |
| 'horizontal_one_box_per_builder'), |
| 'localpath': 'chromium.chrome/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.chromiumos/' |
| 'horizontal_one_box_per_builder'), |
| 'localpath': 'chromium.chromiumos/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.gpu/' |
| 'horizontal_one_box_per_builder'), |
| 'localpath': 'chromium.gpu/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.gpu.fyi/' |
| 'horizontal_one_box_per_builder'), |
| 'localpath': 'chromium.gpu.fyi/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.linux/' |
| 'horizontal_one_box_per_builder'), |
| 'localpath': 'chromium.linux/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.lkgr/' |
| 'horizontal_one_box_per_builder'), |
| 'localpath': 'chromium.lkgr/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.mac/' |
| 'horizontal_one_box_per_builder'), |
| 'localpath': 'chromium.mac/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.memory/' |
| 'horizontal_one_box_per_builder'), |
| 'localpath': 'chromium.memory/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.webkit/' |
| 'horizontal_one_box_per_builder'), |
| 'localpath': 'chromium.webkit/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.memory.full/' |
| 'horizontal_one_box_per_builder'), |
| 'localpath': 'chromium.memory.full/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.perf/' |
| 'horizontal_one_box_per_builder'), |
| 'localpath': 'chromium.perf/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.win/' |
| 'horizontal_one_box_per_builder'), |
| 'localpath': 'chromium.win/horizontal_one_box_per_builder', |
| 'postfetch': one_box_handler, |
| 'maxage': 30, # 30 secs |
| }, |
| |
| # LKGR JSON. |
| { |
| 'remoteurl': |
| ('http://build.chromium.org/p/chromium.lkgr/json/builders/Linux%20x64/' |
| 'builds/-1?as_text=1'), |
| 'localpath': |
| 'chromium.lkgr/json/builders/Linux%20x64/builds/-1/as_text=1.json', |
| 'maxage': 2*60, # 2 mins |
| }, |
| |
| # # Trigger background process update. |
| # { |
| # 'remoteurl': 'http://chromium-build.appspot.com/backend/update' |
| ] |