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()
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__
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)
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__
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)