def __init__(self, name):
     self.fsmLock = RLock()
     self._name = name
     self.stateArray = []
     self._serialNum = FSM.SerialNum
     FSM.SerialNum += 1
     self._broadcastStateChanges = False
     self._state = 'Off'
     self.__requestQueue = []
예제 #2
0
    def __init__(self, appRunner, taskChain='default'):
        self.globalLock.acquire()
        try:
            self.uniqueId = PackageInstaller.nextUniqueId
            PackageInstaller.nextUniqueId += 1
        finally:
            self.globalLock.release()

        self.appRunner = appRunner
        self.taskChain = taskChain

        # If we're to be running on an asynchronous task chain, and
        # the task chain hasn't yet been set up already, create the
        # default parameters now.
        if taskChain != 'default' and not taskMgr.hasTaskChain(self.taskChain):
            taskMgr.setupTaskChain(self.taskChain,
                                   numThreads=1,
                                   threadPriority=TPLow)

        self.callbackLock = Lock()
        self.calledDownloadStarted = False
        self.calledDownloadFinished = False

        # A list of all packages that have been added to the
        # installer.
        self.packageLock = RLock()
        self.packages = []
        self.state = self.S_initial

        # A list of packages that are waiting for their desc files.
        self.needsDescFile = []
        self.descFileTask = None

        # A list of packages that are waiting to be downloaded and
        # installed.
        self.needsDownload = []
        self.downloadTask = None

        # A list of packages that were already done at the time they
        # were passed to addPackage().
        self.earlyDone = []

        # A list of packages that have been successfully installed, or
        # packages that have failed.
        self.done = []
        self.failed = []

        # This task is spawned on the default task chain, to update
        # the status during the download.
        self.progressTask = None

        self.accept('PackageInstaller-%s-allHaveDesc' % self.uniqueId,
                    self.__allHaveDesc)
        self.accept('PackageInstaller-%s-packageStarted' % self.uniqueId,
                    self.__packageStarted)
        self.accept('PackageInstaller-%s-packageDone' % self.uniqueId,
                    self.__packageDone)
예제 #3
0
    def __init__(self, name):
        self.fsmLock = RLock()
        self.name = name
        self._serialNum = FSM.SerialNum
        FSM.SerialNum += 1
        self._broadcastStateChanges = False
        # Initially, we are in the Off state by convention.
        self.state = 'Off'

        # This member records transition requests made by demand() or
        # forceTransition() while the FSM is in transition between
        # states.
        self.__requestQueue = []

        if __debug__:
            from direct.fsm.ClassicFSM import _debugFsms
            import weakref
            _debugFsms[name]=weakref.ref(self)
예제 #4
0
 def __init__(self, name):
     self.fsmLock = RLock()
     self.name = name
     self.stateArray = []
     self._serialNum = FSM.SerialNum
     FSM.SerialNum += 1
     self._broadcastStateChanges = False
     self.state = 'Off'
     self.__requestQueue = []
    def __init__(self, appRunner, taskChain = 'default'):
        self.globalLock.acquire()
        try:
            self.uniqueId = PackageInstaller.nextUniqueId
            PackageInstaller.nextUniqueId += 1
        finally:
            self.globalLock.release()

        self.appRunner = appRunner
        self.taskChain = taskChain

        # If we're to be running on an asynchronous task chain, and
        # the task chain hasn't yet been set up already, create the
        # default parameters now.
        if taskChain != 'default' and not taskMgr.hasTaskChain(self.taskChain):
            taskMgr.setupTaskChain(self.taskChain, numThreads = 1,
                                   threadPriority = TPLow)

        self.callbackLock = Lock()
        self.calledDownloadStarted = False
        self.calledDownloadFinished = False

        # A list of all packages that have been added to the
        # installer.
        self.packageLock = RLock()
        self.packages = []
        self.state = self.S_initial

        # A list of packages that are waiting for their desc files.
        self.needsDescFile = []
        self.descFileTask = None

        # A list of packages that are waiting to be downloaded and
        # installed.
        self.needsDownload = []
        self.downloadTask = None

        # A list of packages that were already done at the time they
        # were passed to addPackage().
        self.earlyDone = []

        # A list of packages that have been successfully installed, or
        # packages that have failed.
        self.done = []
        self.failed = []

        # This task is spawned on the default task chain, to update
        # the status during the download.
        self.progressTask = None

        self.accept('PackageInstaller-%s-allHaveDesc' % self.uniqueId,
                    self.__allHaveDesc)
        self.accept('PackageInstaller-%s-packageStarted' % self.uniqueId,
                    self.__packageStarted)
        self.accept('PackageInstaller-%s-packageDone' % self.uniqueId,
                    self.__packageDone)
예제 #6
0
    def __init__(self, name):
        self.fsmLock = RLock()
        self.name = name
        self.stateArray = []
        self._serialNum = FSM.SerialNum
        FSM.SerialNum += 1
        self._broadcastStateChanges = False
        # Initially, we are in the Off state by convention.
        self.state = 'Off'

        # This member records transition requests made by demand() or
        # forceTransition() while the FSM is in transition between
        # states.
        self.__requestQueue = []

        if __debug__:
            from direct.fsm.ClassicFSM import _debugFsms
            import weakref
            _debugFsms[name]=weakref.ref(self)
