Beispiel #1
0
class MockChannel(object):
    implements(IChannel)

    dbr = _circ = None
    sid = cid = maxcount = 0

    def __init__(self):
        self._C = DeferredManager()
        self._D = DeferredManager()
        self._D.callback(None)

    @property
    def whenCon(self):
        return self._C.get()

    @property
    def whenDis(self):
        return self._D.get()

    def doCon(self):
        self._D = DeferredManager()
        self._C.callback(self)

    def doFail(self):
        d, self._C = self._C, DeferredManager()
        d.callback(None)

    def doLost(self):
        self._C = DeferredManager()
        d, self._D = self._D, DeferredManager()
Beispiel #2
0
class CAcircuit(Protocol):
    implements(IConnectNotify)
    
    def __init__(self, server):
        self.server=server
        self.peer=self.tcpport=None
    
        self.prio, self.version=0, 11

        self.user,self.host="<NOONE>","<ANONYMOUS>"
        
        self.in_buffer=''
        
        self._circ={0 :self.caver,
                    1 :self.forwardchan,
                    2 :self.forwardchan,
                    4 :self.forwardchan,
                    12:self.clearchan,
                    15:self.forwardchan,
                    18:self.createchan,
                    19:self.forwardchan,
                    20:self.caclient,
                    21:self.cahost,
                    23:self.ping,
                   }

        self.__C=DeferredManager()
        self.__D=DeferredManager()
        self.__D.callback(self)
        
        self.channels={}

        self.next_sid=0

    @property
    def whenCon(self):
        return self.__C.get()

    @property
    def whenDis(self):
        return self.__D.get()
    
    def dropchan(self, channel):
        assert channel.sid in self.channels
        self.channels.pop(channel.sid)

    # CA actions

    def caver(self, pkt, x, y):
        self.version=min(defs.CA_VERSION, pkt.count)
        self.prio=pkt.dtype
        log.debug('Version %s',self)

    def caclient(self, pkt, x, y):
        self.user=str(pkt.body).strip('\0')
        log.debug('Update %s',self)

    def cahost(self, pkt, x, y):
        self.host=str(pkt.body).strip('\0')
        # do reverse lookup
        host, aliases, z = socket.gethostbyaddr(self.peer.host)
        if self.host!=host and self.host not in aliases:
            log.warning("""Demoting connection from %s
            reverse lookup against %s failed""",self.peer.host,self.host)
            self.host='<ANONYMOUS>'
            return
        log.debug('Update %s',self)

    def createchan(self, pkt, x, y):
        # Older clients first report version here
        self.version=pkt.p2

        name=str(pkt.body).strip('\0')
        pv = self.server.GetPV(name)

        if pv is None:
            # PV does not exist
            log.debug("Can't create channel for non-existant PV %s",name)
            fail = CAmessage(cmd=26, p1=pkt.p1)
            self.send(fail.pack())
            return

        chan=Channel(self.next_sid, pkt.p1, self.server, self, pv)
        self.channels[chan.sid]=chan
        dtype, maxcount = pv.info(chan)

        ok = CAmessage(cmd=18, dtype=dtype, count=maxcount,
                       p1=pkt.p1, p2=chan.sid)

        rights = CAmessage(cmd=22, p1=pkt.p1, p2=chan.rights)

        self.send(ok.pack()+rights.pack())
        
        self.next_sid=self.next_sid+1
        while self.next_sid in self.channels:
            self.next_sid=self.next_sid+1

    def clearchan(self, pkt, x, y):
        chan=self.channels.get(pkt.p1)
        if not chan:
            log.warning('Attempt to clean non-existent channel')
            return
        
        chan.close()
        ok = CAmessage(cmd=12, p1=pkt.p1, p2=pkt.p2)
        self.send(ok.pack())

    def forwardchan(self, pkt, peer, circuit):
        chan=self.channels.get(pkt.p1)
        if not chan:
            log.warning('Attempt to access non-existent channel')
            return
        chan.dispatch(pkt, peer, circuit)

    def ping(self, pkt, x, y):
        self.send(pkt.pack())

    # socket operations

    def connectionMade(self):
        self.peer=self.transport.getPeer()
        self.tcpport=self.transport.getHost().port

        # before 3.14.12 servers didn't send version until client authenticated
        # from 3.14.12 clients attempting to do TCP name resolution don't authenticate
        # but expect a version message immediately
        pkt=CAmessage(cmd=0, dtype=self.prio, count=defs.CA_VERSION)
        self.send(pkt.pack())
        log.debug('connection from %s',self.peer)
        log.debug('Create %s',self)

        self.server.circuits.add(self)

        self.__D=DeferredManager()
        self.__C.callback(self)

    def connectionLost(self, reason):

        self.__C=DeferredManager()
        D, self.__D = self.__D, None
        D.callback(self)

        self.server.circuits.remove(self)

        log.debug('Destroy %s',self)

    def connectionFailed(self, reason):

        C, self.__C = self.__C, DeferredManager()
        C.callback(None)

    def send(self, msg):
        self.transport.write(msg)

    def sendto(self, msg, peer=None):
        self.transport.write(msg)
    
    def dataReceived(self, msg):

        msg=self.in_buffer+msg

        while msg is not None and len(msg)>=16:
        
            pkt, msg = CAmessage.unpack(msg)
            
            hdl = self._circ.get(pkt.cmd, self.server.dispatch)
        
            hdl(pkt, self, self.peer)

        self.in_buffer=msg # save remaining

    def __str__(self):
        return 'Circuit v4%(version)d to %(peer)s as %(host)s:%(user)s'% \
            self.__dict__
