| #!/usr/bin/python -O |
| # vim: noet : |
| |
| from __future__ import print_function |
| |
| import errno |
| import re |
| import signal |
| import stat |
| import sys |
| import textwrap |
| import time |
| from optparse import OptionParser, OptionValueError |
| |
| try: |
| import portage |
| except ImportError: |
| from os import path as osp |
| sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym")) |
| import portage |
| |
| from portage import os |
| from portage.util import writemsg |
| |
| if sys.hexversion >= 0x3000000: |
| long = int |
| |
| class WorldHandler(object): |
| |
| short_desc = "Fix problems in the world file" |
| |
| def name(): |
| return "world" |
| name = staticmethod(name) |
| |
| def __init__(self): |
| self.invalid = [] |
| self.not_installed = [] |
| self.invalid_category = [] |
| self.okay = [] |
| from portage._sets import load_default_config |
| setconfig = load_default_config(portage.settings, |
| portage.db[portage.settings['EROOT']]) |
| self._sets = setconfig.getSets() |
| |
| def _check_world(self, onProgress): |
| categories = set(portage.settings.categories) |
| eroot = portage.settings['EROOT'] |
| self.world_file = os.path.join(eroot, portage.const.WORLD_FILE) |
| self.found = os.access(self.world_file, os.R_OK) |
| vardb = portage.db[eroot]["vartree"].dbapi |
| |
| from portage._sets import SETPREFIX |
| sets = self._sets |
| world_atoms = list(sets["selected"]) |
| maxval = len(world_atoms) |
| if onProgress: |
| onProgress(maxval, 0) |
| for i, atom in enumerate(world_atoms): |
| if not isinstance(atom, portage.dep.Atom): |
| if atom.startswith(SETPREFIX): |
| s = atom[len(SETPREFIX):] |
| if s in sets: |
| self.okay.append(atom) |
| else: |
| self.not_installed.append(atom) |
| else: |
| self.invalid.append(atom) |
| if onProgress: |
| onProgress(maxval, i+1) |
| continue |
| okay = True |
| if not vardb.match(atom): |
| self.not_installed.append(atom) |
| okay = False |
| if portage.catsplit(atom.cp)[0] not in categories: |
| self.invalid_category.append(atom) |
| okay = False |
| if okay: |
| self.okay.append(atom) |
| if onProgress: |
| onProgress(maxval, i+1) |
| |
| def check(self, onProgress=None): |
| self._check_world(onProgress) |
| errors = [] |
| if self.found: |
| errors += ["'%s' is not a valid atom" % x for x in self.invalid] |
| errors += ["'%s' is not installed" % x for x in self.not_installed] |
| errors += ["'%s' has a category that is not listed in /etc/portage/categories" % x for x in self.invalid_category] |
| else: |
| errors.append(self.world_file + " could not be opened for reading") |
| return errors |
| |
| def fix(self, onProgress=None): |
| world_set = self._sets["selected"] |
| world_set.lock() |
| try: |
| world_set.load() # maybe it's changed on disk |
| before = set(world_set) |
| self._check_world(onProgress) |
| after = set(self.okay) |
| errors = [] |
| if before != after: |
| try: |
| world_set.replace(self.okay) |
| except portage.exception.PortageException: |
| errors.append("%s could not be opened for writing" % \ |
| self.world_file) |
| return errors |
| finally: |
| world_set.unlock() |
| |
| class BinhostHandler(object): |
| |
| short_desc = "Generate a metadata index for binary packages" |
| |
| def name(): |
| return "binhost" |
| name = staticmethod(name) |
| |
| def __init__(self): |
| eroot = portage.settings['EROOT'] |
| self._bintree = portage.db[eroot]["bintree"] |
| self._bintree.populate() |
| self._pkgindex_file = self._bintree._pkgindex_file |
| self._pkgindex = self._bintree._load_pkgindex() |
| |
| def _need_update(self, cpv, data): |
| |
| if "MD5" not in data: |
| return True |
| |
| size = data.get("SIZE") |
| if size is None: |
| return True |
| |
| mtime = data.get("MTIME") |
| if mtime is None: |
| return True |
| |
| pkg_path = self._bintree.getname(cpv) |
| try: |
| s = os.lstat(pkg_path) |
| except OSError as e: |
| if e.errno not in (errno.ENOENT, errno.ESTALE): |
| raise |
| # We can't update the index for this one because |
| # it disappeared. |
| return False |
| |
| try: |
| if long(mtime) != s[stat.ST_MTIME]: |
| return True |
| if long(size) != long(s.st_size): |
| return True |
| except ValueError: |
| return True |
| |
| return False |
| |
| def check(self, onProgress=None): |
| missing = [] |
| cpv_all = self._bintree.dbapi.cpv_all() |
| cpv_all.sort() |
| maxval = len(cpv_all) |
| if onProgress: |
| onProgress(maxval, 0) |
| pkgindex = self._pkgindex |
| missing = [] |
| metadata = {} |
| for d in pkgindex.packages: |
| metadata[d["CPV"]] = d |
| for i, cpv in enumerate(cpv_all): |
| d = metadata.get(cpv) |
| if not d or self._need_update(cpv, d): |
| missing.append(cpv) |
| if onProgress: |
| onProgress(maxval, i+1) |
| errors = ["'%s' is not in Packages" % cpv for cpv in missing] |
| stale = set(metadata).difference(cpv_all) |
| for cpv in stale: |
| errors.append("'%s' is not in the repository" % cpv) |
| return errors |
| |
| def fix(self, onProgress=None): |
| bintree = self._bintree |
| cpv_all = self._bintree.dbapi.cpv_all() |
| cpv_all.sort() |
| missing = [] |
| maxval = 0 |
| if onProgress: |
| onProgress(maxval, 0) |
| pkgindex = self._pkgindex |
| missing = [] |
| metadata = {} |
| for d in pkgindex.packages: |
| metadata[d["CPV"]] = d |
| |
| for i, cpv in enumerate(cpv_all): |
| d = metadata.get(cpv) |
| if not d or self._need_update(cpv, d): |
| missing.append(cpv) |
| |
| stale = set(metadata).difference(cpv_all) |
| if missing or stale: |
| from portage import locks |
| pkgindex_lock = locks.lockfile( |
| self._pkgindex_file, wantnewlockfile=1) |
| try: |
| # Repopulate with lock held. |
| bintree._populate() |
| cpv_all = self._bintree.dbapi.cpv_all() |
| cpv_all.sort() |
| |
| pkgindex = bintree._load_pkgindex() |
| self._pkgindex = pkgindex |
| |
| metadata = {} |
| for d in pkgindex.packages: |
| metadata[d["CPV"]] = d |
| |
| # Recount missing packages, with lock held. |
| del missing[:] |
| for i, cpv in enumerate(cpv_all): |
| d = metadata.get(cpv) |
| if not d or self._need_update(cpv, d): |
| missing.append(cpv) |
| |
| maxval = len(missing) |
| for i, cpv in enumerate(missing): |
| try: |
| metadata[cpv] = bintree._pkgindex_entry(cpv) |
| except portage.exception.InvalidDependString: |
| writemsg("!!! Invalid binary package: '%s'\n" % \ |
| bintree.getname(cpv), noiselevel=-1) |
| |
| if onProgress: |
| onProgress(maxval, i+1) |
| |
| for cpv in set(metadata).difference( |
| self._bintree.dbapi.cpv_all()): |
| del metadata[cpv] |
| |
| # We've updated the pkgindex, so set it to |
| # repopulate when necessary. |
| bintree.populated = False |
| |
| del pkgindex.packages[:] |
| pkgindex.packages.extend(metadata.values()) |
| from portage.util import atomic_ofstream |
| f = atomic_ofstream(self._pkgindex_file) |
| try: |
| self._pkgindex.write(f) |
| finally: |
| f.close() |
| finally: |
| locks.unlockfile(pkgindex_lock) |
| |
| if onProgress: |
| if maxval == 0: |
| maxval = 1 |
| onProgress(maxval, maxval) |
| return None |
| |
| class MoveHandler(object): |
| |
| def __init__(self, tree, porttree): |
| self._tree = tree |
| self._portdb = porttree.dbapi |
| self._update_keys = ["DEPEND", "RDEPEND", "PDEPEND", "PROVIDE"] |
| self._master_repo = \ |
| self._portdb.getRepositoryName(self._portdb.porttree_root) |
| |
| def _grab_global_updates(self): |
| from portage.update import grab_updates, parse_updates |
| retupdates = {} |
| errors = [] |
| |
| for repo_name in self._portdb.getRepositories(): |
| repo = self._portdb.getRepositoryPath(repo_name) |
| updpath = os.path.join(repo, "profiles", "updates") |
| if not os.path.isdir(updpath): |
| continue |
| |
| try: |
| rawupdates = grab_updates(updpath) |
| except portage.exception.DirectoryNotFound: |
| rawupdates = [] |
| upd_commands = [] |
| for mykey, mystat, mycontent in rawupdates: |
| commands, errors = parse_updates(mycontent) |
| upd_commands.extend(commands) |
| errors.extend(errors) |
| retupdates[repo_name] = upd_commands |
| |
| if self._master_repo in retupdates: |
| retupdates['DEFAULT'] = retupdates[self._master_repo] |
| |
| return retupdates, errors |
| |
| def check(self, onProgress=None): |
| allupdates, errors = self._grab_global_updates() |
| # Matching packages and moving them is relatively fast, so the |
| # progress bar is updated in indeterminate mode. |
| match = self._tree.dbapi.match |
| aux_get = self._tree.dbapi.aux_get |
| if onProgress: |
| onProgress(0, 0) |
| for repo, updates in allupdates.items(): |
| if repo == 'DEFAULT': |
| continue |
| if not updates: |
| continue |
| |
| def repo_match(repository): |
| return repository == repo or \ |
| (repo == self._master_repo and \ |
| repository not in allupdates) |
| |
| for i, update_cmd in enumerate(updates): |
| if update_cmd[0] == "move": |
| origcp, newcp = update_cmd[1:] |
| for cpv in match(origcp): |
| if repo_match(aux_get(cpv, ["repository"])[0]): |
| errors.append("'%s' moved to '%s'" % (cpv, newcp)) |
| elif update_cmd[0] == "slotmove": |
| pkg, origslot, newslot = update_cmd[1:] |
| for cpv in match(pkg): |
| slot, prepo = aux_get(cpv, ["SLOT", "repository"]) |
| if slot == origslot and repo_match(prepo): |
| errors.append("'%s' slot moved from '%s' to '%s'" % \ |
| (cpv, origslot, newslot)) |
| if onProgress: |
| onProgress(0, 0) |
| |
| # Searching for updates in all the metadata is relatively slow, so this |
| # is where the progress bar comes out of indeterminate mode. |
| cpv_all = self._tree.dbapi.cpv_all() |
| cpv_all.sort() |
| maxval = len(cpv_all) |
| aux_update = self._tree.dbapi.aux_update |
| meta_keys = self._update_keys + ['repository'] |
| from portage.update import update_dbentries |
| if onProgress: |
| onProgress(maxval, 0) |
| for i, cpv in enumerate(cpv_all): |
| metadata = dict(zip(meta_keys, aux_get(cpv, meta_keys))) |
| repository = metadata.pop('repository') |
| try: |
| updates = allupdates[repository] |
| except KeyError: |
| try: |
| updates = allupdates['DEFAULT'] |
| except KeyError: |
| continue |
| if not updates: |
| continue |
| metadata_updates = update_dbentries(updates, metadata) |
| if metadata_updates: |
| errors.append("'%s' has outdated metadata" % cpv) |
| if onProgress: |
| onProgress(maxval, i+1) |
| return errors |
| |
| def fix(self, onProgress=None): |
| allupdates, errors = self._grab_global_updates() |
| # Matching packages and moving them is relatively fast, so the |
| # progress bar is updated in indeterminate mode. |
| move = self._tree.dbapi.move_ent |
| slotmove = self._tree.dbapi.move_slot_ent |
| if onProgress: |
| onProgress(0, 0) |
| for repo, updates in allupdates.items(): |
| if repo == 'DEFAULT': |
| continue |
| if not updates: |
| continue |
| |
| def repo_match(repository): |
| return repository == repo or \ |
| (repo == self._master_repo and \ |
| repository not in allupdates) |
| |
| for i, update_cmd in enumerate(updates): |
| if update_cmd[0] == "move": |
| move(update_cmd, repo_match=repo_match) |
| elif update_cmd[0] == "slotmove": |
| slotmove(update_cmd, repo_match=repo_match) |
| if onProgress: |
| onProgress(0, 0) |
| |
| # Searching for updates in all the metadata is relatively slow, so this |
| # is where the progress bar comes out of indeterminate mode. |
| self._tree.dbapi.update_ents(allupdates, onProgress=onProgress) |
| return errors |
| |
| class MoveInstalled(MoveHandler): |
| |
| short_desc = "Perform package move updates for installed packages" |
| |
| def name(): |
| return "moveinst" |
| name = staticmethod(name) |
| def __init__(self): |
| eroot = portage.settings['EROOT'] |
| MoveHandler.__init__(self, portage.db[eroot]["vartree"], portage.db[eroot]["porttree"]) |
| |
| class MoveBinary(MoveHandler): |
| |
| short_desc = "Perform package move updates for binary packages" |
| |
| def name(): |
| return "movebin" |
| name = staticmethod(name) |
| def __init__(self): |
| eroot = portage.settings['EROOT'] |
| MoveHandler.__init__(self, portage.db[eroot]["bintree"], portage.db[eroot]['porttree']) |
| |
| class VdbKeyHandler(object): |
| def name(): |
| return "vdbkeys" |
| name = staticmethod(name) |
| |
| def __init__(self): |
| self.list = portage.db[portage.settings["EROOT"]]["vartree"].dbapi.cpv_all() |
| self.missing = [] |
| self.keys = ["HOMEPAGE", "SRC_URI", "KEYWORDS", "DESCRIPTION"] |
| |
| for p in self.list: |
| mydir = os.path.join(portage.settings["EROOT"], portage.const.VDB_PATH, p)+os.sep |
| ismissing = True |
| for k in self.keys: |
| if os.path.exists(mydir+k): |
| ismissing = False |
| break |
| if ismissing: |
| self.missing.append(p) |
| |
| def check(self): |
| return ["%s has missing keys" % x for x in self.missing] |
| |
| def fix(self): |
| |
| errors = [] |
| |
| for p in self.missing: |
| mydir = os.path.join(portage.settings["EROOT"], portage.const.VDB_PATH, p)+os.sep |
| if not os.access(mydir+"environment.bz2", os.R_OK): |
| errors.append("Can't access %s" % (mydir+"environment.bz2")) |
| elif not os.access(mydir, os.W_OK): |
| errors.append("Can't create files in %s" % mydir) |
| else: |
| env = os.popen("bzip2 -dcq "+mydir+"environment.bz2", "r") |
| envlines = env.read().split("\n") |
| env.close() |
| for k in self.keys: |
| s = [l for l in envlines if l.startswith(k+"=")] |
| if len(s) > 1: |
| errors.append("multiple matches for %s found in %senvironment.bz2" % (k, mydir)) |
| elif len(s) == 0: |
| s = "" |
| else: |
| s = s[0].split("=",1)[1] |
| s = s.lstrip("$").strip("\'\"") |
| s = re.sub("(\\\\[nrt])+", " ", s) |
| s = " ".join(s.split()).strip() |
| if s != "": |
| try: |
| keyfile = open(mydir+os.sep+k, "w") |
| keyfile.write(s+"\n") |
| keyfile.close() |
| except (IOError, OSError) as e: |
| errors.append("Could not write %s, reason was: %s" % (mydir+k, e)) |
| |
| return errors |
| |
| class ProgressHandler(object): |
| def __init__(self): |
| self.curval = 0 |
| self.maxval = 0 |
| self.last_update = 0 |
| self.min_display_latency = 0.2 |
| |
| def onProgress(self, maxval, curval): |
| self.maxval = maxval |
| self.curval = curval |
| cur_time = time.time() |
| if cur_time - self.last_update >= self.min_display_latency: |
| self.last_update = cur_time |
| self.display() |
| |
| def display(self): |
| raise NotImplementedError(self) |
| |
| class CleanResume(object): |
| |
| short_desc = "Discard emerge --resume merge lists" |
| |
| def name(): |
| return "cleanresume" |
| name = staticmethod(name) |
| |
| def check(self, onProgress=None): |
| messages = [] |
| mtimedb = portage.mtimedb |
| resume_keys = ("resume", "resume_backup") |
| maxval = len(resume_keys) |
| if onProgress: |
| onProgress(maxval, 0) |
| for i, k in enumerate(resume_keys): |
| try: |
| d = mtimedb.get(k) |
| if d is None: |
| continue |
| if not isinstance(d, dict): |
| messages.append("unrecognized resume list: '%s'" % k) |
| continue |
| mergelist = d.get("mergelist") |
| if mergelist is None or not hasattr(mergelist, "__len__"): |
| messages.append("unrecognized resume list: '%s'" % k) |
| continue |
| messages.append("resume list '%s' contains %d packages" % \ |
| (k, len(mergelist))) |
| finally: |
| if onProgress: |
| onProgress(maxval, i+1) |
| return messages |
| |
| def fix(self, onProgress=None): |
| delete_count = 0 |
| mtimedb = portage.mtimedb |
| resume_keys = ("resume", "resume_backup") |
| maxval = len(resume_keys) |
| if onProgress: |
| onProgress(maxval, 0) |
| for i, k in enumerate(resume_keys): |
| try: |
| if mtimedb.pop(k, None) is not None: |
| delete_count += 1 |
| finally: |
| if onProgress: |
| onProgress(maxval, i+1) |
| if delete_count: |
| mtimedb.commit() |
| |
| def emaint_main(myargv): |
| |
| # Similar to emerge, emaint needs a default umask so that created |
| # files (such as the world file) have sane permissions. |
| os.umask(0o22) |
| |
| # TODO: Create a system that allows external modules to be added without |
| # the need for hard coding. |
| modules = { |
| "world" : WorldHandler, |
| "binhost":BinhostHandler, |
| "moveinst":MoveInstalled, |
| "movebin":MoveBinary, |
| "cleanresume":CleanResume |
| } |
| |
| module_names = list(modules) |
| module_names.sort() |
| module_names.insert(0, "all") |
| |
| def exclusive(option, *args, **kw): |
| var = kw.get("var", None) |
| if var is None: |
| raise ValueError("var not specified to exclusive()") |
| if getattr(parser, var, ""): |
| raise OptionValueError("%s and %s are exclusive options" % (getattr(parser, var), option)) |
| setattr(parser, var, str(option)) |
| |
| |
| usage = "usage: emaint [options] COMMAND" |
| |
| desc = "The emaint program provides an interface to system health " + \ |
| "checks and maintenance. See the emaint(1) man page " + \ |
| "for additional information about the following commands:" |
| |
| usage += "\n\n" |
| for line in textwrap.wrap(desc, 65): |
| usage += "%s\n" % line |
| usage += "\n" |
| usage += " %s" % "all".ljust(15) + \ |
| "Perform all supported commands\n" |
| for m in module_names[1:]: |
| usage += " %s%s\n" % (m.ljust(15), modules[m].short_desc) |
| |
| parser = OptionParser(usage=usage, version=portage.VERSION) |
| parser.add_option("-c", "--check", help="check for problems", |
| action="callback", callback=exclusive, callback_kwargs={"var":"action"}) |
| parser.add_option("-f", "--fix", help="attempt to fix problems", |
| action="callback", callback=exclusive, callback_kwargs={"var":"action"}) |
| parser.action = None |
| |
| |
| (options, args) = parser.parse_args(args=myargv) |
| if len(args) != 1: |
| parser.error("Incorrect number of arguments") |
| if args[0] not in module_names: |
| parser.error("%s target is not a known target" % args[0]) |
| |
| if parser.action: |
| action = parser.action |
| else: |
| print("Defaulting to --check") |
| action = "-c/--check" |
| |
| if args[0] == "all": |
| tasks = modules.values() |
| else: |
| tasks = [modules[args[0]]] |
| |
| |
| if action == "-c/--check": |
| status = "Checking %s for problems" |
| func = "check" |
| else: |
| status = "Attempting to fix %s" |
| func = "fix" |
| |
| isatty = os.environ.get('TERM') != 'dumb' and sys.stdout.isatty() |
| for task in tasks: |
| print(status % task.name()) |
| inst = task() |
| onProgress = None |
| if isatty: |
| progressBar = portage.output.TermProgressBar() |
| progressHandler = ProgressHandler() |
| onProgress = progressHandler.onProgress |
| def display(): |
| progressBar.set(progressHandler.curval, progressHandler.maxval) |
| progressHandler.display = display |
| def sigwinch_handler(signum, frame): |
| lines, progressBar.term_columns = \ |
| portage.output.get_term_size() |
| signal.signal(signal.SIGWINCH, sigwinch_handler) |
| result = getattr(inst, func)(onProgress=onProgress) |
| if isatty: |
| # make sure the final progress is displayed |
| progressHandler.display() |
| print() |
| signal.signal(signal.SIGWINCH, signal.SIG_DFL) |
| if result: |
| print() |
| print("\n".join(result)) |
| print("\n") |
| |
| print("Finished") |
| |
| if __name__ == "__main__": |
| emaint_main(sys.argv[1:]) |