예제 #7
0
class FSM(DirectObject):
    """
    A Finite State Machine.  This is intended to be the base class
    of any number of specific machines, which consist of a collection
    of states and transitions, and rules to switch between states
    according to arbitrary input data.

    The states of an FSM are defined implicitly.  Each state is
    identified by a string, which by convention begins with a capital
    letter.  (Also by convention, strings passed to request that are
    not state names should begin with a lowercase letter.)

    To define specialized behavior when entering or exiting a
    particular state, define a method named enterState() and/or
    exitState(), where "State" is the name of the state, e.g.::

        def enterRed(self):
            ... do stuff ...

        def exitRed(self):
            ... cleanup stuff ...

        def enterYellow(self):
            ... do stuff ...

        def exitYellow(self):
            ... cleanup stuff ...

        def enterGreen(self):
            ... do stuff ...

        def exitGreen(self):
            ... cleanup stuff ...

    Both functions can access the previous state name as
    self.oldState, and the new state name we are transitioning to as
    self.newState.  (Of course, in enterRed(), self.newState will
    always be "Red", and the in exitRed(), self.oldState will always
    be "Red".)

    Both functions are optional.  If either function is omitted, the
    state is still defined, but nothing is done when transitioning
    into (or out of) the state.

    Additionally, you may define a filterState() function for each
    state.  The purpose of this function is to decide what state to
    transition to next, if any, on receipt of a particular input.  The
    input is always a string and a tuple of optional parameters (which
    is often empty), and the return value should either be None to do
    nothing, or the name of the state to transition into.  For
    example::

        def filterRed(self, request, args):
            if request in ['Green']:
                return (request,) + args
            return None

        def filterYellow(self, request, args):
            if request in ['Red']:
                return (request,) + args
            return None

        def filterGreen(self, request, args):
            if request in ['Yellow']:
                return (request,) + args
            return None

    As above, the filterState() functions are optional.  If any is
    omitted, the defaultFilter() method is called instead.  A standard
    implementation of defaultFilter() is provided, which may be
    overridden in a derived class to change the behavior on an
    unexpected transition.

    If self.defaultTransitions is left unassigned, then the standard
    implementation of defaultFilter() will return None for any
    lowercase transition name and allow any uppercase transition name
    (this assumes that an uppercase name is a request to go directly
    to a particular state by name).

    self.state may be queried at any time other than during the
    handling of the enter() and exit() functions.  During these
    functions, self.state contains the value None (you are not really
    in any state during the transition).  However, during a transition
    you *can* query the outgoing and incoming states, respectively,
    via self.oldState and self.newState.  At other times, self.state
    contains the name of the current state.

    Initially, the FSM is in state 'Off'.  It does not call enterOff()
    at construction time; it is simply in Off already by convention.
    If you need to call code in enterOff() to initialize your FSM
    properly, call it explicitly in the constructor.  Similarly, when
    `cleanup()` is called or the FSM is destructed, the FSM transitions
    back to 'Off' by convention.  (It does call enterOff() at this
    point, but does not call exitOff().)

    To implement nested hierarchical FSM's, simply create a nested FSM
    and store it on the class within the appropriate enterState()
    function, and clean it up within the corresponding exitState()
    function.

    There is a way to define specialized transition behavior between
    two particular states.  This is done by defining a from<X>To<Y>()
    function, where X is the old state and Y is the new state.  If this
    is defined, it will be run in place of the exit<X> and enter<Y>
    functions, so if you want that behavior, you'll have to call them
    specifically.  Otherwise, you can completely replace that transition's
    behavior.

    See the code in SampleFSM.py for further examples.
    """

    notify = DirectNotifyGlobal.directNotify.newCategory("FSM")

    SerialNum = 0

    # This member lists the default transitions that are accepted
    # without question by the defaultFilter.  It's a map of state
    # names to a list of legal target state names from that state.
    # Define it only if you want to use the classic FSM model of
    # defining all (or most) of your transitions up front.  If
    # this is set to None (the default), all named-state
    # transitions (that is, those requests whose name begins with
    # a capital letter) are allowed.  If it is set to an empty
    # map, no transitions are implicitly allowed--all transitions
    # must be approved by some filter function.
    defaultTransitions = None

    # An enum class for special states like the DEFAULT or ANY state,
    # that should be treatened by the FSM in a special way
    class EnumStates():
        ANY = 1
        DEFAULT = 2

    def __init__(self, name):
        self.fsmLock = RLock()
        self._name = name
        self.stateArray = []
        self._serialNum = FSM.SerialNum
        FSM.SerialNum += 1
        self._broadcastStateChanges = False
        # Initially, we are in the Off state by convention.
        self.state = 'Off'

        # This member records transition requests made by demand() or
        # forceTransition() while the FSM is in transition between
        # states.
        self.__requestQueue = []

        if __debug__:
            from direct.fsm.ClassicFSM import _debugFsms
            import weakref
            _debugFsms[name] = weakref.ref(self)

    def cleanup(self):
        # A convenience function to force the FSM to clean itself up
        # by transitioning to the "Off" state.
        self.fsmLock.acquire()
        try:
            assert self.state
            if self.state != 'Off':
                self.__setState('Off')
        finally:
            self.fsmLock.release()

    def setBroadcastStateChanges(self, doBroadcast):
        self._broadcastStateChanges = doBroadcast

    def getStateChangeEvent(self):
        # if setBroadcastStateChanges(True), this event will be sent through
        # the messenger on every state change. The new and old states are
        # accessible as self.oldState and self.newState, and the transition
        # functions will already have been called.
        return 'FSM-%s-%s-stateChange' % (self._serialNum, self._name)

    def getCurrentFilter(self):
        if not self.state:
            error = "FSM cannot determine current filter while in transition (%s -> %s)." % (
                self.oldState, self.newState)
            raise AlreadyInTransition(error)

        filter = getattr(self, "filter" + self.state, None)
        if not filter:
            # If there's no matching filterState() function, call
            # defaultFilter() instead.
            filter = self.defaultFilter

        return filter

    def getCurrentOrNextState(self):
        # Returns the current state if we are in a state now, or the
        # state we are transitioning into if we are currently within
        # the enter or exit function for a state.
        self.fsmLock.acquire()
        try:
            if self.state:
                return self.state
            return self.newState
        finally:
            self.fsmLock.release()

    def getCurrentStateOrTransition(self):
        # Returns the current state if we are in a state now, or the
        # transition we are performing if we are currently within
        # the enter or exit function for a state.
        self.fsmLock.acquire()
        try:
            if self.state:
                return self.state
            return '%s -> %s' % (self.oldState, self.newState)
        finally:
            self.fsmLock.release()

    def isInTransition(self):
        self.fsmLock.acquire()
        try:
            return self.state == None
        finally:
            self.fsmLock.release()

    def forceTransition(self, request, *args):
        """Changes unconditionally to the indicated state.  This
        bypasses the filterState() function, and just calls
        exitState() followed by enterState()."""

        self.fsmLock.acquire()
        try:
            assert isinstance(request, str)
            self.notify.debug("%s.forceTransition(%s, %s" %
                              (self._name, request, str(args)[1:]))

            if not self.state:
                # Queue up the request.
                self.__requestQueue.append(
                    PythonUtil.Functor(self.forceTransition, request, *args))
                return

            self.__setState(request, *args)
        finally:
            self.fsmLock.release()

    def demand(self, request, *args):
        """Requests a state transition, by code that does not expect
        the request to be denied.  If the request is denied, raises a
        `RequestDenied` exception.

        Unlike `request()`, this method allows a new request to be made
        while the FSM is currently in transition.  In this case, the
        request is queued up and will be executed when the current
        transition finishes.  Multiple requests will queue up in
        sequence.
        """

        self.fsmLock.acquire()
        try:
            assert isinstance(request, str)
            self.notify.debug("%s.demand(%s, %s" %
                              (self._name, request, str(args)[1:]))
            if not self.state:
                # Queue up the request.
                self.__requestQueue.append(
                    PythonUtil.Functor(self.demand, request, *args))
                return

            if not self.request(request, *args):
                raise RequestDenied("%s (from state: %s)" %
                                    (request, self.state))
        finally:
            self.fsmLock.release()

    def request(self, request, *args):
        """Requests a state transition (or other behavior).  The
        request may be denied by the FSM's filter function.  If it is
        denied, the filter function may either raise an exception
        (`RequestDenied`), or it may simply return None, without
        changing the FSM's state.

        The request parameter should be a string.  The request, along
        with any additional arguments, is passed to the current
        filterState() function.  If filterState() returns a string,
        the FSM transitions to that state.

        The return value is the same as the return value of
        filterState() (that is, None if the request does not provoke a
        state transition, otherwise it is a tuple containing the name
        of the state followed by any optional args.)

        If the FSM is currently in transition (i.e. in the middle of
        executing an enterState or exitState function), an
        `AlreadyInTransition` exception is raised (but see `demand()`,
        which will queue these requests up and apply when the
        transition is complete)."""

        self.fsmLock.acquire()
        try:
            assert isinstance(request, str)
            self.notify.debug("%s.request(%s, %s" %
                              (self._name, request, str(args)[1:]))

            filter = self.getCurrentFilter()
            result = filter(request, args)
            if result:
                if isinstance(result, str):
                    # If the return value is a string, it's just the name
                    # of the state.  Wrap it in a tuple for consistency.
                    result = (result, ) + args

                # Otherwise, assume it's a (name, *args) tuple
                self.__setState(*result)

            return result
        finally:
            self.fsmLock.release()

    def defaultEnter(self, *args):
        """ This is the default function that is called if there is no
        enterState() method for a particular state name. """
        pass

    def defaultExit(self):
        """ This is the default function that is called if there is no
        exitState() method for a particular state name. """
        pass

    def defaultFilter(self, request, args):
        """This is the function that is called if there is no
        filterState() method for a particular state name.

        This default filter function behaves in one of two modes:

        (1) if self.defaultTransitions is None, allow any request
        whose name begins with a capital letter, which is assumed to
        be a direct request to a particular state.  This is similar to
        the old ClassicFSM onUndefTransition=ALLOW, with no explicit
        state transitions listed.

        (2) if self.defaultTransitions is not None, allow only those
        requests explicitly identified in this map.  This is similar
        to the old ClassicFSM onUndefTransition=DISALLOW, with an
        explicit list of allowed state transitions.

        Specialized FSM's may wish to redefine this default filter
        (for instance, to always return the request itself, thus
        allowing any transition.)."""

        if request == 'Off':
            # We can always go to the "Off" state.
            return (request, ) + args

        if self.defaultTransitions is None:
            # If self.defaultTransitions is None, it means to accept
            # all requests whose name begins with a capital letter.
            # These are direct requests to a particular state.
            if request[0].isupper():
                return (request, ) + args
        else:
            # If self.defaultTransitions is not None, it is a map of
            # allowed transitions from each state.  That is, each key
            # of the map is the current state name; for that key, the
            # value is a list of allowed transitions from the
            # indicated state.
            if request in self.defaultTransitions.get(self.state, []):
                # This transition is listed in the defaultTransitions map;
                # accept it.
                return (request, ) + args

            elif FSM.EnumStates.ANY in self.defaultTransitions.get(
                    self.state, []):
                # Whenever we have a '*' as our to transition, we allow
                # to transit to any other state
                return (request, ) + args

            elif request in self.defaultTransitions.get(
                    FSM.EnumStates.ANY, []):
                # If the requested state is in the default transitions
                # from any state list, we also alow to transit to the
                # new state
                return (request, ) + args

            elif FSM.EnumStates.ANY in self.defaultTransitions.get(
                    FSM.EnumStates.ANY, []):
                # This is like we had set the defaultTransitions to None.
                # Any state can transit to any other state
                return (request, ) + args

            elif request in self.defaultTransitions.get(
                    FSM.EnumStates.DEFAULT, []):
                # This is the fallback state that we use whenever no
                # other trnasition was possible
                return (request, ) + args

            # If self.defaultTransitions is not None, it is an error
            # to request a direct state transition (capital letter
            # request) not listed in defaultTransitions and not
            # handled by an earlier filter.
            if request[0].isupper():
                raise RequestDenied("%s (from state: %s)" %
                                    (request, self.state))

        # In either case, we quietly ignore unhandled command
        # (lowercase) requests.
        assert self.notify.debug("%s ignoring request %s from state %s." %
                                 (self._name, request, self.state))
        return None

    def filterOff(self, request, args):
        """From the off state, we can always go directly to any other
        state."""
        if request[0].isupper():
            return (request, ) + args
        return self.defaultFilter(request, args)

    def setStateArray(self, stateArray):
        """array of unique states to iterate through"""
        self.fsmLock.acquire()
        try:
            self.stateArray = stateArray
        finally:
            self.fsmLock.release()

    def requestNext(self, *args):
        """Request the 'next' state in the predefined state array."""
        self.fsmLock.acquire()
        try:
            if self.stateArray:
                if not self.state in self.stateArray:
                    self.request(self.stateArray[0])
                else:
                    cur_index = self.stateArray.index(self.state)
                    new_index = (cur_index + 1) % len(self.stateArray)
                    self.request(self.stateArray[new_index], args)
            else:
                assert self.notifier.debug(
                    "stateArray empty. Can't switch to next.")

        finally:
            self.fsmLock.release()

    def requestPrev(self, *args):
        """Request the 'previous' state in the predefined state array."""
        self.fsmLock.acquire()
        try:
            if self.stateArray:
                if not self.state in self.stateArray:
                    self.request(self.stateArray[0])
                else:
                    cur_index = self.stateArray.index(self.state)
                    new_index = (cur_index - 1) % len(self.stateArray)
                    self.request(self.stateArray[new_index], args)
            else:
                assert self.notifier.debug(
                    "stateArray empty. Can't switch to next.")
        finally:
            self.fsmLock.release()

    def __setState(self, newState, *args):
        # Internal function to change unconditionally to the indicated
        # state.
        assert self.state
        assert self.notify.debug("%s to state %s." % (self._name, newState))

        self.oldState = self.state
        self.newState = newState
        self.state = None

        try:
            if not self.__callFromToFunc(self.oldState, self.newState, *args):
                self.__callExitFunc(self.oldState)
                self.__callEnterFunc(self.newState, *args)
                pass
            pass
        except:
            # If we got an exception during the enter or exit methods,
            # go directly to state "InternalError" and raise up the
            # exception.  This might leave things a little unclean
            # since we've partially transitioned, but what can you do?

            self.state = 'InternalError'
            del self.oldState
            del self.newState
            raise

        if self._broadcastStateChanges:
            messenger.send(self.getStateChangeEvent())

        self.state = newState
        del self.oldState
        del self.newState

        if self.__requestQueue:
            request = self.__requestQueue.pop(0)
            assert self.notify.debug("%s continued queued request." %
                                     (self._name))
            request()

    def __callEnterFunc(self, name, *args):
        # Calls the appropriate enter function when transitioning into
        # a new state, if it exists.
        assert self.state == None and self.newState == name

        func = getattr(self, "enter" + name, None)
        if not func:
            # If there's no matching enterFoo() function, call
            # defaultEnter() instead.
            func = self.defaultEnter
        func(*args)

    def __callFromToFunc(self, oldState, newState, *args):
        # Calls the appropriate fromTo function when transitioning into
        # a new state, if it exists.
        assert self.state == None and self.oldState == oldState and self.newState == newState

        func = getattr(self, "from%sTo%s" % (oldState, newState), None)
        if func:
            func(*args)
            return True
        return False

    def __callExitFunc(self, name):
        # Calls the appropriate exit function when leaving a
        # state, if it exists.
        assert self.state == None and self.oldState == name

        func = getattr(self, "exit" + name, None)
        if not func:
            # If there's no matching exitFoo() function, call
            # defaultExit() instead.
            func = self.defaultExit
        func()

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        """
        Print out something useful about the fsm
        """
        self.fsmLock.acquire()
        try:
            className = self.__class__.__name__
            if self.state:
                str = ('%s FSM:%s in state "%s"' %
                       (className, self._name, self.state))
            else:
                str = ('%s FSM:%s in transition from \'%s\' to \'%s\'' %
                       (className, self._name, self.oldState, self.newState))
            return str
        finally:
            self.fsmLock.release()
