1
0
mirror of https://github.com/JoelBender/bacpypes synced 2025-09-28 22:15:23 +08:00
bacpypes/sandbox/io.py
2017-07-25 13:44:11 -04:00

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)