Beispiel #3
0
class CASet(object):
    """Write to a PV
    """
    def __init__(self,
                 channel,
                 data,
                 dbf=None,
                 meta=META.PLAIN,
                 dbf_conv=None,
                 wait=False):
        """Start a new write request
        
        channel: PV name
        data: value array
        dbf: Field type to write
        meta: Meta data class to send
        dbf_conv: Treat value array as different field type.
                  Does a client-side conversion
        wait: Request notification on completion
        """
        self._chan, self.dbf = channel, dbf
        self._data, self.dbf_conv = data, dbf_conv
        self._meta, self._wait = meta, wait

        self.done, self.ioid = True, None
        self.__D = None

        if dbf_conv is None and dbf is not None:
            self.meta = caMeta(dbf)
        elif dbf_conv is not None:
            self.meta = caMeta(dbf_conv)
        else:
            self.meta = None

        self.restart(data)

    @property
    def complete(self):
        """A Deferred called when the write has finished.
        
        This is either when the request is sent, or when
        confirmation is received depending on the type of
        write.
        """
        return self._comp.get()

    def close(self):
        """Cancel the request
        """
        if self._chan is None:
            return  # already shutdown

        if not self.done and self._comp:
            self._comp.callback(None)

        if self.__D is not None and hasattr(self.__D, 'cancel'):
            d, self.__D = self.__D, None
            d.addErrback(lambda e: e.trap(CancelledError))
            d.cancel()

        self._chan = None

    def restart(self, data):
        """Re-send with new value array
        """
        if not self.done:
            raise RuntimeError('Previous Set not complete')
        self._data = data

        self.done = False

        self._comp = DeferredManager()

        d = self.__D = self._chan.whenCon
        d.addCallback(self._chanOk)

    def _chanOk(self, chan):
        self.__D = None
        if chan is None:
            self.close()
            # channel has shutdown
            return
        assert self._chan is chan

        self.ioid = chan._circ.pendingActions.add(self)

        dbf = self.dbf
        if dbf is None:
            dbf, _ = dbr_to_dbf(chan.dbr)
        dbr = dbf_to_dbr(dbf, self._meta)

        meta = self.meta
        if meta is None:
            meta = caMeta(dbf)

        data = self._data
        cnt = len(data)
        if cnt > chan.maxcount:
            cnt = chan.maxcount
            data = data[:chan.maxcount]

        data, cnt = tostring(data, meta, dbr, cnt)

        log.debug('Set %s to %s', chan, data)

        cmd = 19 if self._wait else 4

        msg = CAmessage(cmd=cmd,
                        size=len(data),
                        dtype=dbr,
                        count=cnt,
                        p1=chan.sid,
                        p2=self.ioid,
                        body=data).pack()
        chan._circ.send(msg)

        if not self._wait:
            log.debug('Send put request (no wait) %s', self._chan.name)
            # do completion here
            self.ioid = None
            self.done = True
            self._comp.callback(ECA_NORMAL)

        else:
            log.debug('Send put request (wait) %s', self._chan.name)
            d = self.__D = self._chan.whenDis
            d.addCallback(self._circuitLost)

        return chan

    def _circuitLost(self, _):
        self.__D = None
        self.ioid = None
        if self.done:
            return

        d = self.__D = self._chan.whenCon
        d.addCallback(self._chanOk)

    def dispatch(self, pkt, circuit, peer=None):
        if pkt.cmd != 19:
            log.warning('Channel %s get ignoring pkt %s', self._chan.name, pkt)
            # wait for real reply
            chan._circ.pendingActions[self.ioid] = self
            return

        self.ioid = None
        self.done = True

        # mark done so callback can restart
        if pkt.p1 == ECA_NORMAL:
            self._comp.callback(ECA_NORMAL)
        else:
            self._comp.errback(pkt.p1)

    def __str__(self):
        cnt = 'Native' if self.count is None else self.count
        return 'Set %s of %s from %s' % (cnt, self.dbr, self._chan)
