Beispiel #1
0
 def _remove():
     # remove the thing
     if type_ == self.TYPE_FOLDER_ONLY:
         # TODO: what do we do if the folder isn't empty?
         with note("Removing dir %s" % path):
             try:
                 os.rmdir(path)
             except OSError as err:
                 from errno import ENOTEMPTY
                 if err.errno == ENOTEMPTY:
                     warn("Directory not empty: {}".format(path))
                 else:
                     raise
     elif type_ == self.TYPE_FILE_ALL:
         note("Removing {}".format(path))
         os.unlink(path)
     elif type_ == self.TYPE_FILE_PART:
         if os.stat(path).st_size == 0:
             note("Removing empty {}".format(path))
             os.unlink(path)
         else:
             note("Refusing to remove non-empty {}".format(path))
     else:
         note("Removing link {}".format(path))
         os.unlink(path)
     _discard()
Beispiel #2
0
    def _tryclean(self, cleaner, conflicts, affected):
        # if the cleaner is not needed, we get rid of it
        # FIXME try/except around the isneeded() call
        if not cleaner.isneeded():
            note("{}: Not needed".format(cleaner.description))
            return

        # run the cleaner now
        with note("Cleaning: {}".format(cleaner.description)):
            # if there are still helpers with claims over things the cleaner
            # wants to remove, then the cleaner needs to wait
            for claim in cleaner.needsclaims():
                if claim in self._claims:
                    note("Postponed: Something else claimed %r" % claim)
                    self._addcleaner(cleaner)
                    return

            try:
                affected.extend(cleaner.makechanges())
            except CleanupObstruction as err:
                why = err.args[0]
                if conflicts == self.RAISE:
                    raise
                if conflicts == self.POSTPONE:
                    note("Postponed: %s" % why)
                    # add the cleaner back in
                    self._addcleaner(cleaner)
                    return
                # NOTE: eventually we'd like to ask the user what to do, but
                # for now we just issue a warning
                assert conflicts in (self.WARN, self.ASK)
                warn("Aborted: %s" % err.why)
Beispiel #3
0
    def __init__(self, cfgpath):
        super(Engine, self).__init__()
        self._cfgpath = cfgpath
        self._old_cleaners = []
        self._new_cleaners = []
        self._helpers = []
        self._old_paths_owned = {}
        self._new_paths_owned = {}
        self._postponed = set()
        # keep track of which things we created ourselves
        self._created = set()
        self._only = set()
        self._section = None
        # another way of keeping track of things we've claimed
        self._claims = set()

        if os.path.isfile(cfgpath):
            with open(cfgpath, 'r') as f:
                raw = f.read()
                data = simplejson.loads(raw)
                if not isinstance(data, dict):
                    raise Exception("Invalid json in %s" % cfgpath)
                for item in data.get('cleaners', []):
                    cleaner = cleanerfromdict(item)
                    if cleaner is None:
                        warn("No cleaner for %s" % repr(item))
                    else:
                        self._old_cleaners.append(cleaner)
                self._old_paths_owned = data.get('paths_owned', {})
                for path in data.get('paths_postponed', []):
                    if path in self._old_paths_owned:
                        self._postponed.add(path)
                for path in data.get('paths_created', []):
                    if path in self._old_paths_owned:
                        self._created.add(path)
Beispiel #4
0
def forget(identifier):
    '''
    Tells homely to forget about a dotfiles repository that was previously
    added. You can then run `homely update` to have homely perform automatic
    cleanup of anything that was installed by that dotfiles repo.

    REPO
        This should be the path to a local dotfiles repository that has already
        been registered using `homely add`. You may specify multiple REPOs to
        remove at once.
    '''
    errors = False
    for one in identifier:
        cfg = RepoListConfig()
        info = cfg.find_by_any(one, "ilc")
        if not info:
            warn("No repos matching %r" % one)
            errors = True
            continue

        # update the config ...
        note("Removing record of repo [%s] at %s" %
             (info.shortid(), info.localrepo.repo_path))
        with saveconfig(RepoListConfig()) as cfg:
            cfg.remove_repo(info.repoid)

    # if there were errors, then don't try and do an update
    if errors:
        sys.exit(1)