예제 #8
0
class PackageInstaller(DirectObject):
    """ This class is used in a p3d runtime environment to manage the
    asynchronous download and installation of packages.  If you just
    want to install a package synchronously, see
    appRunner.installPackage() for a simpler interface.

    To use this class, you should subclass from it and override any of
    the six callback methods: downloadStarted(), packageStarted(),
    packageProgress(), downloadProgress(), packageFinished(),
    downloadFinished().

    Also see DWBPackageInstaller, which does exactly this, to add a
    DirectWaitBar GUI.

    """

    notify = directNotify.newCategory("PackageInstaller")

    globalLock = Lock()
    nextUniqueId = 1

    # This is a chain of state values progressing forward in time.
    S_initial = 0  # addPackage() calls are being made
    S_ready = 1  # donePackages() has been called
    S_started = 2  # download has started
    S_done = 3  # download is over

    class PendingPackage:
        """ This class describes a package added to the installer for
        download. """

        notify = directNotify.newCategory("PendingPackage")

        def __init__(self, packageName, version, host):
            self.packageName = packageName
            self.version = version
            self.host = host

            # This will be filled in properly by checkDescFile() or
            # getDescFile(); in the meantime, set a placeholder.
            self.package = PackageInfo(host, packageName, version)

            # Set true when the package has finished downloading,
            # either successfully or unsuccessfully.
            self.done = False

            # Set true or false when self.done has been set.
            self.success = False

            # Set true when the packageFinished() callback has been
            # delivered.
            self.notified = False

            # These are used to ensure the callbacks only get
            # delivered once for a particular package.
            self.calledPackageStarted = False
            self.calledPackageFinished = False

            # This is the amount of stuff we have to process to
            # install this package, and the amount of stuff we have
            # processed so far.  "Stuff" includes bytes downloaded,
            # bytes uncompressed, and bytes extracted; and each of
            # which is weighted differently into one grand total.  So,
            # the total doesn't really represent bytes; it's a
            # unitless number, which means something only as a ratio
            # to other packages.  Filled in by checkDescFile() or
            # getDescFile().
            self.downloadEffort = 0

            # Similar, but this is the theoretical effort if the
            # package were already downloaded.
            self.prevDownloadedEffort = 0

        def __cmp__(self, pp):
            """ Python comparision function.  This makes all
            PendingPackages withe same (packageName, version, host)
            combination be deemed equivalent. """
            return cmp((self.packageName, self.version, self.host),
                       (pp.packageName, pp.version, pp.host))

        def getProgress(self):
            """ Returns the download progress of this package in the
            range 0..1. """

            return self.package.downloadProgress

        def checkDescFile(self):
            """ Returns true if the desc file is already downloaded
            and good, or false if it needs to be downloaded. """

            if not self.host.hasCurrentContentsFile():
                # If the contents file isn't ready yet, we can't check
                # the desc file yet.
                return False

            # All right, get the package info now.
            package = self.host.getPackage(self.packageName, self.version)
            if not package:
                self.notify.warning(
                    "Package %s %s not known on %s" %
                    (self.packageName, self.version, self.host.hostUrl))
                return False

            self.package = package
            self.package.checkStatus()

            if not self.package.hasDescFile:
                return False

            self.downloadEffort = self.package.getDownloadEffort()
            self.prevDownloadEffort = 0
            if self.downloadEffort == 0:
                self.prevDownloadedEffort = self.package.getPrevDownloadedEffort(
                )

            return True

        def getDescFile(self, http):
            """ Synchronously downloads the desc files required for
            the package. """

            if not self.host.downloadContentsFile(http):
                return False

            # All right, get the package info now.
            package = self.host.getPackage(self.packageName, self.version)
            if not package:
                self.notify.warning(
                    "Package %s %s not known on %s" %
                    (self.packageName, self.version, self.host.hostUrl))
                return False

            self.package = package
            if not self.package.downloadDescFile(http):
                return False

            self.package.checkStatus()
            self.downloadEffort = self.package.getDownloadEffort()
            self.prevDownloadEffort = 0
            if self.downloadEffort == 0:
                self.prevDownloadedEffort = self.package.getPrevDownloadedEffort(
                )

            return True

    def __init__(self, appRunner, taskChain='default'):
        self.globalLock.acquire()
        try:
            self.uniqueId = PackageInstaller.nextUniqueId
            PackageInstaller.nextUniqueId += 1
        finally:
            self.globalLock.release()

        self.appRunner = appRunner
        self.taskChain = taskChain

        # If we're to be running on an asynchronous task chain, and
        # the task chain hasn't yet been set up already, create the
        # default parameters now.
        if taskChain != 'default' and not taskMgr.hasTaskChain(self.taskChain):
            taskMgr.setupTaskChain(self.taskChain,
                                   numThreads=1,
                                   threadPriority=TPLow)

        self.callbackLock = Lock()
        self.calledDownloadStarted = False
        self.calledDownloadFinished = False

        # A list of all packages that have been added to the
        # installer.
        self.packageLock = RLock()
        self.packages = []
        self.state = self.S_initial

        # A list of packages that are waiting for their desc files.
        self.needsDescFile = []
        self.descFileTask = None

        # A list of packages that are waiting to be downloaded and
        # installed.
        self.needsDownload = []
        self.downloadTask = None

        # A list of packages that were already done at the time they
        # were passed to addPackage().
        self.earlyDone = []

        # A list of packages that have been successfully installed, or
        # packages that have failed.
        self.done = []
        self.failed = []

        # This task is spawned on the default task chain, to update
        # the status during the download.
        self.progressTask = None

        self.accept('PackageInstaller-%s-allHaveDesc' % self.uniqueId,
                    self.__allHaveDesc)
        self.accept('PackageInstaller-%s-packageStarted' % self.uniqueId,
                    self.__packageStarted)
        self.accept('PackageInstaller-%s-packageDone' % self.uniqueId,
                    self.__packageDone)

    def destroy(self):
        """ Interrupts all pending downloads.  No further callbacks
        will be made. """
        self.cleanup()

    def cleanup(self):
        """ Interrupts all pending downloads.  No further callbacks
        will be made. """

        self.packageLock.acquire()
        try:
            if self.descFileTask:
                taskMgr.remove(self.descFileTask)
                self.descFileTask = None
            if self.downloadTask:
                taskMgr.remove(self.downloadTask)
                self.downloadTask = None
        finally:
            self.packageLock.release()

        if self.progressTask:
            taskMgr.remove(self.progressTask)
            self.progressTask = None

        self.ignoreAll()

    def addPackage(self, packageName, version=None, hostUrl=None):
        """ Adds the named package to the list of packages to be
        downloaded.  Call donePackages() to finish the list. """

        if self.state != self.S_initial:
            raise ValueError, 'addPackage called after donePackages'

        host = self.appRunner.getHostWithAlt(hostUrl)
        pp = self.PendingPackage(packageName, version, host)

        self.packageLock.acquire()
        try:
            self.__internalAddPackage(pp)
        finally:
            self.packageLock.release()

    def __internalAddPackage(self, pp):
        """ Adds the indicated "pending package" to the appropriate
        list(s) for downloading and installing.  Assumes packageLock
        is already held."""

        if pp in self.packages:
            # Already added.
            return

        self.packages.append(pp)

        # We always add the package to needsDescFile, even if we
        # already have its desc file; this guarantees that packages
        # are downloaded in the order they are added.
        self.needsDescFile.append(pp)
        if not self.descFileTask:
            self.descFileTask = taskMgr.add(self.__getDescFileTask,
                                            'getDescFile',
                                            taskChain=self.taskChain)

    def donePackages(self):
        """ After calling addPackage() for each package to be
        installed, call donePackages() to mark the end of the list.
        This is necessary to determine what the complete set of
        packages is (and therefore how large the total download size
        is).  None of the low-level callbacks will be made before this
        call. """

        if self.state != self.S_initial:
            # We've already been here.
            return

        # Throw the messages for packages that were already done
        # before we started.
        for pp in self.earlyDone:
            self.__donePackage(pp, True)
        self.earlyDone = []

        self.packageLock.acquire()
        try:
            if self.state != self.S_initial:
                return
            self.state = self.S_ready
            if not self.needsDescFile:
                # All package desc files are already available; so begin.
                self.__prepareToStart()
        finally:
            self.packageLock.release()

        if not self.packages:
            # Trivial no-op.
            self.__callDownloadFinished(True)

    def downloadStarted(self):
        """ This callback is made at some point after donePackages()
        is called; at the time of this callback, the total download
        size is known, and we can sensibly report progress through the
        whole. """

        self.notify.info("downloadStarted")

    def packageStarted(self, package):
        """ This callback is made for each package between
        downloadStarted() and downloadFinished() to indicate the start
        of a new package. """

        self.notify.debug("packageStarted: %s" % (package.packageName))

    def packageProgress(self, package, progress):
        """ This callback is made repeatedly between packageStarted()
        and packageFinished() to update the current progress on the
        indicated package only.  The progress value ranges from 0
        (beginning) to 1 (complete). """

        self.notify.debug("packageProgress: %s %s" %
                          (package.packageName, progress))

    def downloadProgress(self, overallProgress):
        """ This callback is made repeatedly between downloadStarted()
        and downloadFinished() to update the current progress through
        all packages.  The progress value ranges from 0 (beginning) to
        1 (complete). """

        self.notify.debug("downloadProgress: %s" % (overallProgress))

    def packageFinished(self, package, success):
        """ This callback is made for each package between
        downloadStarted() and downloadFinished() to indicate that a
        package has finished downloading.  If success is true, there
        were no problems and the package is now installed.

        If this package did not require downloading (because it was
        already downloaded), this callback will be made immediately,
        *without* a corresponding call to packageStarted(), and may
        even be made before downloadStarted(). """

        self.notify.info("packageFinished: %s %s" %
                         (package.packageName, success))

    def downloadFinished(self, success):
        """ This callback is made when all of the packages have been
        downloaded and installed (or there has been some failure).  If
        all packages where successfully installed, success is True.

        If there were no packages that required downloading, this
        callback will be made immediately, *without* a corresponding
        call to downloadStarted(). """

        self.notify.info("downloadFinished: %s" % (success))

    def __prepareToStart(self):
        """ This is called internally when transitioning from S_ready
        to S_started.  It sets up whatever initial values are
        needed.  Assumes self.packageLock is held.  Returns False if
        there were no packages to download, and the state was
        therefore transitioned immediately to S_done. """

        if not self.needsDownload:
            self.state = self.S_done
            return False

        self.state = self.S_started

        assert not self.downloadTask
        self.downloadTask = taskMgr.add(self.__downloadPackageTask,
                                        'downloadPackage',
                                        taskChain=self.taskChain)

        assert not self.progressTask
        self.progressTask = taskMgr.add(self.__progressTask, 'packageProgress')

        return True

    def __allHaveDesc(self):
        """ This method is called internally when all of the pending
        packages have their desc info. """
        working = True

        self.packageLock.acquire()
        try:
            if self.state == self.S_ready:
                # We've already called donePackages(), so move on now.
                working = self.__prepareToStart()
        finally:
            self.packageLock.release()

        if not working:
            self.__callDownloadFinished(True)

    def __packageStarted(self, pp):
        """ This method is called when a single package is beginning
        to download. """

        self.__callDownloadStarted()
        self.__callPackageStarted(pp)

    def __packageDone(self, pp):
        """ This method is called when a single package has been
        downloaded and installed, or has failed. """

        self.__callPackageFinished(pp, pp.success)
        pp.notified = True

        # See if there are more packages to go.
        success = True
        allDone = True
        self.packageLock.acquire()
        try:
            for pp in self.packages:
                if pp.notified:
                    success = success and pp.success
                else:
                    allDone = False
        finally:
            self.packageLock.release()

        if allDone:
            self.__callDownloadFinished(success)

    def __callPackageStarted(self, pp):
        """ Calls the packageStarted() callback for a particular
        package if it has not already been called, being careful to
        avoid race conditions. """

        self.callbackLock.acquire()
        try:
            if not pp.calledPackageStarted:
                self.packageStarted(pp.package)
                self.packageProgress(pp.package, 0)
                pp.calledPackageStarted = True
        finally:
            self.callbackLock.release()

    def __callPackageFinished(self, pp, success):
        """ Calls the packageFinished() callback for a paricular
        package if it has not already been called, being careful to
        avoid race conditions. """

        self.callbackLock.acquire()
        try:
            if not pp.calledPackageFinished:
                if success:
                    self.packageProgress(pp.package, 1)
                self.packageFinished(pp.package, success)
                pp.calledPackageFinished = True
        finally:
            self.callbackLock.release()

    def __callDownloadStarted(self):
        """ Calls the downloadStarted() callback if it has not already
        been called, being careful to avoid race conditions. """

        self.callbackLock.acquire()
        try:
            if not self.calledDownloadStarted:
                self.downloadStarted()
                self.downloadProgress(0)
                self.calledDownloadStarted = True
        finally:
            self.callbackLock.release()

    def __callDownloadFinished(self, success):
        """ Calls the downloadFinished() callback if it has not
        already been called, being careful to avoid race
        conditions. """

        self.callbackLock.acquire()
        try:
            if not self.calledDownloadFinished:
                if success:
                    self.downloadProgress(1)
                self.downloadFinished(success)
                self.calledDownloadFinished = True
        finally:
            self.callbackLock.release()

    def __getDescFileTask(self, task):
        """ This task runs on the aysynchronous task chain; each pass,
        it extracts one package from self.needsDescFile and downloads
        its desc file.  On success, it adds the package to
        self.needsDownload. """

        self.packageLock.acquire()
        try:
            # If we've finished all of the packages that need desc
            # files, stop the task.
            if not self.needsDescFile:
                self.descFileTask = None

                eventName = 'PackageInstaller-%s-allHaveDesc' % self.uniqueId
                messenger.send(eventName, taskChain='default')

                return task.done
            pp = self.needsDescFile[0]
            del self.needsDescFile[0]
        finally:
            self.packageLock.release()

        # Now serve this one package.
        if not pp.checkDescFile():
            if not pp.getDescFile(self.appRunner.http):
                self.__donePackage(pp, False)
                return task.cont

        # This package is now ready to be downloaded.  We always add
        # it to needsDownload, even if it's already downloaded, to
        # guarantee ordering of packages.

        self.packageLock.acquire()
        try:
            # Also add any packages required by this one.
            for packageName, version, host in pp.package.requires:
                pp2 = self.PendingPackage(packageName, version, host)
                self.__internalAddPackage(pp2)
            self.needsDownload.append(pp)
        finally:
            self.packageLock.release()

        return task.cont

    def __downloadPackageTask(self, task):
        """ This task runs on the aysynchronous task chain; each pass,
        it extracts one package from self.needsDownload and downloads
        it. """

        while True:
            self.packageLock.acquire()
            try:
                # If we're done downloading, stop the task.
                if self.state == self.S_done or not self.needsDownload:
                    self.downloadTask = None
                    self.packageLock.release()
                    yield task.done
                    return

                assert self.state == self.S_started
                pp = self.needsDownload[0]
                del self.needsDownload[0]
            except:
                self.packageLock.release()
                raise
            self.packageLock.release()

            # Now serve this one package.
            eventName = 'PackageInstaller-%s-packageStarted' % self.uniqueId
            messenger.send(eventName, [pp], taskChain='default')

            if not pp.package.hasPackage:
                for token in pp.package.downloadPackageGenerator(
                        self.appRunner.http):
                    if token == pp.package.stepContinue:
                        yield task.cont
                    else:
                        break

                if token != pp.package.stepComplete:
                    pc = PStatCollector(
                        ':App:PackageInstaller:donePackage:%s' %
                        (pp.package.packageName))
                    pc.start()
                    self.__donePackage(pp, False)
                    pc.stop()
                    yield task.cont
                    continue

            # Successfully downloaded and installed.
            pc = PStatCollector(':App:PackageInstaller:donePackage:%s' %
                                (pp.package.packageName))
            pc.start()
            self.__donePackage(pp, True)
            pc.stop()

            # Continue the loop without yielding, so we pick up the
            # next package within this same frame.

    def __donePackage(self, pp, success):
        """ Marks the indicated package as done, either successfully
        or otherwise. """
        assert not pp.done

        if success:
            pc = PStatCollector(':App:PackageInstaller:install:%s' %
                                (pp.package.packageName))
            pc.start()
            pp.package.installPackage(self.appRunner)
            pc.stop()

        self.packageLock.acquire()
        try:
            pp.done = True
            pp.success = success
            if success:
                self.done.append(pp)
            else:
                self.failed.append(pp)
        finally:
            self.packageLock.release()

        eventName = 'PackageInstaller-%s-packageDone' % self.uniqueId
        messenger.send(eventName, [pp], taskChain='default')

    def __progressTask(self, task):
        self.callbackLock.acquire()
        try:
            if not self.calledDownloadStarted:
                # We haven't yet officially started the download.
                return task.cont

            if self.calledDownloadFinished:
                # We've officially ended the download.
                self.progressTask = None
                return task.done

            downloadEffort = 0
            currentDownloadSize = 0
            for pp in self.packages:
                downloadEffort += pp.downloadEffort + pp.prevDownloadedEffort
                packageProgress = pp.getProgress()
                currentDownloadSize += pp.downloadEffort * packageProgress + pp.prevDownloadedEffort
                if pp.calledPackageStarted and not pp.calledPackageFinished:
                    self.packageProgress(pp.package, packageProgress)

            if not downloadEffort:
                progress = 1
            else:
                progress = float(currentDownloadSize) / float(downloadEffort)
            self.downloadProgress(progress)

        finally:
            self.callbackLock.release()

        return task.cont
