diff --git a/fandango/CHANGES b/fandango/CHANGES index 8ecd88fb5..a5cdca287 100644 --- a/fandango/CHANGES +++ b/fandango/CHANGES @@ -1,4 +1,4 @@ -RELEASE_NOTE = "Fandango 13.2.*; Cached tango database methods" +RELEASE_NOTE = "Fandango 13.*; Cached tango database methods" """ IMPORTANT: @@ -11,6 +11,25 @@ BUGS PENDING for 13.* (to not be released in master until bugs will be solved) threads.wait: exceptions on ipython exit threads/WorkerDS: uncatched deadlocks +13.9.0 - August 2018 +-------------------- + +DynamicDS: + solves critical bug in get_default_properties + Enable events on raw attribute change + Solve bugs on array events and qualities + + tango_monitor refactored + fixed bugs on EventSource init/del + allow dots on device names (req by Philal) + better event check + set archive event as optional on DynamicDS + export devices: vector/list bug solved by kmadisa + export more attribute properties (by kmadisa) + critical bug solved on XATTR (long uris) + + + 13.7.0 - Abril 2018 ------------------- Add get_attr_name, get_dev_name methods diff --git a/fandango/VERSION b/fandango/VERSION index 447269552..c662e83bd 100644 --- a/fandango/VERSION +++ b/fandango/VERSION @@ -1 +1 @@ -13.8.0 +13.9.0 diff --git a/fandango/callbacks.py b/fandango/callbacks.py index 86a4eac1e..056e42820 100644 --- a/fandango/callbacks.py +++ b/fandango/callbacks.py @@ -610,6 +610,14 @@ def __init__(self, name, keeptime=1000., fake=False, parent=None, **kw): Arguments: loglevel, tango_asynch, pollingPeriod, keeptime, enablePolling, use_events """ + ##Set logging + #self.call__init__(Logger,self.full_name) ##This fails, for unknown reasons + Logger.__init__(self,name) #self.full_name) #This must be done early + + self.setLogLevel(kw.get('loglevel',kw.get('log_level', + kw.get('logLevel',self.DEFAULT_LOG)))) + self.info('Init(%s)'%str(kw)) + #@TODO: All this mangling to be replaced by fandango.parse_tango_model if 0 < name.replace('//','/').count('/') < name.count(':')+3: name += '/state' @@ -637,14 +645,7 @@ def __init__(self, name, keeptime=1000., fake=False, parent=None, **kw): self.normal_name = '/'.join(self.device.split('/')[-3:] +[self.simple_name]) assert self.fake or self.proxy,\ - '%s,%s: A valid device name is needed'%(name,parent) - - ##Set logging - #self.call__init__(Logger,self.full_name) ##This fails, for unknown reasons - Logger.__init__(self,self.full_name) - self.setLogLevel(kw.get('loglevel',kw.get('log_level', - kw.get('logLevel',self.DEFAULT_LOG)))) - self.info('Init(%s)'%str(kw)) + '%s: A valid and existing device is needed'%(name) self.listeners = defaultdict(set) #[] self.event_ids = dict() # An {EventType:ID} dict @@ -706,6 +707,8 @@ def getNormalName(self): return self.normal_name def cleanUp(self): self.debug("cleanUp") + if not hasattr(self,'listeners'): + return while self.listeners: self.removeListener(self.listeners.popitem(),exclude='') if self.isPollingEnabled(): diff --git a/fandango/db.py b/fandango/db.py index cf75d3d83..a62fe98bf 100644 --- a/fandango/db.py +++ b/fandango/db.py @@ -40,8 +40,26 @@ Go to http://mysql-python.sourceforge.net/MySQLdb.html for further information """ -import time,datetime,log,traceback,sys +import os,time,datetime,log,traceback,sys + import MySQLdb +""" +Instead of using the outdated MySQL-python package or Oracle's mysql.connector, +try to install mysqlclient instead: + +https://pypi.org/project/mysqlclient/#description +https://github.com/PyMySQL/mysqlclient-python +https://mysqlclient.readthedocs.io + +To install it on Debian: + + sudo aptitude remove python-mysqldb + sudo aptitude install python-pip + sudo aptitude install libmariadbclient-dev + sudo pip install mysqlclient + +""" + class FriendlyDB(log.Logger): """ diff --git a/fandango/dicts.py b/fandango/dicts.py index afc889f91..1dddf460e 100644 --- a/fandango/dicts.py +++ b/fandango/dicts.py @@ -207,8 +207,12 @@ def start(self): #@self_locked def stop(self): print 'Stopping ThreadDict ...' - if self.threaded and hasattr(self,'event'): self.event.set() - if hasattr(self,'_Thread'): self._Thread.join() + if self.threaded and hasattr(self,'event'): + print('event set') + self.event.set() + if hasattr(self,'_Thread'): + print('thread join') + self._Thread.join() print 'ThreadDict Stopped' def alive(self): diff --git a/fandango/dynamic.py b/fandango/dynamic.py index 1b859005d..b25212046 100644 --- a/fandango/dynamic.py +++ b/fandango/dynamic.py @@ -70,7 +70,7 @@ class PyPLCClass(DynamicDSClass): This calls must be inserted in the __main__ method of your Class.py file and also at the end of the file. 5.AND THE __init__ METHOD: - def __init__(self,cl, name): + df __init__(self,cl, name): #PyTango.Device_3Impl.__init__(self,cl,name) DynamicDS.__init__(self,cl,name,_locals={},useDynStates=True) @@ -132,6 +132,7 @@ def __init__(self,cl, name): if 'DeviceClass' not in dir(PyTango): PyTango.DeviceClass = PyTango.PyDeviceClass MAX_ARRAY_SIZE = 8192 +EVENT_TYPES = '(true|yes|push|archive|[0-9]+)$' class DynamicDS(PyTango.Device_4Impl,Logger): ''' Check fandango.dynamic.__doc__ for more information ...''' @@ -162,6 +163,7 @@ def __init__(self,cl=None,name=None,_globals=None,_locals=None, useDynStates=Tru self.dyn_types = {} self.dyn_states = SortedDict() self.dyn_values = {} #<- That's the main cache used for attribute management + #Should it be caseless? self.dyn_qualities = {} #<- It keeps the dynamic qualities variables self.variables = {} self.state_lock = threading.Lock() @@ -294,11 +296,17 @@ def init_device(self): self._locals.update({k: (lambda argin,cmd=k:self.evalCommand(cmd,argin))}) for i in self.InitDevice: - try: - self.evalAttr(i) - except Exception,e: - print('Unable to execute InitDevice(%s)'%i) - raise e + if str(i).strip().lower() in ('','false'): + continue + elif i not in self.dyn_comms: + self.warning('Unknown %s cmd at init, ignored' % str(i)) + else: + try: + self.evalAttr(i) + except Exception,e: + self.error('Unable to execute InitDevice(%s)' % str(i)) + traceback.print_exc() + raise e except: print(traceback.format_exc()) #self.warning(traceback.format_exc()) self._init_count +=1 @@ -407,7 +415,7 @@ def get_default_properties(self,update=False): dct[prop] = value if update: - for k,v in dct.items(): + for prop,value in dct.items(): setattr(self,prop,value) return dct @@ -655,22 +663,23 @@ def attribute_polling_report(self): @Cached(depth=200,expire=15.) def check_attribute_events(self,aname,poll=False): - self.UseEvents = [u.strip() for u in self.UseEvents] - self.debug('check_attribute_events(%s,%s,%s)'%(aname,poll,self.UseEvents)) - if not len(self.UseEvents): - return False - elif self.UseEvents[0].lower().strip() in ('','no','false'): - return False - elif aname.lower().strip() == 'state': - return True - elif clmatch('(yes|true)$',self.UseEvents[0]) and any(self.check_events_config(aname)): - return True - else: - for s in self.UseEvents: - s,e = s.split(':',1) if ':' in s else (s,'True') - if clmatch(s,aname)and eval(clsub('(true|push|yes)','True',e)): - return e - return False + + self.UseEvents = filter(bool, + (u.split('#')[0].strip().lower() for u in self.UseEvents)) + self.debug('check_attribute_events(%s,%s,%s)' + %(aname,poll,self.UseEvents)) + + if not len(self.UseEvents) or self.UseEvents[0] in ('no','false'): + return '' + elif clmatch('state$',aname) or clmatch(EVENT_TYPES,self.UseEvents[0]): + return 'true' + + for s in self.UseEvents: + s,e = s.split(':',1) if ':' in s else (s,'True') + if clmatch(s,aname): + return e if clmatch(EVENT_TYPES,e.strip()) else '' + + return '' @Cached(depth=200,expire=15.) def check_events_config(self,aname): @@ -685,35 +694,61 @@ def check_events_config(self,aname): return cabs,crel def check_changed_event(self,aname,new_value): + aname = self.get_attr_name(aname) or aname if aname not in self.dyn_values: return False try: v = self.dyn_values[aname].value new_value = getattr(new_value,'value',new_value) cabs,crel = self.check_events_config(aname) - self.debug('In check_changed_event(%s): %s!=%s > (%s,%s)?'%(aname,shortstr(v),shortstr(new_value),cabs,crel)) + self.debug('In check_changed_event(%s): %s!=%s > (%s,%s)?' + %(aname,shortstr(v),shortstr(new_value),cabs,crel)) if v is None: - self.info('In check_changed_event(%s,%s): first value read!'%(aname,shortstr(new_value))) + self.info('In check_changed_event(%s,%s): first value read!' + %(aname,shortstr(new_value))) return True + elif fun.isSequence(new_value) or fun.isSequence(v): - if cabs>0 or crel>0: - self.info('In check_changed_event(%s,%s): list changed!'%(aname,shortstr(new_value))) - return bool(v!=new_value) + v,new_value = fun.notNone(v,[]),fun.notNone(new_value,[]) + changed = len(v)!=len(new_value) \ + or any(v!=vv for v,vv in zip(v,new_value)) + self.info('In check_changed_event(%s,%s): changed = %s' + %(aname,shortstr(new_value),changed)) + return changed else: - try: v,new_value = (float(v) if v is not None else None),float(new_value) + try: + v,new_value = (float(v) if v is not None else None),\ + float(new_value) except Exception,e: - self.info('In check_changed_event(%s): values not evaluable (%s,%s): %s'%(aname,shortstr(v),shortstr(new_value),e)) - return bool((cabs>0 or crel>0) and v!=new_value) #False + self.info('In check_changed_event(%s): ' + 'values not evaluable (%s,%s): %s' + %(aname,shortstr(v),shortstr(new_value),e)) + try: + return v!=new_value #and (cabs>0 or crel>0) + except: + return str(v)!=str(new_value) + if cabs>0 and not v-cabs0 and not v*(1-crel/100.) RF_VAL=ATTR_ALARM if ATTR('RF_ALRM') ... + self.info('There is a Quality for this attribute!:' + +str((aname,exp,shortstr(value)))) + # It will replace $ in the formula for all strings + # matched by .* in the expression + for group in (match.groups()): + # e.g: (.*)_VAL=ATTR_ALARM if ATTR('$_ALRM') ... + # => RF_VAL=ATTR_ALARM if ATTR('RF_ALRM') ... + value=value.replace('$',group,1) + self.dyn_qualities[aname.lower()] = value except Exception,e: - self.warning('In dyn_attr(qualities), re.match(%s(%s),%s(%s)) failed' % (type(exp),exp,type(aname),aname)) + self.warning('In dyn_attr(qualities), re.match(%s(%s),' + '%s(%s)) failed' % (type(exp),exp,type(aname),aname)) print(traceback.format_exc()) if self.CheckDependencies: - [self.check_dependencies(aname) for aname in self.dyn_values.keys()] + [self.check_dependencies(aname) for aname in self.dyn_values] ##Setting up state events: #THESE SETTINGS ARE STILL NEEDED IN TANGO >= 7 - c = self.check_attribute_events('State') - if c and 'state' not in new_polled_attrs: - #To be added at next restart - self.warning('State events are always managed by polling (%s)'%c) - new_polled_attrs['state'] = int(c) if fun.isNumber(c) else self.DEFAULT_POLLING_PERIOD - + for x in ('state','memusage',): + events = self.check_attribute_events(x) + if events and x not in new_polled_attrs: + #To be added at next restart + self.warning('%s events are managed by polling (%s)'%(x,c)) + new_polled_attrs[x] = int(events) if fun.isNumber(events) \ + else self.DEFAULT_POLLING_PERIOD + if x!='state': + self.set_change_event(x,True,False) + if 'archive' in events: + self.set_archive_event(x,True,False) try: self.check_polled_attributes(new_attr=new_polled_attrs) except: - print('DynamicDS.dyn_attr( ... ), unable to set polling for (%s): \n%s'%(new_polled_attrs,traceback.format_exc())) + print('DynamicDS.dyn_attr( ... ), unable to set polling for (%s):' + ' \n%s'%(new_polled_attrs,traceback.format_exc())) - print('DynamicDS.dyn_attr( ... ), finished. Attributes ready to accept request ...') + print('DynamicDS.dyn_attr( ... ), finished. ' + 'Attributes ready to accept request ...') #dyn_attr MUST be an static method, to avoid attribute mismatching (self will be always passed as argument) dyn_attr=staticmethod(dyn_attr) @@ -971,6 +1024,17 @@ def is_allowed(req_type, attr_name=aname,s=self): def get_dyn_attr_list(self): """Gets all dynamic attribute names.""" return self.dyn_attrs.keys() + + def get_attr_name(self,aname): + for k in self.dyn_values: + if k.lower() == str(aname).strip().lower().split('/')[-1]: + return k + return None + + def get_attr_date(self,aname,value): + if type(value) is DynamicAttribute: + return value.date + return time.time() def get_attr_formula(self,aname,full=False): """ @@ -997,6 +1061,35 @@ def get_attr_formula(self,aname,full=False): else: #If no attribute is matching, attribute name is returned return formula + + def get_attr_models(self,attribute): + """ + Given a dynamic attribute name or formula, it will return a + list of tango models appearing on it + """ + formula = self.get_attr_formula(attribute) + matches = re.findall(tango.retango,formula) + #Matches are models split in parts, need to be joined + return ['/'.join(filter(bool,s)) for s in matches] + + def get_attr_quality(self,aname,attr_value): + formula = self.dyn_qualities.get(aname.lower()) or 'Not specified' + self.debug('In get_attr_quality(%s,%s): %s' + % (aname,shortstr(attr_value,15)[:10],formula)) + try: + if aname.lower() in self.dyn_qualities: + self._locals['ATTRIBUTE'] = aname.lower() + self._locals['FORMULAS'] = dict((k,v.formula) for k,v in self.dyn_values.items()) + self._locals['VALUE'] = getattr(attr_value,'value',attr_value) + self._locals['DEFAULT'] = getattr(attr_value,'quality',AttrQuality.ATTR_VALID) + quality = eval(formula,{},self._locals) or AttrQuality.ATTR_VALID + else: + quality = getattr(attr_value,'quality',AttrQuality.ATTR_VALID) + self.debug('\t%s.quality = %s'%(aname,quality)) + return quality + except Exception,e: + self.error('Unable to generate quality for attribute %s: %s\n%s'%(aname,formula,traceback.format_exc())) + return AttrQuality.ATTR_VALID def is_dyn_allowed(self,req_type,attr_name=''): return (time.time()-self.time0) > 1e-3*self.StartupDelay @@ -1004,7 +1097,14 @@ def is_dyn_allowed(self,req_type,attr_name=''): #@Catched #Catched decorator is not compatible with PyTango_Throw_Exception @self_locked def read_dyn_attr(self,attr,fire_event=True): + """ + Method to evaluate attributes from external clients. + + Internally, evalAttr its used instead, triggering push_event if needed + """ #if not USE_STATIC_METHODS: self = self.myClass.DynDev + if isString(attr): + attr = tango.fakeAttributeValue(attr) aname = attr.get_name() tstart=time.time() events = self.check_attribute_events(aname) @@ -1020,8 +1120,8 @@ def read_dyn_attr(self,attr,fire_event=True): self.warning('Unable to reload %s kept values, %s'%(aname,str(e))) try: result = self.evalAttr(aname) - quality = getattr(result,'quality',self.get_quality_for_attribute(aname,result)) - date = self.get_date_for_attribute(aname,result) + quality = getattr(result,'quality',self.get_attr_quality(aname,result)) + date = self.get_attr_date(aname,result) result = self.dyn_types[aname].pytype(result) if hasattr(attr,'set_value_date_quality'): @@ -1095,30 +1195,24 @@ def write_dyn_attr(self,attr,fire_event=True): # Attributes and State Evaluation Methods #------------------------------------------------------------------------------------------------------ - # DYNAMIC ATTRIBUTE EVALUATION ... Copy it to your device and add any method you will need + ## DYNAMIC ATTRIBUTE EVALUATION ... + # Copy it to your device and add any method you will need def evalAttr(self,aname,WRITE=False,VALUE=None,_locals=None): ''' SPECIFIC METHODS DEFINITION DONE IN self._locals!!! @remark Generators don't work inside eval!, use lists instead ''' - self.debug("DynamicDS("+self.get_name()+ ")::evalAttr("+aname+"): ... last value was %s"%shortstr(getattr(self.dyn_values.get(aname,None),'value',None))) + aname = self.get_attr_name(aname) or aname + self.info("DynamicDS(%s)::evalAttr(%s): ... last value was %s" + % (self.get_name(), aname, shortstr( + getattr(self.dyn_values.get(aname,None),'value',None)))) tstart = time.time() aname,formula,compiled = self.get_attr_formula(aname,full=True) try: ##Checking attribute dependencies + # dependencies assigned at dyn_attr by self.check_dependencies() if self.CheckDependencies and aname in self.dyn_values: - #if self.dyn_values[aname].dependencies is None: - #self.debug("In evalAttr ... setting dependencies") - #self.dyn_values[aname].dependencies = set() - #a = aname.lower().strip() - #fs = (formula+'\n'+self.dyn_qualities.get(a,'')).lower() - #for k,v in self.dyn_values.items(): - #ks = k.lower().strip() - #if a==ks: continue - #if ks in fun.re.split("[^'\"_0-9a-zA-Z]",fs): #fun.searchCl("(^|[^'\"_0-9a-z])%s($|[^'\"_0-9a-z])"%k,formula): - #self.dyn_values[aname].dependencies.add(k) #Dependencies are case sensitive - #self.dyn_values[k].keep = True #Updating Last Attribute Values if self.dyn_values[aname].dependencies: @@ -1132,6 +1226,7 @@ def evalAttr(self,aname,WRITE=False,VALUE=None,_locals=None): v = self.dyn_values[k] if k.lower().strip()!=aname.lower().strip() and isinstance(v.value,Exception): self.warning('evalAttr(%s): An exception is rethrowed from attribute %s'%(aname,k)) + print(k,aname,v.value) raise RethrownException(v.value) #Exceptions are passed to dependent attributes else: self._locals[k]=v.value #.value else: @@ -1151,43 +1246,56 @@ def evalAttr(self,aname,WRITE=False,VALUE=None,_locals=None): 'VALUE':VALUE if VALUE is None or aname not in self.dyn_types else self.dyn_types[aname].pytype(VALUE), 'STATE':self.get_state(), 'LOCALS':self._locals, - #'ATTRIBUTES':dict((a,getattr(self.dyn_values[a],'value',None)) for a in self.dyn_values if a in self._locals), 'ATTRIBUTES':sorted(self.dyn_values.keys()), + 'FORMULAS':dict((k,v.formula) for k,v in self.dyn_values.items()), 'XATTRS':self._external_attributes, }) #It is important to keep this values persistent; becoming available for quality/date/state/status management - if _locals is not None: self._locals.update(_locals) #High Priority: variables passed as argument + + if _locals is not None: + #High Priority: variables passed as argument + self._locals.update(_locals) + except Exception,e: self.error('<'*80) self.error(traceback.format_exc()) - for t in (VALUE,type(VALUE),aname,self.dyn_types.get(aname,None),aname in self.dyn_types and self.dyn_types[aname].pytype): + for t in ( + VALUE,type(VALUE),aname,self.dyn_types.get(aname,None),aname + in self.dyn_types and self.dyn_types[aname].pytype): self.warning(str(t)) self.error('<'*80) raise e - if WRITE: self.debug('%s::evalAttr(WRITE): Attribute=%s; formula=%s; VALUE=%s'%(self.get_name(),aname,formula,shortstr(VALUE))) - elif aname in self.dyn_values: self.debug('%s::evalAttr(READ): Attribute=%s; formula=%s;'%(self.get_name(),aname,formula,)) - else: self.info('%s::evalAttr(COMMAND): formula=%s;'%(self.get_name(),formula,)) + + if WRITE: + self.debug('%s::evalAttr(WRITE): Attribute=%s; formula=%s; VALUE=%s'%(self.get_name(),aname,formula,shortstr(VALUE))) + elif aname in self.dyn_values: + self.debug('%s::evalAttr(READ): Attribute=%s; formula=%s;'%(self.get_name(),aname,formula,)) + else: + self.info('%s::evalAttr(COMMAND): formula=%s;'%(self.get_name(),formula,)) result = eval(compiled or formula,self._globals,self._locals) #<<<<< EVAL! self.debug('eval result: '+str(result)) #Push/Keep Read Attributes if not WRITE and aname in self.dyn_values: - quality = self.get_quality_for_attribute(aname,result) + quality = self.get_attr_quality(aname,result) if hasattr(result,'quality'): result.quality = quality - date = self.get_date_for_attribute(aname,result) - has_events = self.check_attribute_events(aname) + date = self.get_attr_date(aname,result) + events = self.check_attribute_events(aname) value = self.dyn_types[aname].pytype(result) - + check = self.check_changed_event(aname,result) + + self.info('events = %s, check = %s' % (events,check)) #Events must be checked before updating the cache - if has_events and ('push' in str(has_events.lower()) or - self.check_changed_event(aname,result)): + if events and (check or 'push' in str(events).lower()): self.info('>'*80) - self.info('Pushing %s event!: %s(%s)'%(aname,type(value),shortstr(value))) + self.info('Pushing %s event!: %s(%s)' + %(aname,type(value),shortstr(value))) self.push_change_event(aname.lower(),value,date,quality) - self.push_archive_event(aname.lower(),value,date,quality) + if 'archive' in str(self.UseEvents).lower(): + self.push_archive_event(aname.lower(),value,date,quality) #Updating the cache: - keep = self.dyn_values[aname].keep or has_events + keep = self.dyn_values[aname].keep or events if keep: old = self.dyn_values[aname].value self.dyn_values[aname].update(value,date,quality) @@ -1200,6 +1308,7 @@ def evalAttr(self,aname,WRITE=False,VALUE=None,_locals=None): except: self.warning('Unable to check state!') self.warning(traceback.format_exc()) + return result except PyTango.DevFailed, e: @@ -1211,6 +1320,7 @@ def evalAttr(self,aname,WRITE=False,VALUE=None,_locals=None): err = e.args[0] self.error(e) raise e #Exception,';'.join([err.origin,err.reason,err.desc]) + except Exception,e: if self.last_attr_exception and self.last_attr_exception[0]>tstart: e = self.last_attr_exception[-1] @@ -1243,7 +1353,8 @@ def evalState(self,formula,_locals={}): {'STATE':self.get_state(),'t':time.time()-self.time0,'NAME': self.get_name(), 'ATTRIBUTES':sorted(self.dyn_values.keys()),#'ATTRIBUTES':dict((a,getattr(self.dyn_values[a],'value',None)) for a in self.dyn_values if a in self._locals), 'FORMULAS':dict((k,v.formula) for k,v in self.dyn_values.items()), - 'WRITE':False,'VALUE':None, + 'WRITE':False, + 'VALUE':None, }) return eval(formula,self._globals,__locals) @@ -1328,16 +1439,6 @@ def log(prio,s,obj=self): #,level=self.log_obj.level): except: print(traceback.format_exc()) return - - def get_attr_models(self,attribute): - """ - Given a dynamic attribute name or formula, it will return a - list of tango models appearing on it - """ - formula = self.get_attr_formula(attribute) - matches = re.findall(tango.retango,formula) - #Matches are models split in parts, need to be joined - return ['/'.join(filter(bool,s)) for s in matches] def getXDevice(self,dname): """ @@ -1361,8 +1462,12 @@ def getXAttr(self,aname,default=None,write=False,wvalue=None): else: device,aname = aname.rsplit('/',1) if '/' in aname else '',aname - (self.info if write else self.debug)("DynamicDS.getXAttr(%s,%s,write=%s): ..."%(device or self.get_name(),aname,write and '%s(%s)'%(type(wvalue),wvalue))) - result = default #Returning an empty list because it is a False iterable value that can be converted to boolean (and False or None cannot be converted to iterable) + (self.info if write else self.debug)("DynamicDS.getXAttr(%s,%s,write=%s): ..." + %(device or self.get_name(),aname,write and '%s(%s)'%(type(wvalue),wvalue))) + + # Returning an empty list because it is a False iterable value that can be converted + # to boolean (and False or None cannot be converted to iterable) + result = default try: if not device: self.info('getXAttr accessing to device itself ... using getAttr instead') @@ -1484,27 +1589,6 @@ def getXCommand(self,cmd,args=None,feedback=None,expected=None): if fun.isString(feedback) and '/' not in feedback: feedback = device+'/'+feedback return tango.TangoCommand(command=cmd,device=device,feedback=feedback,timeout=10.,wait=10.).execute(args,expected=expected) - def get_quality_for_attribute(self,aname,attr_value): - self.debug('In get_quality_for_attribute(%s,%s)' % (aname,shortstr(attr_value,15)[:10])) - formula = self.dyn_qualities.get(aname.lower()) or 'Not specified' - try: - if aname.lower() in self.dyn_qualities: - self._locals['VALUE'] = getattr(attr_value,'value',attr_value) - self._locals['DEFAULT'] = getattr(attr_value,'quality',AttrQuality.ATTR_VALID) - quality = eval(formula,{},self._locals) or AttrQuality.ATTR_VALID - else: - quality = getattr(attr_value,'quality',AttrQuality.ATTR_VALID) - self.debug('\t%s.quality = %s'%(aname,quality)) - return quality - except Exception,e: - self.error('Unable to generate quality for attribute %s: %s\n%s'%(aname,formula,traceback.format_exc())) - return AttrQuality.ATTR_VALID - - def get_date_for_attribute(self,aname,value): - if type(value) is DynamicAttribute: - return value.date - return time.time() - def ForceAttr(self,argin,VALUE=None): ''' Description: The arguments are AttributeName and an optional Value.
This command will force the value of the Attribute or will return the last forced value @@ -1578,17 +1662,18 @@ def rawState(self): def set_state(self,state,push=False): now = time.time() old,self._locals['STATE'] = self._locals.get('STATE',None),state - if self.last_state_change < now-self.DEFAULT_POLLING_PERIOD/1e3: - old = None + last = now - self.last_state_change + diff = (state,(last > self.DEFAULT_POLLING_PERIOD/1e3 and last)) try: - if old!=state: + if diff != (old,False): self.last_state_change = now if push and self.check_attribute_events('state'): - self.info('DynamicDS(%s.set_state(): pushing new state event'%(self.get_name())) + self.info('DynamicDS(%s.set_state(): pushing new state event: %s'%(self.get_name(),diff)) try: self.push_change_event('State',state,now, AttrQuality.ATTR_VALID) - self.push_archive_event('State',state,now, + if 'archive' in str(self.UseEvents).lower(): + self.push_archive_event('State',state,now, AttrQuality.ATTR_VALID) except Exception,e: self.warning('DynamicDS.push_event(State=%s) failed!: %s'%(state,e)) @@ -1607,6 +1692,7 @@ def check_state(self,set_state=True,current=None): if self.state_lock.locked(): self.debug('In DynamicDS.check_state(): lock already acquired') return new_state + self.state_lock.acquire() if self.dyn_states: self.debug('In DynamicDS.check_state()') @@ -1629,7 +1715,8 @@ def check_state(self,set_state=True,current=None): new_state = self.TangoStates[nstate] if new_state!= old_state: self.info('DynamicDS(%s.check_state(): New State is %s := %s'%(self.get_name(),nstate,formula)) - if set_state:self.set_state(new_state,push=True) + if set_state: + self.set_state(new_state,push=True) break except Exception,e: print(traceback.format_exc()) @@ -2129,7 +2216,8 @@ class DynamicAttribute(object): qualityOrder = [AttrQuality.ATTR_VALID, AttrQuality.ATTR_CHANGING, AttrQuality.ATTR_WARNING, AttrQuality.ATTR_ALARM, AttrQuality.ATTR_INVALID] - def __init__(self,value=None,date=0.,quality=AttrQuality.ATTR_VALID): + def __init__(self,value=None,date=0.,quality=AttrQuality.ATTR_VALID,name=''): + self.name = name self.value=value self.max_peak=(value if not hasattr(value,'__len__') else None,0) self.min_peak=(value if not hasattr(value,'__len__') else None,0) diff --git a/fandango/functional.py b/fandango/functional.py index 2387d026b..c67067e02 100644 --- a/fandango/functional.py +++ b/fandango/functional.py @@ -317,7 +317,7 @@ def matchCl(exp,seq,terminate=False,extend=False): for e in exp.split('&')) if re.match('^[!~]',exp): return not matchCl(exp[1:],seq,terminate,extend=True) - return re.match(toRegexp(exp.lower(),terminate=terminate),seq.lower()) + return re.match(toRegexp(exp,terminate=terminate,lower=True),seq.lower()) except: print('matchCl(%s,%s,%s,%s) failed'%(exp,seq,terminate,extend)) raise @@ -406,9 +406,9 @@ def toCl(exp,terminate=False,wildcards=('*',' '),lower=True): exp = exp.replace('(?p<','(?P<') #Preventing missing P clausses return exp -def toRegexp(exp,terminate=False): +def toRegexp(exp,terminate=False,lower=False): """ Case sensitive version of the previous one, for backwards compatibility """ - return toCl(exp,terminate,wildcards=('*',),lower=False) + return toCl(exp,terminate,wildcards=('*',),lower=lower) def filtersmart(seq,filters): """ diff --git a/fandango/qt.py b/fandango/qt.py index 135b3d197..dd66ae880 100644 --- a/fandango/qt.py +++ b/fandango/qt.py @@ -38,7 +38,7 @@ from functools import partial import fandango from fandango.functional import * -from fandango.log import Logger,shortstr +from fandango.log import Logger,shortstr,tracer from fandango.dicts import SortedDict from fandango.objects import Singleton,Decorated,Decorator,ClassDecorator,BoundDecorator @@ -1038,12 +1038,14 @@ def Draggable(QtKlass): class DraggableQtKlass(QtKlass): #,Decorated): __doc__ = Draggable.__doc__ + _qt_klass__ = QtKlass def setDragEventCallback(self,hook,Mimetype=None): self._drageventcallback = hook self.Mimetype = Mimetype def getDragEventCallback(self): + tracer('In Draggable(%s).getDragEventCallback()'%self._qt_klass__) if not getattr(self,'_drageventcallback',None): self.setDragEventCallback(lambda s=self:str(s.text() if hasattr(s,'text') else '')) return self._drageventcallback @@ -1052,26 +1054,45 @@ def mousePressEvent(self, event): '''reimplemented to provide drag events''' #QtKlass.mousePressEvent(self, event) QtKlass.mousePressEvent(self,event) - print 'In Draggable(%s).mousePressEvent'%type(self) - if event.button() == Qt.Qt.LeftButton: self.dragStartPosition = event.pos() + name = self.objectName() + tracer('In Draggable(%s,%s).mousePressEvent'%(name,self._qt_klass__.__name__)) + + if event.button() == Qt.Qt.LeftButton: + self.dragStartPosition = event.pos() + elif event.buttons() & Qt.Qt.RightButton: + tracer('\t right button pressed') + + tracer('Out of Draggable(%s).mousePressEvent'%self._qt_klass__) + + def mouseReleaseEvent(self, event): + '''reimplemented to provide drag events''' + tracer('In Draggable(%s).mouseReleaseEvent'%self._qt_klass__) + QtKlass.mouseReleaseEvent(self,event) def mouseMoveEvent(self, event): '''reimplemented to provide drag events''' + #tracer('In Draggable(%s).mouseMoveEvent'%self._qt_klass__) QtKlass.mouseMoveEvent(self,event) if not event.buttons() & Qt.Qt.LeftButton: + #tracer('Out of Draggable(%s).mouseMoveEvent'%self._qt_klass__) return + call_back = self.getDragEventCallback() mimeData = call_back() + if not isinstance(mimeData,Qt.QMimeData): txt = str(mimeData) mimeData = Qt.QMimeData() if getattr(self,'Mimetype',None): mimeData.setData(self.Mimetype, txt) mimeData.setText(txt) #Order is not trivial, preferred must go first + drag = Qt.QDrag(self) drag.setMimeData(mimeData) drag.setHotSpot(event.pos() - self.rect().topLeft()) dropAction = drag.start(Qt.Qt.CopyAction) + #tracer('Out of Draggable(%s).mouseMoveEvent'%self._qt_klass__) + return DraggableQtKlass QDraggable = Draggable(object) diff --git a/fandango/scripts/WorkerDS b/fandango/scripts/WorkerDS index 7d9501ca4..0b6f7b581 100755 --- a/fandango/scripts/WorkerDS +++ b/fandango/scripts/WorkerDS @@ -5,6 +5,7 @@ DS=$(basename $0) +## The instance argument can be used to override the server name if [ $(echo $1 | grep "/" ) ] ; then IFS='/' read -r DS INSTANCE <<< "$1" else @@ -12,40 +13,69 @@ else INSTANCE=$1 fi -DSORB=$(python -c "import PyTango;print(PyTango.Database().get_property('ORBendPoint',['${DS}/${INSTANCE}']).values()[0][0])" 2>/dev/null) -if [ "$DSORB" ] ; then - DSORB="-ORBendPoint $DSORB" -fi +## @TODO:all these database calls should be done in a single rc.py file + +DSPROPS=$(tango_property -e \ + DSORB=ORBEndPoint.$DS/$INSTANCE \ + LcPATH=Starter/$HOST.StartDSPath \ + DfPATH=PYTHON_CLASSPATH.DeviceClasses \ + PyPATH=PYTHON_CLASSPATH.$DS) + +echo "Tango properties for $DS/$INSTANCE: $DSPROPS" +for p in $DSPROPS; do +# echo $p; + export $p; +done + +# DSORB=$(python -c "import PyTango;print(PyTango.Database().get_property('ORBendPoint',['${DS}/${INSTANCE}']).values()[0][0])" 2>/dev/null) +# if [ "$DSORB" ] ; then +# DSORB="-ORBendPoint $DSORB" +# fi + +export PYTHONPATH=$PYTHONPATH:$LcPATH:$DfPATH:$PyPATH:$(pwd)/python/ +# echo $PYTHONPATH DSPATH=$(python -c "import imp;print(imp.find_module('${DS}')[1])" 2>/dev/null) if [ ! "$DSPATH" ] ; then - DSPATH=$(python -c "import imp;print(imp.find_module('fandango')[1])") - if [ -e "$DSPATH/device/$DS.py" ] ; then - DSPATH=$DSPATH/device - elif [ -e "$DSPATH/interface/$DS.py" ] ; then - DSPATH=$DSPATH/interface + FnPATH=$(python -c "import imp;print(imp.find_module('fandango')[1])") + if [ -e "$FnPATH/device/$DS.py" ] ; then + DSPATH=$FnPATH/device + elif [ -e "$FnPATH/interface/$DS.py" ] ; then + DSPATH=$FnPATH/interface fi fi -DSPATH=$(readlink -f $DSPATH) - -echo "Launching $DSPATH/$DS $INSTANCE at $DSORB" +if [ "$DSPATH" ] ; then + DSPATH=$(readlink -f $DSPATH) +fi -# TODO: if it is mandatory to be in the module path -cd ${DSPATH} +if [ ! -e "${DSPATH}/$DS.py" ] ; then + echo "############## ERROR ###############" + echo "Module $DS not found in PYTHONPATH!?" -if [ $(which screen 2>/dev/null) ] ; then - if [ ! "$(echo $* | grep attach)" ] ; then - echo "run detached" - CMD="screen -dm -S $DS-$INSTANCE " - else - CMD="screen -S $DS-$INSTANCE " - fi else - CMD="" + echo "-------------------------------------------" + echo "Launching ${DSPATH}/$DS $INSTANCE at $DSORB" + + # TODO: if it is mandatory to be in the module path + cd ${DSPATH} + + HASSCREEN=$(which screen 2>/dev/null) + if [ ! "$( echo $1 | grep '\?' )" ] && [ "$HASSCREEN" ] ; then + if [ ! "$(echo $* | grep 'attach')" ] ; then + echo "run detached" + CMD="screen -dm -S $DS-$INSTANCE " + else + CMD="screen -S $DS-$INSTANCE " + fi + else + CMD="" + fi + + CMD="${CMD} python ${DSPATH}/$DS.py $INSTANCE $2 $DSORB" + echo $CMD + $CMD + fi -CMD="${CMD} python ${DSPATH}/$DS.py $INSTANCE $2 $DSORB" -echo $CMD -$CMD diff --git a/fandango/scripts/tango_monitor b/fandango/scripts/tango_monitor index f0ffa9c40..6b01a882f 100755 --- a/fandango/scripts/tango_monitor +++ b/fandango/scripts/tango_monitor @@ -1,33 +1,58 @@ #!/usr/bin/python -""" -usage: devicemonitor [ ]* +__doc__ = """ +usage: + tango_monitor [ ]* """ import sys import time import PyTango +import fandango as fn -def main(): - m = PyTango.DeviceProxy(sys.argv[1]) - +def monitor(d, args=[], events = []): + """ + monitor(device,[attributes]) + """ + + dp = PyTango.DeviceProxy(d) cb = PyTango.utils.EventCallBack() - if len(sys.argv) == 2: - attrs = "state", - else: - attrs = map(str.strip, sys.argv[2:]) - - attrs = map(str.strip, attrs) - - eids = [m.subscribe_event(attr, PyTango.EventType.CHANGE_EVENT, cb) for attr in attrs] - + args = map(str.strip, args) if args else ["state"] + attrs = [a for a in dp.get_attribute_list() + if any(fn.clmatch(r,a) for r in args)] + events = events or [PyTango.EventType.CHANGE_EVENT] + + print('%s matched %d attributes' % (d,len(attrs))) + + eis,worked,failed = [],[],[] + for a in attrs: + try: + for e in events: + eis.append(dp.subscribe_event(a,e,cb)) + worked.append(a) + except: + failed.append(a) + + print('%d attributes NOT provide events: %s' % (len(failed),failed)) + print('%d attributes provide events: %s' % (len(worked),worked)) + print('-'*80 + '\n' + '-'*80) try: while True: time.sleep(1) - except KeyboardInterrupt: - print "Finsihed monitoring" + except: #KeyboardInterrupt + print(fn.excep2str()) + print('-'*80) + print "Finished monitoring" + + [dp.unsubscribe_event(ei) for ei in eis]; if __name__ == '__main__': - main() + import sys + try: + monitor(sys.argv[1],sys.argv[2:]) + except: + print(fn.except2str()) + print(__doc__) + diff --git a/fandango/tango/defaults.py b/fandango/tango/defaults.py index 2fbe07931..c65a586ff 100644 --- a/fandango/tango/defaults.py +++ b/fandango/tango/defaults.py @@ -111,7 +111,7 @@ def loadTaurus(): #Regular Expressions metachars = re.compile('([.][*])|([.][^*])|([$^+\-?{}\[\]|()])') #alnum = '[a-zA-Z_\*][a-zA-Z0-9-_\*]*' #[a-zA-Z0-9-_]+ #Added wildcards -alnum = '(?:[a-zA-Z0-9-_\*]|(?:\.\*))(?:[a-zA-Z0-9-_\*]|(?:\.\*))*' +alnum = '(?:[a-zA-Z0-9-_\*\.]|(?:\.\*))(?:[a-zA-Z0-9-_\*\.]|(?:\.\*))*' no_alnum = '[^a-zA-Z0-9-_]' no_quotes = '(?:^|$|[^\'"a-zA-Z0-9_\./])' rehost = '(?:(?P'+alnum+'(?:\.'+alnum+')?'+'(?:\.'+alnum+')?'\ diff --git a/fandango/tango/methods.py b/fandango/tango/methods.py index f5a7fd50b..6e6924f86 100644 --- a/fandango/tango/methods.py +++ b/fandango/tango/methods.py @@ -52,12 +52,15 @@ def add_new_device(server,klass,device): for c in (server+klass+device): - if re.match('[^a-zA-Z0-9\-\/_\+]',c): + if re.match('[^a-zA-Z0-9\-\/_\+\.]',c): raise Exception,"CharacterNotAllowed('%s')"%c + assert server.count('/')==1 + assert clmatch(alnum,klass) + assert clmatch(retango,device) dev_info = PyTango.DbDevInfo() - dev_info.name = device - dev_info.klass = klass - dev_info.server = server + dev_info.name = device.strip() + dev_info.klass = klass.strip() + dev_info.server = server.strip() get_database().add_device(dev_info) def delete_device(device,server=True):