| # Copyright 1999-2020 Gentoo Authors |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| import errno |
| from portage.util import writemsg |
| from portage.data import secpass |
| import portage |
| from portage import os |
| import pickle |
| |
| |
| class BlockerCache(portage.cache.mappings.MutableMapping): |
| """This caches blockers of installed packages so that dep_check does not |
| have to be done for every single installed package on every invocation of |
| emerge. The cache is invalidated whenever it is detected that something |
| has changed that might alter the results of dep_check() calls: |
| 1) the set of installed packages (including COUNTER) has changed |
| """ |
| |
| # Number of uncached packages to trigger cache update, since |
| # it's wasteful to update it for every vdb change. |
| _cache_threshold = 5 |
| |
| class BlockerData: |
| |
| __slots__ = ("__weakref__", "atoms", "counter") |
| |
| def __init__(self, counter, atoms): |
| self.counter = counter |
| self.atoms = atoms |
| |
| def __init__(self, myroot, vardb): |
| """myroot is ignored in favour of EROOT""" |
| self._vardb = vardb |
| self._cache_filename = os.path.join( |
| vardb.settings["EROOT"], portage.CACHE_PATH, "vdb_blockers.pickle" |
| ) |
| self._cache_version = "1" |
| self._cache_data = None |
| self._modified = set() |
| self._load() |
| |
| def _load(self): |
| try: |
| f = open(self._cache_filename, mode="rb") |
| mypickle = pickle.Unpickler(f) |
| try: |
| mypickle.find_global = None |
| except AttributeError: |
| # TODO: If py3k, override Unpickler.find_class(). |
| pass |
| self._cache_data = mypickle.load() |
| f.close() |
| del f |
| except (SystemExit, KeyboardInterrupt): |
| raise |
| except Exception as e: |
| if isinstance(e, EnvironmentError) and getattr(e, "errno", None) in ( |
| errno.ENOENT, |
| errno.EACCES, |
| ): |
| pass |
| else: |
| writemsg( |
| "!!! Error loading '%s': %s\n" % (self._cache_filename, str(e)), |
| noiselevel=-1, |
| ) |
| del e |
| |
| cache_valid = ( |
| self._cache_data |
| and isinstance(self._cache_data, dict) |
| and self._cache_data.get("version") == self._cache_version |
| and isinstance(self._cache_data.get("blockers"), dict) |
| ) |
| if cache_valid: |
| # Validate all the atoms and counters so that |
| # corruption is detected as soon as possible. |
| invalid_items = set() |
| for k, v in self._cache_data["blockers"].items(): |
| if not isinstance(k, str): |
| invalid_items.add(k) |
| continue |
| try: |
| if portage.catpkgsplit(k) is None: |
| invalid_items.add(k) |
| continue |
| except portage.exception.InvalidData: |
| invalid_items.add(k) |
| continue |
| if not isinstance(v, tuple) or len(v) != 2: |
| invalid_items.add(k) |
| continue |
| counter, atoms = v |
| if not isinstance(counter, int): |
| invalid_items.add(k) |
| continue |
| if not isinstance(atoms, (list, tuple)): |
| invalid_items.add(k) |
| continue |
| invalid_atom = False |
| for atom in atoms: |
| if not isinstance(atom, str): |
| invalid_atom = True |
| break |
| if atom[:1] != "!" or not portage.isvalidatom( |
| atom, allow_blockers=True |
| ): |
| invalid_atom = True |
| break |
| if invalid_atom: |
| invalid_items.add(k) |
| continue |
| |
| for k in invalid_items: |
| del self._cache_data["blockers"][k] |
| if not self._cache_data["blockers"]: |
| cache_valid = False |
| |
| if not cache_valid: |
| self._cache_data = {"version": self._cache_version} |
| self._cache_data["blockers"] = {} |
| self._modified.clear() |
| |
| def flush(self): |
| """If the current user has permission and the internal blocker cache has |
| been updated, save it to disk and mark it unmodified. This is called |
| by emerge after it has processed blockers for all installed packages. |
| Currently, the cache is only written if the user has superuser |
| privileges (since that's required to obtain a lock), but all users |
| have read access and benefit from faster blocker lookups (as long as |
| the entire cache is still valid). The cache is stored as a pickled |
| dict object with the following format: |
| |
| { |
| version : "1", |
| "blockers" : {cpv1:(counter,(atom1, atom2...)), cpv2...}, |
| } |
| """ |
| if len(self._modified) >= self._cache_threshold and secpass >= 2: |
| try: |
| with portage.util.atomic_ofstream(self._cache_filename, mode="wb") as f: |
| pickle.dump(self._cache_data, f, protocol=2) |
| |
| portage.util.apply_secpass_permissions( |
| self._cache_filename, gid=portage.portage_gid, mode=0o644 |
| ) |
| except (IOError, OSError): |
| pass |
| self._modified.clear() |
| |
| def __setitem__(self, cpv, blocker_data): |
| """ |
| Update the cache and mark it as modified for a future call to |
| self.flush(). |
| |
| @param cpv: Package for which to cache blockers. |
| @type cpv: String |
| @param blocker_data: An object with counter and atoms attributes. |
| @type blocker_data: BlockerData |
| """ |
| self._cache_data["blockers"][str(cpv)] = ( |
| blocker_data.counter, |
| tuple(str(x) for x in blocker_data.atoms), |
| ) |
| self._modified.add(cpv) |
| |
| def __iter__(self): |
| if self._cache_data is None: |
| # triggered by python-trace |
| return iter([]) |
| return iter(self._cache_data["blockers"]) |
| |
| def __len__(self): |
| """This needs to be implemented in order to avoid |
| infinite recursion in some cases.""" |
| return len(self._cache_data["blockers"]) |
| |
| def __delitem__(self, cpv): |
| del self._cache_data["blockers"][cpv] |
| |
| def __getitem__(self, cpv): |
| """ |
| @rtype: BlockerData |
| @return: An object with counter and atoms attributes. |
| """ |
| return self.BlockerData(*self._cache_data["blockers"][cpv]) |