Beispiel #4
0
class CAClientChannel(object):
    """Persistent Client Channel
    
    Handles lookups and (re)connection.
    """
    implements(IDispatch, IConnectNotify)

    S_init='Disconnected'
    S_lookup='PV lookup'
    S_waitcirc='Waiting for circuit'
    S_attach='Creating channel'
    S_connect='Connected'

    reconnectDelay=0.1
    
    def __init__(self, name, context):
        """Create a new channel to the named PV.
        
        Note: This will _always_ start a new channel even
        if one already exists.
        """
        self.name, self._ctxt=name, context

        self._connected=False # True, False, or None (shutdown)

        self.status=CBManager() # connection status callback

        self.__eventCon=DeferredManager()
        self.__eventDis=DeferredManager()
        self.__eventDis.callback(self) # initially disconnected

        # Stores a deferred from the client during
        # name and circuit lookup phases
        self.__L=None
        # Store a deferred during the attach phase
        self._d=None

        self.__T=None # reconnect delay timer

        self._chan={18:self._channelOk,
                    22:self._rights,
                    26:self._channelFail,
                    27:self._disconn,
                   }

        self.__Done=self._ctxt.onClose
        self.__Done.addCallback(lambda _:self.close())

        self._reset()

    @property
    def whenCon(self):
        return self.__eventCon.get()

    @property
    def whenDis(self):
        return self.__eventDis.get()

    def close(self):
        """Close the channel.

        This will fail any pending actions.
        Once called channel can not be reused.
        """
        if self._connected is None:
            # shutdown already happened
            return
        log.debug('Channel %s close',self.name)

        self.__Done.cancel()
        self.__Done.addErrback(lambda e:e.trap(CancelledError))

        self._reset()

        if self.__T is not None:
            assert self.__T.active(), 'Timer already expired/cancelled'
            self.__T.cancel()
            self.__T=None

        if self._connected:
            self.__eventDis.callback(self)
        else:
            self.__eventCon.callback(None)
        self._connected=None
        
        # post condition
        # Both __eventDis and __eventCon are fired
        # further connection attempts fail immediately

    def _reset(self, _=None):
        """Reset to disconnected state
        
        Safe to call in all states
        """
        if self._connected is None:
            # shutdown already happened
            return

        log.debug('Channel %s reset',self.name)
        
        if self._connected is True:
            # _d is cleared before _connected is True
            assert self._d is None
            self.status(self, False)

        elif self._d:
            d, self._d = self._d, None
            d.callback(None)

        if self.__L is not None:
            self.__L.addErrback(lambda e:e.trap(CancelledError))
            self.__L.cancel()
            self.__L=None

        self.cid=self.sid=self.dbr=None
        self._circ=None
        self.maxcount=self.rights=0
        self.state=self.S_init

        if self._connected:
            self._connected=False
            self.__eventCon=DeferredManager()
            d, self._eventDis = self.__eventDis, None
            d.callback(self)

        if self.__T is None:
            self.__T=reactor.callLater(self.reconnectDelay,
                                       self._connect)

    @property
    def connected(self):
        """Test if the channel is currently connected
        """
        return self._connected

    @property
    def running(self):
        """Test if the channel has been shut down
        """
        return self._connected is not None

    def _connect(self):
        """Start channel connection sequence

        lookup -> open circuit -> attach channel -> connected

        only safe to call from _reset()
        """
        assert self._connected is False and self.state is self.S_init

        # prevent error when cancelling.
        # _connect is invoked when __T expires
        self.__T=None

        log.debug('Channel %s connecting...',self.name)

        self.state=self.S_lookup

        self.__L=self._ctxt.lookup(self.name)
        self.__L.pause()
        @self.__L.addCallback
        def doneLookup(srv):
            if srv is None:
                # lookups only fail when the client is shutting down
                return

            self.state=self.S_waitcirc

            log.debug('Found %s on %s',self.name,srv)

            return self._ctxt.openCircuit(srv)

        @self.__L.addCallback
        def haveCircuit(circ):
            self.__L=None
            if circ is None:
                # couldn't connect to server
                self._reset()
                return

            self.state=self.S_attach

            log.debug('Attaching %s to %s',self.name,circ)

            # the circuit is passed only if in the connected state
            # but it can drop at any time
            self.__L=d=circ.transport.connector.whenDis
            d.addCallback(self._circuitLost)

            self._d=Deferred()

            self._circ=circ
            circ.addchan(self)

            return self._d

        @self.__L.addCallback
        def attached(conn):
            assert self._d is None, '_d must be None before callback'

            if conn is None:
                # Server died while we were attaching
                return

            elif conn is False:
                # channel not present on server
                log.info('channel %s rejected by server', self.name)
                return

            log.debug('Channel %s Open',self.name)

            self.state=self.S_connect

            self.__eventDis=DeferredManager()
            self.__eventCon.callback(self)
            self._connected=True

            self.status(self, True)

        self.__L.unpause()
        return self.__L

    def _circuitLost(self,_):
        self.__L=None
        log.debug('Channel %s lost circuit',self.name)
        self._reset()

    def _disconn(self, pkt, peer, circuit):
        """Server has stopped providing the channel
        """
        log.warning('Server has disconnected %s',self.name)
        self._reset()

    def _channelOk(self, pkt, peer, circuit):
        self.dbr, self.maxcount=pkt.dtype, pkt.count
        self.sid=pkt.p2

        self._checkReady()

    def _channelFail(self, pkt, peer, circuit):
        log.info('Server %s rejects channel %s',peer,self.name)

        if self._d:
            d, self._d = self._d, None
            d.callback(False)
        self._reset()

    def _rights(self, pkt, peer, circuit):
        self.rights=pkt.p2
        
        self._checkReady()

    def _checkReady(self):
        """Sometimes the access rights message preceeds the
        ack of channel creation...
        """
        if self.sid is None or self.rights is None:
            return
        if self._d:
            d, self._d = self._d, None
            d.callback(True)

    def dispatch(self, pkt, circuit, peer=None):
        assert circuit is self._circ
        
        hdl=self._chan.get(pkt.cmd, self._ctxt.dispatch)
        
        if hdl:
            hdl(pkt, circuit, peer)
        else:
            log.debug('Channel %s received unknown %s',self.name,pkt)

    def __str__(self):
        return 'Channel %(name)s %(state)s %(cid)s:%(sid)s %(dbr)s %(maxcount)s'%self.__dict__