Beispiel #5
0
def include(pyscript):
    path = _repopath2real(pyscript, getrepoinfo().localrepo)
    if not os.path.exists(path):
        warn("{} not found at {}".format(pyscript, path))
        return

    global _include_num
    _include_num += 1

    name = '__imported_by_homely_{}'.format(_include_num)
    try:
        with entersection("/" + pyscript):
            _loadmodule(name, path)
    except Exception:
        import traceback
        warn("Error while including {}: {}".format(pyscript,
                                                   traceback.format_exc()))
Beispiel #6
0
def execute(cmd, stdout=None, stderr=None, expectexit=0, **kwargs):
    # Executes `cmd` in a subprocess. Raises a SystemError if the exit code
    # is different to `expecterror`.
    #
    # The stdout and stderr arguments for the most part work just like
    # homely._ui.run(), with the main difference being that when stdout=None or
    # stderr=None, these two streams will be filtered through the homely's
    # logging functions instead of being sent directly to the python process's
    # stdout/stderr. Also, the stderr argument will default to "STDOUT" so that
    # the timing of the two streams is recorded more accurately.
    #
    # If the process absolutely _must_ talk to a TTY, you can use stdout="TTY",
    # and a SystemError will be raised if homely is being run in
    # non-interactive mode. When using stdout="TTY", you should omit the stderr
    # argument.
    def outputhandler(data, isend, prefix):
        # FIXME: if we only get part of a stream, then we have a potential bug
        # where we only get part of a multi-byte utf-8 character.
        while len(data):
            pos = data.find(b"\n")
            if pos < 0:
                break
            # write out the line
            note(data[0:pos].decode('utf-8'), dash=prefix)
            data = data[pos + 1:]

        if isend:
            if len(data):
                note(data.decode('utf-8'), dash=prefix)
        else:
            # return any remaining data so it can be included at the start of
            # the next run
            return data

    if stdout == "TTY":
        if not allowinteractive():
            raise SystemError("cmd wants interactive mode")

        assert stderr is None
        stdout = None
    else:
        if stdout is None:
            prefix = "1> " if stderr is False else "&> "
            stdout = partial(outputhandler, prefix=prefix)

        if stderr is None:
            if stdout in (False, True):
                stderr = partial(outputhandler, prefix="2> ")
            else:
                stderr = "STDOUT"

    outredir = ' 1> /dev/null' if stdout is False else ''
    if stderr is None:
        errredir = ' 2>&1'
    else:
        errredir = ' 2> /dev/null' if stderr is False else ''

    with note('{}$ {}{}{}'.format(kwargs.get('cwd', ''),
                                  ' '.join(map(shellquote,
                                               cmd)), outredir, errredir)):
        returncode, out, err = run(cmd, stdout=stdout, stderr=stderr, **kwargs)
        if type(expectexit) is int:
            exitok = returncode == expectexit
        else:
            exitok = returncode in expectexit
        if exitok:
            return returncode, out, err

        # still need to dump the stdout/stderr if they were captured
        if out is not None:
            outputhandler(out, True, '1> ')
        if err is not None:
            outputhandler(err, True, '1> ')
        message = "Unexpected exit code {}. Expected {}".format(
            returncode, expectexit)
        warn(message)
        raise SystemError(message)
