| # Copyright 2015 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 logging |
| import os |
| |
| from dependency_manager import base_config |
| from dependency_manager import exceptions |
| |
| |
| DEFAULT_TYPE = 'default' |
| |
| |
| class DependencyManager(object): |
| def __init__(self, configs, supported_config_types=None): |
| """Manages file dependencies found locally or in cloud_storage. |
| |
| Args: |
| configs: A list of instances of BaseConfig or it's subclasses, passed |
| in decreasing order of precedence. |
| supported_config_types: A list of whitelisted config_types. |
| No restrictions if None is specified. |
| |
| Raises: |
| ValueError: If |configs| is not a list of instances of BaseConfig or |
| its subclasses. |
| UnsupportedConfigFormatError: If supported_config_types is specified and |
| configs contains a config not in the supported config_types. |
| |
| Example: DependencyManager([config1, config2, config3]) |
| No requirements on the type of Config, and any dependencies that have |
| local files for the same platform will first look in those from |
| config1, then those from config2, and finally those from config3. |
| """ |
| if configs is None or not isinstance(configs, list): |
| raise ValueError( |
| 'Must supply a list of config files to DependencyManager') |
| # self._lookup_dict is a dictionary with the following format: |
| # { dependency1: {platform1: dependency_info1, |
| # platform2: dependency_info2} |
| # dependency2: {platform1: dependency_info3, |
| # ...} |
| # ...} |
| # |
| # Where the dependencies and platforms are strings, and the |
| # dependency_info's are DependencyInfo instances. |
| self._lookup_dict = {} |
| self.supported_configs = supported_config_types or [] |
| for config in configs: |
| self._UpdateDependencies(config) |
| |
| |
| def FetchPathWithVersion(self, dependency, platform): |
| """Get a path to an executable for |dependency|, downloading as needed. |
| |
| A path to a default executable may be returned if a platform specific |
| version is not specified in the config(s). |
| |
| Args: |
| dependency: Name of the desired dependency, as given in the config(s) |
| used in this DependencyManager. |
| platform: Name of the platform the dependency will run on. Often of the |
| form 'os_architecture'. Must match those specified in the config(s) |
| used in this DependencyManager. |
| Returns: |
| <path>, <version> where: |
| <path> is the path to an executable of |dependency| that will run |
| on |platform|, downloading from cloud storage if needed. |
| <version> is the version of the executable at <path> or None. |
| |
| Raises: |
| NoPathFoundError: If a local copy of the executable cannot be found and |
| a remote path could not be downloaded from cloud_storage. |
| CredentialsError: If cloud_storage credentials aren't configured. |
| PermissionError: If cloud_storage credentials are configured, but not |
| with an account that has permission to download the remote file. |
| NotFoundError: If the remote file does not exist where expected in |
| cloud_storage. |
| ServerError: If an internal server error is hit while downloading the |
| remote file. |
| CloudStorageError: If another error occured while downloading the remote |
| path. |
| FileNotFoundError: If an attempted download was otherwise unsuccessful. |
| |
| """ |
| dependency_info = self._GetDependencyInfo(dependency, platform) |
| if not dependency_info: |
| raise exceptions.NoPathFoundError(dependency, platform) |
| path = dependency_info.GetLocalPath() |
| version = None |
| if not path or not os.path.exists(path): |
| path = dependency_info.GetRemotePath() |
| if not path or not os.path.exists(path): |
| raise exceptions.NoPathFoundError(dependency, platform) |
| version = dependency_info.GetRemotePathVersion() |
| return path, version |
| |
| def FetchPath(self, dependency, platform): |
| """Get a path to an executable for |dependency|, downloading as needed. |
| |
| A path to a default executable may be returned if a platform specific |
| version is not specified in the config(s). |
| |
| Args: |
| dependency: Name of the desired dependency, as given in the config(s) |
| used in this DependencyManager. |
| platform: Name of the platform the dependency will run on. Often of the |
| form 'os_architecture'. Must match those specified in the config(s) |
| used in this DependencyManager. |
| Returns: |
| A path to an executable of |dependency| that will run on |platform|, |
| downloading from cloud storage if needed. |
| |
| Raises: |
| NoPathFoundError: If a local copy of the executable cannot be found and |
| a remote path could not be downloaded from cloud_storage. |
| CredentialsError: If cloud_storage credentials aren't configured. |
| PermissionError: If cloud_storage credentials are configured, but not |
| with an account that has permission to download the remote file. |
| NotFoundError: If the remote file does not exist where expected in |
| cloud_storage. |
| ServerError: If an internal server error is hit while downloading the |
| remote file. |
| CloudStorageError: If another error occured while downloading the remote |
| path. |
| FileNotFoundError: If an attempted download was otherwise unsuccessful. |
| |
| """ |
| path, _ = self.FetchPathWithVersion(dependency, platform) |
| return path |
| |
| def LocalPath(self, dependency, platform): |
| """Get a path to a locally stored executable for |dependency|. |
| |
| A path to a default executable may be returned if a platform specific |
| version is not specified in the config(s). |
| Will not download the executable. |
| |
| Args: |
| dependency: Name of the desired dependency, as given in the config(s) |
| used in this DependencyManager. |
| platform: Name of the platform the dependency will run on. Often of the |
| form 'os_architecture'. Must match those specified in the config(s) |
| used in this DependencyManager. |
| Returns: |
| A path to an executable for |dependency| that will run on |platform|. |
| |
| Raises: |
| NoPathFoundError: If a local copy of the executable cannot be found. |
| """ |
| dependency_info = self._GetDependencyInfo(dependency, platform) |
| if not dependency_info: |
| raise exceptions.NoPathFoundError(dependency, platform) |
| local_path = dependency_info.GetLocalPath() |
| if not local_path or not os.path.exists(local_path): |
| raise exceptions.NoPathFoundError(dependency, platform) |
| return local_path |
| |
| def PrefetchPaths(self, platform, dependencies=None, cloud_storage_retries=3): |
| if not dependencies: |
| dependencies = self._lookup_dict.keys() |
| |
| skipped_deps = [] |
| found_deps = [] |
| missing_deps = [] |
| for dependency in dependencies: |
| dependency_info = self._GetDependencyInfo(dependency, platform) |
| if not dependency_info: |
| # The dependency is only configured for other platforms. |
| skipped_deps.append(dependency) |
| continue |
| local_path = dependency_info.GetLocalPath() |
| if local_path: |
| found_deps.append(dependency) |
| continue |
| fetched_path = None |
| cloud_storage_error = None |
| for _ in range(0, cloud_storage_retries + 1): |
| try: |
| fetched_path = dependency_info.GetRemotePath() |
| except exceptions.CloudStorageError as e: |
| cloud_storage_error = e |
| break |
| if fetched_path: |
| found_deps.append(dependency) |
| else: |
| missing_deps.append(dependency) |
| logging.error( |
| 'Dependency %s could not be found or fetched from cloud storage for' |
| ' platform %s. Error: %s', dependency, platform, |
| cloud_storage_error) |
| if missing_deps: |
| raise exceptions.NoPathFoundError(', '.join(missing_deps), platform) |
| return (found_deps, skipped_deps) |
| |
| def _UpdateDependencies(self, config): |
| """Add the dependency information stored in |config| to this instance. |
| |
| Args: |
| config: An instances of BaseConfig or a subclasses. |
| |
| Raises: |
| UnsupportedConfigFormatError: If supported_config_types was specified |
| and config is not in the supported config_types. |
| """ |
| if not isinstance(config, base_config.BaseConfig): |
| raise ValueError('Must use a BaseConfig or subclass instance with the ' |
| 'DependencyManager.') |
| if (self.supported_configs and |
| config.GetConfigType() not in self.supported_configs): |
| raise exceptions.UnsupportedConfigFormatError(config.GetConfigType(), |
| config.config_path) |
| for dep_info in config.IterDependencyInfo(): |
| dependency = dep_info.dependency |
| platform = dep_info.platform |
| if dependency not in self._lookup_dict: |
| self._lookup_dict[dependency] = {} |
| if platform not in self._lookup_dict[dependency]: |
| self._lookup_dict[dependency][platform] = dep_info |
| else: |
| self._lookup_dict[dependency][platform].Update(dep_info) |
| |
| |
| def _GetDependencyInfo(self, dependency, platform): |
| """Get information for |dependency| on |platform|, or a default if needed. |
| |
| Args: |
| dependency: Name of the desired dependency, as given in the config(s) |
| used in this DependencyManager. |
| platform: Name of the platform the dependency will run on. Often of the |
| form 'os_architecture'. Must match those specified in the config(s) |
| used in this DependencyManager. |
| |
| Returns: The dependency_info for |dependency| on |platform| if it exists. |
| Or the default version of |dependency| if it exists, or None if neither |
| exist. |
| """ |
| if not self._lookup_dict or dependency not in self._lookup_dict: |
| return None |
| dependency_dict = self._lookup_dict[dependency] |
| device_type = platform |
| if not device_type in dependency_dict: |
| device_type = DEFAULT_TYPE |
| return dependency_dict.get(device_type) |
| |