#!/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)