class PackageInstaller(DirectObject):

    """ This class is used in a p3d runtime environment to manage the
    asynchronous download and installation of packages.  If you just
    want to install a package synchronously, see
    appRunner.installPackage() for a simpler interface.

    To use this class, you should subclass from it and override any of
    the six callback methods: downloadStarted(), packageStarted(),
    packageProgress(), downloadProgress(), packageFinished(),
    downloadFinished().

    Also see DWBPackageInstaller, which does exactly this, to add a
    DirectWaitBar GUI.

    """

    notify = directNotify.newCategory("PackageInstaller")

    globalLock = Lock()
    nextUniqueId = 1

    # This is a chain of state values progressing forward in time.
    S_initial = 0    # addPackage() calls are being made
    S_ready = 1      # donePackages() has been called
    S_started = 2    # download has started
    S_done = 3       # download is over

    class PendingPackage:
        """ This class describes a package added to the installer for
        download. """

        notify = directNotify.newCategory("PendingPackage")

        def __init__(self, packageName, version, host):
            self.packageName = packageName
            self.version = version
            self.host = host

            # This will be filled in properly by checkDescFile() or
            # getDescFile(); in the meantime, set a placeholder.
            self.package = PackageInfo(host, packageName, version)

            # Set true when the package has finished downloading,
            # either successfully or unsuccessfully.
            self.done = False

            # Set true or false when self.done has been set.
            self.success = False

            # Set true when the packageFinished() callback has been
            # delivered.
            self.notified = False

            # These are used to ensure the callbacks only get
            # delivered once for a particular package.
            self.calledPackageStarted = False
            self.calledPackageFinished = False

            # This is the amount of stuff we have to process to
            # install this package, and the amount of stuff we have
            # processed so far.  "Stuff" includes bytes downloaded,
            # bytes uncompressed, and bytes extracted; and each of
            # which is weighted differently into one grand total.  So,
            # the total doesn't really represent bytes; it's a
            # unitless number, which means something only as a ratio
            # to other packages.  Filled in by checkDescFile() or
            # getDescFile().
            self.downloadEffort = 0

            # Similar, but this is the theoretical effort if the
            # package were already downloaded.
            self.prevDownloadedEffort = 0

        def __cmp__(self, pp):
            """ Python comparision function.  This makes all
            PendingPackages withe same (packageName, version, host)
            combination be deemed equivalent. """
            return cmp((self.packageName, self.version, self.host),
                       (pp.packageName, pp.version, pp.host))

        def getProgress(self):
            """ Returns the download progress of this package in the
            range 0..1. """

            return self.package.downloadProgress

        def checkDescFile(self):
            """ Returns true if the desc file is already downloaded
            and good, or false if it needs to be downloaded. """

            if not self.host.hasCurrentContentsFile():
                # If the contents file isn't ready yet, we can't check
                # the desc file yet.
                return False

            # All right, get the package info now.
            package = self.host.getPackage(self.packageName, self.version)
            if not package:
                self.notify.warning("Package %s %s not known on %s" % (
                    self.packageName, self.version, self.host.hostUrl))
                return False

            self.package = package
            self.package.checkStatus()

            if not self.package.hasDescFile:
                return False

            self.downloadEffort = self.package.getDownloadEffort()
            self.prevDownloadEffort = 0
            if self.downloadEffort == 0:
                self.prevDownloadedEffort = self.package.getPrevDownloadedEffort()

            return True


        def getDescFile(self, http):
            """ Synchronously downloads the desc files required for
            the package. """

            if not self.host.downloadContentsFile(http):
                return False

            # All right, get the package info now.
            package = self.host.getPackage(self.packageName, self.version)
            if not package:
                self.notify.warning("Package %s %s not known on %s" % (
                    self.packageName, self.version, self.host.hostUrl))
                return False

            self.package = package
            if not self.package.downloadDescFile(http):
                return False

            self.package.checkStatus()
            self.downloadEffort = self.package.getDownloadEffort()
            self.prevDownloadEffort = 0
            if self.downloadEffort == 0:
                self.prevDownloadedEffort = self.package.getPrevDownloadedEffort()

            return True

    def __init__(self, appRunner, taskChain = 'default'):
        self.globalLock.acquire()
        try:
            self.uniqueId = PackageInstaller.nextUniqueId
            PackageInstaller.nextUniqueId += 1
        finally:
            self.globalLock.release()

        self.appRunner = appRunner
        self.taskChain = taskChain

        # If we're to be running on an asynchronous task chain, and
        # the task chain hasn't yet been set up already, create the
        # default parameters now.
        if taskChain != 'default' and not taskMgr.hasTaskChain(self.taskChain):
            taskMgr.setupTaskChain(self.taskChain, numThreads = 1,
                                   threadPriority = TPLow)

        self.callbackLock = Lock()
        self.calledDownloadStarted = False
        self.calledDownloadFinished = False

        # A list of all packages that have been added to the
        # installer.
        self.packageLock = RLock()
        self.packages = []
        self.state = self.S_initial

        # A list of packages that are waiting for their desc files.
        self.needsDescFile = []
        self.descFileTask = None

        # A list of packages that are waiting to be downloaded and
        # installed.
        self.needsDownload = []
        self.downloadTask = None

        # A list of packages that were already done at the time they
        # were passed to addPackage().
        self.earlyDone = []

        # A list of packages that have been successfully installed, or
        # packages that have failed.
        self.done = []
        self.failed = []

        # This task is spawned on the default task chain, to update
        # the status during the download.
        self.progressTask = None

        self.accept('PackageInstaller-%s-allHaveDesc' % self.uniqueId,
                    self.__allHaveDesc)
        self.accept('PackageInstaller-%s-packageStarted' % self.uniqueId,
                    self.__packageStarted)
        self.accept('PackageInstaller-%s-packageDone' % self.uniqueId,
                    self.__packageDone)

    def destroy(self):
        """ Interrupts all pending downloads.  No further callbacks
        will be made. """
        self.cleanup()

    def cleanup(self):
        """ Interrupts all pending downloads.  No further callbacks
        will be made. """

        self.packageLock.acquire()
        try:
            if self.descFileTask:
                taskMgr.remove(self.descFileTask)
                self.descFileTask = None
            if self.downloadTask:
                taskMgr.remove(self.downloadTask)
                self.downloadTask = None
        finally:
            self.packageLock.release()

        if self.progressTask:
            taskMgr.remove(self.progressTask)
            self.progressTask = None

        self.ignoreAll()

    def addPackage(self, packageName, version = None, hostUrl = None):
        """ Adds the named package to the list of packages to be
        downloaded.  Call donePackages() to finish the list. """

        if self.state != self.S_initial:
            raise ValueError, 'addPackage called after donePackages'

        host = self.appRunner.getHostWithAlt(hostUrl)
        pp = self.PendingPackage(packageName, version, host)

        self.packageLock.acquire()
        try:
            self.__internalAddPackage(pp)
        finally:
            self.packageLock.release()

    def __internalAddPackage(self, pp):
        """ Adds the indicated "pending package" to the appropriate
        list(s) for downloading and installing.  Assumes packageLock
        is already held."""

        if pp in self.packages:
            # Already added.
            return

        self.packages.append(pp)

        # We always add the package to needsDescFile, even if we
        # already have its desc file; this guarantees that packages
        # are downloaded in the order they are added.
        self.needsDescFile.append(pp)
        if not self.descFileTask:
            self.descFileTask = taskMgr.add(
                self.__getDescFileTask, 'getDescFile',
                taskChain = self.taskChain)

    def donePackages(self):
        """ After calling addPackage() for each package to be
        installed, call donePackages() to mark the end of the list.
        This is necessary to determine what the complete set of
        packages is (and therefore how large the total download size
        is).  None of the low-level callbacks will be made before this
        call. """

        if self.state != self.S_initial:
            # We've already been here.
            return

        # Throw the messages for packages that were already done
        # before we started.
        for pp in self.earlyDone:
            self.__donePackage(pp, True)
        self.earlyDone = []

        self.packageLock.acquire()
        try:
            if self.state != self.S_initial:
                return
            self.state = self.S_ready
            if not self.needsDescFile:
                # All package desc files are already available; so begin.
                self.__prepareToStart()
        finally:
            self.packageLock.release()

        if not self.packages:
            # Trivial no-op.
            self.__callDownloadFinished(True)

    def downloadStarted(self):
        """ This callback is made at some point after donePackages()
        is called; at the time of this callback, the total download
        size is known, and we can sensibly report progress through the
        whole. """

        self.notify.info("downloadStarted")

    def packageStarted(self, package):
        """ This callback is made for each package between
        downloadStarted() and downloadFinished() to indicate the start
        of a new package. """

        self.notify.debug("packageStarted: %s" % (package.packageName))

    def packageProgress(self, package, progress):
        """ This callback is made repeatedly between packageStarted()
        and packageFinished() to update the current progress on the
        indicated package only.  The progress value ranges from 0
        (beginning) to 1 (complete). """

        self.notify.debug("packageProgress: %s %s" % (package.packageName, progress))

    def downloadProgress(self, overallProgress):
        """ This callback is made repeatedly between downloadStarted()
        and downloadFinished() to update the current progress through
        all packages.  The progress value ranges from 0 (beginning) to
        1 (complete). """

        self.notify.debug("downloadProgress: %s" % (overallProgress))

    def packageFinished(self, package, success):
        """ This callback is made for each package between
        downloadStarted() and downloadFinished() to indicate that a
        package has finished downloading.  If success is true, there
        were no problems and the package is now installed.

        If this package did not require downloading (because it was
        already downloaded), this callback will be made immediately,
        *without* a corresponding call to packageStarted(), and may
        even be made before downloadStarted(). """

        self.notify.info("packageFinished: %s %s" % (package.packageName, success))

    def downloadFinished(self, success):
        """ This callback is made when all of the packages have been
        downloaded and installed (or there has been some failure).  If
        all packages where successfully installed, success is True.

        If there were no packages that required downloading, this
        callback will be made immediately, *without* a corresponding
        call to downloadStarted(). """

        self.notify.info("downloadFinished: %s" % (success))

    def __prepareToStart(self):
        """ This is called internally when transitioning from S_ready
        to S_started.  It sets up whatever initial values are
        needed.  Assumes self.packageLock is held.  Returns False if
        there were no packages to download, and the state was
        therefore transitioned immediately to S_done. """

        if not self.needsDownload:
            self.state = self.S_done
            return False

        self.state = self.S_started

        assert not self.downloadTask
        self.downloadTask = taskMgr.add(
            self.__downloadPackageTask, 'downloadPackage',
            taskChain = self.taskChain)

        assert not self.progressTask
        self.progressTask = taskMgr.add(
            self.__progressTask, 'packageProgress')

        return True

    def __allHaveDesc(self):
        """ This method is called internally when all of the pending
        packages have their desc info. """
        working = True

        self.packageLock.acquire()
        try:
            if self.state == self.S_ready:
                # We've already called donePackages(), so move on now.
                working = self.__prepareToStart()
        finally:
            self.packageLock.release()

        if not working:
            self.__callDownloadFinished(True)

    def __packageStarted(self, pp):
        """ This method is called when a single package is beginning
        to download. """

        self.__callDownloadStarted()
        self.__callPackageStarted(pp)

    def __packageDone(self, pp):
        """ This method is called when a single package has been
        downloaded and installed, or has failed. """

        self.__callPackageFinished(pp, pp.success)
        pp.notified = True

        # See if there are more packages to go.
        success = True
        allDone = True
        self.packageLock.acquire()
        try:
            for pp in self.packages:
                if pp.notified:
                    success = success and pp.success
                else:
                    allDone = False
        finally:
            self.packageLock.release()

        if allDone:
            self.__callDownloadFinished(success)

    def __callPackageStarted(self, pp):
        """ Calls the packageStarted() callback for a particular
        package if it has not already been called, being careful to
        avoid race conditions. """

        self.callbackLock.acquire()
        try:
            if not pp.calledPackageStarted:
                self.packageStarted(pp.package)
                self.packageProgress(pp.package, 0)
                pp.calledPackageStarted = True
        finally:
            self.callbackLock.release()

    def __callPackageFinished(self, pp, success):
        """ Calls the packageFinished() callback for a paricular
        package if it has not already been called, being careful to
        avoid race conditions. """

        self.callbackLock.acquire()
        try:
            if not pp.calledPackageFinished:
                if success:
                    self.packageProgress(pp.package, 1)
                self.packageFinished(pp.package, success)
                pp.calledPackageFinished = True
        finally:
            self.callbackLock.release()

    def __callDownloadStarted(self):
        """ Calls the downloadStarted() callback if it has not already
        been called, being careful to avoid race conditions. """

        self.callbackLock.acquire()
        try:
            if not self.calledDownloadStarted:
                self.downloadStarted()
                self.downloadProgress(0)
                self.calledDownloadStarted = True
        finally:
            self.callbackLock.release()

    def __callDownloadFinished(self, success):
        """ Calls the downloadFinished() callback if it has not
        already been called, being careful to avoid race
        conditions. """

        self.callbackLock.acquire()
        try:
            if not self.calledDownloadFinished:
                if success:
                    self.downloadProgress(1)
                self.downloadFinished(success)
                self.calledDownloadFinished = True
        finally:
            self.callbackLock.release()

    def __getDescFileTask(self, task):

        """ This task runs on the aysynchronous task chain; each pass,
        it extracts one package from self.needsDescFile and downloads
        its desc file.  On success, it adds the package to
        self.needsDownload. """

        self.packageLock.acquire()
        try:
            # If we've finished all of the packages that need desc
            # files, stop the task.
            if not self.needsDescFile:
                self.descFileTask = None

                eventName = 'PackageInstaller-%s-allHaveDesc' % self.uniqueId
                messenger.send(eventName, taskChain = 'default')

                return task.done
            pp = self.needsDescFile[0]
            del self.needsDescFile[0]
        finally:
            self.packageLock.release()

        # Now serve this one package.
        if not pp.checkDescFile():
            if not pp.getDescFile(self.appRunner.http):
                self.__donePackage(pp, False)
                return task.cont

        # This package is now ready to be downloaded.  We always add
        # it to needsDownload, even if it's already downloaded, to
        # guarantee ordering of packages.

        self.packageLock.acquire()
        try:
            # Also add any packages required by this one.
            for packageName, version, host in pp.package.requires:
                pp2 = self.PendingPackage(packageName, version, host)
                self.__internalAddPackage(pp2)
            self.needsDownload.append(pp)
        finally:
            self.packageLock.release()

        return task.cont

    def __downloadPackageTask(self, task):

        """ This task runs on the aysynchronous task chain; each pass,
        it extracts one package from self.needsDownload and downloads
        it. """

        while True:
            self.packageLock.acquire()
            try:
                # If we're done downloading, stop the task.
                if self.state == self.S_done or not self.needsDownload:
                    self.downloadTask = None
                    self.packageLock.release()
                    yield task.done; return

                assert self.state == self.S_started
                pp = self.needsDownload[0]
                del self.needsDownload[0]
            except:
                self.packageLock.release()
                raise
            self.packageLock.release()

            # Now serve this one package.
            eventName = 'PackageInstaller-%s-packageStarted' % self.uniqueId
            messenger.send(eventName, [pp], taskChain = 'default')

            if not pp.package.hasPackage:
                for token in pp.package.downloadPackageGenerator(self.appRunner.http):
                    if token == pp.package.stepContinue:
                        yield task.cont
                    else:
                        break

                if token != pp.package.stepComplete:
                    pc = PStatCollector(':App:PackageInstaller:donePackage:%s' % (pp.package.packageName))
                    pc.start()
                    self.__donePackage(pp, False)
                    pc.stop()
                    yield task.cont
                    continue

            # Successfully downloaded and installed.
            pc = PStatCollector(':App:PackageInstaller:donePackage:%s' % (pp.package.packageName))
            pc.start()
            self.__donePackage(pp, True)
            pc.stop()

            # Continue the loop without yielding, so we pick up the
            # next package within this same frame.

    def __donePackage(self, pp, success):
        """ Marks the indicated package as done, either successfully
        or otherwise. """
        assert not pp.done

        if success:
            pc = PStatCollector(':App:PackageInstaller:install:%s' % (pp.package.packageName))
            pc.start()
            pp.package.installPackage(self.appRunner)
            pc.stop()

        self.packageLock.acquire()
        try:
            pp.done = True
            pp.success = success
            if success:
                self.done.append(pp)
            else:
                self.failed.append(pp)
        finally:
            self.packageLock.release()

        eventName = 'PackageInstaller-%s-packageDone' % self.uniqueId
        messenger.send(eventName, [pp], taskChain = 'default')

    def __progressTask(self, task):
        self.callbackLock.acquire()
        try:
            if not self.calledDownloadStarted:
                # We haven't yet officially started the download.
                return task.cont

            if self.calledDownloadFinished:
                # We've officially ended the download.
                self.progressTask = None
                return task.done

            downloadEffort = 0
            currentDownloadSize = 0
            for pp in self.packages:
                downloadEffort += pp.downloadEffort + pp.prevDownloadedEffort
                packageProgress = pp.getProgress()
                currentDownloadSize += pp.downloadEffort * packageProgress + pp.prevDownloadedEffort
                if pp.calledPackageStarted and not pp.calledPackageFinished:
                    self.packageProgress(pp.package, packageProgress)

            if not downloadEffort:
                progress = 1
            else:
                progress = float(currentDownloadSize) / float(downloadEffort)
            self.downloadProgress(progress)

        finally:
            self.callbackLock.release()

        return task.cont
