| # Copyright 1998-2014 Gentoo Foundation |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| import errno |
| import json |
| import logging |
| import stat |
| import sys |
| |
| try: |
| import cPickle as pickle |
| except ImportError: |
| import pickle |
| |
| from portage import os |
| from portage import _encodings |
| from portage import _os_merge |
| from portage import _unicode_decode |
| from portage import _unicode_encode |
| from portage.exception import PermissionDenied |
| from portage.localization import _ |
| from portage.util import atomic_ofstream |
| from portage.util import writemsg_level |
| from portage.versions import cpv_getkey |
| from portage.locks import lockfile, unlockfile |
| |
| if sys.hexversion >= 0x3000000: |
| # pylint: disable=W0622 |
| basestring = str |
| |
| class PreservedLibsRegistry(object): |
| """ This class handles the tracking of preserved library objects """ |
| |
| # JSON read support has been available since portage-2.2.0_alpha89. |
| _json_write = True |
| |
| _json_write_opts = { |
| "ensure_ascii": False, |
| "indent": "\t", |
| "sort_keys": True |
| } |
| if sys.hexversion < 0x30200F0: |
| # indent only supports int number of spaces |
| _json_write_opts["indent"] = 4 |
| |
| def __init__(self, root, filename): |
| """ |
| @param root: root used to check existence of paths in pruneNonExisting |
| @type root: String |
| @param filename: absolute path for saving the preserved libs records |
| @type filename: String |
| """ |
| self._root = root |
| self._filename = filename |
| self._data = None |
| self._lock = None |
| |
| def lock(self): |
| """Grab an exclusive lock on the preserved libs registry.""" |
| if self._lock is not None: |
| raise AssertionError("already locked") |
| self._lock = lockfile(self._filename) |
| |
| def unlock(self): |
| """Release our exclusive lock on the preserved libs registry.""" |
| if self._lock is None: |
| raise AssertionError("not locked") |
| unlockfile(self._lock) |
| self._lock = None |
| |
| def load(self): |
| """ Reload the registry data from file """ |
| self._data = None |
| f = None |
| content = None |
| try: |
| f = open(_unicode_encode(self._filename, |
| encoding=_encodings['fs'], errors='strict'), 'rb') |
| content = f.read() |
| except EnvironmentError as e: |
| if not hasattr(e, 'errno'): |
| raise |
| elif e.errno == errno.ENOENT: |
| pass |
| elif e.errno == PermissionDenied.errno: |
| raise PermissionDenied(self._filename) |
| else: |
| raise |
| finally: |
| if f is not None: |
| f.close() |
| |
| # content is empty if it's an empty lock file |
| if content: |
| try: |
| self._data = json.loads(_unicode_decode(content, |
| encoding=_encodings['repo.content'], errors='strict')) |
| except SystemExit: |
| raise |
| except Exception as e: |
| try: |
| self._data = pickle.loads(content) |
| except SystemExit: |
| raise |
| except Exception: |
| writemsg_level(_("!!! Error loading '%s': %s\n") % |
| (self._filename, e), level=logging.ERROR, |
| noiselevel=-1) |
| |
| if self._data is None: |
| self._data = {} |
| else: |
| for k, v in self._data.items(): |
| if isinstance(v, (list, tuple)) and len(v) == 3 and \ |
| isinstance(v[2], set): |
| # convert set to list, for write with JSONEncoder |
| self._data[k] = (v[0], v[1], list(v[2])) |
| |
| self._data_orig = self._data.copy() |
| self.pruneNonExisting() |
| |
| def store(self): |
| """ |
| Store the registry data to the file. The existing inode will be |
| replaced atomically, so if that inode is currently being used |
| for a lock then that lock will be rendered useless. Therefore, |
| it is important not to call this method until the current lock |
| is ready to be immediately released. |
| """ |
| if os.environ.get("SANDBOX_ON") == "1" or \ |
| self._data == self._data_orig: |
| return |
| try: |
| f = atomic_ofstream(self._filename, 'wb') |
| if self._json_write: |
| f.write(_unicode_encode( |
| json.dumps(self._data, **self._json_write_opts), |
| encoding=_encodings['repo.content'], errors='strict')) |
| else: |
| pickle.dump(self._data, f, protocol=2) |
| f.close() |
| except EnvironmentError as e: |
| if e.errno != PermissionDenied.errno: |
| writemsg_level("!!! %s %s\n" % (e, self._filename), |
| level=logging.ERROR, noiselevel=-1) |
| else: |
| self._data_orig = self._data.copy() |
| |
| def _normalize_counter(self, counter): |
| """ |
| For simplicity, normalize as a unicode string |
| and strip whitespace. This avoids the need for |
| int conversion and a possible ValueError resulting |
| from vardb corruption. |
| """ |
| if not isinstance(counter, basestring): |
| counter = str(counter) |
| return _unicode_decode(counter).strip() |
| |
| def register(self, cpv, slot, counter, paths): |
| """ Register new objects in the registry. If there is a record with the |
| same packagename (internally derived from cpv) and slot it is |
| overwritten with the new data. |
| @param cpv: package instance that owns the objects |
| @type cpv: CPV (as String) |
| @param slot: the value of SLOT of the given package instance |
| @type slot: String |
| @param counter: vdb counter value for the package instance |
| @type counter: String |
| @param paths: absolute paths of objects that got preserved during an update |
| @type paths: List |
| """ |
| cp = cpv_getkey(cpv) |
| cps = cp+":"+slot |
| counter = self._normalize_counter(counter) |
| if len(paths) == 0 and cps in self._data \ |
| and self._data[cps][0] == cpv and \ |
| self._normalize_counter(self._data[cps][1]) == counter: |
| del self._data[cps] |
| elif len(paths) > 0: |
| if isinstance(paths, set): |
| # convert set to list, for write with JSONEncoder |
| paths = list(paths) |
| self._data[cps] = (cpv, counter, paths) |
| |
| def unregister(self, cpv, slot, counter): |
| """ Remove a previous registration of preserved objects for the given package. |
| @param cpv: package instance whose records should be removed |
| @type cpv: CPV (as String) |
| @param slot: the value of SLOT of the given package instance |
| @type slot: String |
| """ |
| self.register(cpv, slot, counter, []) |
| |
| def pruneNonExisting(self): |
| """ Remove all records for objects that no longer exist on the filesystem. """ |
| |
| os = _os_merge |
| |
| for cps in list(self._data): |
| cpv, counter, _paths = self._data[cps] |
| |
| paths = [] |
| hardlinks = set() |
| symlinks = {} |
| for f in _paths: |
| f_abs = os.path.join(self._root, f.lstrip(os.sep)) |
| try: |
| lst = os.lstat(f_abs) |
| except OSError: |
| continue |
| if stat.S_ISLNK(lst.st_mode): |
| try: |
| symlinks[f] = os.readlink(f_abs) |
| except OSError: |
| continue |
| elif stat.S_ISREG(lst.st_mode): |
| hardlinks.add(f) |
| paths.append(f) |
| |
| # Only count symlinks as preserved if they still point to a hardink |
| # in the same directory, in order to handle cases where a tool such |
| # as eselect-opengl has updated the symlink to point to a hardlink |
| # in a different directory (see bug #406837). The unused hardlink |
| # is automatically found by _find_unused_preserved_libs, since the |
| # soname symlink no longer points to it. After the hardlink is |
| # removed by _remove_preserved_libs, it calls pruneNonExisting |
| # which eliminates the irrelevant symlink from the registry here. |
| for f, target in symlinks.items(): |
| if os.path.join(os.path.dirname(f), target) in hardlinks: |
| paths.append(f) |
| |
| if len(paths) > 0: |
| self._data[cps] = (cpv, counter, paths) |
| else: |
| del self._data[cps] |
| |
| def hasEntries(self): |
| """ Check if this registry contains any records. """ |
| if self._data is None: |
| self.load() |
| return len(self._data) > 0 |
| |
| def getPreservedLibs(self): |
| """ Return a mapping of packages->preserved objects. |
| @return mapping of package instances to preserved objects |
| @rtype Dict cpv->list-of-paths |
| """ |
| if self._data is None: |
| self.load() |
| rValue = {} |
| for cps in self._data: |
| rValue[self._data[cps][0]] = self._data[cps][2] |
| return rValue |