On Thu, Mar 13, 2014 at 3:09 PM, Alec Warner <anta...@gentoo.org> wrote:
> > > > On Wed, Mar 12, 2014 at 11:10 PM, Pavel Kazakov <nullishz...@gentoo.org>wrote: > >> --- >> pym/portage/emaint/main.py | 6 +- >> pym/portage/emaint/modules/merges/__init__.py | 30 +++ >> pym/portage/emaint/modules/merges/merges.py | 281 >> ++++++++++++++++++++++++++ >> 3 files changed, 315 insertions(+), 2 deletions(-) >> create mode 100644 pym/portage/emaint/modules/merges/__init__.py >> create mode 100644 pym/portage/emaint/modules/merges/merges.py >> >> diff --git a/pym/portage/emaint/main.py b/pym/portage/emaint/main.py >> index 6a17027..646883d 100644 >> --- a/pym/portage/emaint/main.py >> +++ b/pym/portage/emaint/main.py >> @@ -98,10 +98,11 @@ def module_opts(module_controller, module): >> class TaskHandler(object): >> """Handles the running of the tasks it is given""" >> >> - def __init__(self, show_progress_bar=True, verbose=True, >> callback=None): >> + def __init__(self, show_progress_bar=True, verbose=True, >> callback=None, module_output=None): >> self.show_progress_bar = show_progress_bar >> self.verbose = verbose >> self.callback = callback >> + self.module_output = module_output >> self.isatty = os.environ.get('TERM') != 'dumb' and >> sys.stdout.isatty() >> self.progress_bar = ProgressBar(self.isatty, >> title="Emaint", max_desc_length=27) >> >> @@ -124,6 +125,7 @@ class TaskHandler(object): >> onProgress = None >> kwargs = { >> 'onProgress': onProgress, >> + 'module_output': self.module_output, >> # pass in a copy of the options so a >> module can not pollute or change >> # them for other tasks if there is more >> to do. >> 'options': options.copy() >> @@ -219,5 +221,5 @@ def emaint_main(myargv): >> # need to pass the parser options dict to the modules >> # so they are available if needed. >> task_opts = options.__dict__ >> - taskmaster = TaskHandler(callback=print_results) >> + taskmaster = TaskHandler(callback=print_results, >> module_output=sys.stdout) >> taskmaster.run_tasks(tasks, func, status, options=task_opts) >> diff --git a/pym/portage/emaint/modules/merges/__init__.py >> b/pym/portage/emaint/modules/merges/__init__.py >> new file mode 100644 >> index 0000000..96ee71b >> --- /dev/null >> +++ b/pym/portage/emaint/modules/merges/__init__.py >> @@ -0,0 +1,30 @@ >> +# Copyright 2005-2014 Gentoo Foundation >> +# Distributed under the terms of the GNU General Public License v2 >> + >> +"""Scan for failed merges and fix them.""" >> + >> + >> +module_spec = { >> + 'name': 'merges', >> + 'description': __doc__, >> + 'provides': { >> + 'merges': { >> + 'name': "merges", >> + 'class': "MergesHandler", >> + 'description': __doc__, >> + 'functions': ['check', 'fix', 'purge'], >> + 'func_desc': { >> + 'purge': { >> + 'short': '-P', 'long': >> '--purge-tracker', >> + 'help': 'Removes the list of >> previously failed merges.' + >> + ' WARNING: Only >> use this option if you plan on' + >> + ' manually fixing >> them or do not want them' >> + ' re-installed.', >> + 'status': "Removing %s", >> + 'action': 'store_true', >> + 'func': 'purge' >> + } >> + } >> + } >> + } >> +} >> diff --git a/pym/portage/emaint/modules/merges/merges.py >> b/pym/portage/emaint/modules/merges/merges.py >> new file mode 100644 >> index 0000000..a99dad4 >> --- /dev/null >> +++ b/pym/portage/emaint/modules/merges/merges.py >> @@ -0,0 +1,281 @@ >> +# Copyright 2005-2014 Gentoo Foundation >> +# Distributed under the terms of the GNU General Public License v2 >> + >> +from _emerge.actions import load_emerge_config >> + >> +import portage >> +from portage import os, _unicode_encode >> +from portage.const import MERGING_IDENTIFIER, PORTAGE_BIN_PATH, >> PRIVATE_PATH, \ >> + VDB_PATH >> +from portage.dep import isvalidatom >> + >> > > import shutil > import subprocess > import sys > import time > > Alphabetical order. > > >> +import time >> +import shutil >> +import sys >> +import subprocess >> + >> +class TrackingFile(object): >> + """File for keeping track of failed merges.""" >> + >> + >> + def __init__(self, tracking_path): >> + """ >> + Create a TrackingFile object. >> + >> + @param tracking_path: file path used to keep track of >> failed merges >> + @type tracking_path: String >> + """ >> + self._tracking_path = _unicode_encode(tracking_path) >> + >> + >> + def save(self, failed_pkgs): >> + """ >> + Save the specified packages that failed to merge. >> + >> + @param failed_pkgs: dictionary of failed packages >> + @type failed_pkgs: dict >> + """ >> + tracking_path = self._tracking_path >> > > You don't appear to do any atomic operations or file locking here, what if > 2 callers are trying to write to the same filename at once? > Normally we write to a temporary file and perform an atomic rename (which > prevents this sort of thing.) > portage.util.write_atomic can do this for you. -A > > >> + with open(tracking_path, 'w') as tracking_file: >> + for pkg, mtime in failed_pkgs.items(): >> + tracking_file.write('%s %s\n' % (pkg, >> mtime)) >> + >> + >> + def load(self): >> + """ >> + Load previously failed merges. >> + >> + @rtype: dict >> + @return: dictionary of packages that failed to merge >> + """ >> > + tracking_path = self._tracking_path >> + if not os.path.exists(tracking_path): >> + return {} >> > > if not self.exists(): > return {} > > >> + failed_pkgs = {} >> + with open(tracking_path, 'r') as tracking_file: >> + for failed_merge in tracking_file: >> + pkg, mtime = failed_merge.strip().split() >> + failed_pkgs[pkg] = mtime >> + return failed_pkgs >> + >> + >> + def exists(self): >> + """ >> + Check if tracking file exists. >> + >> + @rtype: bool >> + @return: true if tracking file exists, false otherwise >> + """ >> + return os.path.exists(self._tracking_path) >> + >> + >> + def purge(self): >> + """Delete previously saved tracking file if one exists.""" >> + if self.exists(): >> + os.remove(self._tracking_path) >> + >> + >> +class MergesHandler(object): >> + """Handle failed package merges.""" >> + >> + short_desc = "Remove failed merges" >> + >> + @staticmethod >> + def name(): >> + return "merges" >> + >> + >> + def __init__(self): >> + """Create MergesHandler object.""" >> + eroot = portage.settings['EROOT'] >> + tracking_path = os.path.join(eroot, PRIVATE_PATH, >> 'failed-merges'); >> + self._tracking_file = TrackingFile(tracking_path) >> + self._vardb_path = os.path.join(eroot, VDB_PATH) >> + >> + >> + def can_progressbar(self, func): >> + return func == 'check' >> + >> + >> + def _scan(self, onProgress=None): >> + """ >> + Scan the file system for failed merges and return any >> found. >> + >> + @param onProgress: function to call for updating progress >> + @type onProgress: Function >> + @rtype: dict >> + @return: dictionary of packages that failed to merges >> + """ >> + failed_pkgs = {} >> + for cat in os.listdir(self._vardb_path): >> + pkgs_path = os.path.join(self._vardb_path, cat) >> + if not os.path.isdir(pkgs_path): >> + continue >> + pkgs = os.listdir(pkgs_path) >> + maxval = len(pkgs) >> + for i, pkg in enumerate(pkgs): >> + if onProgress: >> + onProgress(maxval, i+1) >> + if MERGING_IDENTIFIER in pkg: >> + mtime = >> int(os.stat(os.path.join(pkgs_path, pkg)).st_mtime) >> + pkg = os.path.join(cat, pkg) >> + failed_pkgs[pkg] = mtime >> + return failed_pkgs >> + >> + >> + def _failed_pkgs(self, onProgress=None): >> + """ >> + Return failed packages from both the file system and >> tracking file. >> + >> + @rtype: dict >> + @return: dictionary of packages that failed to merges >> + """ >> + failed_pkgs = self._scan(onProgress) >> > > Perhaps add an __iter__ function to TrackingFile, that implicitly calls > load? > > Then this can be: > for pkg, mtime in self._tracking_file: > > ? > > Just a thought. > > >> + for pkg, mtime in self._tracking_file.load().items(): >> + if pkg not in failed_pkgs: >> + failed_pkgs[pkg] = mtime >> + return failed_pkgs >> + >> + >> + def _remove_failed_dirs(self, failed_pkgs): >> + """ >> + Remove the directories of packages that failed to merge. >> + >> + @param failed_pkgs: failed packages whose directories to >> remove >> + @type failed_pkg: dict >> + """ >> + for failed_pkg in failed_pkgs: >> + pkg_path = os.path.join(self._vardb_path, >> failed_pkg) >> + # delete failed merge directory if it exists (it >> might not exist >> + # if loaded from tracking file) >> + if os.path.exists(pkg_path): >> + shutil.rmtree(pkg_path) >> + # TODO: try removing package contents to prevent >> orphaned >> + # files >> > > I don't get this TODO, can you elaborate? > > >> + >> + >> + def _get_pkg_atoms(self, failed_pkgs, pkg_atoms, pkg_dne): >> + """ >> + Get the package atoms for the specified failed packages. >> + >> + @param failed_pkgs: failed packages to iterate >> + @type failed_pkgs: dict >> + @param pkg_atoms: append package atoms to this set >> + @type pkg_atoms: set >> + @param pkg_dne: append any packages atoms that are >> invalid to this set >> + @type pkg_dne: set >> > > Not following what dne means. > > >> + """ >> + >> + emerge_config = load_emerge_config() >> + portdb = >> emerge_config.target_config.trees['porttree'].dbapi >> + for failed_pkg in failed_pkgs: >> + # validate pkg name >> + pkg_name = '%s' % >> failed_pkg.replace(MERGING_IDENTIFIER, '') >> + pkg_atom = '=%s' % pkg_name >> + >> + if not isvalidatom(pkg_atom): >> + pkg_dne.append( "'%s' is an invalid >> package atom." % pkg_atom) >> + if not portdb.cpv_exists(pkg_name): >> + pkg_dne.append( "'%s' does not exist in >> the portage tree." >> + % pkg_name) >> + pkg_atoms.add(pkg_atom) >> + >> + >> + def _emerge_pkg_atoms(self, module_output, pkg_atoms): >> + """ >> + Emerge the specified packages atoms. >> + >> + @param module_output: output will be written to >> + @type module_output: Class >> + @param pkg_atoms: packages atoms to emerge >> + @type pkg_atoms: set >> + @rtype: list >> + @return: List of results >> + """ >> + # TODO: rewrite code to use portage's APIs instead of a >> subprocess >> + env = { >> + "FEATURES" : "-collision-detect -protect-owned", >> + "PATH" : os.environ["PATH"] >> + } >> + emerge_cmd = ( >> + portage._python_interpreter, >> + '-b', >> + os.path.join(PORTAGE_BIN_PATH, 'emerge'), >> + '--quiet', >> + '--oneshot', >> + '--complete-graph=y' >> + ) >> + results = [] >> + msg = 'Re-Emerging packages that failed to merge...\n' >> + if module_output: >> + module_output.write(msg) >> + else: >> + module_output = subprocess.PIPE >> + results.append(msg) >> + proc = subprocess.Popen(emerge_cmd + tuple(pkg_atoms), >> env=env, >> + stdout=module_output, stderr=sys.stderr) >> + output = proc.communicate()[0] >> > > This seems sort of weird, perhaps: > > output, _ = proc.communicate() > > >> + if output: >> + results.append(output) >> + if proc.returncode != os.EX_OK: >> + emerge_status = "Failed to emerge '%s'" % (' >> '.join(pkg_atoms)) >> + else: >> + emerge_status = "Successfully emerged '%s'" % (' >> '.join(pkg_atoms)) >> + results.append(emerge_status) >> + return results >> + >> + >> + def check(self, **kwargs): >> + """Check for failed merges.""" >> + onProgress = kwargs.get('onProgress', None) >> + failed_pkgs = self._failed_pkgs(onProgress) >> + errors = [] >> + for pkg, mtime in failed_pkgs.items(): >> + mtime_str = time.ctime(int(mtime)) >> + errors.append("'%s' failed to merge on '%s'" % >> (pkg, mtime_str)) >> + return errors >> + >> + >> + def fix(self, **kwargs): >> + """Attempt to fix any failed merges.""" >> + module_output = kwargs.get('module_output', None) >> + failed_pkgs = self._failed_pkgs() >> + if not failed_pkgs: >> + return [ 'No failed merges found. ' ] >> > > Less spacing here: > return ['No failed merges found.'] > > >> + >> + pkg_dne = set() >> + pkg_atoms = set() >> + self._get_pkg_atoms(failed_pkgs, pkg_atoms, pkg_dne) >> + if pkg_dne: >> + return pkg_dne >> + >> + try: >> + self._tracking_file.save(failed_pkgs) >> + except IOError as ex: >> + errors = [ 'Unable to save failed merges to >> tracking file: %s\n' >> + % str(ex) ] >> > > Same here, no spaces before or after [ and ]. > > >> + errors.append(', '.join(sorted(failed_pkgs))) >> + return errors >> + self._remove_failed_dirs(failed_pkgs) >> + results = self._emerge_pkg_atoms(module_output, pkg_atoms) >> + # list any new failed merges >> + for pkg in sorted(self._scan()): >> + results.append("'%s' still found as a failed >> merge." % pkg) >> + # reload config and remove successful packages from >> tracking file >> + emerge_config = load_emerge_config() >> + vardb = emerge_config.target_config.trees['vartree'].dbapi >> + still_failed_pkgs = {} >> + for pkg, mtime in failed_pkgs.items(): >> + pkg_name = '%s' % pkg.replace(MERGING_IDENTIFIER, >> '') >> + if not vardb.cpv_exists(pkg_name): >> + still_failed_pkgs[pkg] = mtime >> + self._tracking_file.save(still_failed_pkgs) >> + return results >> + >> + >> + def purge(self, **kwargs): >> + """Attempt to remove previously saved tracking file.""" >> + if not self._tracking_file.exists(): >> + return ['Tracking file not found.'] >> + self._tracking_file.purge() >> + return ['Removed tracking file.'] >> -- >> 1.8.3.2 >> >> >> >