diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/thandy/lockfile/__init__.py | 286 | ||||
| -rw-r--r-- | lib/thandy/lockfile/linklockfile.py | 69 | ||||
| -rw-r--r-- | lib/thandy/lockfile/mkdirlockfile.py | 79 | ||||
| -rw-r--r-- | lib/thandy/lockfile/pidlockfile.py | 189 | ||||
| -rw-r--r-- | lib/thandy/lockfile/sqlitelockfile.py | 146 | ||||
| -rw-r--r-- | lib/thandy/packagesys/ThpPackages.py | 36 | 
6 files changed, 801 insertions, 4 deletions
diff --git a/lib/thandy/lockfile/__init__.py b/lib/thandy/lockfile/__init__.py new file mode 100644 index 0000000..a167cd8 --- /dev/null +++ b/lib/thandy/lockfile/__init__.py @@ -0,0 +1,286 @@ + +""" +lockfile.py - Platform-independent advisory file locks. + +Requires Python 2.5 unless you apply 2.4.diff +Locking is done on a per-thread basis instead of a per-process basis. + +Usage: + +>>> lock = LockFile('somefile') +>>> try: +...     lock.acquire() +... except AlreadyLocked: +...     print 'somefile', 'is locked already.' +... except LockFailed: +...     print 'somefile', 'can\\'t be locked.' +... else: +...     print 'got lock' +got lock +>>> print lock.is_locked() +True +>>> lock.release() + +>>> lock = LockFile('somefile') +>>> print lock.is_locked() +False +>>> with lock: +...    print lock.is_locked() +True +>>> print lock.is_locked() +False + +>>> lock = LockFile('somefile') +>>> # It is okay to lock twice from the same thread... +>>> with lock: +...     lock.acquire() +... +>>> # Though no counter is kept, so you can't unlock multiple times... +>>> print lock.is_locked() +False + +Exceptions: + +    Error - base class for other exceptions +        LockError - base class for all locking exceptions +            AlreadyLocked - Another thread or process already holds the lock +            LockFailed - Lock failed for some other reason +        UnlockError - base class for all unlocking exceptions +            AlreadyUnlocked - File was not locked. +            NotMyLock - File was locked but not by the current thread/process +""" + +import sys +import socket +import os +import threading +import time +import urllib +import warnings + +# Work with PEP8 and non-PEP8 versions of threading module. +if not hasattr(threading, "current_thread"): +    threading.current_thread = threading.currentThread +if not hasattr(threading.Thread, "get_name"): +    threading.Thread.get_name = threading.Thread.getName + +__all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked', +           'LockFailed', 'UnlockError', 'NotLocked', 'NotMyLock', +           'LinkLockFile', 'MkdirLockFile', 'SQLiteLockFile', +           'LockBase'] + +class Error(Exception): +    """ +    Base class for other exceptions. + +    >>> try: +    ...   raise Error +    ... except Exception: +    ...   pass +    """ +    pass + +class LockError(Error): +    """ +    Base class for error arising from attempts to acquire the lock. + +    >>> try: +    ...   raise LockError +    ... except Error: +    ...   pass +    """ +    pass + +class LockTimeout(LockError): +    """Raised when lock creation fails within a user-defined period of time. + +    >>> try: +    ...   raise LockTimeout +    ... except LockError: +    ...   pass +    """ +    pass + +class AlreadyLocked(LockError): +    """Some other thread/process is locking the file. + +    >>> try: +    ...   raise AlreadyLocked +    ... except LockError: +    ...   pass +    """ +    pass + +class LockFailed(LockError): +    """Lock file creation failed for some other reason. + +    >>> try: +    ...   raise LockFailed +    ... except LockError: +    ...   pass +    """ +    pass + +class UnlockError(Error): +    """ +    Base class for errors arising from attempts to release the lock. + +    >>> try: +    ...   raise UnlockError +    ... except Error: +    ...   pass +    """ +    pass + +class NotLocked(UnlockError): +    """Raised when an attempt is made to unlock an unlocked file. + +    >>> try: +    ...   raise NotLocked +    ... except UnlockError: +    ...   pass +    """ +    pass + +class NotMyLock(UnlockError): +    """Raised when an attempt is made to unlock a file someone else locked. + +    >>> try: +    ...   raise NotMyLock +    ... except UnlockError: +    ...   pass +    """ +    pass + +class LockBase: +    """Base class for platform-specific lock classes.""" +    def __init__(self, path, threaded=True): +        """ +        >>> lock = LockBase('somefile') +        >>> lock = LockBase('somefile', threaded=False) +        """ +        self.path = path +        self.lock_file = os.path.abspath(path) + ".lock" +        self.hostname = socket.gethostname() +        self.pid = os.getpid() +        if threaded: +            t = threading.current_thread() +            # Thread objects in Python 2.4 and earlier do not have ident +            # attrs.  Worm around that. +            ident = getattr(t, "ident", hash(t)) +            self.tname = "-%x" % (ident & 0xffffffff) +        else: +            self.tname = "" +        dirname = os.path.dirname(self.lock_file) +        self.unique_name = os.path.join(dirname, +                                        "%s%s.%s" % (self.hostname, +                                                     self.tname, +                                                     self.pid)) + +    def acquire(self, timeout=None): +        """ +        Acquire the lock. + +        * If timeout is omitted (or None), wait forever trying to lock the +          file. + +        * If timeout > 0, try to acquire the lock for that many seconds.  If +          the lock period expires and the file is still locked, raise +          LockTimeout. + +        * If timeout <= 0, raise AlreadyLocked immediately if the file is +          already locked. +        """ +        raise NotImplemented("implement in subclass") + +    def release(self): +        """ +        Release the lock. + +        If the file is not locked, raise NotLocked. +        """ +        raise NotImplemented("implement in subclass") + +    def is_locked(self): +        """ +        Tell whether or not the file is locked. +        """ +        raise NotImplemented("implement in subclass") + +    def i_am_locking(self): +        """ +        Return True if this object is locking the file. +        """ +        raise NotImplemented("implement in subclass") + +    def break_lock(self): +        """ +        Remove a lock.  Useful if a locking thread failed to unlock. +        """ +        raise NotImplemented("implement in subclass") + +    def __enter__(self): +        """ +        Context manager support. +        """ +        self.acquire() +        return self + +    def __exit__(self, *_exc): +        """ +        Context manager support. +        """ +        self.release() + +def _fl_helper(cls, mod, *args, **kwds): +    warnings.warn("Import from %s module instead of lockfile package" % mod, +                  DeprecationWarning, stacklevel=2) +    # This is a bit funky, but it's only for awhile.  The way the unit tests +    # are constructed this function winds up as an unbound method, so it +    # actually takes three args, not two.  We want to toss out self. +    if not isinstance(args[0], str): +        # We are testing, avoid the first arg +        args = args[1:] +    if len(args) == 1 and not kwds: +        kwds["threaded"] = True +    return cls(*args, **kwds) + +def LinkFileLock(*args, **kwds): +    """Factory function provided for backwards compatibility. + +    Do not use in new code.  Instead, import LinkLockFile from the +    lockfile.linklockfile module. +    """ +    import linklockfile +    return _fl_helper(linklockfile.LinkLockFile, "lockfile.linklockfile", +                      *args, **kwds) + +def MkdirFileLock(*args, **kwds): +    """Factory function provided for backwards compatibility. + +    Do not use in new code.  Instead, import MkdirLockFile from the +    lockfile.mkdirlockfile module. +    """ +    import mkdirlockfile +    return _fl_helper(mkdirlockfile.MkdirLockFile, "lockfile.mkdirlockfile", +                      *args, **kwds) + +def SQLiteFileLock(*args, **kwds): +    """Factory function provided for backwards compatibility. + +    Do not use in new code.  Instead, import SQLiteLockFile from the +    lockfile.mkdirlockfile module. +    """ +    import sqlitelockfile +    return _fl_helper(sqlitelockfile.SQLiteLockFile, "lockfile.sqlitelockfile", +                      *args, **kwds) + +if hasattr(os, "link"): +    import linklockfile as _llf +    LockFile = _llf.LinkLockFile +else: +    import mkdirlockfile as _mlf +    LockFile = _mlf.MkdirLockFile + +FileLock = LockFile + diff --git a/lib/thandy/lockfile/linklockfile.py b/lib/thandy/lockfile/linklockfile.py new file mode 100644 index 0000000..737fd08 --- /dev/null +++ b/lib/thandy/lockfile/linklockfile.py @@ -0,0 +1,69 @@ +from __future__ import absolute_import + +import time +import os + +from . import (LockBase, LockFailed, NotLocked, NotMyLock, LockTimeout, +               AlreadyLocked) + +class LinkLockFile(LockBase): +    """Lock access to a file using atomic property of link(2). + +    >>> lock = LinkLockFile('somefile') +    >>> lock = LinkLockFile('somefile', threaded=False) +    """ + +    def acquire(self, timeout=None): +        try: +            open(self.unique_name, "wb").close() +        except IOError: +            raise LockFailed("failed to create %s" % self.unique_name) + +        end_time = time.time() +        if timeout is not None and timeout > 0: +            end_time += timeout + +        while True: +            # Try and create a hard link to it. +            try: +                os.link(self.unique_name, self.lock_file) +            except OSError: +                # Link creation failed.  Maybe we've double-locked? +                nlinks = os.stat(self.unique_name).st_nlink +                if nlinks == 2: +                    # The original link plus the one I created == 2.  We're +                    # good to go. +                    return +                else: +                    # Otherwise the lock creation failed. +                    if timeout is not None and time.time() > end_time: +                        os.unlink(self.unique_name) +                        if timeout > 0: +                            raise LockTimeout +                        else: +                            raise AlreadyLocked +                    time.sleep(timeout is not None and timeout/10 or 0.1) +            else: +                # Link creation succeeded.  We're good to go. +                return + +    def release(self): +        if not self.is_locked(): +            raise NotLocked +        elif not os.path.exists(self.unique_name): +            raise NotMyLock +        os.unlink(self.unique_name) +        os.unlink(self.lock_file) + +    def is_locked(self): +        return os.path.exists(self.lock_file) + +    def i_am_locking(self): +        return (self.is_locked() and +                os.path.exists(self.unique_name) and +                os.stat(self.unique_name).st_nlink == 2) + +    def break_lock(self): +        if os.path.exists(self.lock_file): +            os.unlink(self.lock_file) + diff --git a/lib/thandy/lockfile/mkdirlockfile.py b/lib/thandy/lockfile/mkdirlockfile.py new file mode 100644 index 0000000..fb78902 --- /dev/null +++ b/lib/thandy/lockfile/mkdirlockfile.py @@ -0,0 +1,79 @@ +from __future__ import absolute_import, division + +import time +import os +import sys +import errno + +from . import (LockBase, LockFailed, NotLocked, NotMyLock, LockTimeout, +               AlreadyLocked) + +class MkdirLockFile(LockBase): +    """Lock file by creating a directory.""" +    def __init__(self, path, threaded=True): +        """ +        >>> lock = MkdirLockFile('somefile') +        >>> lock = MkdirLockFile('somefile', threaded=False) +        """ +        LockBase.__init__(self, path, threaded) +        # Lock file itself is a directory.  Place the unique file name into +        # it. +        self.unique_name  = os.path.join(self.lock_file, +                                         "%s.%s%s" % (self.hostname, +                                                      self.tname, +                                                      self.pid)) + +    def acquire(self, timeout=None): +        end_time = time.time() +        if timeout is not None and timeout > 0: +            end_time += timeout + +        if timeout is None: +            wait = 0.1 +        else: +            wait = max(0, timeout / 10) + +        while True: +            try: +                os.mkdir(self.lock_file) +            except OSError: +                err = sys.exc_info()[1] +                if err.errno == errno.EEXIST: +                    # Already locked. +                    if os.path.exists(self.unique_name): +                        # Already locked by me. +                        return +                    if timeout is not None and time.time() > end_time: +                        if timeout > 0: +                            raise LockTimeout +                        else: +                            # Someone else has the lock. +                            raise AlreadyLocked +                    time.sleep(wait) +                else: +                    # Couldn't create the lock for some other reason +                    raise LockFailed("failed to create %s" % self.lock_file) +            else: +                open(self.unique_name, "wb").close() +                return + +    def release(self): +        if not self.is_locked(): +            raise NotLocked +        elif not os.path.exists(self.unique_name): +            raise NotMyLock +        os.unlink(self.unique_name) +        os.rmdir(self.lock_file) + +    def is_locked(self): +        return os.path.exists(self.lock_file) + +    def i_am_locking(self): +        return (self.is_locked() and +                os.path.exists(self.unique_name)) + +    def break_lock(self): +        if os.path.exists(self.lock_file): +            for name in os.listdir(self.lock_file): +                os.unlink(os.path.join(self.lock_file, name)) +            os.rmdir(self.lock_file) diff --git a/lib/thandy/lockfile/pidlockfile.py b/lib/thandy/lockfile/pidlockfile.py new file mode 100644 index 0000000..10dcb31 --- /dev/null +++ b/lib/thandy/lockfile/pidlockfile.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- + +# pidlockfile.py +# +# Copyright © 2008–2009 Ben Finney <ben+python@benfinney.id.au> +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Python Software Foundation License, version 2 or +# later as published by the Python Software Foundation. +# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. + +""" Lockfile behaviour implemented via Unix PID files. +    """ + +from __future__ import absolute_import + +import os +import sys +import errno +import time + +from . import (LockBase, AlreadyLocked, LockFailed, NotLocked, NotMyLock, +               LockTimeout) + + +class PIDLockFile(LockBase): +    """ Lockfile implemented as a Unix PID file. + +    The lock file is a normal file named by the attribute `path`. +    A lock's PID file contains a single line of text, containing +    the process ID (PID) of the process that acquired the lock. + +    >>> lock = PIDLockFile('somefile') +    >>> lock = PIDLockFile('somefile') +    """ + +    def __init__(self, path, threaded=False): +        # pid lockfiles don't support threaded operation, so always force +        # False as the threaded arg. +        LockBase.__init__(self, path, False) +        dirname = os.path.dirname(self.lock_file) +        basename = os.path.split(self.path)[-1] +        self.unique_name = self.path + +    def read_pid(self): +        """ Get the PID from the lock file. +            """ +        return read_pid_from_pidfile(self.path) + +    def is_locked(self): +        """ Test if the lock is currently held. + +            The lock is held if the PID file for this lock exists. + +            """ +        return os.path.exists(self.path) + +    def i_am_locking(self): +        """ Test if the lock is held by the current process. + +        Returns ``True`` if the current process ID matches the +        number stored in the PID file. +        """ +        return self.is_locked() and os.getpid() == self.read_pid() + +    def acquire(self, timeout=None): +        """ Acquire the lock. + +        Creates the PID file for this lock, or raises an error if +        the lock could not be acquired. +        """ + +        end_time = time.time() +        if timeout is not None and timeout > 0: +            end_time += timeout + +        while True: +            try: +                write_pid_to_pidfile(self.path) +            except OSError, exc: +                if exc.errno == errno.EEXIST: +                    # The lock creation failed.  Maybe sleep a bit. +                    if timeout is not None and time.time() > end_time: +                        if timeout > 0: +                            raise LockTimeout +                        else: +                            raise AlreadyLocked +                    time.sleep(timeout is not None and timeout/10 or 0.1) +                else: +                    raise LockFailed +            else: +                return + +    def release(self): +        """ Release the lock. + +            Removes the PID file to release the lock, or raises an +            error if the current process does not hold the lock. + +            """ +        if not self.is_locked(): +            raise NotLocked +        if not self.i_am_locking(): +            raise NotMyLock +        remove_existing_pidfile(self.path) + +    def break_lock(self): +        """ Break an existing lock. + +            Removes the PID file if it already exists, otherwise does +            nothing. + +            """ +        remove_existing_pidfile(self.path) + +def read_pid_from_pidfile(pidfile_path): +    """ Read the PID recorded in the named PID file. + +        Read and return the numeric PID recorded as text in the named +        PID file. If the PID file cannot be read, or if the content is +        not a valid PID, return ``None``. + +        """ +    pid = None +    try: +        pidfile = open(pidfile_path, 'r') +    except IOError: +        pass +    else: +        # According to the FHS 2.3 section on PID files in /var/run: +        #  +        #   The file must consist of the process identifier in +        #   ASCII-encoded decimal, followed by a newline character. +        #  +        #   Programs that read PID files should be somewhat flexible +        #   in what they accept; i.e., they should ignore extra +        #   whitespace, leading zeroes, absence of the trailing +        #   newline, or additional lines in the PID file. + +        line = pidfile.readline().strip() +        try: +            pid = int(line) +        except ValueError: +            pass +        pidfile.close() + +    return pid + + +def write_pid_to_pidfile(pidfile_path): +    """ Write the PID in the named PID file. + +        Get the numeric process ID (“PID”) of the current process +        and write it to the named file as a line of text. + +        """ +    open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY) +    open_mode = 0644 +    pidfile_fd = os.open(pidfile_path, open_flags, open_mode) +    pidfile = os.fdopen(pidfile_fd, 'w') + +    # According to the FHS 2.3 section on PID files in /var/run: +    # +    #   The file must consist of the process identifier in +    #   ASCII-encoded decimal, followed by a newline character. For +    #   example, if crond was process number 25, /var/run/crond.pid +    #   would contain three characters: two, five, and newline. + +    pid = os.getpid() +    line = "%(pid)d\n" % vars() +    pidfile.write(line) +    pidfile.close() + + +def remove_existing_pidfile(pidfile_path): +    """ Remove the named PID file if it exists. + +        Removing a PID file that doesn't already exist puts us in the +        desired state, so we ignore the condition if the file does not +        exist. + +        """ +    try: +        os.remove(pidfile_path) +    except OSError, exc: +        if exc.errno == errno.ENOENT: +            pass +        else: +            raise diff --git a/lib/thandy/lockfile/sqlitelockfile.py b/lib/thandy/lockfile/sqlitelockfile.py new file mode 100644 index 0000000..d044d2a --- /dev/null +++ b/lib/thandy/lockfile/sqlitelockfile.py @@ -0,0 +1,146 @@ +from __future__ import absolute_import, division + +import time +import os + +from . import LockBase, NotLocked, NotMyLock, LockTimeout, AlreadyLocked + +class SQLiteLockFile(LockBase): +    "Demonstrate SQL-based locking." + +    testdb = None + +    def __init__(self, path, threaded=True): +        """ +        >>> lock = SQLiteLockFile('somefile') +        >>> lock = SQLiteLockFile('somefile', threaded=False) +        """ +        LockBase.__init__(self, path, threaded) +        self.lock_file = unicode(self.lock_file) +        self.unique_name = unicode(self.unique_name) + +        if SQLiteLockFile.testdb is None: +            import tempfile +            _fd, testdb = tempfile.mkstemp() +            os.close(_fd) +            os.unlink(testdb) +            del _fd, tempfile +            SQLiteLockFile.testdb = testdb + +        import sqlite3 +        self.connection = sqlite3.connect(SQLiteLockFile.testdb) +         +        c = self.connection.cursor() +        try: +            c.execute("create table locks" +                      "(" +                      "   lock_file varchar(32)," +                      "   unique_name varchar(32)" +                      ")") +        except sqlite3.OperationalError: +            pass +        else: +            self.connection.commit() +            import atexit +            atexit.register(os.unlink, SQLiteLockFile.testdb) + +    def acquire(self, timeout=None): +        end_time = time.time() +        if timeout is not None and timeout > 0: +            end_time += timeout + +        if timeout is None: +            wait = 0.1 +        elif timeout <= 0: +            wait = 0 +        else: +            wait = timeout / 10 + +        cursor = self.connection.cursor() + +        while True: +            if not self.is_locked(): +                # Not locked.  Try to lock it. +                cursor.execute("insert into locks" +                               "  (lock_file, unique_name)" +                               "  values" +                               "  (?, ?)", +                               (self.lock_file, self.unique_name)) +                self.connection.commit() + +                # Check to see if we are the only lock holder. +                cursor.execute("select * from locks" +                               "  where unique_name = ?", +                               (self.unique_name,)) +                rows = cursor.fetchall() +                if len(rows) > 1: +                    # Nope.  Someone else got there.  Remove our lock. +                    cursor.execute("delete from locks" +                                   "  where unique_name = ?", +                                   (self.unique_name,)) +                    self.connection.commit() +                else: +                    # Yup.  We're done, so go home. +                    return +            else: +                # Check to see if we are the only lock holder. +                cursor.execute("select * from locks" +                               "  where unique_name = ?", +                               (self.unique_name,)) +                rows = cursor.fetchall() +                if len(rows) == 1: +                    # We're the locker, so go home. +                    return +                     +            # Maybe we should wait a bit longer. +            if timeout is not None and time.time() > end_time: +                if timeout > 0: +                    # No more waiting. +                    raise LockTimeout +                else: +                    # Someone else has the lock and we are impatient.. +                    raise AlreadyLocked + +            # Well, okay.  We'll give it a bit longer. +            time.sleep(wait) + +    def release(self): +        if not self.is_locked(): +            raise NotLocked +        if not self.i_am_locking(): +            raise NotMyLock((self._who_is_locking(), self.unique_name)) +        cursor = self.connection.cursor() +        cursor.execute("delete from locks" +                       "  where unique_name = ?", +                       (self.unique_name,)) +        self.connection.commit() + +    def _who_is_locking(self): +        cursor = self.connection.cursor() +        cursor.execute("select unique_name from locks" +                       "  where lock_file = ?", +                       (self.lock_file,)) +        return cursor.fetchone()[0] +         +    def is_locked(self): +        cursor = self.connection.cursor() +        cursor.execute("select * from locks" +                       "  where lock_file = ?", +                       (self.lock_file,)) +        rows = cursor.fetchall() +        return not not rows + +    def i_am_locking(self): +        cursor = self.connection.cursor() +        cursor.execute("select * from locks" +                       "  where lock_file = ?" +                       "    and unique_name = ?", +                       (self.lock_file, self.unique_name)) +        return not not cursor.fetchall() + +    def break_lock(self): +        cursor = self.connection.cursor() +        cursor.execute("delete from locks" +                       "  where lock_file = ?", +                       (self.lock_file,)) +        self.connection.commit() diff --git a/lib/thandy/packagesys/ThpPackages.py b/lib/thandy/packagesys/ThpPackages.py index b87c7b4..3ca9d52 100644 --- a/lib/thandy/packagesys/ThpPackages.py +++ b/lib/thandy/packagesys/ThpPackages.py @@ -6,7 +6,10 @@ import zipfile  import tempfile  import time +from lockfile import LockFile +  import thandy.util +import thandy.formats  import thandy.packagesys.PackageSystem as PS  import thandy.packagesys.PackageDB as PDB @@ -90,12 +93,21 @@ class ThpInstaller(PS.Installer):          if self._thp_root is None:            raise Exception("There is no THP_INSTALL_ROOT variable set") -        pkg = ThpPackage(os.path.join(self._cacheRoot, self._relPath[1:])) -        self._db.insert(pkg.getAll()) +        lockfile = os.path.join(self._thp_db_root, ".lock") +        lock = LockFile(lockfile) +        try: +            lock.acquire() +            pkg = ThpPackage(os.path.join(self._cacheRoot, self._relPath[1:])) +            shutil.copytree() +        except AlreadyLocked: +            print "You can't run more than one instance of Thandy" +        except LockFailed: +            print "Can't acquire lock on %s" % lockfile +#        self._db.insert(pkg.getAll()) +#        self._db.statusInstalled(pkg.getAll())  #        self._db.delete(pkg.getAll()) -        print self._db.exists(pkg.get("package_name")) +#        print self._db.exists(pkg.get("package_name")) -#        shutil.copytree()      def remove(self):          print "Running thp remover" @@ -104,6 +116,7 @@ class ThpPackage(object):      def __init__(self, thp_path):          self._thp_path = thp_path          self._metadata = None +        self._valid = False          self._process() @@ -118,6 +131,7 @@ class ThpPackage(object):          thpFile.extractall(tmpPath)          contents = open(os.path.join(tmpPath, "meta", "package.json")).read()          self._metadata = json.loads(contents) +        print self._validateFiles(tmpPath)          thandy.util.deltree(tmpPath) @@ -127,3 +141,17 @@ class ThpPackage(object):      def getAll(self):          return self._metadata + +    def isValid(self): +        return self._valid + +    def _validateFiles(self, tmpPath): +        for manifest in self._metadata['manifest']: +            name = manifest['name'] +            digest = manifest['digest'] +            is_config = manifest['is_config'] +            f = open(os.path.join(tmpPath, "content", name), "rb") +            newdigest = thandy.formats.formatHash(thandy.formats.getFileDigest(f)) +            f.close() +            if newdigest != digest: +                return (False, [name, digest, newdigest])  | 
