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
5import json
6import logging
7import operator
8
9from appengine_url_fetcher import AppEngineUrlFetcher
10import url_constants
11
12
13class 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
45class 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