blob: 49fd11f1459094c50577a76840a5c9624ac19641 [file] [log] [blame]
# 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.
import json
import logging
import operator
from appengine_url_fetcher import AppEngineUrlFetcher
import url_constants
class ChannelInfo(object):
'''Represents a Chrome channel with three pieces of information. |channel| is
one of 'stable', 'beta', 'dev', or 'trunk'. |branch| and |version| correspond
with each other, and represent different releases of Chrome. Note that
|branch| and |version| can occasionally be the same for separate channels
(i.e. 'beta' and 'dev'), so all three fields are required to uniquely
identify a channel.
'''
def __init__(self, channel, branch, version):
assert isinstance(channel, basestring), channel
assert isinstance(branch, basestring), branch
# TODO(kalman): Assert that this is a string. One day Chromium will probably
# be served out of a git repository and the versions will no longer be ints.
assert isinstance(version, int) or version == 'trunk', version
self.channel = channel
self.branch = branch
self.version = version
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return '%s%s' % (type(self).__name__, repr(self.__dict__))
def __str__(self):
return repr(self)
class BranchUtility(object):
'''Provides methods for working with Chrome channel, branch, and version
data served from OmahaProxy.
'''
def __init__(self, fetch_url, history_url, fetcher, object_store_creator):
self._fetcher = fetcher
def create_object_store(category):
return object_store_creator.Create(BranchUtility, category=category)
self._branch_object_store = create_object_store('branch')
self._version_object_store = create_object_store('version')
self._fetch_result = self._fetcher.FetchAsync(fetch_url)
self._history_result = self._fetcher.FetchAsync(history_url)
@staticmethod
def Create(object_store_creator):
return BranchUtility(url_constants.OMAHA_PROXY_URL,
url_constants.OMAHA_DEV_HISTORY,
AppEngineUrlFetcher(),
object_store_creator)
@staticmethod
def GetAllChannelNames():
return ('stable', 'beta', 'dev', 'trunk')
@staticmethod
def NewestChannel(channels):
channels = set(channels)
for channel in reversed(BranchUtility.GetAllChannelNames()):
if channel in channels:
return channel
def Newer(self, channel_info):
'''Given a ChannelInfo object, returns a new ChannelInfo object
representing the next most recent Chrome version/branch combination.
'''
if channel_info.channel == 'trunk':
return None
if channel_info.channel == 'stable':
stable_info = self.GetChannelInfo('stable')
if channel_info.version < stable_info.version:
return self.GetStableChannelInfo(channel_info.version + 1)
names = self.GetAllChannelNames()
return self.GetAllChannelInfo()[names.index(channel_info.channel) + 1]
def Older(self, channel_info):
'''Given a ChannelInfo object, returns a new ChannelInfo object
representing the previous Chrome version/branch combination.
'''
if channel_info.channel == 'stable':
if channel_info.version <= 5:
# BranchUtility can't access branch data from before Chrome version 5.
return None
return self.GetStableChannelInfo(channel_info.version - 1)
names = self.GetAllChannelNames()
return self.GetAllChannelInfo()[names.index(channel_info.channel) - 1]
@staticmethod
def SplitChannelNameFromPath(path):
'''Splits the channel name out of |path|, returning the tuple
(channel_name, real_path). If the channel cannot be determined then returns
(None, path).
'''
if '/' in path:
first, second = path.split('/', 1)
else:
first, second = (path, '')
if first in BranchUtility.GetAllChannelNames():
return (first, second)
return (None, path)
def GetAllBranches(self):
return tuple((channel, self.GetChannelInfo(channel).branch)
for channel in BranchUtility.GetAllChannelNames())
def GetAllVersions(self):
return tuple(self.GetChannelInfo(channel).version
for channel in BranchUtility.GetAllChannelNames())
def GetAllChannelInfo(self):
return tuple(self.GetChannelInfo(channel)
for channel in BranchUtility.GetAllChannelNames())
def GetChannelInfo(self, channel):
version = self._ExtractFromVersionJson(channel, 'version')
if version != 'trunk':
version = int(version)
return ChannelInfo(channel,
self._ExtractFromVersionJson(channel, 'branch'),
version)
def GetStableChannelInfo(self, version):
'''Given a |version| corresponding to a 'stable' version of Chrome, returns
a ChannelInfo object representing that version.
'''
return ChannelInfo('stable', self.GetBranchForVersion(version), version)
def _ExtractFromVersionJson(self, channel_name, data_type):
'''Returns the branch or version number for a channel name.
'''
if channel_name == 'trunk':
return 'trunk'
if data_type == 'branch':
object_store = self._branch_object_store
elif data_type == 'version':
object_store = self._version_object_store
data = object_store.Get(channel_name).Get()
if data is not None:
return data
try:
version_json = json.loads(self._fetch_result.Get().content)
except Exception as e:
# This can happen if omahaproxy is misbehaving, which we've seen before.
# Quick hack fix: just serve from trunk until it's fixed.
logging.error('Failed to fetch or parse branch from omahaproxy: %s! '
'Falling back to "trunk".' % e)
return 'trunk'
numbers = {}
for entry in version_json:
if entry['os'] not in ['win', 'linux', 'mac', 'cros']:
continue
for version in entry['versions']:
if version['channel'] != channel_name:
continue
if data_type == 'branch':
number = version['version'].split('.')[2]
elif data_type == 'version':
number = version['version'].split('.')[0]
if number not in numbers:
numbers[number] = 0
else:
numbers[number] += 1
sorted_numbers = sorted(numbers.iteritems(),
key=operator.itemgetter(1),
reverse=True)
object_store.Set(channel_name, sorted_numbers[0][0])
return sorted_numbers[0][0]
def GetBranchForVersion(self, version):
'''Returns the most recent branch for a given chrome version number using
data stored on omahaproxy (see url_constants).
'''
if version == 'trunk':
return 'trunk'
branch = self._branch_object_store.Get(str(version)).Get()
if branch is not None:
return branch
version_json = json.loads(self._history_result.Get().content)
for entry in version_json['events']:
# Here, entry['title'] looks like: '<title> - <version>.##.<branch>.##'
version_title = entry['title'].split(' - ')[1].split('.')
if version_title[0] == str(version):
self._branch_object_store.Set(str(version), version_title[2])
return version_title[2]
raise ValueError('The branch for %s could not be found.' % version)
def GetChannelForVersion(self, version):
'''Returns the name of the development channel corresponding to a given
version number.
'''
for channel_info in self.GetAllChannelInfo():
if channel_info.channel == 'stable' and version <= channel_info.version:
return channel_info.channel
if version == channel_info.version:
return channel_info.channel
def GetLatestVersionNumber(self):
'''Returns the most recent version number found using data stored on
omahaproxy.
'''
latest_version = self._version_object_store.Get('latest').Get()
if latest_version is not None:
return latest_version
version_json = json.loads(self._history_result.Get().content)
latest_version = 0
for entry in version_json['events']:
version_title = entry['title'].split(' - ')[1].split('.')
version = int(version_title[0])
if version > latest_version:
latest_version = version
self._version_object_store.Set('latest', latest_version)
return latest_version