Home | History | Annotate | Download | only in server2
      1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import json
      6 import logging
      7 import operator
      8 
      9 from appengine_url_fetcher import AppEngineUrlFetcher
     10 import url_constants
     11 
     12 
     13 class ChannelInfo(object):
     14   '''Represents a Chrome channel with three pieces of information. |channel| is
     15   one of 'stable', 'beta', 'dev', or 'master'. |branch| and |version| correspond
     16   with each other, and represent different releases of Chrome. Note that
     17   |branch| and |version| can occasionally be the same for separate channels
     18   (i.e. 'beta' and 'dev'), so all three fields are required to uniquely
     19   identify a channel.
     20   '''
     21 
     22   def __init__(self, channel, branch, version):
     23     assert isinstance(channel, basestring), channel
     24     assert isinstance(branch, basestring), branch
     25     # TODO(kalman): Assert that this is a string. One day Chromium will probably
     26     # be served out of a git repository and the versions will no longer be ints.
     27     assert isinstance(version, int) or version == 'master', version
     28     self.channel = channel
     29     self.branch = branch
     30     self.version = version
     31 
     32   def __eq__(self, other):
     33     return self.__dict__ == other.__dict__
     34 
     35   def __ne__(self, other):
     36     return not (self == other)
     37 
     38   def __repr__(self):
     39     return '%s%s' % (type(self).__name__, repr(self.__dict__))
     40 
     41   def __str__(self):
     42     return repr(self)
     43 
     44 
     45 class BranchUtility(object):
     46   '''Provides methods for working with Chrome channel, branch, and version
     47   data served from OmahaProxy.
     48   '''
     49 
     50   def __init__(self, fetch_url, history_url, fetcher, object_store_creator):
     51     self._fetcher = fetcher
     52     def create_object_store(category):
     53       return object_store_creator.Create(BranchUtility, category=category)
     54     self._branch_object_store = create_object_store('branch')
     55     self._version_object_store = create_object_store('version')
     56     self._fetch_result = self._fetcher.FetchAsync(fetch_url)
     57     self._history_result = self._fetcher.FetchAsync(history_url)
     58 
     59   @staticmethod
     60   def Create(object_store_creator):
     61     return BranchUtility(url_constants.OMAHA_PROXY_URL,
     62                          url_constants.OMAHA_DEV_HISTORY,
     63                          AppEngineUrlFetcher(),
     64                          object_store_creator)
     65 
     66   @staticmethod
     67   def GetAllChannelNames():
     68     return ('stable', 'beta', 'dev', 'master')
     69 
     70   @staticmethod
     71   def NewestChannel(channels):
     72     channels = set(channels)
     73     for channel in reversed(BranchUtility.GetAllChannelNames()):
     74       if channel in channels:
     75         return channel
     76 
     77   def Newer(self, channel_info):
     78     '''Given a ChannelInfo object, returns a new ChannelInfo object
     79     representing the next most recent Chrome version/branch combination.
     80     '''
     81     if channel_info.channel == 'master':
     82       return None
     83     if channel_info.channel == 'stable':
     84       stable_info = self.GetChannelInfo('stable')
     85       if channel_info.version < stable_info.version:
     86         return self.GetStableChannelInfo(channel_info.version + 1)
     87     names = self.GetAllChannelNames()
     88     return self.GetAllChannelInfo()[names.index(channel_info.channel) + 1]
     89 
     90   def Older(self, channel_info):
     91     '''Given a ChannelInfo object, returns a new ChannelInfo object
     92     representing the previous Chrome version/branch combination.
     93     '''
     94     if channel_info.channel == 'stable':
     95       if channel_info.version <= 5:
     96         # BranchUtility can't access branch data from before Chrome version 5.
     97         return None
     98       return self.GetStableChannelInfo(channel_info.version - 1)
     99     names = self.GetAllChannelNames()
    100     return self.GetAllChannelInfo()[names.index(channel_info.channel) - 1]
    101 
    102   @staticmethod
    103   def SplitChannelNameFromPath(path):
    104     '''Splits the channel name out of |path|, returning the tuple
    105     (channel_name, real_path). If the channel cannot be determined then returns
    106     (None, path).
    107     '''
    108     if '/' in path:
    109       first, second = path.split('/', 1)
    110     else:
    111       first, second = (path, '')
    112     if first in BranchUtility.GetAllChannelNames():
    113       return (first, second)
    114     return (None, path)
    115 
    116   def GetAllBranches(self):
    117     return tuple((channel, self.GetChannelInfo(channel).branch)
    118             for channel in BranchUtility.GetAllChannelNames())
    119 
    120   def GetAllVersions(self):
    121     return tuple(self.GetChannelInfo(channel).version
    122             for channel in BranchUtility.GetAllChannelNames())
    123 
    124   def GetAllChannelInfo(self):
    125     return tuple(self.GetChannelInfo(channel)
    126             for channel in BranchUtility.GetAllChannelNames())
    127 
    128 
    129   def GetChannelInfo(self, channel):
    130     version = self._ExtractFromVersionJson(channel, 'version')
    131     if version != 'master':
    132       version = int(version)
    133     return ChannelInfo(channel,
    134                        self._ExtractFromVersionJson(channel, 'branch'),
    135                        version)
    136 
    137   def GetStableChannelInfo(self, version):
    138     '''Given a |version| corresponding to a 'stable' version of Chrome, returns
    139     a ChannelInfo object representing that version.
    140     '''
    141     return ChannelInfo('stable', self.GetBranchForVersion(version), version)
    142 
    143   def _ExtractFromVersionJson(self, channel_name, data_type):
    144     '''Returns the branch or version number for a channel name.
    145     '''
    146     if channel_name == 'master':
    147       return 'master'
    148 
    149     if data_type == 'branch':
    150       object_store = self._branch_object_store
    151     elif data_type == 'version':
    152       object_store = self._version_object_store
    153 
    154     data = object_store.Get(channel_name).Get()
    155     if data is not None:
    156       return data
    157 
    158     try:
    159       version_json = json.loads(self._fetch_result.Get().content)
    160     except Exception as e:
    161       # This can happen if omahaproxy is misbehaving, which we've seen before.
    162       # Quick hack fix: just serve from master until it's fixed.
    163       logging.error('Failed to fetch or parse branch from omahaproxy: %s! '
    164                     'Falling back to "master".' % e)
    165       return 'master'
    166 
    167     numbers = {}
    168     for entry in version_json:
    169       if entry['os'] not in ('win', 'linux', 'mac', 'cros'):
    170         continue
    171       for version in entry['versions']:
    172         if version['channel'] != channel_name:
    173           continue
    174         if data_type == 'branch':
    175           number = version['version'].split('.')[2]
    176         elif data_type == 'version':
    177           number = version['version'].split('.')[0]
    178         if number not in numbers:
    179           numbers[number] = 0
    180         else:
    181           numbers[number] += 1
    182 
    183     sorted_numbers = sorted(numbers.iteritems(),
    184                             key=operator.itemgetter(1),
    185                             reverse=True)
    186     object_store.Set(channel_name, sorted_numbers[0][0])
    187     return sorted_numbers[0][0]
    188 
    189   def GetBranchForVersion(self, version):
    190     '''Returns the most recent branch for a given chrome version number using
    191     data stored on omahaproxy (see url_constants).
    192     '''
    193     if version == 'master':
    194       return 'master'
    195 
    196     branch = self._branch_object_store.Get(str(version)).Get()
    197     if branch is not None:
    198       return branch
    199 
    200     version_json = json.loads(self._history_result.Get().content)
    201     for entry in version_json:
    202       version_title = entry['version'].split('.')
    203       if version_title[0] == str(version):
    204         self._branch_object_store.Set(str(version), version_title[2])
    205         return version_title[2]
    206 
    207     raise ValueError('The branch for %s could not be found.' % version)
    208 
    209   def GetChannelForVersion(self, version):
    210     '''Returns the name of the development channel corresponding to a given
    211     version number.
    212     '''
    213     for channel_info in self.GetAllChannelInfo():
    214       if channel_info.channel == 'stable' and version <= channel_info.version:
    215         return channel_info.channel
    216       if version == channel_info.version:
    217         return channel_info.channel
    218 
    219   def GetLatestVersionNumber(self):
    220     '''Returns the most recent version number found using data stored on
    221     omahaproxy.
    222     '''
    223     latest_version = self._version_object_store.Get('latest').Get()
    224     if latest_version is not None:
    225       return latest_version
    226 
    227     version_json = json.loads(self._history_result.Get().content)
    228     latest_version = 0
    229     for entry in version_json:
    230       version_title = entry['version'].split('.')
    231       version = int(version_title[0])
    232       if version > latest_version:
    233         latest_version = version
    234 
    235     self._version_object_store.Set('latest', latest_version)
    236     return latest_version
    237