예제 #10
0
class FSM(DirectObject):
    """
    A Finite State Machine.  This is intended to be the base class
    of any number of specific machines, which consist of a collection
    of states and transitions, and rules to switch between states
    according to arbitrary input data.

    The states of an FSM are defined implicitly.  Each state is
    identified by a string, which by convention begins with a capital
    letter.  (Also by convention, strings passed to request that are
    not state names should begin with a lowercase letter.)

    To define specialized behavior when entering or exiting a
    particular state, define a method named enterState() and/or
    exitState(), where "State" is the name of the state, e.g.:

    def enterRed(self):
        ... do stuff ...

    def exitRed(self):
        ... cleanup stuff ...

    def enterYellow(self):
        ... do stuff ...

    def exitYellow(self):
        ... cleanup stuff ...

    def enterGreen(self):
        ... do stuff ...

    def exitGreen(self):
        ... cleanup stuff ...

    Both functions can access the previous state name as
    self.oldState, and the new state name we are transitioning to as
    self.newState.  (Of course, in enterRed(), self.newState will
    always be "Red", and the in exitRed(), self.oldState will always
    be "Red".)

    Both functions are optional.  If either function is omitted, the
    state is still defined, but nothing is done when transitioning
    into (or out of) the state.

    Additionally, you may define a filterState() function for each
    state.  The purpose of this function is to decide what state to
    transition to next, if any, on receipt of a particular input.  The
    input is always a string and a tuple of optional parameters (which
    is often empty), and the return value should either be None to do
    nothing, or the name of the state to transition into.  For
    example:

    def filterRed(self, request, args):
        if request in ['Green']:
            return (request,) + args
        return None

    def filterYellow(self, request, args):
        if request in ['Red']:
            return (request,) + args
        return None

    def filterGreen(self, request, args):
        if request in ['Yellow']:
            return (request,) + args
        return None

    As above, the filterState() functions are optional.  If any is
    omitted, the defaultFilter() method is called instead.  A standard
    implementation of defaultFilter() is provided, which may be
    overridden in a derived class to change the behavior on an
    unexpected transition.

    If self.defaultTransitions is left unassigned, then the standard
    implementation of defaultFilter() will return None for any
    lowercase transition name and allow any uppercase transition name
    (this assumes that an uppercase name is a request to go directly
    to a particular state by name).

    self.state may be queried at any time other than during the
    handling of the enter() and exit() functions.  During these
    functions, self.state contains the value None (you are not really
    in any state during the transition).  However, during a transition
    you *can* query the outgoing and incoming states, respectively,
    via self.oldState and self.newState.  At other times, self.state
    contains the name of the current state.

    Initially, the FSM is in state 'Off'.  It does not call enterOff()
    at construction time; it is simply in Off already by convention.
    If you need to call code in enterOff() to initialize your FSM
    properly, call it explicitly in the constructor.  Similarly, when
    cleanup() is called or the FSM is destructed, the FSM transitions
    back to 'Off' by convention.  (It does call enterOff() at this
    point, but does not call exitOff().)

    To implement nested hierarchical FSM's, simply create a nested FSM
    and store it on the class within the appropriate enterState()
    function, and clean it up within the corresponding exitState()
    function.

    There is a way to define specialized transition behavior between
    two particular states.  This is done by defining a from<X>To<Y>()
    function, where X is the old state and Y is the new state.  If this
    is defined, it will be run in place of the exit<X> and enter<Y>
    functions, so if you want that behavior, you'll have to call them
    specifically.  Otherwise, you can completely replace that transition's
    behavior.

    See the code in SampleFSM.py for further examples.
    """

    notify = DirectNotifyGlobal.directNotify.newCategory("FSM")

    SerialNum = 0

    # This member lists the default transitions that are accepted
    # without question by the defaultFilter.  It's a map of state
    # names to a list of legal target state names from that state.
    # Define it only if you want to use the classic FSM model of
    # defining all (or most) of your transitions up front.  If
    # this is set to None (the default), all named-state
    # transitions (that is, those requests whose name begins with
    # a capital letter) are allowed.  If it is set to an empty
    # map, no transitions are implicitly allowed--all transitions
    # must be approved by some filter function.
    defaultTransitions = None

    def __init__(self, name):
        self.fsmLock = RLock()
        self.name = name
        self.stateArray = []
        self._serialNum = FSM.SerialNum
        FSM.SerialNum += 1
        self._broadcastStateChanges = False
        # Initially, we are in the Off state by convention.
        self.state = 'Off'

        # This member records transition requests made by demand() or
        # forceTransition() while the FSM is in transition between
        # states.
        self.__requestQueue = []

        if __debug__:
            from direct.fsm.ClassicFSM import _debugFsms
            import weakref
            _debugFsms[name]=weakref.ref(self)

    def cleanup(self):
        # A convenience function to force the FSM to clean itself up
        # by transitioning to the "Off" state.
        self.fsmLock.acquire()
        try:
            assert self.state
            if self.state != 'Off':
                self.__setState('Off')
        finally:
            self.fsmLock.release()

    def setBroadcastStateChanges(self, doBroadcast):
        self._broadcastStateChanges = doBroadcast
    def getStateChangeEvent(self):
        # if setBroadcastStateChanges(True), this event will be sent through
        # the messenger on every state change. The new and old states are
        # accessible as self.oldState and self.newState, and the transition
        # functions will already have been called.
        return 'FSM-%s-%s-stateChange' % (self._serialNum, self.name)

    def getCurrentFilter(self):
        if not self.state:
            error = "FSM cannot determine current filter while in transition (%s -> %s)." % (self.oldState, self.newState)
            raise AlreadyInTransition, error

        filter = getattr(self, "filter" + self.state, None)
        if not filter:
            # If there's no matching filterState() function, call
            # defaultFilter() instead.
            filter = self.defaultFilter
            
        return filter

    def getCurrentOrNextState(self):
        # Returns the current state if we are in a state now, or the
        # state we are transitioning into if we are currently within
        # the enter or exit function for a state.
        self.fsmLock.acquire()
        try:
            if self.state:
                return self.state
            return self.newState
        finally:
            self.fsmLock.release()

    def getCurrentStateOrTransition(self):
        # Returns the current state if we are in a state now, or the
        # transition we are performing if we are currently within
        # the enter or exit function for a state.
        self.fsmLock.acquire()
        try:
            if self.state:
                return self.state
            return '%s -> %s' % (self.oldState, self.newState)
        finally:
            self.fsmLock.release()

    def isInTransition(self):
        self.fsmLock.acquire()
        try:
            return self.state == None
        finally:
            self.fsmLock.release()

    def forceTransition(self, request, *args):
        """Changes unconditionally to the indicated state.  This
        bypasses the filterState() function, and just calls
        exitState() followed by enterState()."""

        self.fsmLock.acquire()
        try:
            assert isinstance(request, types.StringTypes)
            self.notify.debug("%s.forceTransition(%s, %s" % (
                self.name, request, str(args)[1:]))

            if not self.state:
                # Queue up the request.
                self.__requestQueue.append(PythonUtil.Functor(
                    self.forceTransition, request, *args))
                return

            self.__setState(request, *args)
        finally:
            self.fsmLock.release()

    def demand(self, request, *args):
        """Requests a state transition, by code that does not expect
        the request to be denied.  If the request is denied, raises a
        RequestDenied exception.

        Unlike request(), this method allows a new request to be made
        while the FSM is currently in transition.  In this case, the
        request is queued up and will be executed when the current
        transition finishes.  Multiple requests will queue up in
        sequence.
        """

        self.fsmLock.acquire()
        try:
            assert isinstance(request, types.StringTypes)
            self.notify.debug("%s.demand(%s, %s" % (
                self.name, request, str(args)[1:]))
            if not self.state:
                # Queue up the request.
                self.__requestQueue.append(PythonUtil.Functor(
                    self.demand, request, *args))
                return

            if not self.request(request, *args):
                raise RequestDenied, "%s (from state: %s)" % (request, self.state)
        finally:
            self.fsmLock.release()

    def request(self, request, *args):
        """Requests a state transition (or other behavior).  The
        request may be denied by the FSM's filter function.  If it is
        denied, the filter function may either raise an exception
        (RequestDenied), or it may simply return None, without
        changing the FSM's state.

        The request parameter should be a string.  The request, along
        with any additional arguments, is passed to the current
        filterState() function.  If filterState() returns a string,
        the FSM transitions to that state.

        The return value is the same as the return value of
        filterState() (that is, None if the request does not provoke a
        state transition, otherwise it is a tuple containing the name
        of the state followed by any optional args.)

        If the FSM is currently in transition (i.e. in the middle of
        executing an enterState or exitState function), an
        AlreadyInTransition exception is raised (but see demand(),
        which will queue these requests up and apply when the
        transition is complete)."""

        self.fsmLock.acquire()
        try:
            assert isinstance(request, types.StringTypes)
            self.notify.debug("%s.request(%s, %s" % (
                self.name, request, str(args)[1:]))

            filter = self.getCurrentFilter()
            result = filter(request, args)
            if result:
                if isinstance(result, types.StringTypes):
                    # If the return value is a string, it's just the name
                    # of the state.  Wrap it in a tuple for consistency.
                    result = (result,) + args

                # Otherwise, assume it's a (name, *args) tuple
                self.__setState(*result)

            return result
        finally:
            self.fsmLock.release()

    def defaultEnter(self, *args):
        """ This is the default function that is called if there is no
        enterState() method for a particular state name. """
        pass

    def defaultExit(self):
        """ This is the default function that is called if there is no
        exitState() method for a particular state name. """
        pass

    def defaultFilter(self, request, args):
        """This is the function that is called if there is no
        filterState() method for a particular state name.

        This default filter function behaves in one of two modes:

        (1) if self.defaultTransitions is None, allow any request
        whose name begins with a capital letter, which is assumed to
        be a direct request to a particular state.  This is similar to
        the old ClassicFSM onUndefTransition=ALLOW, with no explicit
        state transitions listed.

        (2) if self.defaultTransitions is not None, allow only those
        requests explicitly identified in this map.  This is similar
        to the old ClassicFSM onUndefTransition=DISALLOW, with an
        explicit list of allowed state transitions.

        Specialized FSM's may wish to redefine this default filter
        (for instance, to always return the request itself, thus
        allowing any transition.)."""

        if request == 'Off':
            # We can always go to the "Off" state.
            return (request,) + args

        if self.defaultTransitions is None:
            # If self.defaultTransitions is None, it means to accept
            # all requests whose name begins with a capital letter.
            # These are direct requests to a particular state.
            if request[0] in string.uppercase:
                return (request,) + args
        else:
            # If self.defaultTransitions is not None, it is a map of
            # allowed transitions from each state.  That is, each key
            # of the map is the current state name; for that key, the
            # value is a list of allowed transitions from the
            # indicated state.
            if request in self.defaultTransitions.get(self.state, []):
                # This transition is listed in the defaultTransitions map;
                # accept it.
                return (request,) + args

            # If self.defaultTransitions is not None, it is an error
            # to request a direct state transition (capital letter
            # request) not listed in defaultTransitions and not
            # handled by an earlier filter.
            if request[0] in string.uppercase:
                raise RequestDenied, "%s (from state: %s)" % (request, self.state)

        # In either case, we quietly ignore unhandled command
        # (lowercase) requests.
        assert self.notify.debug("%s ignoring request %s from state %s." % (self.name, request, self.state))
        return None

    def filterOff(self, request, args):
        """From the off state, we can always go directly to any other
        state."""
        if request[0] in string.uppercase:
            return (request,) + args
        return self.defaultFilter(request, args)
        

    def setStateArray(self, stateArray):
        """array of unique states to iterate through"""
        self.fsmLock.acquire()
        try:
            self.stateArray = stateArray
        finally:
            self.fsmLock.release()


    def requestNext(self, *args):
        """Request the 'next' state in the predefined state array."""
        self.fsmLock.acquire()
        try:
            if self.stateArray:
                if not self.state in self.stateArray:
                    self.request(self.stateArray[0])
                else:
                    cur_index = self.stateArray.index(self.state)
                    new_index = (cur_index + 1) % len(self.stateArray)
                    self.request(self.stateArray[new_index], args)
            else:
                assert self.notifier.debug(
                                    "stateArray empty. Can't switch to next.")

        finally:
            self.fsmLock.release()

    def requestPrev(self, *args):
        """Request the 'previous' state in the predefined state array."""
        self.fsmLock.acquire()
        try:
            if self.stateArray:
                if not self.state in self.stateArray:
                    self.request(self.stateArray[0])
                else:
                    cur_index = self.stateArray.index(self.state)
                    new_index = (cur_index - 1) % len(self.stateArray)
                    self.request(self.stateArray[new_index], args)
            else:
                assert self.notifier.debug(
                                    "stateArray empty. Can't switch to next.")
        finally:
            self.fsmLock.release()

    def __setState(self, newState, *args):
        # Internal function to change unconditionally to the indicated
        # state.
        assert self.state
        assert self.notify.debug("%s to state %s." % (self.name, newState))

        self.oldState = self.state
        self.newState = newState
        self.state = None

        try:
            if not self.__callFromToFunc(self.oldState, self.newState, *args):
                self.__callExitFunc(self.oldState)
                self.__callEnterFunc(self.newState, *args)
                pass
            pass
        except:
            # If we got an exception during the enter or exit methods,
            # go directly to state "InternalError" and raise up the
            # exception.  This might leave things a little unclean
            # since we've partially transitioned, but what can you do?

            self.state = 'InternalError'
            del self.oldState
            del self.newState
            raise

        if self._broadcastStateChanges:
            messenger.send(self.getStateChangeEvent())

        self.state = newState
        del self.oldState
        del self.newState

        if self.__requestQueue:
            request = self.__requestQueue.pop(0)
            assert self.notify.debug("%s continued queued request." % (self.name))
            request()

    def __callEnterFunc(self, name, *args):
        # Calls the appropriate enter function when transitioning into
        # a new state, if it exists.
        assert self.state == None and self.newState == name

        func = getattr(self, "enter" + name, None)
        if not func:
            # If there's no matching enterFoo() function, call
            # defaultEnter() instead.
            func = self.defaultEnter
        func(*args)

    def __callFromToFunc(self, oldState, newState, *args):
        # Calls the appropriate fromTo function when transitioning into
        # a new state, if it exists.
        assert self.state == None and self.oldState == oldState and self.newState == newState

        func = getattr(self, "from%sTo%s" % (oldState,newState), None)
        if func:
            func(*args)
            return True
        return False

    def __callExitFunc(self, name):
        # Calls the appropriate exit function when leaving a
        # state, if it exists.
        assert self.state == None and self.oldState == name

        func = getattr(self, "exit" + name, None)
        if not func:
            # If there's no matching exitFoo() function, call
            # defaultExit() instead.
            func = self.defaultExit
        func()

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        """
        Print out something useful about the fsm
        """
        self.fsmLock.acquire()
        try:
            className = self.__class__.__name__
            if self.state:
                str = ('%s FSM:%s in state "%s"' % (className, self.name, self.state))
            else:
                str = ('%s FSM:%s in transition from \'%s\' to \'%s\'' % (className, self.name, self.oldState, self.newState))
            return str
        finally:
            self.fsmLock.release()
