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