Beispiel #7
0
    def _trycleanpath(self, path, type_, conflicts):
        def _discard():
            note("    Forgetting about %s %s" % (type_, path))
            self._old_paths_owned.pop(path)
            self._postponed.discard(path)
            self._created.discard(path)
            self._savecfg()

        def _remove():
            # remove the thing
            if type_ == self.TYPE_FOLDER_ONLY:
                # TODO: what do we do if the folder isn't empty?
                with note("Removing dir %s" % path):
                    try:
                        os.rmdir(path)
                    except OSError as err:
                        from errno import ENOTEMPTY
                        if err.errno == ENOTEMPTY:
                            warn("Directory not empty: {}".format(path))
                        else:
                            raise
            elif type_ == self.TYPE_FILE_ALL:
                note("Removing {}".format(path))
                os.unlink(path)
            elif type_ == self.TYPE_FILE_PART:
                if os.stat(path).st_size == 0:
                    note("Removing empty {}".format(path))
                    os.unlink(path)
                else:
                    note("Refusing to remove non-empty {}".format(path))
            else:
                note("Removing link {}".format(path))
                os.unlink(path)
            _discard()

        def _postpone():
            with note("Postponing cleanup of path: {}".format(path)):
                self._postponed.add(path)
                assert path not in self._new_paths_owned
                self._new_paths_owned[path] = type_
                self._old_paths_owned.pop(path)
                self._savecfg()

        # if we didn't create the path, then we don't need to clean it up
        if path not in self._created:
            return _discard()

        # if the path no longer exists, we have nothing to do
        if not _exists(path):
            return _discard()

        # if the thing has the wrong type, we'll issue an note() and just skip
        if type_ in (self.TYPE_FILE_PART, self.TYPE_FILE_ALL):
            correcttype = os.path.isfile(path)
        elif type_ in (self.TYPE_FOLDER_ONLY, self.TYPE_FOLDER_ALL):
            correcttype = os.path.isdir(path)
        else:
            assert type_ == self.TYPE_LINK
            correcttype = os.path.islink(path)
        if not correcttype:
            with note("Ignoring: Won't remove {} as it is no longer a {}"
                      .format(path, type_)):
                return _discard()

        # work out if there is another path we need to remove first
        for otherpath in self._old_paths_owned:
            if otherpath != path and isnecessarypath(path, otherpath):
                # If there's another path we need to do first, then don't do
                # anything just yet. NOTE: there is an assertion to ensure that
                # we can't get stuck in an infinite loop repeatedly not
                # removing things.
                return

        # if any helpers want the path, don't delete it
        wantedby = None
        for c in self._new_cleaners:
            if c.wantspath(path):
                wantedby = c
                break

        if not wantedby:
            for otherpath in self._new_paths_owned:
                if isnecessarypath(path, otherpath):
                    wantedby = otherpath

        if wantedby:
            # if we previously postponed this path, keep postponing it
            # while there are still things hanging around that want it
            if path in self._postponed:
                return _postpone()
            if conflicts == self.ASK:
                raise Exception("TODO: ask the user what to do")  # noqa
                # NOTE: options are:
                # A) postpone until it can run later
                # B) discard it
            if conflicts == self.RAISE:
                raise CleanupConflict(conflictpath=path, pathwanter=wantedby)
            if conflicts == self.POSTPONE:
                return _postpone()
            assert conflicts == self.WARN
            warn("Couldn't clean up path {}; it is still needed for {}"
                 .format(path, wantedby))
            return _discard()

        # if nothing else wants this path, clean it up now
        return _remove()