Beispiel #5
0
class CAGet(object):
    """A non-recuring request for data
    """
    implements(IDispatch)

    done = True
    ioid = None
    __D = None

    def __init__(self,
                 channel,
                 dbf=None,
                 count=None,
                 meta=META.PLAIN,
                 dbf_conv=None):
        """Start a new request.
        
        channel: PV name
        dbf: Field type requested from server
        count: length requested from server
        meta: Meta-data class requested
        dbf_conv: Additional client side conversion
                  before data is returned to user.

        Note: Server will never return more then count
              elements, but may return less.
        """
        self._chan, self.dbf = channel, dbf
        self._meta, self.count = meta, count
        self.dbf_conv = dbf_conv

        if dbf_conv is None and dbf is not None:
            self.meta = caMeta(dbf)
        elif dbf_conv is not None:
            self.meta = caMeta(dbf_conv)
        else:
            self.meta = None

        self.restart()

    def close(self):
        """Cancel request
        """
        if self._chan is None:
            return  # already shutdown

        if not self.done:
            self._result.callback(None)
        self.done = True

        if self.ioid is not None and self._chan._circ is not None:
            self._chan._circ.pendingActions.remove(self.ioid)

        if self.__D is not None and hasattr(self.__D, 'cancel'):
            self.__D.addErrback(lambda e: e.trap(CancelledError))
            self.__D.cancel()
            self.__D = None

        self._chan = None

    def restart(self):
        """Restart request
        
        Send a new request to the server.
        
        Note: A no-op if a request is currently in progress.
        """
        if not self.done:
            return

        self.done = False

        self._result = DeferredManager()

        d = self.__D = self._chan.whenCon
        d.addCallback(self._chanOk)

    @property
    def data(self):
        """A Deferred which will be called with the result
        
        Will be called with a tuple (value array, caMeta)
        """
        return self._result.get()

    def _chanOk(self, chan):
        self.__D = None
        if chan is None:
            # channel has shutdown
            self.close()
            return

        assert self._chan is chan

        ver = chan._circ.version

        self.ioid = chan._circ.pendingActions.add(self)

        dbf = self.dbf
        if dbf is None:
            dbf, _ = dbr_to_dbf(chan.dbr)
        dbr = dbf_to_dbr(dbf, self._meta)

        # use dynamic array length whenever possible
        cnt = self.count if ver < 13 else 0
        if cnt is None or cnt > chan.maxcount:
            cnt = chan.maxcount

        msg = CAmessage(cmd=15,
                        dtype=dbr,
                        count=cnt,
                        p1=chan.sid,
                        p2=self.ioid).pack()
        chan._circ.send(msg)

        d = self.__D = self._chan.whenDis
        d.addCallback(self._circuitLost)

        return chan

    def _circuitLost(self, _):
        self.__D = None
        self.ioid = None
        if self.done:
            return

        d = self._chan.eventCon
        d.addCallback(self._chanOk)

    def dispatch(self, pkt, circuit, peer=None):
        if pkt.cmd != 15:
            log.warning('Channel %s get ignoring pkt %s', self._chan.name, pkt)
            # wait for real reply
            chan._circ.pendingActions[self.ioid] = self
            return

        meta = self.meta
        if meta is None:
            dbf, _ = dbr_to_dbf(pkt.dtype)
            meta = caMeta(dbf)

        data = fromstring(pkt.body, pkt.dtype, pkt.count, meta)

        self.ioid = None
        self.done = True
        self._result.callback(data)

    def __str__(self):
        cnt = 'Native' if self.count is None else self.count
        return 'Get %s of %s from %s' % (cnt, self.dbr, self._chan)