class FSM(DirectObject):
    notify = DirectNotifyGlobal.directNotify.newCategory('FSM')
    SerialNum = 0
    defaultTransitions = None

    def __init__(self, name):
        self.fsmLock = RLock()
        self._name = name
        self.stateArray = []
        self._serialNum = FSM.SerialNum
        FSM.SerialNum += 1
        self._broadcastStateChanges = False
        self._state = 'Off'
        self.__requestQueue = []

    def cleanup(self):
        self.fsmLock.acquire()
        try:
            if self._state != 'Off':
                self.__setState('Off')
        finally:
            self.fsmLock.release()

    def setBroadcastStateChanges(self, doBroadcast):
        self._broadcastStateChanges = doBroadcast

    def getStateChangeEvent(self):
        return 'FSM-%s-%s-stateChange' % (self._serialNum, self._name)

    def getCurrentFilter(self):
        if not self._state:
            error = 'FSM cannot determine current filter while in transition (%s -> %s).' % (
                self.oldState, self.newState)
            raise AlreadyInTransition, error
        filter = getattr(self, 'filter' + self._state, None)
        if not filter:
            filter = self.defaultFilter
        return filter

    def getCurrentOrNextState(self):
        self.fsmLock.acquire()
        try:
            if self._state:
                return self._state
            return self.newState
        finally:
            self.fsmLock.release()

    def getCurrentStateOrTransition(self):
        self.fsmLock.acquire()
        try:
            if self._state:
                return self._state
            return '%s -> %s' % (self.oldState, self.newState)
        finally:
            self.fsmLock.release()

    def isInTransition(self):
        self.fsmLock.acquire()
        try:
            return self._state == None
        finally:
            self.fsmLock.release()

        return

    def forceTransition(self, request, *args):
        self.fsmLock.acquire()
        try:
            self.notify.debug('%s.forceTransition(%s, %s' %
                              (self._name, request, str(args)[1:]))
            if not self._state:
                self.__requestQueue.append(
                    PythonUtil.Functor(self.forceTransition, request, *args))
                return
            self.__setState(request, *args)
        finally:
            self.fsmLock.release()

    def demand(self, request, *args):
        self.fsmLock.acquire()
        try:
            self.notify.debug('%s.demand(%s, %s' %
                              (self._name, request, str(args)[1:]))
            if not self._state:
                self.__requestQueue.append(
                    PythonUtil.Functor(self.demand, request, *args))
                return
            if not self.request(request, *args):
                raise RequestDenied, '%s (from state: %s)' % (request,
                                                              self._state)
        finally:
            self.fsmLock.release()

    def request(self, request, *args):
        self.fsmLock.acquire()
        try:
            self.notify.debug('%s.request(%s, %s' %
                              (self._name, request, str(args)[1:]))
            filter = self.getCurrentFilter()
            result = filter(request, args)
            if result:
                if isinstance(result, types.StringTypes):
                    result = (result, ) + args
                self.__setState(*result)
            return result
        finally:
            self.fsmLock.release()

    def defaultEnter(self, *args):
        pass

    def defaultExit(self):
        pass

    def defaultFilter(self, request, args):
        if request == 'Off':
            return (request, ) + args
        if self.defaultTransitions is None:
            if request[0].isupper():
                return (request, ) + args
        else:
            if request in self.defaultTransitions.get(self._state, []):
                return (request, ) + args
        if request[0].isupper():
            raise RequestDenied, '%s (from state: %s)' % (request, self._state)
        return

    def filterOff(self, request, args):
        if request[0].isupper():
            return (request, ) + args
        return self.defaultFilter(request, args)

    def setStateArray(self, stateArray):
        self.fsmLock.acquire()
        try:
            self.stateArray = stateArray
        finally:
            self.fsmLock.release()

    def requestNext(self, *args):
        self.fsmLock.acquire()
        try:
            if self.stateArray:
                if self._state not in self.stateArray:
                    self.request(self.stateArray[0])
                else:
                    cur_index = self.stateArray.index(self._state)
                    new_index = (cur_index + 1) % len(self.stateArray)
                    self.request(self.stateArray[new_index], args)
        finally:
            self.fsmLock.release()

    def requestPrev(self, *args):
        self.fsmLock.acquire()
        try:
            if self.stateArray:
                if self.state not in self.stateArray:
                    self.request(self.stateArray[0])
                else:
                    cur_index = self.stateArray.index(self._state)
                    new_index = (cur_index - 1) % len(self.stateArray)
                    self.request(self.stateArray[new_index], args)
        finally:
            self.fsmLock.release()

    def __setState(self, newState, *args):
        self.oldState = self._state
        self.newState = newState
        self._state = None
        try:
            if not self.__callFromToFunc(self.oldState, self.newState, *args):
                self.__callExitFunc(self.oldState)
                self.__callEnterFunc(self.newState, *args)
        except:
            self._state = 'InternalError'
            del self.oldState
            del self.newState
            raise

        if self._broadcastStateChanges:
            messenger.send(self.getStateChangeEvent())
        self._state = newState
        del self.oldState
        del self.newState
        if self.__requestQueue:
            request = self.__requestQueue.pop(0)
            request()
        return

    def __callEnterFunc(self, name, *args):
        func = getattr(self, 'enter' + name, None)
        if not func:
            func = self.defaultEnter
        func(*args)
        return

    def __callFromToFunc(self, oldState, newState, *args):
        func = getattr(self, 'from%sTo%s' % (oldState, newState), None)
        if func:
            func(*args)
            return True
        return False

    def __callExitFunc(self, name):
        func = getattr(self, 'exit' + name, None)
        if not func:
            func = self.defaultExit
        func()
        return

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        self.fsmLock.acquire()
        try:
            className = self.__class__.__name__
            if self._state:
                str = '%s FSM:%s in state "%s"' % (className, self._name,
                                                   self._state)
            else:
                str = "%s FSM:%s in transition from '%s' to '%s'" % (
                    className, self._name, self.oldState, self.newState)
            return str
        finally:
            self.fsmLock.release()
