1# Copyright 2015 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 logging 6import os 7 8from dependency_manager import base_config 9from dependency_manager import exceptions 10 11 12DEFAULT_TYPE = 'default' 13 14 15class DependencyManager(object): 16 def __init__(self, configs, supported_config_types=None): 17 """Manages file dependencies found locally or in cloud_storage. 18 19 Args: 20 configs: A list of instances of BaseConfig or it's subclasses, passed 21 in decreasing order of precedence. 22 supported_config_types: A list of whitelisted config_types. 23 No restrictions if None is specified. 24 25 Raises: 26 ValueError: If |configs| is not a list of instances of BaseConfig or 27 its subclasses. 28 UnsupportedConfigFormatError: If supported_config_types is specified and 29 configs contains a config not in the supported config_types. 30 31 Example: DependencyManager([config1, config2, config3]) 32 No requirements on the type of Config, and any dependencies that have 33 local files for the same platform will first look in those from 34 config1, then those from config2, and finally those from config3. 35 """ 36 if configs is None or not isinstance(configs, list): 37 raise ValueError( 38 'Must supply a list of config files to DependencyManager') 39 # self._lookup_dict is a dictionary with the following format: 40 # { dependency1: {platform1: dependency_info1, 41 # platform2: dependency_info2} 42 # dependency2: {platform1: dependency_info3, 43 # ...} 44 # ...} 45 # 46 # Where the dependencies and platforms are strings, and the 47 # dependency_info's are DependencyInfo instances. 48 self._lookup_dict = {} 49 self.supported_configs = supported_config_types or [] 50 for config in configs: 51 self._UpdateDependencies(config) 52 53 54 def FetchPathWithVersion(self, dependency, platform): 55 """Get a path to an executable for |dependency|, downloading as needed. 56 57 A path to a default executable may be returned if a platform specific 58 version is not specified in the config(s). 59 60 Args: 61 dependency: Name of the desired dependency, as given in the config(s) 62 used in this DependencyManager. 63 platform: Name of the platform the dependency will run on. Often of the 64 form 'os_architecture'. Must match those specified in the config(s) 65 used in this DependencyManager. 66 Returns: 67 <path>, <version> where: 68 <path> is the path to an executable of |dependency| that will run 69 on |platform|, downloading from cloud storage if needed. 70 <version> is the version of the executable at <path> or None. 71 72 Raises: 73 NoPathFoundError: If a local copy of the executable cannot be found and 74 a remote path could not be downloaded from cloud_storage. 75 CredentialsError: If cloud_storage credentials aren't configured. 76 PermissionError: If cloud_storage credentials are configured, but not 77 with an account that has permission to download the remote file. 78 NotFoundError: If the remote file does not exist where expected in 79 cloud_storage. 80 ServerError: If an internal server error is hit while downloading the 81 remote file. 82 CloudStorageError: If another error occured while downloading the remote 83 path. 84 FileNotFoundError: If an attempted download was otherwise unsuccessful. 85 86 """ 87 dependency_info = self._GetDependencyInfo(dependency, platform) 88 if not dependency_info: 89 raise exceptions.NoPathFoundError(dependency, platform) 90 path = dependency_info.GetLocalPath() 91 version = None 92 if not path or not os.path.exists(path): 93 path = dependency_info.GetRemotePath() 94 if not path or not os.path.exists(path): 95 raise exceptions.NoPathFoundError(dependency, platform) 96 version = dependency_info.GetRemotePathVersion() 97 return path, version 98 99 def FetchPath(self, dependency, platform): 100 """Get a path to an executable for |dependency|, downloading as needed. 101 102 A path to a default executable may be returned if a platform specific 103 version is not specified in the config(s). 104 105 Args: 106 dependency: Name of the desired dependency, as given in the config(s) 107 used in this DependencyManager. 108 platform: Name of the platform the dependency will run on. Often of the 109 form 'os_architecture'. Must match those specified in the config(s) 110 used in this DependencyManager. 111 Returns: 112 A path to an executable of |dependency| that will run on |platform|, 113 downloading from cloud storage if needed. 114 115 Raises: 116 NoPathFoundError: If a local copy of the executable cannot be found and 117 a remote path could not be downloaded from cloud_storage. 118 CredentialsError: If cloud_storage credentials aren't configured. 119 PermissionError: If cloud_storage credentials are configured, but not 120 with an account that has permission to download the remote file. 121 NotFoundError: If the remote file does not exist where expected in 122 cloud_storage. 123 ServerError: If an internal server error is hit while downloading the 124 remote file. 125 CloudStorageError: If another error occured while downloading the remote 126 path. 127 FileNotFoundError: If an attempted download was otherwise unsuccessful. 128 129 """ 130 path, _ = self.FetchPathWithVersion(dependency, platform) 131 return path 132 133 def LocalPath(self, dependency, platform): 134 """Get a path to a locally stored executable for |dependency|. 135 136 A path to a default executable may be returned if a platform specific 137 version is not specified in the config(s). 138 Will not download the executable. 139 140 Args: 141 dependency: Name of the desired dependency, as given in the config(s) 142 used in this DependencyManager. 143 platform: Name of the platform the dependency will run on. Often of the 144 form 'os_architecture'. Must match those specified in the config(s) 145 used in this DependencyManager. 146 Returns: 147 A path to an executable for |dependency| that will run on |platform|. 148 149 Raises: 150 NoPathFoundError: If a local copy of the executable cannot be found. 151 """ 152 dependency_info = self._GetDependencyInfo(dependency, platform) 153 if not dependency_info: 154 raise exceptions.NoPathFoundError(dependency, platform) 155 local_path = dependency_info.GetLocalPath() 156 if not local_path or not os.path.exists(local_path): 157 raise exceptions.NoPathFoundError(dependency, platform) 158 return local_path 159 160 def PrefetchPaths(self, platform, dependencies=None, cloud_storage_retries=3): 161 if not dependencies: 162 dependencies = self._lookup_dict.keys() 163 164 skipped_deps = [] 165 found_deps = [] 166 missing_deps = [] 167 for dependency in dependencies: 168 dependency_info = self._GetDependencyInfo(dependency, platform) 169 if not dependency_info: 170 # The dependency is only configured for other platforms. 171 skipped_deps.append(dependency) 172 continue 173 local_path = dependency_info.GetLocalPath() 174 if local_path: 175 found_deps.append(dependency) 176 continue 177 fetched_path = None 178 cloud_storage_error = None 179 for _ in range(0, cloud_storage_retries + 1): 180 try: 181 fetched_path = dependency_info.GetRemotePath() 182 except exceptions.CloudStorageError as e: 183 cloud_storage_error = e 184 break 185 if fetched_path: 186 found_deps.append(dependency) 187 else: 188 missing_deps.append(dependency) 189 logging.error( 190 'Dependency %s could not be found or fetched from cloud storage for' 191 ' platform %s. Error: %s', dependency, platform, 192 cloud_storage_error) 193 if missing_deps: 194 raise exceptions.NoPathFoundError(', '.join(missing_deps), platform) 195 return (found_deps, skipped_deps) 196 197 def _UpdateDependencies(self, config): 198 """Add the dependency information stored in |config| to this instance. 199 200 Args: 201 config: An instances of BaseConfig or a subclasses. 202 203 Raises: 204 UnsupportedConfigFormatError: If supported_config_types was specified 205 and config is not in the supported config_types. 206 """ 207 if not isinstance(config, base_config.BaseConfig): 208 raise ValueError('Must use a BaseConfig or subclass instance with the ' 209 'DependencyManager.') 210 if (self.supported_configs and 211 config.GetConfigType() not in self.supported_configs): 212 raise exceptions.UnsupportedConfigFormatError(config.GetConfigType(), 213 config.config_path) 214 for dep_info in config.IterDependencyInfo(): 215 dependency = dep_info.dependency 216 platform = dep_info.platform 217 if dependency not in self._lookup_dict: 218 self._lookup_dict[dependency] = {} 219 if platform not in self._lookup_dict[dependency]: 220 self._lookup_dict[dependency][platform] = dep_info 221 else: 222 self._lookup_dict[dependency][platform].Update(dep_info) 223 224 225 def _GetDependencyInfo(self, dependency, platform): 226 """Get information for |dependency| on |platform|, or a default if needed. 227 228 Args: 229 dependency: Name of the desired dependency, as given in the config(s) 230 used in this DependencyManager. 231 platform: Name of the platform the dependency will run on. Often of the 232 form 'os_architecture'. Must match those specified in the config(s) 233 used in this DependencyManager. 234 235 Returns: The dependency_info for |dependency| on |platform| if it exists. 236 Or the default version of |dependency| if it exists, or None if neither 237 exist. 238 """ 239 if not self._lookup_dict or dependency not in self._lookup_dict: 240 return None 241 dependency_dict = self._lookup_dict[dependency] 242 device_type = platform 243 if not device_type in dependency_dict: 244 device_type = DEFAULT_TYPE 245 return dependency_dict.get(device_type) 246 247