bin/misc-functions.sh: use pipe for find ${D}${D}
[portage.git] / bin / emaint
index 407f8a66d7371926f8fc6eb1bd7e5f0edefe7654..f0e497822c066166500b53ae8b6de1dc3bd8d887 100755 (executable)
 #!/usr/bin/python -O
-# vim: noet :
+# Copyright 2005-2013 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+"""System health checks and maintenance utilities.
+"""
 
 from __future__ import print_function
 
-import re
-import signal
 import sys
-import textwrap
-import time
-from optparse import OptionParser, OptionValueError
-
+import errno
+# This block ensures that ^C interrupts are handled quietly.
 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
-
-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["ROOT"]])
-               self._sets = setconfig.getSets()
-
-       def _check_world(self, onProgress):
-               categories = set(portage.settings.categories)
-               myroot = portage.settings["ROOT"]
-               self.world_file = os.path.join(myroot, portage.const.WORLD_FILE)
-               self.found = os.access(self.world_file, os.R_OK)
-               vardb = portage.db[myroot]["vartree"].dbapi
-
-               from portage.sets import SETPREFIX
-               sets = self._sets
-               world_atoms = list(sets["world"])
-               maxval = len(world_atoms)
-               if onProgress:
-                       onProgress(maxval, 0)
-               for i, atom in enumerate(world_atoms):
-                       if not portage.isvalidatom(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
-                       cp = portage.dep_getkey(atom)
-                       okay = True
-                       if not vardb.match(atom):
-                               self.not_installed.append(atom)
-                               okay = False
-                       if portage.catsplit(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 += map(lambda x: "'%s' is not a valid atom" % x, self.invalid)
-                       errors += map(lambda x: "'%s' is not installed" % x, self.not_installed)
-                       errors += map(lambda x: "'%s' has a category that is not listed in /etc/portage/categories" % x, 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["world"]
-               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):
-               myroot = portage.settings["ROOT"]
-               self._bintree = portage.db[myroot]["bintree"]
-               self._bintree.populate()
-               self._pkgindex_file = self._bintree._pkgindex_file
-               self._pkgindex = self._bintree._load_pkgindex()
-
-       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 "MD5" not in 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 "MD5" not in 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 "MD5" not in 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.itervalues())
-                               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):
-               self._tree = tree
-               self._portdir = tree.settings["PORTDIR"]
-               self._update_keys = ["DEPEND", "RDEPEND", "PDEPEND", "PROVIDE"]
-
-       def _grab_global_updates(self, portdir):
-               from portage.update import grab_updates, parse_updates
-               updpath = os.path.join(portdir, "profiles", "updates")
-               try:
-                       rawupdates = grab_updates(updpath)
-               except portage.exception.DirectoryNotFound:
-                       rawupdates = []
-               upd_commands = []
-               errors = []
-               for mykey, mystat, mycontent in rawupdates:
-                       commands, errors = parse_updates(mycontent)
-                       upd_commands.extend(commands)
-                       errors.extend(errors)
-               return upd_commands, errors
-
-       def check(self, onProgress=None):
-               updates, errors = self._grab_global_updates(self._portdir)
-               # 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 i, update_cmd in enumerate(updates):
-                       if update_cmd[0] == "move":
-                               origcp, newcp = update_cmd[1:]
-                               for cpv in match(origcp):
-                                       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 = aux_get(cpv, ["SLOT"])[0]
-                                       if slot == origslot:
-                                               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
-               update_keys = self._update_keys
-               from portage.update import update_dbentries
-               if onProgress:
-                       onProgress(maxval, 0)
-               for i, cpv in enumerate(cpv_all):
-                       metadata = dict(zip(update_keys, aux_get(cpv, update_keys)))
-                       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):
-               updates, errors = self._grab_global_updates(self._portdir)
-               # 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 i, update_cmd in enumerate(updates):
-                       if update_cmd[0] == "move":
-                               move(update_cmd)
-                       elif update_cmd[0] == "slotmove":
-                               slotmove(update_cmd)
-                       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(updates, onProgress=onProgress)
-               return errors
+       import signal
 
-class MoveInstalled(MoveHandler):
+       def exithandler(signum,frame):
+               signal.signal(signal.SIGINT, signal.SIG_IGN)
+               signal.signal(signal.SIGTERM, signal.SIG_IGN)
+               sys.exit(1)
 
-       short_desc = "Perform package move updates for installed packages"
+       signal.signal(signal.SIGINT, exithandler)
+       signal.signal(signal.SIGTERM, exithandler)
+       signal.signal(signal.SIGPIPE, signal.SIG_DFL)
 
-       def name():
-               return "moveinst"
-       name = staticmethod(name)
-       def __init__(self):
-               myroot = portage.settings["ROOT"]
-               MoveHandler.__init__(self, portage.db[myroot]["vartree"])
+except KeyboardInterrupt:
+       sys.exit(1)
 
-class MoveBinary(MoveHandler):
+from os import path as osp
+pym_path = osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym")
+sys.path.insert(0, pym_path)
+import portage
+portage._internal_caller = True
+from portage.emaint.main import emaint_main
 
-       short_desc = "Perform package move updates for binary packages"
-
-       def name():
-               return "movebin"
-       name = staticmethod(name)
-       def __init__(self):
-               myroot = portage.settings["ROOT"]
-               MoveHandler.__init__(self, portage.db[myroot]["bintree"])
-
-class VdbKeyHandler(object):
-       def name():
-               return "vdbkeys"
-       name = staticmethod(name)
-
-       def __init__(self):
-               self.list = portage.db["/"]["vartree"].dbapi.cpv_all()
-               self.missing = []
-               self.keys = ["HOMEPAGE", "SRC_URI", "KEYWORDS", "DESCRIPTION"]
-               
-               for p in self.list:
-                       mydir = os.path.join(os.sep, portage.settings["ROOT"], 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(os.sep, portage.settings["ROOT"], 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 = modules.keys()
-       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 = 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__":
+try:
        emaint_main(sys.argv[1:])
+except IOError as e:
+       if e.errno == errno.EACCES:
+               print("\nemaint: Need superuser access")
+               sys.exit(1)
+       else:
+               raise