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 sys 6 7 from docs_server_utils import ToUnicode 8 from file_system import FileNotFoundError 9 from future import Future 10 from path_util import AssertIsDirectory, AssertIsFile, ToDirectory 11 from third_party.json_schema_compiler import json_parse 12 from third_party.json_schema_compiler.memoize import memoize 13 from third_party.motemplate import Motemplate 14 15 16 _CACHEABLE_FUNCTIONS = set() 17 _SINGLE_FILE_FUNCTIONS = set() 18 19 20 def _GetUnboundFunction(fn): 21 '''Functions bound to an object are separate from the unbound 22 defintion. This causes issues when checking for cache membership, 23 so always get the unbound function, if possible. 24 ''' 25 return getattr(fn, 'im_func', fn) 26 27 28 def Cache(fn): 29 '''A decorator which can be applied to the compilation function 30 passed to CompiledFileSystem.Create, indicating that file/list data 31 should be cached. 32 33 This decorator should be listed first in any list of decorators, along 34 with the SingleFile decorator below. 35 ''' 36 _CACHEABLE_FUNCTIONS.add(_GetUnboundFunction(fn)) 37 return fn 38 39 40 def SingleFile(fn): 41 '''A decorator which can be optionally applied to the compilation function 42 passed to CompiledFileSystem.Create, indicating that the function only 43 needs access to the file which is given in the function's callback. When 44 this is the case some optimisations can be done. 45 46 Note that this decorator must be listed first in any list of decorators to 47 have any effect. 48 ''' 49 _SINGLE_FILE_FUNCTIONS.add(_GetUnboundFunction(fn)) 50 return fn 51 52 53 def Unicode(fn): 54 '''A decorator which can be optionally applied to the compilation function 55 passed to CompiledFileSystem.Create, indicating that the function processes 56 the file's data as Unicode text. 57 ''' 58 59 # The arguments passed to fn can be (self, path, data) or (path, data). In 60 # either case the last argument is |data|, which should be converted to 61 # Unicode. 62 def convert_args(args): 63 args = list(args) 64 args[-1] = ToUnicode(args[-1]) 65 return args 66 67 return lambda *args: fn(*convert_args(args)) 68 69 70 class _CacheEntry(object): 71 def __init__(self, cache_data, version): 72 73 self.cache_data = cache_data 74 self.version = version 75 76 77 class CompiledFileSystem(object): 78 '''This class caches FileSystem data that has been processed. 79 ''' 80 81 class Factory(object): 82 '''A class to build a CompiledFileSystem backed by |file_system|. 83 ''' 84 85 def __init__(self, object_store_creator): 86 self._object_store_creator = object_store_creator 87 88 def Create(self, file_system, compilation_function, cls, category=None): 89 '''Creates a CompiledFileSystem view over |file_system| that populates 90 its cache by calling |compilation_function| with (path, data), where 91 |data| is the data that was fetched from |path| in |file_system|. 92 93 The namespace for the compiled file system is derived similar to 94 ObjectStoreCreator: from |cls| along with an optional |category|. 95 ''' 96 assert isinstance(cls, type) 97 assert not cls.__name__[0].islower() # guard against non-class types 98 full_name = [cls.__name__, file_system.GetIdentity()] 99 if category is not None: 100 full_name.append(category) 101 def create_object_store(my_category): 102 # The read caches can start populated (start_empty=False) because file 103 # updates are picked up by the stat - but only if the compilation 104 # function is affected by a single file. If the compilation function is 105 # affected by other files (e.g. compiling a list of APIs available to 106 # extensions may be affected by both a features file and the list of 107 # files in the API directory) then this optimisation won't work. 108 return self._object_store_creator.Create( 109 CompiledFileSystem, 110 category='/'.join(full_name + [my_category]), 111 start_empty=compilation_function not in _SINGLE_FILE_FUNCTIONS) 112 return CompiledFileSystem(file_system, 113 compilation_function, 114 create_object_store('file'), 115 create_object_store('list')) 116 117 @memoize 118 def ForJson(self, file_system): 119 '''A CompiledFileSystem specifically for parsing JSON configuration data. 120 These are memoized over file systems tied to different branches. 121 ''' 122 return self.Create(file_system, 123 Cache(SingleFile(lambda _, data: 124 json_parse.Parse(ToUnicode(data)))), 125 CompiledFileSystem, 126 category='json') 127 128 @memoize 129 def ForTemplates(self, file_system): 130 '''Creates a CompiledFileSystem for parsing templates. 131 ''' 132 return self.Create( 133 file_system, 134 SingleFile(lambda path, text: Motemplate(ToUnicode(text), name=path)), 135 CompiledFileSystem) 136 137 @memoize 138 def ForUnicode(self, file_system): 139 '''Creates a CompiledFileSystem for Unicode text processing. 140 ''' 141 return self.Create( 142 file_system, 143 SingleFile(lambda _, text: ToUnicode(text)), 144 CompiledFileSystem, 145 category='text') 146 147 def __init__(self, 148 file_system, 149 compilation_function, 150 file_object_store, 151 list_object_store): 152 self._file_system = file_system 153 self._compilation_function = compilation_function 154 self._file_object_store = file_object_store 155 self._list_object_store = list_object_store 156 157 def _Get(self, store, key): 158 if _GetUnboundFunction(self._compilation_function) in _CACHEABLE_FUNCTIONS: 159 return store.Get(key) 160 return Future(value=None) 161 162 def _Set(self, store, key, value): 163 if _GetUnboundFunction(self._compilation_function) in _CACHEABLE_FUNCTIONS: 164 store.Set(key, value) 165 166 def _RecursiveList(self, path): 167 '''Returns a Future containing the recursive directory listing of |path| as 168 a flat list of paths. 169 ''' 170 def split_dirs_from_files(paths): 171 '''Returns a tuple (dirs, files) where |dirs| contains the directory 172 names in |paths| and |files| contains the files. 173 ''' 174 result = [], [] 175 for path in paths: 176 result[0 if path.endswith('/') else 1].append(path) 177 return result 178 179 def add_prefix(prefix, paths): 180 return [prefix + path for path in paths] 181 182 # Read in the initial list of files. Do this eagerly (i.e. not part of the 183 # asynchronous Future contract) because there's a greater chance to 184 # parallelise fetching with the second layer (can fetch multiple paths). 185 try: 186 first_layer_dirs, first_layer_files = split_dirs_from_files( 187 self._file_system.ReadSingle(path).Get()) 188 except FileNotFoundError: 189 return Future(exc_info=sys.exc_info()) 190 191 if not first_layer_dirs: 192 return Future(value=first_layer_files) 193 194 def get_from_future_listing(listings): 195 '''Recursively lists files from directory listing |futures|. 196 ''' 197 dirs, files = [], [] 198 for dir_name, listing in listings.iteritems(): 199 new_dirs, new_files = split_dirs_from_files(listing) 200 # |dirs| are paths for reading. Add the full prefix relative to 201 # |path| so that |file_system| can find the files. 202 dirs += add_prefix(dir_name, new_dirs) 203 # |files| are not for reading, they are for returning to the caller. 204 # This entire function set (i.e. GetFromFileListing) is defined to 205 # not include the fetched-path in the result, however, |dir_name| 206 # will be prefixed with |path|. Strip it. 207 assert dir_name.startswith(path) 208 files += add_prefix(dir_name[len(path):], new_files) 209 if dirs: 210 files += self._file_system.Read(dirs).Then( 211 get_from_future_listing).Get() 212 return files 213 214 return self._file_system.Read(add_prefix(path, first_layer_dirs)).Then( 215 lambda results: first_layer_files + get_from_future_listing(results)) 216 217 def GetFromFile(self, path, skip_not_found=False): 218 '''Calls |compilation_function| on the contents of the file at |path|. 219 If |skip_not_found| is True, then None is passed to |compilation_function|. 220 ''' 221 AssertIsFile(path) 222 223 try: 224 version = self._file_system.Stat(path).version 225 except FileNotFoundError: 226 if skip_not_found: 227 version = None 228 else: 229 return Future(exc_info=sys.exc_info()) 230 231 cache_entry = self._Get(self._file_object_store, path).Get() 232 if (cache_entry is not None) and (version == cache_entry.version): 233 return Future(value=cache_entry.cache_data) 234 235 def compile_(files): 236 cache_data = self._compilation_function(path, files) 237 self._Set(self._file_object_store, path, _CacheEntry(cache_data, version)) 238 return cache_data 239 240 return self._file_system.ReadSingle( 241 path, skip_not_found=skip_not_found).Then(compile_) 242 243 def GetFromFileListing(self, path): 244 '''Calls |compilation_function| on the listing of the files at |path|. 245 Assumes that the path given is to a directory. 246 ''' 247 AssertIsDirectory(path) 248 249 try: 250 version = self._file_system.Stat(path).version 251 except FileNotFoundError: 252 return Future(exc_info=sys.exc_info()) 253 254 cache_entry = self._Get(self._list_object_store, path).Get() 255 if (cache_entry is not None) and (version == cache_entry.version): 256 return Future(value=cache_entry.cache_data) 257 258 def compile_(files): 259 cache_data = self._compilation_function(path, files) 260 self._Set(self._list_object_store, path, _CacheEntry(cache_data, version)) 261 return cache_data 262 return self._RecursiveList(path).Then(compile_) 263 264 # _GetFileVersionFromCache and _GetFileListingVersionFromCache are exposed 265 # *only* so that ChainedCompiledFileSystem can optimise its caches. *Do not* 266 # use these methods otherwise, they don't do what you want. Use 267 # FileSystem.Stat on the FileSystem that this CompiledFileSystem uses. 268 269 def _GetFileVersionFromCache(self, path): 270 cache_entry = self._Get(self._file_object_store, path).Get() 271 if cache_entry is not None: 272 return Future(value=cache_entry.version) 273 stat_future = self._file_system.StatAsync(path) 274 return Future(callback=lambda: stat_future.Get().version) 275 276 def _GetFileListingVersionFromCache(self, path): 277 path = ToDirectory(path) 278 cache_entry = self._Get(self._list_object_store, path).Get() 279 if cache_entry is not None: 280 return Future(value=cache_entry.version) 281 stat_future = self._file_system.StatAsync(path) 282 return Future(callback=lambda: stat_future.Get().version) 283 284 def GetIdentity(self): 285 return self._file_system.GetIdentity() 286