예제 #12
0
class FSM(DirectObject):
    __module__ = __name__
    notify = DirectNotifyGlobal.directNotify.newCategory('FSM')
    SerialNum = 0
    defaultTransitions = None

    def __init__(self, name):
        self.fsmLock = RLock()
        self.name = name
        self.stateArray = []
        self._serialNum = FSM.SerialNum
        FSM.SerialNum += 1
        self._broadcastStateChanges = False
        self.state = 'Off'
        self.__requestQueue = []

    def cleanup(self):
        self.fsmLock.acquire()
        try:
            if self.state != 'Off':
                self.__setState('Off')
        finally:
            self.fsmLock.release()

    def setBroadcastStateChanges(self, doBroadcast):
        self._broadcastStateChanges = doBroadcast

    def getStateChangeEvent(self):
        return 'FSM-%s-%s-stateChange' % (self._serialNum, self.name)

    def getCurrentFilter(self):
        if not self.state:
            error = 'FSM cannot determine current filter while in transition (%s -> %s).' % (self.oldState, self.newState)
            raise AlreadyInTransition, error
        filter = getattr(self, 'filter' + self.state, None)
        if not filter:
            filter = self.defaultFilter
        return filter

    def getCurrentOrNextState(self):
        self.fsmLock.acquire()
        try:
            if self.state:
                return self.state
            return self.newState
        finally:
            self.fsmLock.release()

    def getCurrentStateOrTransition(self):
        self.fsmLock.acquire()
        try:
            if self.state:
                return self.state
            return '%s -> %s' % (self.oldState, self.newState)
        finally:
            self.fsmLock.release()

    def isInTransition(self):
        self.fsmLock.acquire()
        try:
            return self.state == None
        finally:
            self.fsmLock.release()

        return

    def forceTransition(self, request, *args):
        self.fsmLock.acquire()
        try:
            self.notify.debug('%s.forceTransition(%s, %s' % (self.name, request, str(args)[1:]))
            if not self.state:
                self.__requestQueue.append(PythonUtil.Functor(self.forceTransition, request, *args))
                return
            self.__setState(request, *args)
        finally:
            self.fsmLock.release()

    def demand(self, request, *args):
        self.fsmLock.acquire()
        try:
            self.notify.debug('%s.demand(%s, %s' % (self.name, request, str(args)[1:]))
            if not self.state:
                self.__requestQueue.append(PythonUtil.Functor(self.demand, request, *args))
                return
            if not self.request(request, *args):
                raise RequestDenied, '%s (from state: %s)' % (request, self.state)
        finally:
            self.fsmLock.release()

    def request(self, request, *args):
        self.fsmLock.acquire()
        try:
            self.notify.debug('%s.request(%s, %s' % (self.name, request, str(args)[1:]))
            filter = self.getCurrentFilter()
            result = filter(request, args)
            if result:
                if isinstance(result, types.StringTypes):
                    result = (result,) + args
                self.__setState(*result)
            return result
        finally:
            self.fsmLock.release()

    def defaultEnter(self, *args):
        pass

    def defaultExit(self):
        pass

    def defaultFilter(self, request, args):
        if request == 'Off':
            return (request,) + args
        if self.defaultTransitions is None:
            if request[0] in string.uppercase:
                return (request,) + args
        else:
            if request in self.defaultTransitions.get(self.state, []):
                return (request,) + args
            if request[0] in string.uppercase:
                raise RequestDenied, '%s (from state: %s)' % (request, self.state)
        return

    def filterOff(self, request, args):
        if request[0] in string.uppercase:
            return (request,) + args
        return self.defaultFilter(request, args)

    def setStateArray(self, stateArray):
        self.fsmLock.acquire()
        try:
            self.stateArray = stateArray
        finally:
            self.fsmLock.release()

    def requestNext(self, *args):
        self.fsmLock.acquire()
        try:
            if self.stateArray:
                if self.state not in self.stateArray:
                    self.request(self.stateArray[0])
                else:
                    cur_index = self.stateArray.index(self.state)
                    new_index = (cur_index + 1) % len(self.stateArray)
                    self.request(self.stateArray[new_index], args)
        finally:
            self.fsmLock.release()

    def requestPrev(self, *args):
        self.fsmLock.acquire()
        try:
            if self.stateArray:
                if self.state not in self.stateArray:
                    self.request(self.stateArray[0])
                else:
                    cur_index = self.stateArray.index(self.state)
                    new_index = (cur_index - 1) % len(self.stateArray)
                    self.request(self.stateArray[new_index], args)
        finally:
            self.fsmLock.release()

    def __setState(self, newState, *args):
        self.oldState = self.state
        self.newState = newState
        self.state = None
        try:
            if not self.__callFromToFunc(self.oldState, self.newState, *args):
                self.__callExitFunc(self.oldState)
                self.__callEnterFunc(self.newState, *args)
        except:
            self.state = 'InternalError'
            del self.oldState
            del self.newState
            raise

        if self._broadcastStateChanges:
            messenger.send(self.getStateChangeEvent())
        self.state = newState
        del self.oldState
        del self.newState
        if self.__requestQueue:
            request = self.__requestQueue.pop(0)
            request()
        return

    def __callEnterFunc(self, name, *args):
        func = getattr(self, 'enter' + name, None)
        if not func:
            func = self.defaultEnter
        func(*args)
        return

    def __callFromToFunc(self, oldState, newState, *args):
        func = getattr(self, 'from%sTo%s' % (oldState, newState), None)
        if func:
            func(*args)
            return True
        return False

    def __callExitFunc(self, name):
        func = getattr(self, 'exit' + name, None)
        if not func:
            func = self.defaultExit
        func()
        return

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        self.fsmLock.acquire()
        try:
            className = self.__class__.__name__
            if self.state:
                str = '%s FSM:%s in state "%s"' % (className, self.name, self.state)
            else:
                str = "%s FSM:%s in transition from '%s' to '%s'" % (className,
                 self.name,
                 self.oldState,
                 self.newState)
            return str
        finally:
            self.fsmLock.release()