Beispiel #8
0
    def run(self, helper):
        assert isinstance(helper, Helper)

        cfg_modified = False

        # what claims does this helper make?
        self._claims.update(*helper.getclaims())

        # take ownership of paths
        for path, type_ in helper.pathsownable().items():
            cfg_modified = True
            self._new_paths_owned[path] = type_
            self._old_paths_owned.pop(path, None)

        # get a cleaner for this helper
        cleaner = helper.getcleaner()

        if helper.isdone():
            # if there is already a cleaner for this thing, add and remove it
            # so it hangs around. If there is no cleaner but the thing is
            # already done, it means we shouldn't be cleaning it up
            if cleaner is not None:
                cfg_modified = True
                if self._removecleaner(cleaner):
                    self._addcleaner(cleaner)
                note("{}: Already done".format(helper.description))
        else:
            # remove and add the cleaner so that we know it will try to clean
            # up, since we know we will be making the change
            if cleaner is not None:
                cfg_modified = True
                self._removecleaner(cleaner)
                self._addcleaner(cleaner)
            # if the helper isn't already done, tell it to do its thing now
            with note("{}: Running ...".format(helper.description)):
                # take ownership of any paths that don't exist yet!
                for path, type_ in helper.pathsownable().items():
                    if type_ in (self.TYPE_FILE_ALL, self.TYPE_FOLDER_ALL):
                        exists = path in self._created
                    elif type_ in (self.TYPE_FILE_PART, self.TYPE_FOLDER_ONLY):
                        exists = os.path.exists(path)
                    else:
                        exists = os.path.islink(path)
                    if not exists:
                        self._created.add(path)
                        cfg_modified = True

                if cfg_modified:
                    # save the updated config before we try anything
                    self._savecfg()
                    cfg_modified = False

                try:
                    helper.makechanges()
                except HelperError as err:
                    warn("Failed: %s" % err.args[0])
        self._helpers.append(helper)

        # save the config now if we were successful
        if cfg_modified:
            # save the updated config before we try anything
            self._savecfg()
Beispiel #9
0
def execute(cmd, stdout=None, stderr=None, expectexit=0, **kwargs):
    # Executes `cmd` in a subprocess. Raises a SystemError if the exit code
    # is different to `expecterror`.
    #
    # The stdout and stderr arguments for the most part work just like
    # homely._ui.run(), with the main difference being that when stdout=None or
    # stderr=None, these two streams will be filtered through the homely's
    # logging functions instead of being sent directly to the python process's
    # stdout/stderr. Also, the stderr argument will default to "STDOUT" so that
    # the timing of the two streams is recorded more accurately.
    #
    # If the process absolutely _must_ talk to a TTY, you can use stdout="TTY",
    # and a SystemError will be raised if homely is being run in
    # non-interactive mode. When using stdout="TTY", you should omit the stderr
    # argument.
    def outputhandler(data, isend, prefix):
        # FIXME: if we only get part of a stream, then we have a potential bug
        # where we only get part of a multi-byte utf-8 character.
        while len(data):
            pos = data.find(b"\n")
            if pos < 0:
                break
            # write out the line
            note(data[0:pos].decode('utf-8'), dash=prefix)
            data = data[pos+1:]

        if isend:
            if len(data):
                note(data.decode('utf-8'), dash=prefix)
        else:
            # return any remaining data so it can be included at the start of
            # the next run
            return data

    if stdout == "TTY":
        if not allowinteractive():
            raise SystemError("cmd wants interactive mode")

        assert stderr is None
        stdout = None
    else:
        if stdout is None:
            prefix = "1> " if stderr is False else "&> "
            stdout = partial(outputhandler, prefix=prefix)

        if stderr is None:
            if stdout in (False, True):
                stderr = partial(outputhandler, prefix="2> ")
            else:
                stderr = "STDOUT"

    outredir = ' 1> /dev/null' if stdout is False else ''
    if stderr is None:
        errredir = ' 2>&1'
    else:
        errredir = ' 2> /dev/null' if stderr is False else ''

    with note('{}$ {}{}{}'.format(kwargs.get('cwd', ''),
                                  ' '.join(map(shellquote, cmd)),
                                  outredir,
                                  errredir)):
        returncode, out, err = run(cmd, stdout=stdout, stderr=stderr, **kwargs)
        if type(expectexit) is int:
            exitok = returncode == expectexit
        else:
            exitok = returncode in expectexit
        if exitok:
            return returncode, out, err

        # still need to dump the stdout/stderr if they were captured
        if out is not None:
            outputhandler(out, True, '1> ')
        if err is not None:
            outputhandler(err, True, '1> ')
        message = "Unexpected exit code {}. Expected {}".format(
            returncode, expectexit)
        warn(message)
        raise SystemError(message)