Home | History | Annotate | Download | only in server2
      1 # Copyright 2013 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 posixpath
      6 
      7 from api_models import GetNodeCategories
      8 from api_schema_graph import APISchemaGraph
      9 from branch_utility import BranchUtility, ChannelInfo
     10 from compiled_file_system import CompiledFileSystem, SingleFile, Unicode
     11 from extensions_paths import API_PATHS, JSON_TEMPLATES
     12 from features_bundle import FeaturesBundle
     13 from file_system import FileNotFoundError
     14 from schema_processor import SchemaProcessor
     15 from third_party.json_schema_compiler.memoize import memoize
     16 from third_party.json_schema_compiler.model import UnixName
     17 
     18 
     19 _DEVTOOLS_API = 'devtools_api.json'
     20 _EXTENSION_API = 'extension_api.json'
     21 # The version where api_features.json is first available.
     22 _API_FEATURES_MIN_VERSION = 28
     23 # The version where permission_ and manifest_features.json are available and
     24 # presented in the current format.
     25 _ORIGINAL_FEATURES_MIN_VERSION = 20
     26 # API schemas are aggregated in extension_api.json up to this version.
     27 _EXTENSION_API_MAX_VERSION = 17
     28 # The earliest version for which we have SVN data.
     29 _SVN_MIN_VERSION = 5
     30 
     31 
     32 def _GetChannelFromFeatures(api_name, features):
     33   '''Finds API channel information for |api_name| from |features|.
     34   Returns None if channel information for the API cannot be located.
     35   '''
     36   feature = features.Get().get(api_name)
     37   return feature.get('channel') if feature else None
     38 
     39 
     40 def _GetChannelFromAPIFeatures(api_name, features_bundle):
     41   return _GetChannelFromFeatures(api_name, features_bundle.GetAPIFeatures())
     42 
     43 
     44 def _GetChannelFromManifestFeatures(api_name, features_bundle):
     45   # _manifest_features.json uses unix_style API names.
     46   api_name = UnixName(api_name)
     47   return _GetChannelFromFeatures(api_name,
     48                                  features_bundle.GetManifestFeatures())
     49 
     50 
     51 def _GetChannelFromPermissionFeatures(api_name, features_bundle):
     52   return _GetChannelFromFeatures(api_name,
     53                                  features_bundle.GetPermissionFeatures())
     54 
     55 
     56 def _GetAPISchemaFilename(api_name, file_system, version):
     57   '''Gets the name of the file which may contain the schema for |api_name| in
     58   |file_system|, or None if the API is not found. Note that this may be the
     59   single _EXTENSION_API file which all APIs share in older versions of Chrome,
     60   in which case it is unknown whether the API actually exists there.
     61   '''
     62   if version == 'master' or version > _ORIGINAL_FEATURES_MIN_VERSION:
     63     # API schema filenames switch format to unix_hacker_style.
     64     api_name = UnixName(api_name)
     65 
     66   # Devtools API names have 'devtools.' prepended to them.
     67   # The corresponding filenames do not.
     68   if 'devtools_' in api_name:
     69     api_name = api_name.replace('devtools_', '')
     70 
     71   for api_path in API_PATHS:
     72     try:
     73       for base, _, filenames in file_system.Walk(api_path):
     74         for ext in ('json', 'idl'):
     75           filename = '%s.%s' % (api_name, ext)
     76           if filename in filenames:
     77             return posixpath.join(api_path, base, filename)
     78           if _EXTENSION_API in filenames:
     79             return posixpath.join(api_path, base, _EXTENSION_API)
     80     except FileNotFoundError:
     81       continue
     82   return None
     83 
     84 
     85 class AvailabilityInfo(object):
     86   '''Represents availability data for an API. |scheduled| is a version number
     87   specifying when dev and beta APIs will become stable, or None if that data
     88   is unknown.
     89   '''
     90   def __init__(self, channel_info, scheduled=None):
     91     assert isinstance(channel_info, ChannelInfo)
     92     assert isinstance(scheduled, int) or scheduled is None
     93     self.channel_info = channel_info
     94     self.scheduled = scheduled
     95 
     96   def __eq__(self, other):
     97     return self.__dict__ == other.__dict__
     98 
     99   def __ne__(self, other):
    100     return not (self == other)
    101 
    102   def __repr__(self):
    103     return '%s%s' % (type(self).__name__, repr(self.__dict__))
    104 
    105   def __str__(self):
    106     return repr(self)
    107 
    108 
    109 class AvailabilityFinder(object):
    110   '''Generates availability information for APIs by looking at API schemas and
    111   _features files over multiple release versions of Chrome.
    112   '''
    113 
    114   def __init__(self,
    115                branch_utility,
    116                compiled_fs_factory,
    117                file_system_iterator,
    118                host_file_system,
    119                object_store_creator,
    120                platform,
    121                schema_processor_factory):
    122     self._branch_utility = branch_utility
    123     self._compiled_fs_factory = compiled_fs_factory
    124     self._file_system_iterator = file_system_iterator
    125     self._host_file_system = host_file_system
    126     self._object_store_creator = object_store_creator
    127     def create_object_store(category):
    128       return object_store_creator.Create(
    129           AvailabilityFinder, category='/'.join((platform, category)))
    130     self._top_level_object_store = create_object_store('top_level')
    131     self._node_level_object_store = create_object_store('node_level')
    132     self._json_fs = compiled_fs_factory.ForJson(self._host_file_system)
    133     self._platform = platform
    134     # When processing the API schemas, we retain inlined types in the schema
    135     # so that there are not missing nodes in the APISchemaGraphs when trying
    136     # to lookup availability.
    137     self._schema_processor = schema_processor_factory.Create(True)
    138 
    139   def _GetPredeterminedAvailability(self, api_name):
    140     '''Checks a configuration file for hardcoded (i.e. predetermined)
    141     availability information for an API.
    142     '''
    143     api_info = self._json_fs.GetFromFile(
    144         JSON_TEMPLATES + 'api_availabilities.json').Get().get(api_name)
    145     if api_info is None:
    146       return None
    147     if api_info['channel'] == 'stable':
    148       return AvailabilityInfo(
    149           self._branch_utility.GetStableChannelInfo(api_info['version']))
    150     return AvailabilityInfo(
    151         self._branch_utility.GetChannelInfo(api_info['channel']))
    152 
    153   @memoize
    154   def _CreateAPISchemaFileSystem(self, file_system):
    155     '''Creates a CompiledFileSystem for parsing raw JSON or IDL API schema
    156     data and formatting it so that it can be used to create APISchemaGraphs.
    157     '''
    158     def process_schema(path, data):
    159       return self._schema_processor.Process(path, data)
    160     return self._compiled_fs_factory.Create(file_system,
    161                                             SingleFile(Unicode(process_schema)),
    162                                             CompiledFileSystem,
    163                                             category='api-schema')
    164 
    165   def _GetAPISchema(self, api_name, file_system, version):
    166     '''Searches |file_system| for |api_name|'s API schema data, and processes
    167     and returns it if found.
    168     '''
    169     api_filename = _GetAPISchemaFilename(api_name, file_system, version)
    170     if api_filename is None:
    171       # No file for the API could be found in the given |file_system|.
    172       return None
    173 
    174     schema_fs = self._CreateAPISchemaFileSystem(file_system)
    175     api_schemas = schema_fs.GetFromFile(api_filename).Get()
    176     matching_schemas = [api for api in api_schemas
    177                         if api['namespace'] == api_name]
    178     # There should only be a single matching schema per file, or zero in the
    179     # case of no API data being found in _EXTENSION_API.
    180     assert len(matching_schemas) <= 1
    181     return matching_schemas or None
    182 
    183   def _HasAPISchema(self, api_name, file_system, version):
    184     '''Whether or not an API schema for |api_name| exists in the given
    185     |file_system|.
    186     '''
    187     filename = _GetAPISchemaFilename(api_name, file_system, version)
    188     if filename is None:
    189       return False
    190     if filename.endswith(_EXTENSION_API) or filename.endswith(_DEVTOOLS_API):
    191       return self._GetAPISchema(api_name, file_system, version) is not None
    192     return True
    193 
    194   def _CheckStableAvailability(self,
    195                                api_name,
    196                                file_system,
    197                                version,
    198                                earliest_version=None):
    199     '''Checks for availability of an API, |api_name|, on the stable channel.
    200     Considers several _features.json files, file system existence, and
    201     extension_api.json depending on the given |version|.
    202     |earliest_version| is the version of Chrome at which |api_name| first became
    203     available. It should only be given when checking stable availability for
    204     API nodes, so it can be used as an alternative to the check for filesystem
    205     existence.
    206     '''
    207     earliest_version = earliest_version or _SVN_MIN_VERSION
    208     if version < earliest_version:
    209       # SVN data isn't available below this version.
    210       return False
    211     features_bundle = self._CreateFeaturesBundle(file_system)
    212     available_channel = None
    213     if version >= _API_FEATURES_MIN_VERSION:
    214       # The _api_features.json file first appears in version 28 and should be
    215       # the most reliable for finding API availability.
    216       available_channel = _GetChannelFromAPIFeatures(api_name,
    217                                                      features_bundle)
    218     if version >= _ORIGINAL_FEATURES_MIN_VERSION:
    219       # The _permission_features.json and _manifest_features.json files are
    220       # present in Chrome 20 and onwards. Use these if no information could be
    221       # found using _api_features.json.
    222       available_channel = (
    223           available_channel or
    224           _GetChannelFromPermissionFeatures(api_name, features_bundle) or
    225           _GetChannelFromManifestFeatures(api_name, features_bundle))
    226       if available_channel is not None:
    227         return available_channel == 'stable'
    228 
    229     # |earliest_version| == _SVN_MIN_VERSION implies we're dealing with an API.
    230     # Fall back to a check for file system existence if the API is not
    231     # stable in any of the _features.json files, or if the _features files
    232     # do not exist (version 19 and earlier).
    233     if earliest_version == _SVN_MIN_VERSION:
    234       return self._HasAPISchema(api_name, file_system, version)
    235     # For API nodes, assume it's available if |version| is greater than the
    236     # version the node became available (which it is, because of the first
    237     # check).
    238     return True
    239 
    240   def _CheckChannelAvailability(self, api_name, file_system, channel_info):
    241     '''Searches through the _features files in a given |file_system|, falling
    242     back to checking the file system for API schema existence, to determine
    243     whether or not an API is available on the given channel, |channel_info|.
    244     '''
    245     features_bundle = self._CreateFeaturesBundle(file_system)
    246     available_channel = (
    247         _GetChannelFromAPIFeatures(api_name, features_bundle) or
    248         _GetChannelFromPermissionFeatures(api_name, features_bundle) or
    249         _GetChannelFromManifestFeatures(api_name, features_bundle))
    250     if (available_channel is None and
    251         self._HasAPISchema(api_name, file_system, channel_info.version)):
    252       # If an API is not represented in any of the _features files, but exists
    253       # in the filesystem, then assume it is available in this version.
    254       # The chrome.windows API is an example of this.
    255       available_channel = channel_info.channel
    256     # If the channel we're checking is the same as or newer than the
    257     # |available_channel| then the API is available at this channel.
    258     newest = BranchUtility.NewestChannel((available_channel,
    259                                           channel_info.channel))
    260     return available_channel is not None and newest == channel_info.channel
    261 
    262   def _CheckChannelAvailabilityForNode(self,
    263                                        node_name,
    264                                        file_system,
    265                                        channel_info,
    266                                        earliest_channel_info):
    267     '''Searches through the _features files in a given |file_system| to
    268     determine whether or not an API node is available on the given channel,
    269     |channel_info|. |earliest_channel_info| is the earliest channel the node
    270     was introduced.
    271     '''
    272     features_bundle = self._CreateFeaturesBundle(file_system)
    273     available_channel = None
    274     # Only API nodes can have their availability overriden on a per-node basis,
    275     # so we only need to check _api_features.json.
    276     if channel_info.version >= _API_FEATURES_MIN_VERSION:
    277       available_channel = _GetChannelFromAPIFeatures(node_name, features_bundle)
    278     if (available_channel is None and
    279         channel_info.version >= earliest_channel_info.version):
    280       # Most API nodes inherit their availabiltity from their parent, so don't
    281       # explicitly appear in _api_features.json. For example, "tabs.create"
    282       # isn't listed; it inherits from "tabs". Assume these are available at
    283       # |channel_info|.
    284       available_channel = channel_info.channel
    285     newest = BranchUtility.NewestChannel((available_channel,
    286                                           channel_info.channel))
    287     return available_channel is not None and newest == channel_info.channel
    288 
    289   @memoize
    290   def _CreateFeaturesBundle(self, file_system):
    291     return FeaturesBundle(file_system,
    292                           self._compiled_fs_factory,
    293                           self._object_store_creator,
    294                           self._platform)
    295 
    296   def _CheckAPIAvailability(self, api_name, file_system, channel_info):
    297     '''Determines the availability for an API at a certain version of Chrome.
    298     Two branches of logic are used depending on whether or not the API is
    299     determined to be 'stable' at the given version.
    300     '''
    301     if channel_info.channel == 'stable':
    302       return self._CheckStableAvailability(api_name,
    303                                            file_system,
    304                                            channel_info.version)
    305     return self._CheckChannelAvailability(api_name,
    306                                           file_system,
    307                                           channel_info)
    308 
    309   def _FindScheduled(self, api_name, earliest_version=None):
    310     '''Determines the earliest version of Chrome where the API is stable.
    311     Unlike the code in GetAPIAvailability, this checks if the API is stable
    312     even when Chrome is in dev or beta, which shows that the API is scheduled
    313     to be stable in that verison of Chrome. |earliest_version| is the version
    314     |api_name| became first available. Only use it when finding scheduled
    315     availability for nodes.
    316     '''
    317     def check_scheduled(file_system, channel_info):
    318       return self._CheckStableAvailability(api_name,
    319                                            file_system,
    320                                            channel_info.version,
    321                                            earliest_version=earliest_version)
    322 
    323     stable_channel = self._file_system_iterator.Descending(
    324         self._branch_utility.GetChannelInfo('dev'), check_scheduled)
    325 
    326     return stable_channel.version if stable_channel else None
    327 
    328   def _CheckAPINodeAvailability(self, node_name, earliest_channel_info):
    329     '''Gets availability data for a node by checking _features files.
    330     '''
    331     def check_node_availability(file_system, channel_info):
    332       return self._CheckChannelAvailabilityForNode(node_name,
    333                                                    file_system,
    334                                                    channel_info,
    335                                                    earliest_channel_info)
    336     channel_info = (self._file_system_iterator.Descending(
    337         self._branch_utility.GetChannelInfo('dev'), check_node_availability) or
    338         earliest_channel_info)
    339 
    340     if channel_info.channel == 'stable':
    341       scheduled = None
    342     else:
    343       scheduled = self._FindScheduled(
    344           node_name,
    345           earliest_version=earliest_channel_info.version)
    346 
    347     return AvailabilityInfo(channel_info, scheduled=scheduled)
    348 
    349   def GetAPIAvailability(self, api_name):
    350     '''Performs a search for an API's top-level availability by using a
    351     HostFileSystemIterator instance to traverse multiple version of the
    352     SVN filesystem.
    353     '''
    354     availability = self._top_level_object_store.Get(api_name).Get()
    355     if availability is not None:
    356       return availability
    357 
    358     # Check for predetermined availability and cache this information if found.
    359     availability = self._GetPredeterminedAvailability(api_name)
    360     if availability is not None:
    361       self._top_level_object_store.Set(api_name, availability)
    362       return availability
    363 
    364     def check_api_availability(file_system, channel_info):
    365       return self._CheckAPIAvailability(api_name, file_system, channel_info)
    366 
    367     channel_info = self._file_system_iterator.Descending(
    368         self._branch_utility.GetChannelInfo('dev'),
    369         check_api_availability)
    370     if channel_info is None:
    371       # The API wasn't available on 'dev', so it must be a 'master'-only API.
    372       channel_info = self._branch_utility.GetChannelInfo('master')
    373 
    374     # If the API is not stable, check when it will be scheduled to be stable.
    375     if channel_info.channel == 'stable':
    376       scheduled = None
    377     else:
    378       scheduled = self._FindScheduled(api_name)
    379 
    380     availability = AvailabilityInfo(channel_info, scheduled=scheduled)
    381 
    382     self._top_level_object_store.Set(api_name, availability)
    383     return availability
    384 
    385   def GetAPINodeAvailability(self, api_name):
    386     '''Returns an APISchemaGraph annotated with each node's availability (the
    387     ChannelInfo at the oldest channel it's available in).
    388     '''
    389     availability_graph = self._node_level_object_store.Get(api_name).Get()
    390     if availability_graph is not None:
    391       return availability_graph
    392 
    393     def assert_not_none(value):
    394       assert value is not None
    395       return value
    396 
    397     availability_graph = APISchemaGraph()
    398     host_fs = self._host_file_system
    399     master_stat = assert_not_none(host_fs.Stat(_GetAPISchemaFilename(
    400         api_name, host_fs, 'master')))
    401 
    402     # Weird object thing here because nonlocal is Python 3.
    403     previous = type('previous', (object,), {'stat': None, 'graph': None})
    404 
    405     def update_availability_graph(file_system, channel_info):
    406       # If we can't find a filename, skip checking at this branch.
    407       # For example, something could have a predetermined availability of 23,
    408       # but it doesn't show up in the file system until 26.
    409       # We know that the file will become available at some point.
    410       #
    411       # The problem with this is that at the first version where the API file
    412       # exists, we'll get a huge chunk of new objects that don't match
    413       # the predetermined API availability.
    414       version_filename = _GetAPISchemaFilename(api_name,
    415                                                file_system,
    416                                                channel_info.version)
    417       if version_filename is None:
    418         # Continue the loop at the next version.
    419         return True
    420 
    421       version_stat = assert_not_none(file_system.Stat(version_filename))
    422 
    423       # Important optimisation: only re-parse the graph if the file changed in
    424       # the last revision. Parsing the same schema and forming a graph on every
    425       # iteration is really expensive.
    426       if version_stat == previous.stat:
    427         version_graph = previous.graph
    428       else:
    429         # Keep track of any new schema elements from this version by adding
    430         # them to |availability_graph|.
    431         #
    432         # Calling |availability_graph|.Lookup() on the nodes being updated
    433         # will return the |annotation| object -- the current |channel_info|.
    434         version_graph = APISchemaGraph(
    435             api_schema=self._GetAPISchema(api_name,
    436                                           file_system,
    437                                           channel_info.version))
    438         def annotator(node_name):
    439           return self._CheckAPINodeAvailability('%s.%s' % (api_name, node_name),
    440                                                 channel_info)
    441 
    442         availability_graph.Update(version_graph.Subtract(availability_graph),
    443                                   annotator)
    444 
    445       previous.stat = version_stat
    446       previous.graph = version_graph
    447 
    448       # Continue looping until there are no longer differences between this
    449       # version and master.
    450       return version_stat != master_stat
    451 
    452     self._file_system_iterator.Ascending(
    453         self.GetAPIAvailability(api_name).channel_info,
    454         update_availability_graph)
    455 
    456     self._node_level_object_store.Set(api_name, availability_graph)
    457     return availability_graph
    458