class _WaitMS(_WaitBase): def __init__(self, scriptRunner, msec): self._waitTimer = Timer() _WaitBase.__init__(self, scriptRunner) self._waitTimer.start(msec / 1000.0, self._continue) def cancelWait(self): self._waitTimer.cancel()
class _WaitThread(_WaitBase): def __init__(self, scriptRunner, func, *args, **kargs): # print "_WaitThread.__init__(%r, *%r, **%r)" % (func, args, kargs) self._pollTimer = Timer() _WaitBase.__init__(self, scriptRunner) if not callable(func): raise ValueError("%r is not callable" % func) self.queue = Queue.Queue() self.func = func self.threadObj = threading.Thread(target=self.threadFunc, args=args, kwargs=kargs) self.threadObj.setDaemon(True) self.threadObj.start() self._pollTimer.start(_PollDelaySec, self.checkEnd) # print "_WaitThread__init__(%r) done" % self.func def checkEnd(self): if self.threadObj.isAlive(): self._pollTimer.start(_PollDelaySec, self.checkEnd) return # print "_WaitThread(%r).checkEnd: thread done" % self.func retVal = self.queue.get() # print "_WaitThread(%r).checkEnd; retVal=%r" % (self.func, retVal) self._continue(val=retVal) def cleanup(self): # print "_WaitThread(%r).cleanup" % self.func self._pollTimer.cancel() self.threadObj = None def threadFunc(self, *args, **kargs): retVal = self.func(*args, **kargs) self.queue.put(retVal)
class KeyDispatcher(object): """ A keyword dispatcher sets keyword variables based on keyword/value data. Inputs: - name: used as the actor when the dispatcher reports errors - connection: an RO.Conn.HubConnection object or similar; if omitted, an RO.Conn.HubConnection.NullConnection is used, which is useful for testing. - logFunc: a function that logs a message. Argument list must be: (msgStr, severity, actor, cmdr) where the first argument is positional and the others are by name Fields: - readUnixTime: unix time at which last message received from connection; 0 if no message ever received. """ def __init__(self, name = "KeyDispatcher", connection = None, logFunc = None, ): self.name = name self.readUnixTime = 0 self._isConnected = False # keyVarListDict keys are (actor, keyword) tuples and values are lists of KeyVariables self.keyVarListDict = {} # cmdDict keys are command ID and values are KeyCommands self.cmdDict = {} # refreshCmdDict contains information about keyVar refresh commands: # key is: actor, refresh command, e.g. as returned by keyVar.getRefreshInfo() # refresh command: set of keyVars that use this command self.refreshCmdDict = {} self._checkCmdTimer = Timer() self._checkRemCmdTimer = Timer() self._refreshAllTimer = Timer() self._refreshNextTimer = Timer() if connection: self.connection = connection self.connection.addReadCallback(self.doRead) self.connection.addStateCallback(self._connStateCallback) else: self.connection = RO.Comm.HubConnection.NullConnection() self._isConnected = self.connection.isConnected self.userCmdIDGen = RO.Alg.IDGen(1, _CmdNumWrap) self.refreshCmdIDGen = RO.Alg.IDGen(_CmdNumWrap + 1, 2 * _CmdNumWrap) self.setLogFunc(logFunc) self.refreshAllVar() self.checkCmdTimeouts() def abortCmdByID(self, cmdID): """Abort the command with the specified ID. Issue the command specified by cmdVar.abortCmdStr, if present. Report the command as failed. Has no effect if the command was never dispatched (cmdID == None) or has already finished. """ if cmdID == None: return cmdVar = self.cmdDict.get(cmdID) if not cmdVar: return # check isDone if cmdVar.isDone(): return # if relevant, issue abort command, with no callbacks if cmdVar.abortCmdStr and self._isConnected: abortCmd = RO.KeyVariable.CmdVar( cmdStr = cmdVar.abortCmdStr, actor = cmdVar.actor, ) self.executeCmd(abortCmd) # report command as aborted errMsgDict = self.makeMsgDict ( cmdID = cmdVar.cmdID, dataStr = "Aborted; Actor=%r; Cmd=%r" % ( cmdVar.actor, cmdVar.cmdStr), ) self._replyCmdVar(cmdVar, errMsgDict) def addKeyVar(self, keyVar): """ Adds a keyword variable to the list. Inputs: - keyVar: the keyword variable; typically of class RO.KeyVariable but can be any object that: - has property: keyword (a string) - has method "set" with arguments: - valueTuple (positional): a tuple of one or more values for the keyword the values may be strings, even if another type is expected - keyword (by name): the keyword - msgDict (by name): the full message dictionary """ dictKey = (keyVar.actor, keyVar.keyword.lower()) # get list of keyVars, adding it if not already present keyList = self.keyVarListDict.setdefault(dictKey, []) # append new keyVar to the list keyList.append(keyVar) if keyVar.hasRefreshCmd(): refreshInfo = keyVar.getRefreshInfo() keyVarSet = self.refreshCmdDict.get(refreshInfo) if keyVarSet: keyVarSet.add(keyVar) else: self.refreshCmdDict[refreshInfo] = set((keyVar,)) if self._isConnected: self._refreshAllTimer.start(_ShortInterval, self.refreshAllVar, False) def checkCmdTimeouts(self): """Check all pending commands for timeouts""" # print "RO.KeyDispatcher.checkCmdTimeouts()" # cancel pending update, if any self._checkCmdTimer.cancel() self._checkRemCmdTimer.cancel() # iterate over a copy of the values # so we can modify the dictionary while checking command timeouts cmdVarIter = iter(list(self.cmdDict.values())) self._checkRemCmdTimeouts(cmdVarIter) def dispatch(self, msgDict): """ Updates the appropriate entries based on the supplied message data. Inputs: - msgDict: message dictionary. Required fields: - cmdr: name of commander that triggered the message (string) - cmdID: command ID that triggered the message (int) - actor: the actor that generated the message (string) - msgType: message type (character) - data: dict of keyword: data_tuple entries; data_tuple is always a tuple, even if it contains one or zero values """ # print "dispatching", msgDict # extract user number, command number and data dictionary; die if absent cmdr = msgDict["cmdr"] cmdID = msgDict["cmdID"] actor = msgDict["actor"] msgType = msgDict["msgType"] dataDict = msgDict["data"] # handle keywords # note: keywords from actor keys.<actor> # should be handled as if from <actor> if actor.startswith("keys."): keyActor = actor[5:] else: keyActor = actor for keywd, valueTuple in dataDict.items(): dictKey = (keyActor, keywd.lower()) keyVarList = self.keyVarListDict.get(dictKey, []) for keyVar in keyVarList: try: keyVar.set(valueTuple, msgDict = msgDict) except: traceback.print_exc(file=sys.stderr) # if you are the commander for this message, # execute the command callback (if any) if cmdr == self.connection.cmdr: # get the command for this command id, if any cmdVar = self.cmdDict.get(cmdID, None) if cmdVar != None: # send reply but don't log (that's already been done) self._replyCmdVar(cmdVar, msgDict, doLog=False) def doRead(self, sock, msgStr): """Reads, parses and dispatches a message from the hub Sets self.readUnixTime to time.time() """ # parse message; if it fails, log it as an error self.readUnixTime = time.time() try: msgDict = RO.ParseMsg.parseHubMsg(msgStr) except Exception as e: self.logMsg( msgStr = "CouldNotParse; Msg=%r; Text=%r" % (msgStr, RO.StringUtil.strFromException(e)), severity = RO.Constants.sevError, ) return # log message self.logMsgDict(msgDict) # dispatch message try: self.dispatch(msgDict) except Exception as e: sys.stderr.write("Could not dispatch: %r\n" % (msgDict,)) traceback.print_exc(file=sys.stderr) def executeCmd(self, cmdVar): """Executes the command (of type RO.KeyVariable.CmdVar) by performing the following tasks: - Sets the command number - Sets the start time - Puts the command on the keyword dispatcher queue - Issues the command to the server Inputs: - cmdVar: the command, of class RO.KeyVariable.CmdVar Note: - we always increment cmdID since every command must have a unique command ID (even commands that go to different actors); this simplifies the dispatcher code and also makes the hub's life easier (since it can report certain kinds of failures using actor=hub). """ if not self._isConnected: errMsgDict = self.makeMsgDict( dataStr = "Failed; Actor=%r; Cmd=%r; Text=\"not connected\"" % ( cmdVar.actor, cmdVar.cmdStr), ) self._replyCmdVar(cmdVar, errMsgDict) return while True: if cmdVar.isRefresh: cmdID = next(self.refreshCmdIDGen) else: cmdID = next(self.userCmdIDGen) if cmdID not in self.cmdDict: break self.cmdDict[cmdID] = cmdVar cmdVar._setStartInfo(self, cmdID) try: fullCmd = "%d %s %s" % (cmdVar.cmdID, cmdVar.actor, cmdVar.cmdStr) self.connection.writeLine (fullCmd) # self.logMsg ( # msgStr = fullCmd, # actor = cmdVar.actor, # cmdID = cmdVar.cmdID, # ) # print "executing:", fullCmd except Exception as e: errMsgDict = self.makeMsgDict( cmdID = cmdVar.cmdID, dataStr = "WriteFailed; Actor=%r; Cmd=%r; Text=%r" % ( cmdVar.actor, cmdVar.cmdStr, RO.StringUtil.strFromException(e)), ) self._replyCmdVar(cmdVar, errMsgDict) @staticmethod def getMaxUserCmdID(): """Return the maximum user command ID number. User command ID numbers range from 1 through getMaxUserCmdID() Refresh command ID numbers range from getMaxUserCmdID() + 1 through 2 * getMaxUserCmdID() """ return _CmdNumWrap def logMsg(self, msgStr, severity = RO.Constants.sevNormal, actor = "TUI", cmdr = None, cmdID = 0, ): """Writes a message to the log. If no logFunc was supplied then the message is printed to stderr. On error, prints a message to stderr and returns normally. Inputs: - msgStr: message to display; a final \n is appended - severity: message severity (an RO.Constants.sevX constant) - actor: name of actor - cmdr: commander; defaults to self """ if not self.logFunc: sys.stderr.write(msgStr + "\n") return try: self.logFunc( msgStr, severity = severity, actor = actor, cmdr = cmdr, cmdID = cmdID, ) except Exception as e: sys.stderr.write("Could not log: %r; severity=%r; actor=%r; cmdr=%r\n" % \ (msgStr, severity, actor, cmdr)) traceback.print_exc(file=sys.stderr) def logMsgDict(self, msgDict): try: msgType = msgDict["msgType"].lower() severity = RO.KeyVariable.TypeDict[msgType][1] self.logMsg( msgStr = msgDict["msgStr"], severity = severity, actor = msgDict["actor"], cmdr = msgDict["cmdr"], cmdID = msgDict["cmdID"], ) except Exception as e: sys.stderr.write("Could not log message dict:\n%r\n" % (msgDict,)) traceback.print_exc(file=sys.stderr) def makeMsgDict(self, cmdr = None, cmdID = 0, actor = None, msgType = "f", dataStr = "", ): """Generate a hub message based on the supplied data. Useful for reporting internal errors. """ if cmdr == None: cmdr = self.connection.cmdr if actor == None: actor = self.name headerStr = "%s %d %s %s" % ( cmdr, cmdID, actor, msgType, ) msgStr = " ".join((headerStr, dataStr)) try: return RO.ParseMsg.parseHubMsg(msgStr) except Exception as e: sys.stderr.write("Could not make message dict from %r; error: %s" % (msgStr, e)) traceback.print_exc(file=sys.stderr) msgDict = RO.ParseMsg.parseHubMsg(headerStr) msgDict["msgStr"] = msgStr msgDict["data"] = {} return msgDict def refreshAllVar(self, resetAll=True): """Examines all keywords, looking for ones that need updating and issues the appropriate refresh commands. Inputs: - resetAll: reset all keyword variables to notCurrent """ # print "refreshAllVar()" # cancel pending update, if any self._refreshAllTimer.cancel() self._refreshNextTimer.cancel() if resetAll: # clear the refresh command dict # and invalidate all keyVars # (leave pending refresh commands alone; they will time out) for keyVarList in list(self.keyVarListDict.values()): for keyVar in keyVarList: keyVar.setNotCurrent() self._sendNextRefreshCmd() def removeKeyVar(self, keyVar): """ Removes the specified keyword variable, returning whatever was removed, or None if keyVar not found. See also "add". Inputs: - keyVar: the keyword variable to remove Returns: - keyVar, if present, None otherwise. """ dictKey = (keyVar.actor, keyVar.keywd.lower()) keyVarList = self.keyVarListDict.get(dictKey, []) if keyVar not in keyVarList: return None keyVarList.remove(keyVar) # remove refresh command, if present keyVarSet = self.refreshCmdDict.get(keyVar.getRefreshInfo()) if keyVarSet and keyVar in keyVarSet: keyVarSet.remove(keyVar) if not keyVarSet: # that was the only keyVar using this refresh command del(self.refreshCmdDict[keyVar.getRefreshInfo()]) return keyVar def setLogFunc(self, logFunc=None): """Sets the log output device, or clears it if none specified. The function must take the following arguments: (msgStr, severity, actor, cmdr) where the first argument is positional and the others are by name """ self.logFunc = logFunc def _updateRefreshCmds(self): """Update the cache of refresh commands by scanning the keyVars. """ self.refreshCmdDict = {} for keyVarList in self.keyVarListDict.values(): for keyVar in keyVarList: if keyVar.hasRefreshCmd(): refreshInfo = keyVar.getRefreshInfo() keyVarSet = self.refreshCmdDict.get(refreshInfo) if keyVarSet: keyVarSet.add(keyVar) else: self.refreshCmdDict[refreshInfo] = set((keyVar,)) def _checkRemCmdTimeouts(self, cmdVarIter): """Helper function for checkCmdTimeouts. Check the remaining command variables in cmdVarIter. If a timeout is found, time out that one command and schedule myself to run again shortly (thereby giving other events a chance to run). Once the iterator is exhausted, schedule my parent function checkCmdTimeouts to run at the usual interval later. """ # print "RO.KeyDispatcher._checkRemCmdTimeouts(%s)" % cmdVarIter try: errMsgDict = None currTime = time.time() for cmdVar in cmdVarIter: # if cmd still exits (i.e. has not been deleted for other reasons) # check if it has a time limit and has timed out if cmdVar.cmdID not in self.cmdDict: continue if not self._isConnected: errMsgDict = self.makeMsgDict ( cmdID = cmdVar.cmdID, dataStr = "Aborted; Actor=%r; Cmd=%r; Text=\"disconnected\"" % ( cmdVar.actor, cmdVar.cmdStr), ) # no connection, so cannot send abort command cmdVar.abortCmdStr = "" break elif cmdVar.maxEndTime and (cmdVar.maxEndTime < currTime): # time out this command errMsgDict = self.makeMsgDict ( cmdID = cmdVar.cmdID, dataStr = "Timeout; Actor=%r; Cmd=%r" % ( cmdVar.actor, cmdVar.cmdStr), ) break if errMsgDict: self._replyCmdVar(cmdVar, errMsgDict) # schedule myself to run again shortly # (thereby giving other time to other events) # continuing where I left off self._checkRemCmdTimer.start(_ShortInterval, self._checkRemCmdTimeouts, cmdVarIter) except: sys.stderr.write ("RO.KeyDispatcher._checkRemCmdTimeouts failed\n") traceback.print_exc(file=sys.stderr) # finished checking all commands in the current cmdVarIter; # schedule a new checkCmdTimeouts at the usual interval self._checkCmdTimer.start(_TimeoutInterval, self.checkCmdTimeouts) def _connStateCallback(self, conn): """If connection state changes, update refresh variables. """ wasConnected = self._isConnected self._isConnected = conn.isConnected if wasConnected != self._isConnected: self._refreshAllTimer.start(_ShortInterval, self.refreshAllVar) def _refreshCmdCallback(self, msgType, msgDict, cmdVar): """Refresh command callback; complain if command failed or some keyVars not updated """ if not cmdVar.isDone: return refreshInfo = (cmdVar.actor, cmdVar.cmdStr) keyVarSet = self.refreshCmdDict.get(refreshInfo, set()) if cmdVar.didFail(): keyVarNamesStr = ", ".join(sorted([kv.keyword for kv in keyVarSet])) errMsg = "Refresh command %s %s failed; keyVars not refreshed: %s" % \ (cmdVar.actor, cmdVar.cmdStr, keyVarNamesStr) self.logMsg( msgStr = errMsg, severity = RO.Constants.sevWarning, cmdID = cmdVar.cmdID, ) elif keyVarSet: aKeyVar = next(iter(keyVarSet)) actor = aKeyVar.actor missingKeyVarNamesStr = ", ".join(sorted([kv.keyword for kv in keyVarSet if not kv.isCurrent()])) if missingKeyVarNamesStr: errMsg = "No refresh data for %s keyVars: %s" % (actor, missingKeyVarNamesStr) self.logMsg(errMsg, severity=RO.Constants.sevWarning) else: # all of the keyVars were removed or there is a bug errMsg = "Warning: refresh command %s %s finished but no keyVars found\n" % refreshInfo self.logMsg(errMsg, severity=RO.Constants.sevWarning) def _replyCmdVar(self, cmdVar, msgDict, doLog=True): """Send a message to a command variable and optionally log it. If the command is done, delete it from the command dict. If the command is a refresh command and is done, update the refresh command dict accordingly. Inputs: - cmdVar command variable (RO.KeyVariable.CmdVar) - msgDict message to send """ if doLog: self.logMsgDict(msgDict) cmdVar.reply(msgDict) if cmdVar.isDone() and cmdVar.cmdID != None: try: del (self.cmdDict[cmdVar.cmdID]) except KeyError: sys.stderr.write("KeyDispatcher bug: tried to delete cmd %s=%s but it was missing\n" % \ (cmdVar.cmdID, cmdVar)) def _sendNextRefreshCmd(self, refreshCmdItemIter=None): """Helper function for refreshAllVar. Plow through a keyVarList iterator until a refresh command is found that is wanted, issue it, then schedule a call for myself for ASAP (giving other events a chance to execute first). Inputs: - refreshCmdItemIter: iterator over items in refreshCmdDict; if None then set to self.refreshCmdDict.iteritems() """ # print "_sendNextRefreshCmd(%s)" % (refreshCmdItemIter,) if not self._isConnected: return if refreshCmdItemIter == None: self._updateRefreshCmds() refreshCmdItemIter = iter(self.refreshCmdDict.items()) try: refreshCmdInfo, keyVarSet = next(refreshCmdItemIter) except StopIteration: return actor, cmdStr = refreshCmdInfo try: cmdVar = RO.KeyVariable.CmdVar ( actor = actor, cmdStr = cmdStr, timeLim = _RefreshTimeLim, callFunc = self._refreshCmdCallback, isRefresh = True, ) self.executeCmd(cmdVar) except: sys.stderr.write("%s._sendNextRefreshCmd: refresh command %s failed:\n" % (self.__class__.__name__, cmdVar,)) traceback.print_exc(file=sys.stderr) self._refreshNextTimer.start(_ShortInterval, self._sendNextRefreshCmd, refreshCmdItemIter)
class KeyDispatcher(object): """ A keyword dispatcher sets keyword variables based on keyword/value data. Inputs: - name: used as the actor when the dispatcher reports errors - connection: an RO.Conn.HubConnection object or similar; if omitted, an RO.Conn.HubConnection.NullConnection is used, which is useful for testing. - logFunc: a function that logs a message. Argument list must be: (msgStr, severity, actor, cmdr) where the first argument is positional and the others are by name Fields: - readUnixTime: unix time at which last message received from connection; 0 if no message ever received. """ def __init__( self, name="KeyDispatcher", connection=None, logFunc=None, ): self.name = name self.readUnixTime = 0 self._isConnected = False # keyVarListDict keys are (actor, keyword) tuples and values are lists of KeyVariables self.keyVarListDict = {} # cmdDict keys are command ID and values are KeyCommands self.cmdDict = {} # refreshCmdDict contains information about keyVar refresh commands: # key is: actor, refresh command, e.g. as returned by keyVar.getRefreshInfo() # refresh command: set of keyVars that use this command self.refreshCmdDict = {} self._checkCmdTimer = Timer() self._checkRemCmdTimer = Timer() self._refreshAllTimer = Timer() self._refreshNextTimer = Timer() if connection: self.connection = connection self.connection.addReadCallback(self.doRead) self.connection.addStateCallback(self._connStateCallback) else: self.connection = RO.Comm.HubConnection.NullConnection() self._isConnected = self.connection.isConnected self.userCmdIDGen = RO.Alg.IDGen(1, _CmdNumWrap) self.refreshCmdIDGen = RO.Alg.IDGen(_CmdNumWrap + 1, 2 * _CmdNumWrap) self.setLogFunc(logFunc) self.refreshAllVar() self.checkCmdTimeouts() def abortCmdByID(self, cmdID): """Abort the command with the specified ID. Issue the command specified by cmdVar.abortCmdStr, if present. Report the command as failed. Has no effect if the command was never dispatched (cmdID is None) or has already finished. """ if cmdID is None: return cmdVar = self.cmdDict.get(cmdID) if not cmdVar: return # check isDone if cmdVar.isDone(): return # if relevant, issue abort command, with no callbacks if cmdVar.abortCmdStr and self._isConnected: abortCmd = RO.KeyVariable.CmdVar( cmdStr=cmdVar.abortCmdStr, actor=cmdVar.actor, ) self.executeCmd(abortCmd) # report command as aborted errMsgDict = self.makeMsgDict( cmdID=cmdVar.cmdID, dataStr="Aborted; Actor=%r; Cmd=%r" % (cmdVar.actor, cmdVar.cmdStr), ) self._replyCmdVar(cmdVar, errMsgDict) def addKeyVar(self, keyVar): """ Adds a keyword variable to the list. Inputs: - keyVar: the keyword variable; typically of class RO.KeyVariable but can be any object that: - has property: keyword (a string) - has method "set" with arguments: - valueTuple (positional): a tuple of one or more values for the keyword the values may be strings, even if another type is expected - keyword (by name): the keyword - msgDict (by name): the full message dictionary """ dictKey = (keyVar.actor, keyVar.keyword.lower()) # get list of keyVars, adding it if not already present keyList = self.keyVarListDict.setdefault(dictKey, []) # append new keyVar to the list keyList.append(keyVar) if keyVar.hasRefreshCmd(): refreshInfo = keyVar.getRefreshInfo() keyVarSet = self.refreshCmdDict.get(refreshInfo) if keyVarSet: keyVarSet.add(keyVar) else: self.refreshCmdDict[refreshInfo] = set((keyVar, )) if self._isConnected: self._refreshAllTimer.start(_ShortInterval, self.refreshAllVar, False) def checkCmdTimeouts(self): """Check all pending commands for timeouts""" # print "RO.KeyDispatcher.checkCmdTimeouts()" # cancel pending update, if any self._checkCmdTimer.cancel() self._checkRemCmdTimer.cancel() # iterate over a copy of the values # so we can modify the dictionary while checking command timeouts cmdVarIter = iter(self.cmdDict.values()) self._checkRemCmdTimeouts(cmdVarIter) def dispatch(self, msgDict): """ Updates the appropriate entries based on the supplied message data. Inputs: - msgDict: message dictionary. Required fields: - cmdr: name of commander that triggered the message (string) - cmdID: command ID that triggered the message (int) - actor: the actor that generated the message (string) - msgType: message type (character) - data: dict of keyword: data_tuple entries; data_tuple is always a tuple, even if it contains one or zero values """ # print "dispatching", msgDict # extract user number, command number and data dictionary; die if absent cmdr = msgDict["cmdr"] cmdID = msgDict["cmdID"] actor = msgDict["actor"] dataDict = msgDict["data"] # handle keywords # note: keywords from actor keys.<actor> # should be handled as if from <actor> if actor.startswith("keys."): keyActor = actor[5:] else: keyActor = actor for keywd, valueTuple in dataDict.iteritems(): dictKey = (keyActor, keywd.lower()) keyVarList = self.keyVarListDict.get(dictKey, []) for keyVar in keyVarList: try: keyVar.set(valueTuple, msgDict=msgDict) except Exception: traceback.print_exc(file=sys.stderr) # if you are the commander for this message, # execute the command callback (if any) if cmdr == self.connection.cmdr: # get the command for this command id, if any cmdVar = self.cmdDict.get(cmdID, None) if cmdVar is not None: # send reply but don't log (that's already been done) self._replyCmdVar(cmdVar, msgDict, doLog=False) def doRead(self, sock, msgStr): """Reads, parses and dispatches a message from the hub Sets self.readUnixTime to time.time() """ # parse message; if it fails, log it as an error self.readUnixTime = time.time() try: msgDict = RO.ParseMsg.parseHubMsg(msgStr) except Exception as e: self.logMsg( msgStr="CouldNotParse; Msg=%r; Text=%r" % (msgStr, RO.StringUtil.strFromException(e)), severity=RO.Constants.sevError, ) return # log message self.logMsgDict(msgDict) # dispatch message try: self.dispatch(msgDict) except Exception as e: sys.stderr.write("Could not dispatch: %r\n" % (msgDict, )) traceback.print_exc(file=sys.stderr) def executeCmd(self, cmdVar): """Executes the command (of type RO.KeyVariable.CmdVar) by performing the following tasks: - Sets the command number - Sets the start time - Puts the command on the keyword dispatcher queue - Issues the command to the server Inputs: - cmdVar: the command, of class RO.KeyVariable.CmdVar Note: - we always increment cmdID since every command must have a unique command ID (even commands that go to different actors); this simplifies the dispatcher code and also makes the hub's life easier (since it can report certain kinds of failures using actor=hub). """ if not self._isConnected: errMsgDict = self.makeMsgDict( dataStr="Failed; Actor=%r; Cmd=%r; Text=\"not connected\"" % (cmdVar.actor, cmdVar.cmdStr), ) self._replyCmdVar(cmdVar, errMsgDict) return while True: if cmdVar.isRefresh: cmdID = next(self.refreshCmdIDGen) else: cmdID = next(self.userCmdIDGen) if cmdID not in self.cmdDict: break self.cmdDict[cmdID] = cmdVar cmdVar._setStartInfo(self, cmdID) try: fullCmd = "%d %s %s" % (cmdVar.cmdID, cmdVar.actor, cmdVar.cmdStr) self.connection.writeLine(fullCmd) # self.logMsg ( # msgStr = fullCmd, # actor = cmdVar.actor, # cmdID = cmdVar.cmdID, # ) # print "executing:", fullCmd except Exception as e: errMsgDict = self.makeMsgDict( cmdID=cmdVar.cmdID, dataStr="WriteFailed; Actor=%r; Cmd=%r; Text=%r" % (cmdVar.actor, cmdVar.cmdStr, RO.StringUtil.strFromException(e)), ) self._replyCmdVar(cmdVar, errMsgDict) @staticmethod def getMaxUserCmdID(): """Return the maximum user command ID number. User command ID numbers range from 1 through getMaxUserCmdID() Refresh command ID numbers range from getMaxUserCmdID() + 1 through 2 * getMaxUserCmdID() """ return _CmdNumWrap def logMsg( self, msgStr, severity=RO.Constants.sevNormal, actor="TUI", cmdr=None, cmdID=0, ): """Writes a message to the log. If no logFunc was supplied then the message is printed to stderr. On error, prints a message to stderr and returns normally. Inputs: - msgStr: message to display; a final \n is appended - severity: message severity (an RO.Constants.sevX constant) - actor: name of actor - cmdr: commander; defaults to self """ if not self.logFunc: sys.stderr.write(msgStr + "\n") return try: self.logFunc( msgStr, severity=severity, actor=actor, cmdr=cmdr, cmdID=cmdID, ) except Exception: sys.stderr.write("Could not log: %r; severity=%r; actor=%r; cmdr=%r\n" % \ (msgStr, severity, actor, cmdr)) traceback.print_exc(file=sys.stderr) def logMsgDict(self, msgDict): try: msgType = msgDict["msgType"].lower() severity = RO.KeyVariable.TypeDict[msgType][1] self.logMsg( msgStr=msgDict["msgStr"], severity=severity, actor=msgDict["actor"], cmdr=msgDict["cmdr"], cmdID=msgDict["cmdID"], ) except Exception: sys.stderr.write("Could not log message dict:\n%r\n" % (msgDict, )) traceback.print_exc(file=sys.stderr) def makeMsgDict( self, cmdr=None, cmdID=0, actor=None, msgType="f", dataStr="", ): """Generate a hub message based on the supplied data. Useful for reporting internal errors. """ if cmdr is None: cmdr = self.connection.cmdr if actor is None: actor = self.name headerStr = "%s %d %s %s" % ( cmdr, cmdID, actor, msgType, ) msgStr = " ".join((headerStr, dataStr)) try: return RO.ParseMsg.parseHubMsg(msgStr) except Exception as e: sys.stderr.write("Could not make message dict from %r; error: %s" % (msgStr, e)) traceback.print_exc(file=sys.stderr) msgDict = RO.ParseMsg.parseHubMsg(headerStr) msgDict["msgStr"] = msgStr msgDict["data"] = {} return msgDict def refreshAllVar(self, resetAll=True): """Examines all keywords, looking for ones that need updating and issues the appropriate refresh commands. Inputs: - resetAll: reset all keyword variables to notCurrent """ # print "refreshAllVar()" # cancel pending update, if any self._refreshAllTimer.cancel() self._refreshNextTimer.cancel() if resetAll: # clear the refresh command dict # and invalidate all keyVars # (leave pending refresh commands alone; they will time out) for keyVarList in self.keyVarListDict.values(): for keyVar in keyVarList: keyVar.setNotCurrent() self._sendNextRefreshCmd() def removeKeyVar(self, keyVar): """ Removes the specified keyword variable, returning whatever was removed, or None if keyVar not found. See also "add". Inputs: - keyVar: the keyword variable to remove Returns: - keyVar, if present, None otherwise. """ dictKey = (keyVar.actor, keyVar.keywd.lower()) keyVarList = self.keyVarListDict.get(dictKey, []) if keyVar not in keyVarList: return None keyVarList.remove(keyVar) # remove refresh command, if present keyVarSet = self.refreshCmdDict.get(keyVar.getRefreshInfo()) if keyVarSet and keyVar in keyVarSet: keyVarSet.remove(keyVar) if not keyVarSet: # that was the only keyVar using this refresh command del (self.refreshCmdDict[keyVar.getRefreshInfo()]) return keyVar def setLogFunc(self, logFunc=None): """Sets the log output device, or clears it if none specified. The function must take the following arguments: (msgStr, severity, actor, cmdr) where the first argument is positional and the others are by name """ self.logFunc = logFunc def _updateRefreshCmds(self): """Update the cache of refresh commands by scanning the keyVars. """ self.refreshCmdDict = {} for keyVarList in self.keyVarListDict.itervalues(): for keyVar in keyVarList: if keyVar.hasRefreshCmd(): refreshInfo = keyVar.getRefreshInfo() keyVarSet = self.refreshCmdDict.get(refreshInfo) if keyVarSet: keyVarSet.add(keyVar) else: self.refreshCmdDict[refreshInfo] = set((keyVar, )) def _checkRemCmdTimeouts(self, cmdVarIter): """Helper function for checkCmdTimeouts. Check the remaining command variables in cmdVarIter. If a timeout is found, time out that one command and schedule myself to run again shortly (thereby giving other events a chance to run). Once the iterator is exhausted, schedule my parent function checkCmdTimeouts to run at the usual interval later. """ # print "RO.KeyDispatcher._checkRemCmdTimeouts(%s)" % cmdVarIter try: errMsgDict = None currTime = time.time() for cmdVar in cmdVarIter: # if cmd still exits (i.e. has not been deleted for other reasons) # check if it has a time limit and has timed out if cmdVar.cmdID not in self.cmdDict: continue if not self._isConnected: errMsgDict = self.makeMsgDict( cmdID=cmdVar.cmdID, dataStr= "Aborted; Actor=%r; Cmd=%r; Text=\"disconnected\"" % (cmdVar.actor, cmdVar.cmdStr), ) # no connection, so cannot send abort command cmdVar.abortCmdStr = "" break elif cmdVar.maxEndTime and (cmdVar.maxEndTime < currTime): # time out this command errMsgDict = self.makeMsgDict( cmdID=cmdVar.cmdID, dataStr="Timeout; Actor=%r; Cmd=%r" % (cmdVar.actor, cmdVar.cmdStr), ) break if errMsgDict: self._replyCmdVar(cmdVar, errMsgDict) # schedule myself to run again shortly # (thereby giving other time to other events) # continuing where I left off self._checkRemCmdTimer.start(_ShortInterval, self._checkRemCmdTimeouts, cmdVarIter) except Exception: sys.stderr.write("RO.KeyDispatcher._checkRemCmdTimeouts failed\n") traceback.print_exc(file=sys.stderr) # finished checking all commands in the current cmdVarIter; # schedule a new checkCmdTimeouts at the usual interval self._checkCmdTimer.start(_TimeoutInterval, self.checkCmdTimeouts) def _connStateCallback(self, conn): """If connection state changes, update refresh variables. """ wasConnected = self._isConnected self._isConnected = conn.isConnected if wasConnected != self._isConnected: self._refreshAllTimer.start(_ShortInterval, self.refreshAllVar) def _refreshCmdCallback(self, msgType, msgDict, cmdVar): """Refresh command callback; complain if command failed or some keyVars not updated """ if not cmdVar.isDone: return refreshInfo = (cmdVar.actor, cmdVar.cmdStr) keyVarSet = self.refreshCmdDict.get(refreshInfo, set()) if cmdVar.didFail(): keyVarNamesStr = ", ".join(sorted([kv.keyword for kv in keyVarSet])) errMsg = "Refresh command %s %s failed; keyVars not refreshed: %s" % \ (cmdVar.actor, cmdVar.cmdStr, keyVarNamesStr) self.logMsg( msgStr=errMsg, severity=RO.Constants.sevWarning, cmdID=cmdVar.cmdID, ) elif keyVarSet: aKeyVar = next(iter(keyVarSet)) actor = aKeyVar.actor missingKeyVarNamesStr = ", ".join( sorted([kv.keyword for kv in keyVarSet if not kv.isCurrent()])) if missingKeyVarNamesStr: errMsg = "No refresh data for %s keyVars: %s" % ( actor, missingKeyVarNamesStr) self.logMsg(errMsg, severity=RO.Constants.sevWarning) else: # all of the keyVars were removed or there is a bug errMsg = "Warning: refresh command %s %s finished but no keyVars found\n" % refreshInfo self.logMsg(errMsg, severity=RO.Constants.sevWarning) def _replyCmdVar(self, cmdVar, msgDict, doLog=True): """Send a message to a command variable and optionally log it. If the command is done, delete it from the command dict. If the command is a refresh command and is done, update the refresh command dict accordingly. Inputs: - cmdVar command variable (RO.KeyVariable.CmdVar) - msgDict message to send """ if doLog: self.logMsgDict(msgDict) cmdVar.reply(msgDict) if cmdVar.isDone() and cmdVar.cmdID is not None: try: del (self.cmdDict[cmdVar.cmdID]) except KeyError: sys.stderr.write("KeyDispatcher bug: tried to delete cmd %s=%s but it was missing\n" % \ (cmdVar.cmdID, cmdVar)) def _sendNextRefreshCmd(self, refreshCmdItemIter=None): """Helper function for refreshAllVar. Plow through a keyVarList iterator until a refresh command is found that is wanted, issue it, then schedule a call for myself for ASAP (giving other events a chance to execute first). Inputs: - refreshCmdItemIter: iterator over items in refreshCmdDict; if None then set to self.refreshCmdDict.iteritems() """ # print "_sendNextRefreshCmd(%s)" % (refreshCmdItemIter,) if not self._isConnected: return if refreshCmdItemIter is None: self._updateRefreshCmds() refreshCmdItemIter = self.refreshCmdDict.iteritems() try: refreshCmdInfo, keyVarSet = next(refreshCmdItemIter) except StopIteration: return actor, cmdStr = refreshCmdInfo try: cmdVar = RO.KeyVariable.CmdVar( actor=actor, cmdStr=cmdStr, timeLim=_RefreshTimeLim, callFunc=self._refreshCmdCallback, isRefresh=True, ) self.executeCmd(cmdVar) except Exception: sys.stderr.write( "%s._sendNextRefreshCmd: refresh command %s failed:\n" % ( self.__class__.__name__, cmdVar, )) traceback.print_exc(file=sys.stderr) self._refreshNextTimer.start(_ShortInterval, self._sendNextRefreshCmd, refreshCmdItemIter)
class PermsTableWdg(Tkinter.Frame): """Inputs: - master master widget - statusBar status bar to handle commands. - readOnlyCallback a function that is called when the readOnly state changes; the function receives one argument: isReadOnly: True for read only, False otherwise. Note that isReadOnly always starts out True. """ def __init__(self, master, statusBar, readOnlyCallback = None, ): Tkinter.Frame.__init__(self, master) self._statusBar = statusBar self._tuiModel = TUI.TUIModel.getModel() self._readOnlyCallback = readOnlyCallback self._actorList = ActorList(startCol=1) self._progDict = {} # prog name: prog perms self._titleWdgSet = [] self._titleBorder = Tkinter.Frame(self, borderwidth=2, relief="sunken") self._titleBorder.grid(row=0, column=0, sticky="ew") self._titleBorder.grid_columnconfigure(1, weight=1) self._titleFrame = Tkinter.Frame(self._titleBorder, borderwidth=0) self._titleFrame.grid(row=0, column=0, sticky="w") self._scrollWdg = RO.Wdg.ScrolledWdg( master = self, hscroll = False, vscroll = True, borderwidth = 2, relief = "sunken", ) self._scrollWdg.grid(row=1, column=0, sticky="nsew") self._tableFrame = Tkinter.Frame(self._scrollWdg.getWdgParent(), borderwidth=0) self._vertMeasWdg = Tkinter.Frame(self._tableFrame) self._vertMeasWdg.grid(row=0, column=0, sticky="wns") self._scrollWdg.setWdg( wdg = self._tableFrame, vincr = self._vertMeasWdg, ) self.grid_rowconfigure(1, weight=1) self._nextRow = 0 self._readOnly = True self._updActorTimer = Timer() self.permsModel = TUI.Models.PermsModel.getModel() self.permsModel.actors.addCallback(self._updActors) self.permsModel.authList.addCallback(self._updAuthList) self.permsModel.lockedActors.addCallback(self._updLockedActors) self.permsModel.programs.addCallback(self._updPrograms) self._lockoutRow = 3 self._lockoutWdg = _LockoutPerms( master = self._titleFrame, actorList = self._actorList, readOnly = self._readOnly, row = self._lockoutRow, statusBar = self._statusBar, ) statusBar.dispatcher.connection.addStateCallback(self.__connStateCallback) def purge(self): """Remove unregistered programs. """ knownProgs = self.permsModel.programs.get()[0] # use items instead of iteritems so we can modify as we go for prog, progPerms in self._progDict.items(): if progPerms.isRegistered() or prog in knownProgs: continue progPerms.delete() del(self._progDict[prog]) def sort(self): """Sort existing programs and redisplay all data. """ self._actorList.clearAllTitleWdg() for wdg in self._titleWdgSet: wdg.destroy() for col, actor in self._actorList.getColActorList(): if not actor: # insert dividor self._addTitle(" ", col) else: titleLabel = self._addTitle(actor, col) self._actorList.setTitleWdg(actor, titleLabel) self._lockoutWdg.display(row=self._lockoutRow) progNames = self._progDict.keys() progNames.sort() self._nextRow = 0 for prog in progNames: progPerms = self._progDict[prog] progPerms.display(row=self._nextRow) self._nextRow += 1 def _addProg(self, prog): """Create and display a new program. Called when the hub informs this widget of a new program (to add a program send the suitable command to the hub, don't just call this method). """ prog = prog.upper() newProg = _ProgPerms( master = self._tableFrame, prog = prog, actorList = self._actorList, readOnly = self._readOnly, row = self._nextRow, statusBar = self._statusBar, ) self._nextRow += 1 self._progDict[prog] = newProg def _addTitle(self, text, col): """Create and grid a title label and two associated width measuring frames (one in the title frame, one in the main frame). Inputs: - text text for title - col column for title Returns the title label """ # print "_addTitle(%r, %r)" % (text, col) strWdg = RO.Wdg.StrLabel( master = self._titleFrame, text = text, ) strWdg.grid(row=0, column=col) titleSpacer = Tkinter.Frame(self._titleFrame) titleSpacer.grid(row=1, column=col, sticky="new") mainSpacer = Tkinter.Frame(self._tableFrame) mainSpacer.grid(row=0, column=col, sticky="new") self._titleWdgSet += [strWdg, titleSpacer, mainSpacer] def dotitle(evt): # print "dotitle: titlewidth = %r, mainwidth = %r" % ( # titleSpacer.winfo_width(), mainSpacer.winfo_width(), # ) if titleSpacer.winfo_width() > mainSpacer.winfo_width(): mainSpacer["width"] = titleSpacer.winfo_width() titleSpacer.bind("<Configure>", dotitle) def domain(evt): # print "domain: titlewidth = %r, mainwidth = %r" % ( # titleSpacer.winfo_width(), mainSpacer.winfo_width(), # ) if mainSpacer.winfo_width() > titleSpacer.winfo_width(): titleSpacer["width"] = mainSpacer.winfo_width() mainSpacer.bind("<Configure>", domain) return strWdg def __connStateCallback(self, conn): """If the connection closes, clear all programs from the list. """ if self._progDict and not conn.isConnected: for prog, progPerms in self._progDict.items(): progPerms.delete() del(self._progDict[prog]) def _updActors(self, actors, isCurrent=True, **kargs): """Perms list of actors updated. """ # print "%s._updActors(%r)" % (self.__class__, actors,) if not isCurrent: return if self._actorList.isSameActors(actors): return if not self._readOnly and self._actorList: self._statusBar.setMsg("Updating actors", severity=RO.Constants.sevWarning, isTemp = True, duration=_NewActorDelay * 1000.0) self._setReadOnly(True) self._updActorTimer.start(_NewActorDelay, self._setReadOnly, False) self._actorList.setActors(actors) # Update lockout and each program self._lockoutWdg.updateActorList() for progPerms in self._progDict.itervalues(): progPerms.updateActorList() # display new header and everything self.sort() def _updPrograms(self, programs, isCurrent=True, **kargs): """Hub's list of registered programs updated. Delete old programs based on this info, but don't add new ones (instead, look for an authList entry for the new program, so we get auth info at the same time). """ if not isCurrent: return # print "_updPrograms(%r)" % (programs,) # raise program names to uppercase programs = [prog.upper() for prog in programs] if self._tuiModel.getProgID().upper() not in programs: # print "my prog=%s is not in programs=%s; currReadOnly=%s" % (prog, programs, self._readOnly) self._setReadOnly(True) # mark unregistered programs anyUnreg = False for prog, progPerms in self._progDict.iteritems(): if prog not in programs: # mark progPerms as unregistered anyUnreg = True progPerms.setRegistered(False) # if read only, then automatically purge (if necessary) and sort if self._readOnly: if anyUnreg: self.purge() self.sort() def _setReadOnly(self, readOnly): """Set read only state. """ readOnly = bool(readOnly) if self._readOnly != readOnly: self._readOnly = readOnly # print "toggling readOnly to", self._readOnly self._lockoutWdg.setReadOnly(self._readOnly) for progPerms in self._progDict.itervalues(): progPerms.setReadOnly(self._readOnly) if self._readOnlyCallback: self._readOnlyCallback(self._readOnly) def _updAuthList(self, progAuthList, isCurrent=True, **kargs): """New authList received. progAuthList is: - program name - 0 or more actorList """ if not isCurrent: return # print "_updAuthList(%r)" % (progAuthList,) prog = progAuthList[0].upper() authActors = progAuthList[1:] if prog == self._tuiModel.getProgID().upper(): # this is info about me (my program); check if I can set permissions readOnly = "perms" not in authActors # print "prog=%s is me; readOnly=%s, currReadOnly=%s, actorList=%s" % (prog, readOnly, self._readOnly, authActors) self._setReadOnly(readOnly) isNew = prog not in self._progDict if isNew: # print "program %s is not in program dict; adding" % (prog,) self._addProg(prog) progPerms = self._progDict[prog] progPerms.setRegistered(True) progPerms.setCurrActors(authActors) def _updLockedActors(self, lockedActors, isCurrent=True, **kargs): """Hub's locked actor list updated. """ if not isCurrent: return self._lockoutWdg.setCurrActors(lockedActors)
class BackgroundKwds(object): """Processes various keywords that are handled in the background. Also verify that we're getting data from the hub (also detects computer sleep) and try to refresh variables if there is a problem. """ def __init__(self, maxTimeErr = 10.0, checkConnInterval = 5.0, maxEntryAge = 60.0, ): """Create BackgroundKwds Inputs: - maxTimeErr: maximum clock error (sec) before a warning is printed - checkConnInterval: interval (sec) at which to check connection - maxEntryAge: maximum age of log entry (sec) """ self.maxTimeErr = float(maxTimeErr) self.checkConnInterval = float(checkConnInterval) self.maxEntryAge = float(maxEntryAge) self.tuiModel = TUI.TUIModel.getModel() self.tccModel = TUI.TCC.TCCModel.getModel() self.connection = self.tuiModel.getConnection() self.dispatcher = self.tuiModel.dispatcher self.didSetUTCMinusTAI = False self.checkConnTimer = Timer() self.clockType = None # set to "UTC" or "TAI" if keeping that time system self.tccModel.utcMinusTAI.addIndexedCallback(self.setUTCMinusTAI, ind=0, callNow=False) self.connection.addStateCallback(self.connCallback, callNow=True) def connCallback(self, conn): """Called when connection changes state When connected check the connection regularly, when not, don't """ if conn.isConnected: self.checkConnTimer.start(self.checkConnInterval, self.checkConnection) self.checkClock() else: self.checkConnTimer.cancel() def checkConnection(self): """Check for aliveness of connection by looking at the time of the last hub message """ doQueue = True try: entryAge = time.time() - self.dispatcher.readUnixTime if entryAge > self.maxEntryAge: self.tuiModel.logMsg( "No data seen in %s seconds; testing the connection" % (self.maxEntryAge,), severity = RO.Constants.sevWarning) cmdVar = RO.KeyVariable.CmdVar( actor = "hub", cmdStr = "version", timeLim = 5.0, dispatcher = self.dispatcher, callFunc=self.checkCmdCallback, ) doQueue = False finally: if doQueue: self.checkConnTimer.start(self.checkConnInterval, self.checkConnection) def checkClock(self): """Check computer clock by asking the TCC for time """ cmdVar = RO.KeyVariable.CmdVar( actor = "tcc", cmdStr = "show time", timeLim = 2.0, dispatcher = self.dispatcher, callFunc = self.checkClockCallback, keyVars = (self.tccModel.tai,), ) def checkCmdCallback(self, msgType, msgDict, cmdVar): if not cmdVar.isDone(): return doQueue = True try: if cmdVar.didFail(): self.connection.disconnect(isOK = False, reason="Connection is dead") doQueue = False TUI.PlaySound.cmdFailed() else: self.dispatcher.refreshAllVar() finally: if doQueue: self.checkConnTimer.start(self.checkConnInterval, self.checkConnection) def checkClockCallback(self, msgType, msgDict, cmdVar): """Callback from TCC "show time" command Determine if clock is keeping UTC, TAI or something else, and act accordingly. """ if not cmdVar.isDone(): return if cmdVar.didFail(): self.tuiModel.logMsg( "clock check failed: tcc show time failed; assuming UTC", severity = RO.Constants.sevError, ) return currTAI = cmdVar.getLastKeyVarData(self.tccModel.tai, ind=0) if currTAI is None: self.tuiModel.logMsg( "clock check failed: current TAI unknown; assuming UTC", severity = RO.Constants.sevError, ) return if not self.didSetUTCMinusTAI: self.tuiModel.logMsg( "clock check failed: UTC-TAI unknown; assuming UTC", severity = RO.Constants.sevError, ) return utcMinusTAI = RO.Astro.Tm.getUTCMinusTAI() currUTC = utcMinusTAI + currTAI RO.Astro.Tm.setClockError(0) clockUTC = RO.Astro.Tm.utcFromPySec() * RO.PhysConst.SecPerDay if abs(clockUTC - currUTC) < 3.0: # clock keeps accurate UTC (as well as we can figure); set time error to 0 self.clockType = "UTC" self.tuiModel.logMsg("Your computer clock is keeping UTC") elif abs(clockUTC - currTAI) < 3.0: # clock keeps accurate TAI (as well as we can figure); set time error to UTC-TAI self.clockType = "TAI" RO.Astro.Tm.setClockError(-utcMinusTAI) self.tuiModel.logMsg("Your computer clock is keeping TAI") else: # clock system unknown or not keeping accurate time; adjust based on current UTC self.clockType = None timeError = clockUTC - currUTC RO.Astro.Tm.setClockError(timeError) self.tuiModel.logMsg( "Your computer clock is off by = %f.1 seconds" % (timeError,), severity = RO.Constants.sevWarning, ) def setUTCMinusTAI(self, utcMinusTAI, isCurrent=1, keyVar=None): """Updates UTC-TAI in RO.Astro.Tm """ if isCurrent and utcMinusTAI != None: RO.Astro.Tm.setUTCMinusTAI(utcMinusTAI) self.didSetUTCMinusTAI = True if self.clockType == "TAI": RO.Astro.Tm.setClockError(-utcMinusTAI)