mirror of
https://github.com/JoelBender/bacpypes
synced 2025-09-28 22:15:23 +08:00
1298 lines
39 KiB
Python
1298 lines
39 KiB
Python
#!/usr/bin/python
|
|
|
|
"""
|
|
IO Module
|
|
"""
|
|
|
|
import sys
|
|
import logging
|
|
|
|
from time import time as _time
|
|
|
|
import threading
|
|
import cPickle
|
|
from bisect import bisect_left
|
|
from collections import deque
|
|
|
|
from bacpypes.debugging import bacpypes_debugging, DebugContents, ModuleLogger
|
|
|
|
from bacpypes.core import deferred
|
|
|
|
from bacpypes.comm import PDU, Client, bind
|
|
from bacpypes.task import FunctionTask
|
|
from bacpypes.udp import UDPDirector
|
|
|
|
# some debugging
|
|
_debug = 0
|
|
_log = ModuleLogger(globals())
|
|
_commlog = logging.getLogger(__name__ + "._commlog")
|
|
|
|
#
|
|
# IOCB States
|
|
#
|
|
|
|
IDLE = 0 # has not been submitted
|
|
PENDING = 1 # queued, waiting for processing
|
|
ACTIVE = 2 # being processed
|
|
COMPLETED = 3 # finished
|
|
ABORTED = 4 # finished in a bad way
|
|
|
|
_stateNames = {
|
|
IDLE: 'IDLE',
|
|
PENDING: 'PENDING',
|
|
ACTIVE: 'ACTIVE',
|
|
COMPLETED: 'COMPLETED',
|
|
ABORTED: 'ABORTED',
|
|
}
|
|
|
|
#
|
|
# IOQController States
|
|
#
|
|
|
|
CTRL_IDLE = 0 # nothing happening
|
|
CTRL_ACTIVE = 1 # working on an iocb
|
|
CTRL_WAITING = 1 # waiting between iocb requests (throttled)
|
|
|
|
_ctrlStateNames = {
|
|
CTRL_IDLE: 'IDLE',
|
|
CTRL_ACTIVE: 'ACTIVE',
|
|
CTRL_WAITING: 'WAITING',
|
|
}
|
|
|
|
# dictionary of local controllers
|
|
_local_controllers = {}
|
|
_proxy_server = None
|
|
|
|
# special abort error
|
|
TimeoutError = RuntimeError("timeout")
|
|
|
|
#
|
|
# _strftime
|
|
#
|
|
|
|
def _strftime():
|
|
return "%011.6f" % (_time() % 3600,)
|
|
|
|
#
|
|
# IOCB - Input Output Control Block
|
|
#
|
|
|
|
_identNext = 1
|
|
_identLock = threading.Lock()
|
|
|
|
@bacpypes_debugging
|
|
class IOCB(DebugContents):
|
|
|
|
_debug_contents = \
|
|
( 'args', 'kwargs'
|
|
, 'ioState', 'ioResponse-', 'ioError'
|
|
, 'ioController', 'ioServerRef', 'ioControllerRef', 'ioClientID', 'ioClientAddr'
|
|
, 'ioComplete', 'ioCallback+', 'ioQueue', 'ioPriority', 'ioTimeout'
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
global _identNext
|
|
|
|
# lock the identity sequence number
|
|
_identLock.acquire()
|
|
|
|
# generate a unique identity for this block
|
|
ioID = _identNext
|
|
_identNext += 1
|
|
|
|
# release the lock
|
|
_identLock.release()
|
|
|
|
# debugging postponed until ID acquired
|
|
if _debug: IOCB._debug("__init__(%d) %r %r", ioID, args, kwargs)
|
|
|
|
# save the ID
|
|
self.ioID = ioID
|
|
|
|
# save the request parameters
|
|
self.args = args
|
|
self.kwargs = kwargs
|
|
|
|
# start with an idle request
|
|
self.ioState = IDLE
|
|
self.ioResponse = None
|
|
self.ioError = None
|
|
|
|
# blocks are bound to a controller
|
|
self.ioController = None
|
|
|
|
# blocks could reference a local or remote server
|
|
self.ioServerRef = None
|
|
self.ioControllerRef = None
|
|
self.ioClientID = None
|
|
self.ioClientAddr = None
|
|
|
|
# each block gets a completion event
|
|
self.ioComplete = threading.Event()
|
|
self.ioComplete.clear()
|
|
|
|
# applications can set a callback functions
|
|
self.ioCallback = []
|
|
|
|
# request is not currently queued
|
|
self.ioQueue = None
|
|
|
|
# extract the priority if it was given
|
|
self.ioPriority = kwargs.get('_priority', 0)
|
|
if '_priority' in kwargs:
|
|
if _debug: IOCB._debug(" - ioPriority: %r", self.ioPriority)
|
|
del kwargs['_priority']
|
|
|
|
# request has no timeout
|
|
self.ioTimeout = None
|
|
|
|
def add_callback(self, fn, *args, **kwargs):
|
|
"""Pass a function to be called when IO is complete."""
|
|
if _debug: IOCB._debug("add_callback(%d) %r %r %r", self.ioID, fn, args, kwargs)
|
|
|
|
# store it
|
|
self.ioCallback.append((fn, args, kwargs))
|
|
|
|
# already complete?
|
|
if self.ioComplete.isSet():
|
|
self.trigger()
|
|
|
|
def wait(self, *args):
|
|
"""Wait for the completion event to be set."""
|
|
if _debug: IOCB._debug("wait(%d) %r", self.ioID, args)
|
|
|
|
# waiting from a non-daemon thread could be trouble
|
|
self.ioComplete.wait(*args)
|
|
|
|
def trigger(self):
|
|
"""Set the event and make the callback."""
|
|
if _debug: IOCB._debug("trigger(%d)", self.ioID)
|
|
|
|
# if it's queued, remove it from its queue
|
|
if self.ioQueue:
|
|
if _debug: IOCB._debug(" - dequeue")
|
|
self.ioQueue.remove(self)
|
|
|
|
# if there's a timer, cancel it
|
|
if self.ioTimeout:
|
|
if _debug: IOCB._debug(" - cancel timeout")
|
|
self.ioTimeout.suspend_task()
|
|
|
|
# set the completion event
|
|
self.ioComplete.set()
|
|
|
|
# make the callback
|
|
for fn, args, kwargs in self.ioCallback:
|
|
if _debug: IOCB._debug(" - callback fn: %r %r %r", fn, args, kwargs)
|
|
fn(self, *args, **kwargs)
|
|
|
|
def complete(self, msg):
|
|
"""Called to complete a transaction, usually when process_io has
|
|
shipped the IOCB off to some other thread or function."""
|
|
if _debug: IOCB._debug("complete(%d) %r", self.ioID, msg)
|
|
|
|
if self.ioController:
|
|
# pass to controller
|
|
self.ioController.complete_io(self, msg)
|
|
else:
|
|
# just fill in the data
|
|
self.ioState = COMPLETED
|
|
self.ioResponse = msg
|
|
self.trigger()
|
|
|
|
def abort(self, err):
|
|
"""Called by a client to abort a transaction."""
|
|
if _debug: IOCB._debug("abort(%d) %r", self.ioID, err)
|
|
|
|
if self.ioController:
|
|
# pass to controller
|
|
self.ioController.abort_io(self, err)
|
|
elif self.ioState < COMPLETED:
|
|
# just fill in the data
|
|
self.ioState = ABORTED
|
|
self.ioError = err
|
|
self.trigger()
|
|
|
|
def set_timeout(self, delay, err=TimeoutError):
|
|
"""Called to set a transaction timer."""
|
|
if _debug: IOCB._debug("set_timeout(%d) %r err=%r", self.ioID, delay, err)
|
|
|
|
# if one has already been created, cancel it
|
|
if self.ioTimeout:
|
|
self.ioTimeout.suspend_task()
|
|
else:
|
|
self.ioTimeout = FunctionTask(self.abort, err)
|
|
|
|
# (re)schedule it
|
|
self.ioTimeout.install_task(_time() + delay)
|
|
|
|
def __repr__(self):
|
|
xid = id(self)
|
|
if (xid < 0): xid += (1L << 32)
|
|
|
|
sname = self.__module__ + '.' + self.__class__.__name__
|
|
desc = "(%d)" % (self.ioID,)
|
|
|
|
return '<' + sname + desc + ' instance at 0x%08x' % (xid,) + '>'
|
|
|
|
#
|
|
# IOChainMixIn
|
|
#
|
|
|
|
@bacpypes_debugging
|
|
class IOChainMixIn(DebugContents):
|
|
|
|
_debugContents = ( 'ioChain++', )
|
|
|
|
def __init__(self, iocb):
|
|
if _debug: IOChainMixIn._debug("__init__ %r", iocb)
|
|
|
|
# save a refence back to the iocb
|
|
self.ioChain = iocb
|
|
|
|
# set the callback to follow the chain
|
|
self.add_callback(self.chain_callback)
|
|
|
|
# if we're not chained, there's no notification to do
|
|
if not self.ioChain:
|
|
return
|
|
|
|
# this object becomes its controller
|
|
iocb.ioController = self
|
|
|
|
# consider the parent active
|
|
iocb.ioState = ACTIVE
|
|
|
|
try:
|
|
if _debug: IOChainMixIn._debug(" - encoding")
|
|
|
|
# let the derived class set the args and kwargs
|
|
self.Encode()
|
|
|
|
if _debug: IOChainMixIn._debug(" - encode complete")
|
|
except:
|
|
# extract the error and abort the request
|
|
err = sys.exc_info()[1]
|
|
if _debug: IOChainMixIn._exception(" - encoding exception: %r", err)
|
|
|
|
iocb.abort(err)
|
|
|
|
def chain_callback(self, iocb):
|
|
"""Callback when this iocb completes."""
|
|
if _debug: IOChainMixIn._debug("chain_callback %r", iocb)
|
|
|
|
# if we're not chained, there's no notification to do
|
|
if not self.ioChain:
|
|
return
|
|
|
|
# refer to the chained iocb
|
|
iocb = self.ioChain
|
|
|
|
try:
|
|
if _debug: IOChainMixIn._debug(" - decoding")
|
|
|
|
# let the derived class transform the data
|
|
self.Decode()
|
|
|
|
if _debug: IOChainMixIn._debug(" - decode complete")
|
|
except:
|
|
# extract the error and abort
|
|
err = sys.exc_info()[1]
|
|
if _debug: IOChainMixIn._exception(" - decoding exception: %r", err)
|
|
|
|
iocb.ioState = ABORTED
|
|
iocb.ioError = err
|
|
|
|
# break the references
|
|
self.ioChain = None
|
|
iocb.ioController = None
|
|
|
|
# notify the client
|
|
iocb.trigger()
|
|
|
|
def abort_io(self, iocb, err):
|
|
"""Forward the abort downstream."""
|
|
if _debug: IOChainMixIn._debug("abort_io %r %r", iocb, err)
|
|
|
|
# make sure we're being notified of an abort request from
|
|
# the iocb we are chained from
|
|
if iocb is not self.ioChain:
|
|
raise RuntimeError, "broken chain"
|
|
|
|
# call my own abort(), which may forward it to a controller or
|
|
# be overridden by IOGroup
|
|
self.abort(err)
|
|
|
|
def encode(self):
|
|
"""Hook to transform the request, called when this IOCB is
|
|
chained."""
|
|
if _debug: IOChainMixIn._debug("encode (pass)")
|
|
|
|
# by default do nothing, the arguments have already been supplied
|
|
|
|
def decode(self):
|
|
"""Hook to transform the response, called when this IOCB is
|
|
completed."""
|
|
if _debug: IOChainMixIn._debug("decode")
|
|
|
|
# refer to the chained iocb
|
|
iocb = self.ioChain
|
|
|
|
# if this has completed successfully, pass it up
|
|
if self.ioState == COMPLETED:
|
|
if _debug: IOChainMixIn._debug(" - completed: %r", self.ioResponse)
|
|
|
|
# change the state and transform the content
|
|
iocb.ioState = COMPLETED
|
|
iocb.ioResponse = self.ioResponse
|
|
|
|
# if this aborted, pass that up too
|
|
elif self.ioState == ABORTED:
|
|
if _debug: IOChainMixIn._debug(" - aborted: %r", self.ioError)
|
|
|
|
# change the state
|
|
iocb.ioState = ABORTED
|
|
iocb.ioError = self.ioError
|
|
|
|
else:
|
|
raise RuntimeError, "invalid state: %d" % (self.ioState,)
|
|
|
|
#
|
|
# IOChain
|
|
#
|
|
|
|
class IOChain(IOCB, IOChainMixIn):
|
|
|
|
def __init__(self, chain, *args, **kwargs):
|
|
"""Initialize a chained control block."""
|
|
if _debug: IOChain._debug("__init__ %r %r %r", chain, args, kwargs)
|
|
|
|
# initialize IOCB part to pick up the ioID
|
|
IOCB.__init__(self, *args, **kwargs)
|
|
IOChainMixIn.__init__(self, chain)
|
|
|
|
#
|
|
# IOGroup
|
|
#
|
|
|
|
@bacpypes_debugging
|
|
class IOGroup(IOCB, DebugContents):
|
|
|
|
_debugContents = ('ioMembers',)
|
|
|
|
def __init__(self):
|
|
"""Initialize a group."""
|
|
if _debug: IOGroup._debug("__init__")
|
|
IOCB.__init__(self)
|
|
|
|
# start with an empty list of members
|
|
self.ioMembers = []
|
|
|
|
# start out being done. When an IOCB is added to the
|
|
# group that is not already completed, this state will
|
|
# change to PENDING.
|
|
self.ioState = COMPLETED
|
|
self.ioComplete.set()
|
|
|
|
def add(self, iocb):
|
|
"""Add an IOCB to the group, you can also add other groups."""
|
|
if _debug: IOGroup._debug("Add %r", iocb)
|
|
|
|
# add this to our members
|
|
self.ioMembers.append(iocb)
|
|
|
|
# assume all of our members have not completed yet
|
|
self.ioState = PENDING
|
|
self.ioComplete.clear()
|
|
|
|
# when this completes, call back to the group. If this
|
|
# has already completed, it will trigger
|
|
iocb.add_callback(self.group_callback)
|
|
|
|
def group_callback(self, iocb):
|
|
"""Callback when a child iocb completes."""
|
|
if _debug: IOGroup._debug("group_callback %r", iocb)
|
|
|
|
# check all the members
|
|
for iocb in self.ioMembers:
|
|
if not iocb.ioComplete.isSet():
|
|
if _debug: IOGroup._debug(" - waiting for child: %r", iocb)
|
|
break
|
|
else:
|
|
if _debug: IOGroup._debug(" - all children complete")
|
|
# everything complete
|
|
self.ioState = COMPLETED
|
|
self.trigger()
|
|
|
|
def abort(self, err):
|
|
"""Called by a client to abort all of the member transactions.
|
|
When the last pending member is aborted the group callback
|
|
function will be called."""
|
|
if _debug: IOGroup._debug("abort %r", err)
|
|
|
|
# change the state to reflect that it was killed
|
|
self.ioState = ABORTED
|
|
self.ioError = err
|
|
|
|
# abort all the members
|
|
for iocb in self.ioMembers:
|
|
iocb.abort(err)
|
|
|
|
# notify the client
|
|
self.trigger()
|
|
|
|
#
|
|
# IOQueue - Input Output Queue
|
|
#
|
|
|
|
@bacpypes_debugging
|
|
class IOQueue:
|
|
|
|
def __init__(self, name):
|
|
if _debug: IOQueue._debug("__init__ %r", name)
|
|
|
|
self.queue = []
|
|
self.notempty = threading.Event()
|
|
self.notempty.clear()
|
|
|
|
def put(self, iocb):
|
|
"""Add an IOCB to a queue. This is usually called by the function
|
|
that filters requests and passes them out to the correct processing
|
|
thread."""
|
|
if _debug: IOQueue._debug("put %r", iocb)
|
|
|
|
# requests should be pending before being queued
|
|
if iocb.ioState != PENDING:
|
|
raise RuntimeError, "invalid state transition"
|
|
|
|
# save that it might have been empty
|
|
wasempty = not self.notempty.isSet()
|
|
|
|
# add the request to the end of the list of iocb's at same priority
|
|
priority = iocb.ioPriority
|
|
item = (priority, iocb)
|
|
self.queue.insert(bisect_left(self.queue, (priority+1,)), item)
|
|
|
|
# point the iocb back to this queue
|
|
iocb.ioQueue = self
|
|
|
|
# set the event, queue is no longer empty
|
|
self.notempty.set()
|
|
|
|
return wasempty
|
|
|
|
def get(self, block=1, delay=None):
|
|
"""Get a request from a queue, optionally block until a request
|
|
is available."""
|
|
if _debug: IOQueue._debug("get block=%r delay=%r", block, delay)
|
|
|
|
# if the queue is empty and we do not block return None
|
|
if not block and not self.notempty.isSet():
|
|
return None
|
|
|
|
# wait for something to be in the queue
|
|
if delay:
|
|
self.notempty.wait(delay)
|
|
if not self.notempty.isSet():
|
|
return None
|
|
else:
|
|
self.notempty.wait()
|
|
|
|
# extract the first element
|
|
priority, iocb = self.queue[0]
|
|
del self.queue[0]
|
|
iocb.ioQueue = None
|
|
|
|
# if the queue is empty, clear the event
|
|
qlen = len(self.queue)
|
|
if not qlen:
|
|
self.notempty.clear()
|
|
|
|
# return the request
|
|
return iocb
|
|
|
|
def remove(self, iocb):
|
|
"""Remove a control block from the queue, called if the request
|
|
is canceled/aborted."""
|
|
if _debug: IOQueue._debug("remove %r", iocb)
|
|
|
|
# remove the request from the queue
|
|
for i, item in enumerate(self.queue):
|
|
if iocb is item[1]:
|
|
if _debug: IOQueue._debug(" - found at %d", i)
|
|
del self.queue[i]
|
|
|
|
# if the queue is empty, clear the event
|
|
qlen = len(self.queue)
|
|
if not qlen:
|
|
self.notempty.clear()
|
|
|
|
break
|
|
else:
|
|
if _debug: IOQueue._debug(" - not found")
|
|
|
|
def abort(self, err):
|
|
"""abort all of the control blocks in the queue."""
|
|
if _debug: IOQueue._debug("abort %r", err)
|
|
|
|
# send aborts to all of the members
|
|
try:
|
|
for iocb in self.queue:
|
|
iocb.ioQueue = None
|
|
iocb.abort(err)
|
|
|
|
# flush the queue
|
|
self.queue = []
|
|
|
|
# the queue is now empty, clear the event
|
|
self.notempty.clear()
|
|
except ValueError:
|
|
pass
|
|
|
|
#
|
|
# IOController
|
|
#
|
|
|
|
@bacpypes_debugging
|
|
class IOController:
|
|
|
|
def __init__(self, name=None):
|
|
"""Initialize a controller."""
|
|
if _debug: IOController._debug("__init__ name=%r", name)
|
|
|
|
# save the name
|
|
self.name = name
|
|
|
|
# register the name
|
|
if name is not None:
|
|
if name in _local_controllers:
|
|
raise RuntimeError, "already a local controller called '%s': %r" % (name, _local_controllers[name])
|
|
_local_controllers[name] = self
|
|
|
|
def abort(self, err):
|
|
"""Abort all requests, no default implementation."""
|
|
pass
|
|
|
|
def request_io(self, iocb):
|
|
"""Called by a client to start processing a request."""
|
|
if _debug: IOController._debug("request_io %r", iocb)
|
|
|
|
# bind the iocb to this controller
|
|
iocb.ioController = self
|
|
|
|
try:
|
|
# hopefully there won't be an error
|
|
err = None
|
|
|
|
# change the state
|
|
iocb.ioState = PENDING
|
|
|
|
# let derived class figure out how to process this
|
|
self.process_io(iocb)
|
|
except:
|
|
# extract the error
|
|
err = sys.exc_info()[1]
|
|
|
|
# if there was an error, abort the request
|
|
if err:
|
|
self.abort_io(iocb, err)
|
|
|
|
def process_io(self, iocb):
|
|
"""Figure out how to respond to this request. This must be
|
|
provided by the derived class."""
|
|
raise NotImplementedError, "IOController must implement process_io()"
|
|
|
|
def active_io(self, iocb):
|
|
"""Called by a handler to notify the controller that a request is
|
|
being processed."""
|
|
if _debug: IOController._debug("active_io %r", iocb)
|
|
|
|
# requests should be idle or pending before coming active
|
|
if (iocb.ioState != IDLE) and (iocb.ioState != PENDING):
|
|
raise RuntimeError, "invalid state transition (currently %d)" % (iocb.ioState,)
|
|
|
|
# change the state
|
|
iocb.ioState = ACTIVE
|
|
|
|
def complete_io(self, iocb, msg):
|
|
"""Called by a handler to return data to the client."""
|
|
if _debug: IOController._debug("complete_io %r %r", iocb, msg)
|
|
|
|
# if it completed, leave it alone
|
|
if iocb.ioState == COMPLETED:
|
|
pass
|
|
|
|
# if it already aborted, leave it alone
|
|
elif iocb.ioState == ABORTED:
|
|
pass
|
|
|
|
else:
|
|
# change the state
|
|
iocb.ioState = COMPLETED
|
|
iocb.ioResponse = msg
|
|
|
|
# notify the client
|
|
iocb.trigger()
|
|
|
|
def abort_io(self, iocb, err):
|
|
"""Called by a handler or a client to abort a transaction."""
|
|
if _debug: IOController._debug("abort_io %r %r", iocb, err)
|
|
|
|
# if it completed, leave it alone
|
|
if iocb.ioState == COMPLETED:
|
|
pass
|
|
|
|
# if it already aborted, leave it alone
|
|
elif iocb.ioState == ABORTED:
|
|
pass
|
|
|
|
else:
|
|
# change the state
|
|
iocb.ioState = ABORTED
|
|
iocb.ioError = err
|
|
|
|
# notify the client
|
|
iocb.trigger()
|
|
|
|
#
|
|
# IOQController
|
|
#
|
|
|
|
@bacpypes_debugging
|
|
class IOQController(IOController):
|
|
|
|
wait_time = 0.0
|
|
|
|
def __init__(self, name=None):
|
|
"""Initialize a queue controller."""
|
|
if _debug: IOQController._debug("__init__ name=%r", name)
|
|
|
|
# give ourselves a nice name
|
|
if not name:
|
|
name = self.__class__.__name__
|
|
IOController.__init__(self, name)
|
|
|
|
# start idle
|
|
self.state = CTRL_IDLE
|
|
|
|
# no active iocb
|
|
self.active_iocb = None
|
|
|
|
# create an IOQueue for iocb's requested when not idle
|
|
self.ioQueue = IOQueue(str(name) + "/Queue")
|
|
|
|
def abort(self, err):
|
|
"""Abort all pending requests."""
|
|
if _debug: IOQController._debug("abort %r", err)
|
|
|
|
if (self.state == CTRL_IDLE):
|
|
if _debug: IOQController._debug(" - idle")
|
|
return
|
|
|
|
while True:
|
|
iocb = self.ioQueue.get()
|
|
if not iocb:
|
|
break
|
|
if _debug: IOQController._debug(" - iocb: %r", iocb)
|
|
|
|
# change the state
|
|
iocb.ioState = ABORTED
|
|
iocb.ioError = err
|
|
|
|
# notify the client
|
|
iocb.trigger()
|
|
|
|
if (self.state != CTRL_IDLE):
|
|
if _debug: IOQController._debug(" - busy after aborts")
|
|
|
|
def request_io(self, iocb):
|
|
"""Called by a client to start processing a request."""
|
|
if _debug: IOQController._debug("request_io %r", iocb)
|
|
|
|
# bind the iocb to this controller
|
|
iocb.ioController = self
|
|
|
|
# if we're busy, queue it
|
|
if (self.state != CTRL_IDLE):
|
|
if _debug: IOQController._debug(" - busy, request queued")
|
|
|
|
iocb.ioState = PENDING
|
|
self.ioQueue.put(iocb)
|
|
return
|
|
|
|
try:
|
|
# hopefully there won't be an error
|
|
err = None
|
|
|
|
# let derived class figure out how to process this
|
|
self.process_io(iocb)
|
|
except:
|
|
# extract the error
|
|
err = sys.exc_info()[1]
|
|
|
|
# if there was an error, abort the request
|
|
if err:
|
|
self.abort_io(iocb, err)
|
|
|
|
def process_io(self, iocb):
|
|
"""Figure out how to respond to this request. This must be
|
|
provided by the derived class."""
|
|
raise NotImplementedError, "IOController must implement process_io()"
|
|
|
|
def active_io(self, iocb):
|
|
"""Called by a handler to notify the controller that a request is
|
|
being processed."""
|
|
if _debug: IOQController._debug("active_io %r", iocb)
|
|
|
|
# base class work first, setting iocb state and timer data
|
|
IOController.active_io(self, iocb)
|
|
|
|
# change our state
|
|
self.state = CTRL_ACTIVE
|
|
|
|
# keep track of the iocb
|
|
self.active_iocb = iocb
|
|
|
|
def complete_io(self, iocb, msg):
|
|
"""Called by a handler to return data to the client."""
|
|
if _debug: IOQController._debug("complete_io %r %r", iocb, msg)
|
|
|
|
# check to see if it is completing the active one
|
|
if iocb is not self.active_iocb:
|
|
raise RuntimeError, "not the current iocb"
|
|
|
|
# normal completion
|
|
IOController.complete_io(self, iocb, msg)
|
|
|
|
# no longer an active iocb
|
|
self.active_iocb = None
|
|
|
|
# check to see if we should wait a bit
|
|
if self.wait_time:
|
|
# change our state
|
|
self.state = CTRL_WAITING
|
|
|
|
# schedule a call in the future
|
|
task = FunctionTask(IOQController._wait_trigger, self)
|
|
task.install_task(_time() + self.wait_time)
|
|
|
|
else:
|
|
# change our state
|
|
self.state = CTRL_IDLE
|
|
|
|
# look for more to do
|
|
deferred(IOQController._trigger, self)
|
|
|
|
def abort_io(self, iocb, err):
|
|
"""Called by a handler or a client to abort a transaction."""
|
|
if _debug: IOQController._debug("abort_io %r %r", iocb, err)
|
|
|
|
# normal abort
|
|
IOController.abort_io(self, iocb, err)
|
|
|
|
# check to see if it is completing the active one
|
|
if iocb is not self.active_iocb:
|
|
if _debug: IOQController._debug(" - not current iocb")
|
|
return
|
|
|
|
# no longer an active iocb
|
|
self.active_iocb = None
|
|
|
|
# change our state
|
|
self.state = CTRL_IDLE
|
|
|
|
# look for more to do
|
|
deferred(IOQController._trigger, self)
|
|
|
|
def _trigger(self):
|
|
"""Called to launch the next request in the queue."""
|
|
if _debug: IOQController._debug("_trigger")
|
|
|
|
# if we are busy, do nothing
|
|
if self.state != CTRL_IDLE:
|
|
if _debug: IOQController._debug(" - not idle")
|
|
return
|
|
|
|
# if there is nothing to do, return
|
|
if not self.ioQueue.queue:
|
|
if _debug: IOQController._debug(" - empty queue")
|
|
return
|
|
|
|
# get the next iocb
|
|
iocb = self.ioQueue.get()
|
|
|
|
try:
|
|
# hopefully there won't be an error
|
|
err = None
|
|
|
|
# let derived class figure out how to process this
|
|
self.process_io(iocb)
|
|
except:
|
|
# extract the error
|
|
err = sys.exc_info()[1]
|
|
|
|
# if there was an error, abort the request
|
|
if err:
|
|
self.abort_io(iocb, err)
|
|
|
|
# if we're idle, call again
|
|
if self.state == CTRL_IDLE:
|
|
deferred(IOQController._trigger, self)
|
|
|
|
def _wait_trigger(self):
|
|
"""Called to launch the next request in the queue."""
|
|
if _debug: IOQController._debug("_wait_trigger")
|
|
|
|
# make sure we are waiting
|
|
if (self.state != CTRL_WAITING):
|
|
raise RuntimeError, "not waiting"
|
|
|
|
# change our state
|
|
self.state = CTRL_IDLE
|
|
|
|
# look for more to do
|
|
IOQController._trigger(self)
|
|
|
|
#
|
|
# IOProxy
|
|
#
|
|
|
|
@bacpypes_debugging
|
|
class IOProxy:
|
|
|
|
def __init__(self, controllerName, serverName=None, requestLimit=None):
|
|
"""Create an IO client. It implements request_io like a controller, but
|
|
passes requests on to a local controller if it happens to be in the
|
|
same process, or the IOProxyServer instance to forward on for processing."""
|
|
if _debug: IOProxy._debug("__init__ %r serverName=%r, requestLimit=%r", controllerName, serverName, requestLimit)
|
|
|
|
# save the server reference
|
|
self.ioControllerRef = controllerName
|
|
self.ioServerRef = serverName
|
|
|
|
# set a limit on how many requests can be submitted
|
|
self.ioRequestLimit = requestLimit
|
|
self.ioPending = set()
|
|
self.ioBlocked = deque()
|
|
|
|
# bind to a local controller if possible
|
|
if not serverName:
|
|
self.ioBind = _local_controllers.get(controllerName, None)
|
|
if self.ioBind:
|
|
if _debug: IOProxy._debug(" - local bind successful")
|
|
else:
|
|
if _debug: IOProxy._debug(" - local bind deferred")
|
|
else:
|
|
self.ioBind = None
|
|
if _debug: IOProxy._debug(" - bind deferred")
|
|
|
|
def request_io(self, iocb, urgent=False):
|
|
"""Called by a client to start processing a request."""
|
|
if _debug: IOProxy._debug("request_io %r urgent=%r", iocb, urgent)
|
|
global _proxy_server
|
|
|
|
# save the server and controller reference
|
|
iocb.ioServerRef = self.ioServerRef
|
|
iocb.ioControllerRef = self.ioControllerRef
|
|
|
|
# check to see if it needs binding
|
|
if not self.ioBind:
|
|
# if the server is us, look for a local controller
|
|
if not self.ioServerRef:
|
|
self.ioBind = _local_controllers.get(self.ioControllerRef, None)
|
|
if not self.ioBind:
|
|
iocb.abort("no local controller %s" % (self.ioControllerRef,))
|
|
return
|
|
if _debug: IOProxy._debug(" - local bind successful")
|
|
else:
|
|
if not _proxy_server:
|
|
_proxy_server = IOProxyServer()
|
|
|
|
self.ioBind = _proxy_server
|
|
if _debug: IOProxy._debug(" - proxy bind successful: %r", self.ioBind)
|
|
|
|
# if this isn't urgent and there is a limit, see if we've reached it
|
|
if (not urgent) and self.ioRequestLimit:
|
|
# call back when this is completed
|
|
iocb.add_callback(self._proxy_trigger)
|
|
|
|
# check for the limit
|
|
if len(self.ioPending) < self.ioRequestLimit:
|
|
if _debug: IOProxy._debug(" - cleared for launch")
|
|
|
|
self.ioPending.add(iocb)
|
|
self.ioBind.request_io(iocb)
|
|
else:
|
|
# save it for later
|
|
if _debug: IOProxy._debug(" - save for later")
|
|
|
|
self.ioBlocked.append(iocb)
|
|
else:
|
|
# just pass it along
|
|
self.ioBind.request_io(iocb)
|
|
|
|
def _proxy_trigger(self, iocb):
|
|
"""This has completed, remove it from the set of pending requests
|
|
and see if it's OK to start up the next one."""
|
|
if _debug: IOProxy._debug("_proxy_trigger %r", iocb)
|
|
|
|
if iocb not in self.ioPending:
|
|
if _debug: IOProxy._warning("iocb not pending: %r", iocb)
|
|
else:
|
|
self.ioPending.remove(iocb)
|
|
|
|
# check to send another one
|
|
if (len(self.ioPending) < self.ioRequestLimit) and self.ioBlocked:
|
|
nextio = self.ioBlocked.popleft()
|
|
if _debug: IOProxy._debug(" - cleared for launch: %r", nextio)
|
|
|
|
# this one is now pending
|
|
self.ioPending.add(nextio)
|
|
self.ioBind.request_io(nextio)
|
|
|
|
#
|
|
# IOServer
|
|
#
|
|
|
|
PORT = 8002
|
|
SERVER_TIMEOUT = 60
|
|
|
|
@bacpypes_debugging
|
|
class IOServer(IOController, Client):
|
|
|
|
def __init__(self, addr=('',PORT)):
|
|
"""Initialize the remote IO handler."""
|
|
if _debug: IOServer._debug("__init__ %r", addr)
|
|
IOController.__init__(self)
|
|
|
|
# create a UDP director
|
|
self.server = UDPDirector(addr)
|
|
bind(self, self.server)
|
|
|
|
# dictionary of IOCBs as a server
|
|
self.remoteIOCB = {}
|
|
|
|
def confirmation(self, pdu):
|
|
if _debug: IOServer._debug('confirmation %r', pdu)
|
|
|
|
addr = pdu.pduSource
|
|
request = pdu.pduData
|
|
|
|
try:
|
|
# parse the request
|
|
request = cPickle.loads(request)
|
|
if _debug: _commlog.debug(">>> %s: S %s %r" % (_strftime(), str(addr), request))
|
|
|
|
# pick the message
|
|
if (request[0] == 0):
|
|
self.new_iocb(addr, *request[1:])
|
|
elif (request[0] == 1):
|
|
self.complete_iocb(addr, *request[1:])
|
|
elif (request[0] == 2):
|
|
self.abort_iocb(addr, *request[1:])
|
|
except:
|
|
# extract the error
|
|
err = sys.exc_info()[1]
|
|
IOServer._exception("error %r processing %r from %r", err, request, addr)
|
|
|
|
def callback(self, iocb):
|
|
"""Callback when an iocb is completed by a local controller and the
|
|
result needs to be sent back to the client."""
|
|
if _debug: IOServer._debug("callback %r", iocb)
|
|
|
|
# make sure it's one of ours
|
|
if not self.remoteIOCB.has_key(iocb):
|
|
IOServer._warning("IOCB not owned by server: %r", iocb)
|
|
return
|
|
|
|
# get the client information
|
|
clientID, clientAddr = self.remoteIOCB[iocb]
|
|
|
|
# we're done with this
|
|
del self.remoteIOCB[iocb]
|
|
|
|
# build a response
|
|
if iocb.ioState == COMPLETED:
|
|
response = (1, clientID, iocb.ioResponse)
|
|
elif iocb.ioState == ABORTED:
|
|
response = (2, clientID, iocb.ioError)
|
|
else:
|
|
raise RuntimeError, "IOCB invalid state"
|
|
|
|
if _debug: _commlog.debug("<<< %s: S %s %r" % (_strftime(), clientAddr, response))
|
|
|
|
response = cPickle.dumps( response, 1 )
|
|
|
|
# send it to the client
|
|
self.request(PDU(response, destination=clientAddr))
|
|
|
|
def abort(self, err):
|
|
"""Called by a local application to abort all transactions."""
|
|
if _debug: IOServer._debug("abort %r", err)
|
|
|
|
for iocb in self.remoteIOCB.keys():
|
|
self.abort_io(iocb, err)
|
|
|
|
def abort_io(self, iocb, err):
|
|
"""Called by a local client or a local controlled to abort a transaction."""
|
|
if _debug: IOServer._debug("abort_io %r %r", iocb, err)
|
|
|
|
# if it completed, leave it alone
|
|
if iocb.ioState == COMPLETED:
|
|
pass
|
|
|
|
# if it already aborted, leave it alone
|
|
elif iocb.ioState == ABORTED:
|
|
pass
|
|
|
|
elif self.remoteIOCB.has_key(iocb):
|
|
# get the client information
|
|
clientID, clientAddr = self.remoteIOCB[iocb]
|
|
|
|
# we're done with this
|
|
del self.remoteIOCB[iocb]
|
|
|
|
# build an abort response
|
|
response = (2, clientID, err)
|
|
if _debug: _commlog.debug("<<< %s: S %s %r" % (_strftime(), clientAddr, response))
|
|
|
|
response = cPickle.dumps( response, 1 )
|
|
|
|
# send it to the client
|
|
self.socket.sendto( response, clientAddr )
|
|
|
|
else:
|
|
IOServer._error("no reference to aborting iocb: %r", iocb)
|
|
|
|
# change the state
|
|
iocb.ioState = ABORTED
|
|
iocb.ioError = err
|
|
|
|
# notify the client
|
|
iocb.trigger()
|
|
|
|
def new_iocb(self, clientAddr, iocbid, controllerName, args, kwargs):
|
|
"""Called when the server receives a new request."""
|
|
if _debug: IOServer._debug("new_iocb %r %r %r %r %r", clientAddr, iocbid, controllerName, args, kwargs)
|
|
|
|
# look for a controller
|
|
controller = _local_controllers.get(controllerName, None)
|
|
if not controller:
|
|
# create a nice error message
|
|
err = RuntimeError("no local controller '%s'" % (controllerName, ))
|
|
|
|
# build an abort response
|
|
response = (2, iocbid, err)
|
|
if _debug: _commlog.debug("<<< %s: S %s %r" % (_strftime(), clientAddr, response))
|
|
|
|
response = cPickle.dumps( response, 1 )
|
|
|
|
# send it to the server
|
|
self.request(PDU(response, destination=clientAddr))
|
|
|
|
else:
|
|
# create an IOCB
|
|
iocb = IOCB(*args, **kwargs)
|
|
if _debug: IOServer._debug(" - local IOCB %r bound to remote %r", iocb.ioID, iocbid)
|
|
|
|
# save a reference to it
|
|
self.remoteIOCB[iocb] = (iocbid, clientAddr)
|
|
|
|
# make sure we're notified when it completes
|
|
iocb.add_callback(self.callback)
|
|
|
|
# pass it along
|
|
controller.request_io(iocb)
|
|
|
|
def abort_iocb(self, addr, iocbid, err):
|
|
"""Called when the client or server receives an abort request."""
|
|
if _debug: IOServer._debug("abort_iocb %r %r %r", addr, iocbid, err)
|
|
|
|
# see if this came from a client
|
|
for iocb in self.remoteIOCB.keys():
|
|
clientID, clientAddr = self.remoteIOCB[iocb]
|
|
if (addr == clientAddr) and (clientID == iocbid):
|
|
break
|
|
else:
|
|
IOServer._error("no reference to aborting iocb %r from %r", iocbid, addr)
|
|
return
|
|
if _debug: IOServer._debug(" - local IOCB %r bound to remote %r", iocb.ioID, iocbid)
|
|
|
|
# we're done with this
|
|
del self.remoteIOCB[iocb]
|
|
|
|
# clear the callback, we already know
|
|
iocb.ioCallback = []
|
|
|
|
# tell the local controller about the abort
|
|
iocb.abort(err)
|
|
|
|
#
|
|
# IOProxyServer
|
|
#
|
|
|
|
SERVER_TIMEOUT = 60
|
|
|
|
@bacpypes_debugging
|
|
class IOProxyServer(IOController, Client):
|
|
|
|
def __init__(self, addr=('', 0), name=None):
|
|
"""Initialize the remote IO handler."""
|
|
if _debug: IOProxyServer._debug("__init__")
|
|
IOController.__init__(self, name=name)
|
|
|
|
# create a UDP director
|
|
self.server = UDPDirector(addr)
|
|
bind(self, self.server)
|
|
if _debug: IOProxyServer._debug(" - bound to %r", self.server.socket.getsockname())
|
|
|
|
# dictionary of IOCBs as a client
|
|
self.localIOCB = {}
|
|
|
|
def confirmation(self, pdu):
|
|
if _debug: IOProxyServer._debug('confirmation %r', pdu)
|
|
|
|
addr = pdu.pduSource
|
|
request = pdu.pduData
|
|
|
|
try:
|
|
# parse the request
|
|
request = cPickle.loads(request)
|
|
if _debug: _commlog.debug(">>> %s: P %s %r" % (_strftime(), addr, request))
|
|
|
|
# pick the message
|
|
if (request[0] == 1):
|
|
self.complete_iocb(addr, *request[1:])
|
|
elif (request[0] == 2):
|
|
self.abort_iocb(addr, *request[1:])
|
|
except:
|
|
# extract the error
|
|
err = sys.exc_info()[1]
|
|
IOProxyServer._exception("error %r processing %r from %r", err, request, addr)
|
|
|
|
def process_io(self, iocb):
|
|
"""Package up the local IO request and send it to the server."""
|
|
if _debug: IOProxyServer._debug("process_io %r", iocb)
|
|
|
|
# save a reference in our dictionary
|
|
self.localIOCB[iocb.ioID] = iocb
|
|
|
|
# start a default timer if one hasn't already been set
|
|
if not iocb.ioTimeout:
|
|
iocb.set_timeout( SERVER_TIMEOUT, RuntimeError("no response from " + iocb.ioServerRef))
|
|
|
|
# build a message
|
|
request = (0, iocb.ioID, iocb.ioControllerRef, iocb.args, iocb.kwargs)
|
|
if _debug: _commlog.debug("<<< %s: P %s %r" % (_strftime(), iocb.ioServerRef, request))
|
|
|
|
request = cPickle.dumps( request, 1 )
|
|
|
|
# send it to the server
|
|
self.request(PDU(request, destination=(iocb.ioServerRef, PORT)))
|
|
|
|
def abort(self, err):
|
|
"""Called by a local application to abort all transactions, local
|
|
and remote."""
|
|
if _debug: IOProxyServer._debug("abort %r", err)
|
|
|
|
for iocb in self.localIOCB.values():
|
|
self.abort_io(iocb, err)
|
|
|
|
def abort_io(self, iocb, err):
|
|
"""Called by a local client or a local controlled to abort a transaction."""
|
|
if _debug: IOProxyServer._debug("abort_io %r %r", iocb, err)
|
|
|
|
# if it completed, leave it alone
|
|
if iocb.ioState == COMPLETED:
|
|
pass
|
|
|
|
# if it already aborted, leave it alone
|
|
elif iocb.ioState == ABORTED:
|
|
pass
|
|
|
|
elif self.localIOCB.has_key(iocb.ioID):
|
|
# delete the dictionary reference
|
|
del self.localIOCB[iocb.ioID]
|
|
|
|
# build an abort request
|
|
request = (2, iocb.ioID, err)
|
|
if _debug: _commlog.debug("<<< %s: P %s %r" % (_strftime(), iocb.ioServerRef, request))
|
|
|
|
request = cPickle.dumps( request, 1 )
|
|
|
|
# send it to the server
|
|
self.request(PDU(request, destination=(iocb.ioServerRef, PORT)))
|
|
|
|
else:
|
|
raise RuntimeError, "no reference to aborting iocb: %r" % (iocb.ioID,)
|
|
|
|
# change the state
|
|
iocb.ioState = ABORTED
|
|
iocb.ioError = err
|
|
|
|
# notify the client
|
|
iocb.trigger()
|
|
|
|
def complete_iocb(self, serverAddr, iocbid, msg):
|
|
"""Called when the client receives a response to a request."""
|
|
if _debug: IOProxyServer._debug("complete_iocb %r %r %r", serverAddr, iocbid, msg)
|
|
|
|
# assume nothing
|
|
iocb = None
|
|
|
|
# make sure this is a local request
|
|
if not self.localIOCB.has_key(iocbid):
|
|
IOProxyServer._error("no reference to IOCB %r", iocbid)
|
|
if _debug: IOProxyServer._debug(" - localIOCB: %r", self.localIOCB)
|
|
else:
|
|
# get the iocb
|
|
iocb = self.localIOCB[iocbid]
|
|
|
|
# delete the dictionary reference
|
|
del self.localIOCB[iocbid]
|
|
|
|
if iocb:
|
|
# change the state
|
|
iocb.ioState = COMPLETED
|
|
iocb.ioResponse = msg
|
|
|
|
# notify the client
|
|
iocb.trigger()
|
|
|
|
def abort_iocb(self, addr, iocbid, err):
|
|
"""Called when the client or server receives an abort request."""
|
|
if _debug: IOProxyServer._debug("abort_iocb %r %r %r", addr, iocbid, err)
|
|
|
|
if not self.localIOCB.has_key(iocbid):
|
|
raise RuntimeError, "no reference to aborting iocb: %r" % (iocbid,)
|
|
|
|
# get the iocb
|
|
iocb = self.localIOCB[iocbid]
|
|
|
|
# delete the dictionary reference
|
|
del self.localIOCB[iocbid]
|
|
|
|
# change the state
|
|
iocb.ioState = ABORTED
|
|
iocb.ioError = err
|
|
|
|
# notify the client
|
|
iocb.trigger()
|
|
|
|
#
|
|
# abort
|
|
#
|
|
|
|
@bacpypes_debugging
|
|
def abort(err):
|
|
"""Abort everything, everywhere."""
|
|
if _debug: abort._debug("abort %r", err)
|
|
|
|
# start with the server
|
|
if IOServer._highlander:
|
|
IOServer._highlander.abort(err)
|
|
|
|
# now do everything local
|
|
for controller in _local_controllers.values():
|
|
controller.abort(err)
|