def __init__(self, manager, identity, config): self._abortTime = None self._timeout = None self._xmlAddresses = None self._finishInitialization = threading.Event() self._initialized = False self._monsters = defaultdict(list) self._requestQueue = deque() self._faxReply = None self._delayMode = threading.Event() self._delayStart = 0 self._faxState = None self._faxCommands = [] self._success = None self._lastXmlUpdate = 0 self._lastFaxCheck = 0 # last request the bot made to FaxBot self._lastRequest, self._lastRequestTime = None, None # last monster in the fax log self._lastFax, self._lastFaxTime = None, None # username of last faxer / last time bot got a message from faxbot self._lastFaxUname, self._lastFaxBotTime = None, None self._lastFaxCheck = 0 super(FaxModule2, self).__init__(manager, identity, config)
def __init__(self, hbSys=None, **kwargs): self.__hb = None self.__registered = threading.Lock() self.__lock = threading.RLock() if hbSys is not None: self.heartbeatRegister(hbSys) super(HeartbeatSubsystem.HeartbeatCapable, self).__init__(**kwargs)
def __init__(self, name, identity, evSys=None, **kwargs): self.__lock = threading.RLock() self.__type = name self.__id = "__" + identity.lower().strip() + "__" self.__ev = None self.__registered = threading.Lock() if evSys is not None: self.eventRegister(evSys) super(EventSubsystem.EventCapable, self).__init__(**kwargs)
def __init__(self, queue): self._log = logging.getLogger("heartbeat") self.queue = queue self._stopEvent = threading.Event() self.id = str(uuid.uuid4()) super(HeartbeatSubsystem._HeartbeatTaskThread, self).__init__(name="Heartbeat-Task")
def __init__(self, parent, identity, iData, config): """ Initialize the BaseClanDungeonChannelManager """ self.__eventLock = threading.RLock() # lock for reading events # LOCK THIS BEFORE # LOCKING self._syncLock self.__initialized = False self.__lastEvents = None self._lastEventCheck = 0 self._logEntryDb = [] printDbLoad = False if self._csvFile is not None: self._logEntryDb = database.csvDatabase(self._csvFile) printDbLoad = True with self.__raidlogDownloadLock: if self.__initialRaidlog is None: rl = ClanRaidLogRequest(iData.session) result = tryRequest(rl, numTries=5, initialDelay=0.5, scaleFactor=2) self.__initialRaidlog = result self.__lastEvents = self.__initialRaidlog super(BaseClanDungeonChannelManager, self).__init__(parent, identity, iData, config) if printDbLoad: self._log.info("Loaded {} entries of data from {}.".format( len(self._logEntryDb), self._csvFile)) self.__initialized = True
def __init__(self, numThreads, period, stopEvent): self._log = logging.getLogger("heartbeat") self._n = numThreads self._t = period self._stopEvent = stopEvent self.queue = Queue.Queue() self._objs = [] self._lock = threading.RLock() self._threads = [] super(HeartbeatSubsystem._HeartbeatMainThread, self).__init__(name="Heartbeat-Main")
def __init__(self, session, props, invMan, db): self._lastOnlineCheck = 0 self._lastCanReceiveItemsCheck = 0 self._lastCanReceiveItems = False self._db = db self._receivedMessages = defaultdict(list) self._clearedItems = dict((iid, False) for iid in _doNotSendItems) logConfig.setFileHandler("mail-handler", 'log/mailhandler.log') self._log = logging.getLogger("mail-handler") self._log.info("---- Mail Handler startup ----") if self._db.version > 1: raise Exception("MailHandler cannot use database version {}" .format(self._db.version)) self._s = session self._m = MailboxManager(session) self._props = props self._invMan = invMan self.__lock = threading.RLock() self._event = threading.Event() # set when thread should do something self._stopEvent = threading.Event() self._name = db.createMailTransactionTable() self._initialize() self._event.set() super(MailHandler, self).__init__(name="MailHandler")
class EventSubsystem(object): """ The class that handles the event subsystem. To use the event subsystem in your class, derive from EventSubsystem.EventCapable. See full documentation in that class. """ _lock = threading.RLock() class EventException(Exception): pass class DuplicateObjectException(Exception): pass class __EventObject(object): def __init__(self, obj, typeString, identityString, callback): self.obj = weakref.ref(obj) self.type = typeString.lower().strip() self.id = identityString.lower().strip() self.callback = callback class EventCapable(EmptyObject): """An event-capable class has two "names": a name, which describes its class, and an identity, which is a string unique to each instance. Event-capable classes can be registered to one EventSubsystem instance at maximum. Registration can either be done automatically in __init__() or using the eventRegister() method. The eventUnregister() method removes the instance from its EventSubsystem. To raise an event, use the _raiseEvent() method. Each event has a subject, which should be a string. A receiver can be specified, meaning only a matching object will be notified of the event. To match by type name, simply specify the type name as the receiver. To match by identity, use __identity__, where identity is the identity name of the object (for example, "__system__"). If the receiver is None, the event will be broadcast to all objects registered to the EventSubsystem. Events also have a data attribute, which is a dict that may hold additional information. When an event is raised and received by an event-capable class, the _eventCallback() method is called, with an EventData struct as the argument. The programmer should check the subject of the event to see if it should be processed. Optionally, the object can send a reply back to the object that raised the event using the _eventReply() method. This does not trigger an event; instead, the EventSubsystem collects all replies into a list, which is returned when _raiseEvent() has completed. Each _eventReply() call will create an EventData object in the returned list. _eventReply() should ONLY be called inside the _eventCallback() function. It is possible to raise an event inside another event. Events are tracked in a stack. Event behavior is single-threaded. Be careful raising events inside threads. The Event Subsystem uses internal locking, so be careful not to cause any deadlocks. """ def __init__(self, name, identity, evSys=None, **kwargs): self.__lock = threading.RLock() self.__type = name self.__id = "__" + identity.lower().strip() + "__" self.__ev = None self.__registered = threading.Lock() if evSys is not None: self.eventRegister(evSys) super(EventSubsystem.EventCapable, self).__init__(**kwargs) def __del__(self): if self.__ev is not None: self.eventUnregister() def __eventCallback(self, eData): self._eventCallback(eData) @property def eventSubsystem(self): return self.__ev def eventRegister(self, evSubsystem): if not self.__registered.acquire(False): raise EventSubsystem.EventException( "Object {} ({}) is already assigned" " to an event subsystem." .format(self.__id, self.__type)) with self.__lock: evSubsystem.registerObject( self, self.__type, self.__id, self.__eventCallback) self.__ev = evSubsystem def eventUnregister(self): with self.__lock: if self.__ev is not None: self.__ev.unregisterObject(self) self.__ev = None try: self.__registered.release() except threading.ThreadError: pass def _raiseEvent(self, subject, receiver=None, data=None, **kwargs): if data is None: data = {} if self.__ev is None: raise EventSubsystem.EventException( "Object {} ({}) has no event subsystem." .format(self.__id, self.__type)) return self.__ev.raiseEvent( self.__type, self.__id, receiver, subject, data) def _eventReply(self, data=None, **kwargs): if data is None: data = {} if self.__ev is None: raise EventSubsystem.EventException( "Object {} ({}) has no event subsystem." .format(self.__id, self.__type)) self.__ev.eventReply(self.__type, self.__id, data) def _eventCallback(self, eData): pass def __init__(self): self._eventObjects = [] self._replyStack = [] self._eventStack = [] self._log = logging.getLogger("events") def _clearDead(self): with self._lock: self._eventObjects = [eo for eo in self._eventObjects if eo.obj() is not None] def registerObject(self, obj, typeString, identityString, callback): with self._lock: newEO = self.__EventObject( obj, typeString, identityString, callback) if any(True for eo in self._eventObjects if eo.id == newEO.id): raise EventSubsystem.DuplicateObjectException( "An object with identity {} is already registered." .format(newEO.id)) if any(True for eo in self._eventObjects if eo.obj() is obj): raise EventSubsystem.DuplicateObjectException( "Duplicate object {} ({}) registered." .format(newEO.id, newEO.type)) self._eventObjects.append(newEO) # print("Registered object {!s} with event subsystem." # .format(obj)) def unregisterObject(self, obj): with self._lock: self._clearDead() if obj is not None: oldSize = len(self._eventObjects) self._eventObjects = [ eo for eo in self._eventObjects if eo.obj() is not obj and obj is not None] sizeDiff = oldSize - len(self._eventObjects) if sizeDiff == 0: raise ValueError("Object {!s} is not registered." .format(obj)) elif sizeDiff > 1: raise Exception( "Internal error: duplicate objects {!s} " "detected in event registry.".format(obj)) def raiseEvent(self, senderType, senderId, receiver, subject, data): with self._lock: if subject is None: raise self.EventException("Null event not allowed.") e = EventData(senderType, senderId, receiver, subject, data) eventDepth = len(self._eventStack) self._log.debug("{}Event raised: {}" .format(" " * eventDepth,e)) self._clearDead() self._eventStack.append(senderId) self._replyStack.append([]) if receiver is not None: receiver = receiver.lower().strip() for eventObj in self._eventObjects: if (receiver is None or receiver == eventObj.type or receiver == eventObj.id): o = eventObj.obj() if o is not None: # may modify self._replies reply = eventObj.callback(e) if reply is not None: self._log.warning("Object {} returned a value " "from its event callback. The " "_eventReply method should be " "used to communicate with the " "calling object." .format(o)) replies = self._replyStack.pop() self._eventStack.pop() return replies def eventReply(self, rType, rId, data): with self._lock: if not self._eventStack: raise EventSubsystem.EventException( "Can't reply outside of event") eventDepth = len(self._eventStack) self._log.debug("{}Received reply: {} ({}): {}" .format(" " * eventDepth, rId, rType, data)) self._replyStack[-1].append(EventData( rType, rId, self._eventStack[-1], "reply", data))
class BaseClanDungeonChannelManager(MultiChannelManager): """ Subclass of MultiChannelManager that incorporates Dungeon chat and Raid Logs. Subclasses of this manager should specialize these functions for individual channel. Modules under this manager are initialized with event data from the clan raid log and also receive periodic updates. Dungeon chat is separated and passed to the process_dungeon extended call, and log data is passed there as well. Periodic log updates are processed as well. """ capabilities = set(['chat', 'inventory', 'admin', 'hobopolis', 'dread']) _csvFile = None # override this in child classes __initialRaidlog = None __raidlogDownloadLock = threading.RLock() _lastChatNum = None delay = 300 def __init__(self, parent, identity, iData, config): """ Initialize the BaseClanDungeonChannelManager """ self.__eventLock = threading.RLock() # lock for reading events # LOCK THIS BEFORE # LOCKING self._syncLock self.__initialized = False self.__lastEvents = None self._lastEventCheck = 0 self._logEntryDb = [] printDbLoad = False if self._csvFile is not None: self._logEntryDb = database.csvDatabase(self._csvFile) printDbLoad = True with self.__raidlogDownloadLock: if self.__initialRaidlog is None: rl = ClanRaidLogRequest(iData.session) result = tryRequest(rl, numTries=5, initialDelay=0.5, scaleFactor=2) self.__initialRaidlog = result self.__lastEvents = self.__initialRaidlog super(BaseClanDungeonChannelManager, self).__init__(parent, identity, iData, config) if printDbLoad: self._log.info("Loaded {} entries of data from {}.".format( len(self._logEntryDb), self._csvFile)) self.__initialized = True def _configure(self, config): """ Additional configuration for the log_check_interval option """ super(BaseClanDungeonChannelManager, self)._configure(config) try: self.delay = min(self.delay, int(config.setdefault('log_check_interval', 15))) except ValueError: raise Exception("Error in module config: " "(BaseClanDungeonChannelManager) " "log_check_interval must be integral") def _moduleInitData(self): """ The initData here is the last read raid log events. """ d = self._filterEvents(self.lastEvents) d['event-db'] = self._logEntryDb return d @property def lastEvents(self): """ get the last-read events """ with self.__eventLock: return copy.deepcopy(self.__lastEvents) @lastEvents.setter def lastEvents(self, val): with self.__eventLock: self.__lastEvents = copy.deepcopy(val) @lastEvents.deleter def lastEvents(self): with self.__eventLock: del self.__lastEvents def _getRaidLog(self, noThrow=True, force=False): """ Access the raid log and store it locally """ with self.__raidlogDownloadLock: if not self.__initialized and not force: return self.lastEvents self._log.debug("Reading clan raid logs...") rl = ClanRaidLogRequest(self.session) result = tryRequest(rl, nothrow=noThrow, numTries=5, initialDelay=0.5, scaleFactor=2) if result is None: self._log.warning("Could not read clan raid logs.") return self.lastEvents with self._syncLock: self._raiseEvent("new_raid_log", None, LogDict(result)) return result def _updateLogs(self, force=False): """ Read new logs and parse them if it is time. Then call the process_log extended call of each module. """ with self.__raidlogDownloadLock: if time.time() - self._lastEventCheck >= self.delay or force: result = self._getRaidLog() return result return self.lastEvents def _processDungeonChat(self, msg, checkNum): """ This function is called when messages are received from Dungeon. Like parseChat, the value checkNum is identical for chats received at the same time. """ replies = [] with self.__raidlogDownloadLock: if self._lastChatNum != checkNum: # get new events self._lastChatNum = checkNum evts = self._updateLogs(force=True) else: evts = self.lastEvents with self.__eventLock: raidlog = self._filterEvents(evts) with self._syncLock: txt = msg['text'] for m in self._modules: mod = m.module printStr = mod.extendedCall('process_dungeon', txt, raidlog) if printStr is not None: replies.extend(printStr.split("\n")) self._syncState() return replies def parseChat(self, msg, checkNum): """ Override of parseChat to split dungeon chat off from "regular" chat """ if self._chatApplies(msg, checkNum): if msg['userName'].lower() == 'dungeon' and msg['userId'] == -2: return self._processDungeonChat(msg, checkNum) else: return self._processChat(msg, checkNum) return [] def cleanup(self): with self.__eventLock: self.__initialized = False MultiChannelManager.cleanup(self) def _heartbeat(self): """ Update logs in heartbeat """ if self.__initialized: self._updateLogs() super(BaseClanDungeonChannelManager, self)._heartbeat() def _eventCallback(self, eData): MultiChannelManager._eventCallback(self, eData) if eData.subject == "new_raid_log": raidlog = dict((k, v) for k, v in eData.data.items()) self.lastEvents = raidlog if not self.__initialized: return self._notifyModulesOfNewRaidLog(raidlog) elif eData.subject == "request_raid_log": if self.lastEvents is not None: self._eventReply(LogDict(self.lastEvents)) def _notifyModulesOfNewRaidLog(self, raidlog): # it's important not to process the log while responding # to chat, so we need a lock here. if not self.__initialized: return # important to keep this ordering for the locks with self.__eventLock: with self._syncLock: self._log.debug("{} received new log".format(self.identity)) self._lastEventCheck = time.time() filteredLog = self._filterEvents(raidlog) self._handleNewRaidlog(filteredLog) for m in self._modules: mod = m.module mod.extendedCall('process_log', filteredLog) self._syncState() def _dbMatchRaidLog(self, raidlog): try: eventList = [] for e in raidlog['events']: e['db-match'] = {} for dbEntry in self._logEntryDb: if dbEntry['category'].strip() == e['category']: if re.search(dbEntry['regex'], e['event']): if not e['db-match']: e['db-match'] = dbEntry else: raise KeyError("Duplicate match in database: " "event {} matches '{}' and " "'{}'".format( e['event'], e['db-match']['regex'], dbEntry['regex'])) eventList.append(e) raidlog['events'] = eventList return raidlog except Exception: print(raidlog) raise ############# Override the following: def _filterEvents(self, raidlog): """ This function is used by subclasses to remove unrelated event information. """ return self._dbMatchRaidLog(raidlog) def _handleNewRaidlog(self, raidlog): """ This function is called when new raidlogs are downloaded. """ pass @abc.abstractmethod def active(self): pass
def __init__(self, s, c, props, inv, configFile, db, exitEvent): """ Initialize the BotSystem """ self._exitEvent = exitEvent self._initialized = False # trigger to stop heartbeat subsystem self._hbStop = threading.Event() # start subsystems try: oldTxt = None hbSys = HeartbeatSubsystem(numThreads=6, period=5, stopEvent=self._hbStop) evSys = EventSubsystem() # initialize subsystems super(BotSystem, self).__init__(name="sys.system", identity="system", evSys=evSys, hbSys=hbSys) if exitEvent.is_set(): sys.exit() # copy arguments self._s = s self._c = c self._props = props self._inv = inv self._db = db self._log = logging.getLogger() # initialize some RunProperties data now that we are logged on self._log.debug("Getting my userId...") r1 = StatusRequest(self._s) d1 = tryRequest(r1) self._props.userId = int(d1['playerid']) self._log.info("Getting my clan...") r2 = UserProfileRequest(self._s, self._props.userId) d2 = tryRequest(r2) self._props.clan = d2.get('clanId', -1) self._log.info("I am a member of clan #{} [{}]!".format( self._props.clan, d2.get('clanName', "Not in any clan"))) # config file stuff self._config = None self._overWriteConfig = False oldTxt = self._loadConfig(configFile) # listen to channels self._initializeChatChannels(self._config) c.getNewChatMessages() # discard old PM's and whatnot # initialize directors self._dir = None iData = InitData(s, c, props, inv, db) self._log.info("Starting communication system...") self._dir = CommunicationDirector(self, iData, self._config['director']) self._lastCheckedChat = 0 self._initialized = True except: self._hbStop.set() raise finally: # rewrite config file (values may be modified by modules/managers) if oldTxt is not None: self._saveConfig(configFile, oldTxt)
def main(curFolder=None, connection=None): import __main__ if curFolder == None: curFolder = os.path.dirname(os.path.abspath(inspect.getfile(__main__))) props = processArgv(sys.argv, curFolder) # set current folder props.connection = connection crashWait = 60 myDb = Database(databaseName) loginWait = 0 log = logging.getLogger() logging.getLogger("requests").setLevel(logging.INFO) logging.getLogger("urllib3").setLevel(logging.INFO) # register signals signal.signal(signal.SIGTERM, signalHandler) signal.signal(signal.SIGINT, signalHandler) try: # windows signals signal.signal(signal.CTRL_C_EVENT, signalHandler) signal.signal(signal.CTRL_BREAK_EVENT, signalHandler) except: pass try: while loginWait >= 0 and not exitEvent.is_set(): ### LOGIN LOOP ### if loginWait > 0: log.info("Sleeping for {} seconds.".format(loginWait)) time.sleep(loginWait) props.refresh() # main section of login loop if exitEvent.is_set(): break (loginWait, fastCrash) = loginLoop(myDb, props) if fastCrash: # fast crash: perform exponential back-off crashWait = min(2 * 60 * 60, crashWait * 2) loginWait = crashWait log.info("New crash wait: {}".format(crashWait)) else: # reset exponential back-off crashWait = 60 _reset_traceback() except: raise finally: # close a bunch of stuff log.info("Main thread done.") # wait for other threads to close threads = [t for t in threading.enumerate() if t.daemon == False] numThreads = len(threads) delayTime = 0 log.info("Waiting for threads to close: {}".format(threads)) while numThreads > 1: try: time.sleep(delayTime) delayTime = 0.25 if props is not None: try: props.close() except: pass threads = [ t for t in threading.enumerate() if t.daemon == False ] numThreadsNew = len(threads) if numThreadsNew != numThreads: log.debug( "Waiting for threads to close: {}".format(threads)) numThreads = numThreadsNew except: pass log.info("-------- System Shutdown --------\n") if loginWait == -2: if props.connection is not None: props.connection.send("restart") else: # restart program log.info("Invoking manual restart.") python = sys.executable os.execl(python, python, *sys.argv) else: if props.connection is not None: props.connection.send("stop") try: props.connection.close() except Exception: pass
import cwbot.util.DebugThreading as threading from cwbot.util.DebugThreading import _reset_traceback from cwbot.processArgv import processArgv from cwbot.sys.BotSystem import BotSystem from cwbot.common.exceptions import ManualException, ManualRestartException, \ FatalError from cwbot.kolextra.manager.ChatManager import ChatManager from cwbot.kolextra.request.SendMessageRequest import SendMessageRequest from cwbot.kolextra.manager.InventoryManager import InventoryManager from cwbot.util.tryRequest import tryRequest from cwbot.database import database from kol.Session import Session import kol.Error from cwbot.sys.database import Database exitEvent = threading.Event() databaseName = 'data/cwbot.db' def openSession(props): """ Log in to the KoL servers. """ log = logging.getLogger() s = Session() s.login(props.userName, props.password) log.info("Logged in.") return s def createChatManager(s): """ Open a new chat manager. """ log = logging.getLogger()
class BaseManager(EventSubsystem.EventCapable, HeartbeatSubsystem.HeartbeatCapable): """ Base class for all manager objects. Every subclass MUST impliment a capabilities attribute that is a list of strings. Managers are the middle tier of processing. The CommunicationDirector holds many managers, and each manager holds many modules. The job of a manager is to filter information. The CommunicationDirector passes every Kmail and Chat to every manager. Each manager filters this information, passing applicable Kmails/Chats to each of its modules. Manager filtering should be "all-or-nothing": Managers should decide if a Kmail/Chat is applicable, and if so, pass it to each of its modules. It is not the job of a manager to determine which of its modules should process which chat/kmail. It is also the manager's job to handle permissions by checking if a user has the required permission before passing chats/kmails to modules. The same applies to checking in-clan status. A manager may also pass supplementary information to its modules, by both supplying information via the _moduleInitData method and possibly through other methods. Managers are also in charge of syncing the state of their constituent modules by periodically calling _syncState(), which utilizes the sqlite3 database. """ __metaclass__ = ManagerMetaClass capabilities = ['inventory', 'chat'] __clanMembers = set([]) __clanNonMembers = {} _syncLock = threading.RLock() # lock for syncing state def __init__(self, parent, identity, iData, config): """ Initialize the BaseManager. When you call this from a derived class, the following occurs: 1. The manager is linked to the Heartbeat and Event subsystems. 2. Various variables are established. 3. The _configure() method is called. 4. The modules in the config map are added to self._modules. 5. The _initialize() method is called. """ self._initialized = False super(BaseManager, self).__init__(name="sys.{}".format(identity), identity=identity, evSys=parent.eventSubsystem, hbSys=parent.heartbeatSubsystem) self.__configureOnline = False self.__initializeOnline = False self._s = iData.session self._c = iData.chatManager logConfig.setFileHandler(identity, "log/{}.log".format(identity)) self._log = logging.getLogger(identity) self._log.info("----- Manager {} startup -----".format(identity)) self._invMan = iData.inventoryManager self._props = iData.properties self._db = iData.database self.identity = identity self.syncTime = 300 self._lastSync = time.time() self._db.createStateTable() self._persist = self._db.loadStateTable(self.identity) self._modules = [] self.__parent = weakref.ref(parent) self._configure(config) self._addModules(config) self._initialize() self._initialized = True def _configure(self, config): """ Perform configuration of the Manager. This should be overridden in derived classes. But be sure to call its parent's _configure() method too. Otherwise, self.syncTime will be set to 300. """ try: self.syncTime = config['sync_interval'] except ValueError: raise Exception("sync_interval must be integral") def _addModules(self, config): """ Dynamically import the modules specified in modules.ini. This should not be overridden. """ base = config['base'] # loop through modules for k, v in config.items(): if isinstance(v, dict): cfg = v perm = toTypeOrNone(v['permission'], str) priority = v['priority'] clanOnly = v['clan_only'] # import class try: ModuleClass = easyImportClass(base, v['type']) except ImportError: raise FatalError("Error importing module/class {0} " "from base {1}. Either the module does " "not exist, or there was an error. To " "check for errors, use the command line " "'python -m {1}.{0}'; the actual path " "may vary.".format(v['type'], base)) self._modules.append( ModuleEntry(ModuleClass, priority, perm, clanOnly, self, k, cfg)) # sort by decreasing priority self._modules.sort(key=lambda x: -x.priority) self._log.info("---- {} creating module instances... ----".format( self.identity)) for m in self._modules: self._log.info( "Creating {0.className} with priority " "{0.priority}, permission {0.permission}.".format(m)) try: m.createInstance() except TypeError as e: self._log.exception("Error!") raise FatalError("Error instantiating class {}: {}".format( m.className, e.args[0])) self._log.info("---- All modules created. ----") def _initialize(self): """ Runs after _addModules. If there is additional initialization to do, you should override this, but be sure to call the parent's _initialize() method to properly initialize the modules. """ self._log.debug("Initializing...") d = self._moduleInitData() self._log.debug("Loaded initialization data.") with self._syncLock: self._log.debug("Checking persistent state...") try: if len(self._persist) == 0: self._persist['__init__'] = "" except ValueError: self._clearPersist() self._log.debug("Preparing to initialize modules...") self._initializeModules(d) self._log.debug("Performing initial state sync...") self._syncState(force=True) def _moduleInitData(self): """ This is the initialization data that is passed when initializing each module. """ return {} def _initializeModules(self, initData): """(Re)initialize processors. If persistent state is present, it is loaded and passed to the module's initialize() method; if absent, the module's initialState property is used instead. If an error occurs, the initialState is used as well and the old state is deleted. """ hbSys = self.heartbeatSubsystem with self._syncLock: for m in self._modules: mod = m.module self._log.info("Initializing {} ({}).".format( mod.id, mod.__class__.__name__)) try: state = self._persist.get(mod.id, None) if state is None: self._log.info("Null state for module {}, using " "default...".format(mod.id)) state = mod.initialState if len(str(state)) > 500: self._log.debug("Initializing module {} ({}) with " "state {{TOO LONG TO FIT}}".format( mod.id, mod.__class__.__name__)) else: self._log.debug("Initializing module {} ({}) with " "state {}".format( mod.id, mod.__class__.__name__, state)) mod.initialize(state, initData) except (KeyboardInterrupt, SystemExit, SyntaxError, FatalError, NameError, ImportError, MemoryError, NotImplementedError, SystemError, TypeError): raise except Exception as e: self._log.exception("ERROR initializing module " "with persistent state") mod.initializationFailed(state, initData, e) mod.heartbeatRegister(hbSys) self._log.info("---- Finished initializing modules ----") def _clearPersist(self): """ Remove all persistent state data. Note that states are periodically synced, so if you don't also reset each module, this will essentially do nothing. """ with self._syncLock: self._db.updateStateTable(self.identity, {}, purge=True) self._persist = self._db.loadStateTable(self.identity) def _syncState(self, force=False): ''' Store persistent data for Modules in the database. ''' with self._syncLock: if self._persist is None: return for m in self._modules: mod = m.module self._persist[mod.id] = mod.state if time.time() - self._lastSync > self.syncTime or force: self._log.debug("Syncing state for {}".format(self.identity)) try: self._db.updateStateTable(self.identity, self._persist) except Exception as e: for k, v in self._persist.items(): try: encode([k, v]) # check if JSON error except: raise ValueError("Error encoding state {} for " "module {}: {}".format( v, k, e.args)) raise self._lastSync = time.time() def checkClan(self, uid): """ Check if a user is in the same clan as the bot or if they are on the whitelist. Returns {} if user is not in clan. Otherwise returns the user record, a dict with keys 'userId', 'userName', 'karma', 'rankName', 'whitelist', and 'inClan'. Note that 'karma' will be zero if the user is whitelisted and outside the clan (which will be indicated by the 'inClan' field equal to False). """ if uid <= 0: return { 'inClan': True, 'userId': uid, 'rankName': 'SPECIAL', 'userName': str(uid), 'karma': 1, 'whitelist': False } info = self.director.clanMemberInfo(uid) return info def cleanup(self): """ run cleanup operations before bot shutdown. This MUST be called before shutting down by the CommunicationDirector. """ with self._syncLock: self._log.info("Cleaning up manager {}...".format(self.identity)) self._log.debug("Cleanup: syncing states...") self._syncState(force=True) self._initialized = False self._persist = None for m in reversed(self._modules): mod = m.module self._log.debug( "Cleanup: unregistering heartbeat for {}...".format(mod.id)) mod.heartbeatUnregister() self._log.debug("Cleanup: unregistering events for {}...".format( mod.id)) mod.eventUnregister() self._log.debug("Cleanup: cleaning up module {}...".format(mod.id)) mod.cleanup() self._log.debug("Unregistering heartbeat...") self.heartbeatUnregister() self._log.debug("Unregistering events...") self.eventUnregister() self._log.info("Done cleaning up manager {}".format(self.identity)) self._modules = None self._log.info("----- Manager shut down. -----\n") @property def director(self): """ Get a reference to the CommunicationDirector. """ parent = self.__parent() if parent is not None: return parent return None @property def session(self): """ Get the current session (for pyKol requests). """ return self._s @property def properties(self): """ Get the current RunProperties (load various information) """ return self._props @property def inventoryManager(self): """ Get the current InventoryManager """ return self._invMan @property def chatManager(self): return self._c def defaultChannel(self): """ Get the default chat channel for this manager. May be overridden in derived classes. If no channel is specified in sendChatMessage(), self.defaultChannel is used. By default, this uses the current chat channel (i.e., not the "listened" channels, the main one). """ return self.chatManager.currentChannel def sendChatMessage(self, text, channel=None, waitForReply=False, raw=False): """ Send a chat message with specified text. If no channel is specified, self.defaultChannel is used. If waitForReply is true, the chatManager will block until response data is loaded; otherwise, the chat is sent asynchronously and no response is available. If raw is true, the chat is sent undecorated; if false, the chat is sanitized to avoid /command injections and is decorated in emote format. """ if channel is None: channel = self.defaultChannel() if channel is None or channel == "DEFAULT": channel = self.chatManager.currentChannel useEmote = not raw return self.director.sendChat(channel, text, waitForReply, useEmote) def whisper(self, uid, text, waitForReply=False): """ Send a private message to the specified user. """ return self.director.whisper(uid, text, waitForReply) def sendKmail(self, message): """ Send a Kmail that is not a reply. message should be a Kmail object from the common.kmailContainer package. """ self.director.sendKmail(message) def parseChat(self, msg, checkNum): """ This function is called by the CommunicationDirector every time a new chat is received. The manager can choose to ignore the chat or to process it. To ignore the chat, just return []. To process it, pass the chat to each module and return a LIST of all the replies that are not None. """ return [] def parseKmail(self, msg): """ Parse Kmail and return any replies in a LIST of KmailResponses in the same fashion as the parseChat method. """ return [] def kmailFailed(self, module, message, exception): """ This is called by the CommunicationDirector if a kmail fails to send for some reason. """ if module is not None: module.extendedCall('message_send_failed', message, exception) def _heartbeat(self): """ By default, the heartbeat calls syncState(), so in derived classes be sure to do that too or call the parent _heartbeat(). """ if self._initialized: self._syncState()
def main(curFolder=None, connection=None): import __main__ if curFolder == None: curFolder = os.path.dirname(os.path.abspath(inspect.getfile(__main__))) props = processArgv(sys.argv, curFolder) # set current folder props.connection = connection crashWait = 60 myDb = Database(databaseName) loginWait = 0 log = logging.getLogger() logging.getLogger("requests").setLevel(logging.INFO) logging.getLogger("urllib3").setLevel(logging.INFO) # register signals signal.signal(signal.SIGTERM, signalHandler) signal.signal(signal.SIGINT, signalHandler) try: # windows signals signal.signal(signal.CTRL_C_EVENT, signalHandler) signal.signal(signal.CTRL_BREAK_EVENT, signalHandler) except: pass try: while loginWait >= 0 and not exitEvent.is_set(): ### LOGIN LOOP ### if loginWait > 0: log.info("Sleeping for {} seconds.".format(loginWait)) time.sleep(loginWait) props.refresh() # main section of login loop if exitEvent.is_set(): break (loginWait, fastCrash) = loginLoop(myDb, props) if fastCrash: # fast crash: perform exponential back-off crashWait = min(2*60*60, crashWait*2) loginWait = crashWait log.info("New crash wait: {}".format(crashWait)) else: # reset exponential back-off crashWait = 60 _reset_traceback() except: raise finally: # close a bunch of stuff log.info("Main thread done.") # wait for other threads to close threads = [t for t in threading.enumerate() if t.daemon == False] numThreads = len(threads) delayTime = 0 log.info("Waiting for threads to close: {}".format(threads)) while numThreads > 1: try: time.sleep(delayTime) delayTime = 0.25 if props is not None: try: props.close() except: pass threads = [t for t in threading.enumerate() if t.daemon == False] numThreadsNew = len(threads) if numThreadsNew != numThreads: log.debug("Waiting for threads to close: {}" .format(threads)) numThreads = numThreadsNew except: pass log.info("-------- System Shutdown --------\n") if loginWait == -2: if props.connection is not None: props.connection.send("restart") else: # restart program log.info("Invoking manual restart.") python = sys.executable os.execl(python, python, *sys.argv) else: if props.connection is not None: props.connection.send("stop") try: props.connection.close() except Exception: pass
class FaxModule2(BaseChatModule): """ A module that handles faxing, including fax lookup for unknown monster codes, reporting on incoming faxes, and reporting what's in the fax machine. Configuration options: faxbot_timeout - time to wait until giving up on a fax request [def. = 90] url_timeout - time to try to load XML page before timing out [def. = 15] [[[[xml]]]] BOTNAME = URL_TO_XML [[[[alias]]]] ALIASNAME = ALIAS (monster alias name) Configuration example: [[[[xml]]]] 1 = http://hogsofdestiny.com/faxbot/faxbot.xml 2 = http://faust.kolbots.com/faustbot.xml 3 = https://sourceforge.net/p/easyfax/code/HEAD/tree/Easyfax.xml?format=raw [[[[success]]]] FaxBot = has copied faustbot = has been delivered Easyfax = fax is ready [[[[alias]]]] lobsterfrogman = lfm # now you can type '!fax lfm' """ requiredCapabilities = ['chat'] _name = "fax" __lock = threading.RLock() _faxWait = 60 _xmlMins = 30 _checkFrequency = 15 _defaultXml = { '1': "http://faust.kolbots.com/faustbot.xml", '2': "https://sourceforge.net/p/easyfax/" "code/HEAD/tree/Easyfax.xml?format=raw" } _defaultSuccess = { 'faustbot': "has been delivered", 'Easyfax': "fax is ready" } def __init__(self, manager, identity, config): self._abortTime = None self._timeout = None self._xmlAddresses = None self._finishInitialization = threading.Event() self._initialized = False self._monsters = defaultdict(list) self._requestQueue = deque() self._faxReply = None self._delayMode = threading.Event() self._delayStart = 0 self._faxState = None self._faxCommands = [] self._success = None self._lastXmlUpdate = 0 self._lastFaxCheck = 0 # last request the bot made to FaxBot self._lastRequest, self._lastRequestTime = None, None # last monster in the fax log self._lastFax, self._lastFaxTime = None, None # username of last faxer / last time bot got a message from faxbot self._lastFaxUname, self._lastFaxBotTime = None, None self._lastFaxCheck = 0 super(FaxModule2, self).__init__(manager, identity, config) def _configure(self, config): try: self._abortTime = int(config.setdefault('faxbot_timeout', 90)) self._timeout = int(config.setdefault('url_timeout', 15)) self._xmlAddresses = config.setdefault('xml', self._defaultXml) success = config.setdefault('success', self._defaultSuccess) self._success = {''.join(k.lower()): v for k, v in success.items()} except ValueError: raise Exception("Fax Module config error: " "faxbot_timeout, url_timeout must be integral") self._alias = config.setdefault('alias', {'lobsterfrogman': 'lfm'}) def initialize(self, state, initData): self._finishInitialization.set() @property def state(self): return {} @property def initialState(self): return {} def getFaxMatch(self, args): '''Look up the monster in the list using fuzzy matching. ''' splitArgs = args.split() # did we force? if any(s for s in splitArgs if s.strip().lower() == "force"): # make a new monster return _FaxMatch(splitArgs[0], True, 'forcing') # make list of all possible names/codes/aliases nameList = {} for k, v in self._monsters.items(): names = set(chain.from_iterable(val.nameList for val in v)) nameList[k] = names simplify = (lambda x: x.replace("'", "").replace("_", " ").replace( "-", " ").lower()) sArgs = simplify(args) # first, check for exact code/name/alias matches matches = [] for k, names in nameList.items(): if any(True for name in names if sArgs == simplify(name)): matches.append(k) if len(matches) == 1: return _FaxMatch(matches[0], False, 'exact match') # next, check for "close" matches scoreDiff = 15 scores = {} for k, names in nameList.items(): score1 = max( fuzz.partial_token_set_ratio(simplify(name), sArgs) for name in names) scores[k] = score1 maxScore = max(scores.values()) fuzzyMatchKeys = set(k for k, v in scores.items() if v >= maxScore - scoreDiff) # also check for args as a subset of string or code detokenize = lambda x: ''.join(re.split(r"'|_|-| ", x)).lower() dArgs = detokenize(args) subsetMatchKeys = set() for k, names in nameList.items(): if any(True for name in names if dArgs in detokenize(name)): subsetMatchKeys.add(k) ls = len(subsetMatchKeys) lf = len(fuzzyMatchKeys) matchKeys = subsetMatchKeys | fuzzyMatchKeys lm = len(matchKeys) if ls == 0 and lf == 1: m = matchKeys.pop() return _FaxMatch(m, False, "fuzzy match") elif lm == 1: m = matchKeys.pop() return _FaxMatch(m, False, "subset match") elif lm > 1 and lm < 6: possibleMatchStr = ", ".join( (self._monsters[k][0].name for k in matchKeys)) return _FaxMatch( None, False, ("Did you mean one of: {}?".format(possibleMatchStr))) elif lm > 1: return _FaxMatch(None, False, ("Matched {} monster names/codes; " "please be more specific. Send \"!fax list\" for" " monster list.".format(ls + lf))) return _FaxMatch( None, False, "No known monster with name/code " "matching '{0}'. " "Use '!fax {0} force' to force, " "or send \"!fax list\" for a list.".format(args)) def checkForNewFax(self, announceInChat=True): """ See if a new fax has arrived, and possibly announce it if it has. """ with self.__lock: self._lastFaxCheck = utcTime() replyStr = None lastFaxTime = self._lastFaxTime lastFaxUname = self._lastFaxUname lastFaxMonster = self._lastFax event = self.updateLastFax() if (self._lastFax is not None and (lastFaxTime != self._lastFaxTime or lastFaxUname != self._lastFaxUname or lastFaxMonster != self._lastFax)): self.log("Received new fax {}".format(event)) return replyStr def updateLastFax(self): """ Update what's in the Fax machine """ with self.__lock: # suppress annoying output from pyKol kol.util.Report.removeOutputSection("*") try: r = ClanLogPartialRequest(self.session) log = self.tryRequest(r, numTries=5, initialDelay=0.25, scaleFactor=1.5) finally: kol.util.Report.addOutputSection("*") faxEvents = [ event for event in log['entries'] if event['type'] == CLAN_LOG_FAX ] lastEvent = None if len(faxEvents) == 0 else faxEvents[0] if lastEvent is None: self._lastFax = None self._lastFaxTime = None self._lastFaxUname = None else: self._lastFax = lastEvent['monster'] lastFaxTimeAz = tz.localize(lastEvent['date']) lastFaxTimeUtc = lastFaxTimeAz.astimezone(utc) self._lastFaxTime = calendar.timegm(lastFaxTimeUtc.timetuple()) self._lastFaxUname = lastEvent['userName'] return lastEvent def printLastFax(self): """ Get the chat text to represent what's in the Fax machine. """ if utcTime() - self._lastFaxCheck >= self._checkFrequency: self.checkForNewFax(False) if self._lastFax is None: return "I can't tell what's in the fax machine." elapsed = utcTime() - self._lastFaxTime timeStr = "{} minutes".format(int((elapsed + 59) // 60)) return ("The fax has been holding a(n) {} for the last {}. " "(Send \"!fax list\" for a list of monsters.)".format( self._lastFax, timeStr)) def faxMonster(self, args, isPM): """Send a request, if not waiting on another request.""" with self.__lock: (monster, force, message) = self.getFaxMatch(args) matches = self._monsters.get(monster, []) if monster is None or not matches: return message if isPM: str1 = "Matched {} ({})\n".format(matches[0].name, message) return str1 + "\n".join( "/w {} {}".format(m.faxbot.name, m.code) for m in matches) if self._delayMode.is_set(): return ("Please wait {} more seconds to request a fax.".format( int(self._faxWait - time.time() + self._delayStart))) if self._requestQueue: return "I am still waiting on my last request." self._requestQueue.extend(matches) return "Requested {} ({})...".format(matches[0].name, message) def _processCommand(self, message, cmd, args): if cmd == "fax": if args.lower() == "list": return self._sendMonsterList(message['userId']) if args != "": isPM = (message['type'] == "private") return self.faxMonster(args, isPM) else: return self.printLastFax() with self.__lock: if self._faxState: if message.get('userId', 0) == self._faxState.requestId: self.log("Received {} PM: {}".format( self._faxState.requestId, message['text'])) self._faxReply = message['text'] return None def _sendMonsterList(self, uid): text = ("Available monsters:\n\n" + "\n".join(sorted(self._monsters.keys()))) self.sendKmail(Kmail(uid, text)) return "Monster list sent." def _refreshMonsterList(self): genLen = lambda gen: sum(1 for _ in gen) entryCount = genLen(chain.from_iterable(self._monsters.values())) self.log("Updating xml... ({} entries)".format(entryCount)) for _, v in self._monsters.items(): v = [ entry for entry in v if entry.faxbot.xml in self._xmlAddresses.values() ] # clear empty entries monsters = defaultdict(list) monsters.update({k: v for k, v in self._monsters.items() if v}) self._monsters = monsters entryCount2 = genLen(chain.from_iterable(self._monsters.values())) if entryCount != entryCount2: self._log("Removed {} entries due to config file mismatch.".format( entryCount - entryCount2)) numTries = 3 for key in sorted(self._xmlAddresses.keys()): address = self._xmlAddresses[key] txt = None for _ in range(numTries): try: txt = urlopen(address, timeout=self._timeout).read() d = xmltodict.parse(txt) except (HTTPError, URLError, socket.timeout, socket.error, ExpatError) as e: self.log("Error loading webpage " "for fax list: {}: {}".format( e.__class__.__name__, e.args)) else: entryCount = genLen( chain.from_iterable(self._monsters.values())) d1 = d[d.keys()[0]] try: faxbot = _Faxbot(d1['botdata']['name'].encode('ascii'), int(d1['botdata']['playerid']), address) except KeyError: continue monsters = d1['monsterlist']['monsterdata'] newMonsters = {} for monster in monsters: mname = unidecode(monster['actual_name']).lower() code = unidecode(monster['command']).lower() name = unidecode(monster['name']) newMonsters[mname] = FaxMonsterEntry( name, code, faxbot) for n, alias in self._alias.items(): if n.lower().strip() in [ mname, code, name.lower().strip() ]: newMonsters[mname].addAlias(alias) for k, v in self._monsters.items(): self._monsters[k] = [ entry for entry in v if entry.faxbot.xml != address ] for mname, monster in newMonsters.items(): self._monsters[mname].append(monster) entryCount2 = genLen( chain.from_iterable(self._monsters.values())) # clear empty entries monsters = defaultdict(list) monsters.update( {k: v for k, v in self._monsters.items() if v}) self._monsters = monsters self.log("Net change of {} entries from {} xml ({} -> {})". format(entryCount2 - entryCount, faxbot.name, entryCount, entryCount2)) break self._lastXmlUpdate = time.time() def _heartbeat(self): if self._finishInitialization.is_set(): self._finishInitialization.clear() self._refreshMonsterList() self._initialized = True if self._initialized: with self.__lock: # are we waiting for a request? if self._faxState: # check if we received a reply request = self._requestQueue[0] if self._faxReply: # check if it matches regex = self._success[''.join( request.faxbot.name.lower())] if re.search(regex, self._faxReply): # matched! self.chat("{} has delivered a(n) {}.".format( request.faxbot.name, request.name)) self._requestQueue.clear() self._delayMode.set() self._delayStart = time.time() self._faxCommands = [] else: # not a match. self.chat("{} reply: {}".format( request.faxbot.name, self._faxReply)) self._requestQueue.popleft() if not self._requestQueue: self.chat("Could not receive fax. " "Try one of: {}".format(", ".join( self._faxCommands))) self._faxCommands = [] self._faxReply = None self._faxState = None else: # no fax reply yet if (time.time() - self._faxState.requestTime > self._abortTime): self.chat("{} did not reply.".format( request.faxbot.name)) self._requestQueue.popleft() self._faxState = None self._faxReply = None if not self._requestQueue: self.chat("Could not receive fax. " "Try one of: {}".format(", ".join( self._faxCommands))) self._faxCommands = [] elif self._delayMode.is_set(): if time.time() - self._delayStart > self._faxWait: self._delayMode.clear() self._delayStart = 0 elif self._requestQueue: request = self._requestQueue[0] self.chat("Requesting {} from {}...".format( request.name, request.faxbot.name)) self._faxState = _FaxState(requestTime=time.time(), requestId=request.faxbot.id) self.whisper(request.faxbot.id, request.code) self._faxCommands.append("/w {} {}".format( request.faxbot.name, request.code)) elif time.time() - self._lastXmlUpdate > 60 * self._xmlMins: self._refreshMonsterList() def _eventCallback(self, eData): s = eData.subject if s == "state": if eData.to is None: self._eventReply( {'warning': '(omitted for general state inquiry)'}) else: self._eventReply(self.state) def _availableCommands(self): return { 'fax': "!fax: check the contents of the fax machine. " "'!fax MONSTERNAME' requests a fax from FaxBot." }
def __init__(self, obj, callback): self.obj = weakref.ref(obj) self.callback = callback self.done = threading.Event() self.stop = threading.Event() self.lock = threading.RLock()
def __init__(self, numThreads, period, stopEvent=threading.Event()): self._log = logging.getLogger("heartbeat") self._thread = self._HeartbeatMainThread(numThreads, period, stopEvent) self._thread.start()
class HeartbeatSubsystem(object): """ The class that handles the heartbeat (simple threading) subsystem. To use the heartbeat subsystem in your class, derive from HeartbeatSubsystem.HeartbeatCapable. See full documentation in that class. """ _lock = threading.RLock() class DuplicateObjectException(Exception): pass class HeartbeatException(Exception): pass class _HeartbeatObject(object): def __init__(self, obj, callback): self.obj = weakref.ref(obj) self.callback = callback self.done = threading.Event() self.stop = threading.Event() self.lock = threading.RLock() class HeartbeatCapable(EmptyObject): """An heartbeat-capable class has a _heartbeat() method, which is called periodically in a separate thread. The frequency of this method call is configurable when constructing the HeartbeatSubsystem. To enable the heartbeat, use the heartbeatRegister() method with the HeartbeatSubsystem object to which the object is bound. Each object may be registered to only one HeartbeatSubsystem. To stop the heartbeat, use the heartbeatUnregister() method. """ def __init__(self, hbSys=None, **kwargs): self.__hb = None self.__registered = threading.Lock() self.__lock = threading.RLock() if hbSys is not None: self.heartbeatRegister(hbSys) super(HeartbeatSubsystem.HeartbeatCapable, self).__init__(**kwargs) def __del__(self): self.heartbeatUnregister() def __heartbeat(self): # a lock is unnecessary here, since the task thread has a lock # already self._heartbeat() @property def heartbeatSubsystem(self): return self.__hb def heartbeatRegister(self, hbSubsystem): if not self.__registered.acquire(False): raise HeartbeatSubsystem.HeartbeatException( "Object {} ({}) is already assigned" " to an event subsystem.".format(self.__id, self.__type)) with self.__lock: hbSubsystem.registerObject(self, self.__heartbeat) self.__hb = hbSubsystem def heartbeatUnregister(self): with self.__lock: if self.__hb is not None: self.__hb.unregisterObject(self) self.__hb = None try: self.__registered.release() except threading.ThreadError: pass def _heartbeat(self): pass class _HeartbeatTaskThread(ExceptionThread): def __init__(self, queue): self._log = logging.getLogger("heartbeat") self.queue = queue self._stopEvent = threading.Event() self.id = str(uuid.uuid4()) super(HeartbeatSubsystem._HeartbeatTaskThread, self).__init__(name="Heartbeat-Task") def stop(self): self._stopEvent.set() def _run(self): self._log.debug("Heartbeat task thread {} started.".format( self.id)) while not self._stopEvent.is_set(): task = None try: task = self.queue.get(True, 0.1) except Queue.Empty: pass if task is not None: with task.lock: if not task.stop.is_set(): obj = task.obj() if obj is not None: task.callback() task.done.set() self.queue.task_done() class _HeartbeatMainThread(ExceptionThread): def __init__(self, numThreads, period, stopEvent): self._log = logging.getLogger("heartbeat") self._n = numThreads self._t = period self._stopEvent = stopEvent self.queue = Queue.Queue() self._objs = [] self._lock = threading.RLock() self._threads = [] super(HeartbeatSubsystem._HeartbeatMainThread, self).__init__(name="Heartbeat-Main") def _run(self): self._initialize() reAddList = deque() try: while not self._stopEvent.is_set(): #time.sleep(0.001) time.sleep(1) self._checkThreadExceptions() self._clearDead() with self._lock: curTime = time.time() for obj in self._objs: o = obj.obj() if o is not None: if obj.done.is_set(): obj.done.clear() reAddList.append((curTime, obj)) while (reAddList and reAddList[0][0] + self._t < curTime): self._enqueue(reAddList[0][1]) reAddList.popleft() finally: for th in self._threads: th.stop() for th in self._threads: self._log.debug("Joining thread {}...".format(th.id)) th.join() def registerObject(self, obj, callback): with self._lock: self._clearDead() newHO = HeartbeatSubsystem._HeartbeatObject(obj, callback) if any(True for ho in self._objs if obj is ho.obj() and obj is not None): raise HeartbeatSubsystem.DuplicateObjectException( "Object {!s} is already registered.".format(obj)) self._objs.append(newHO) self._enqueue(newHO) def unregisterObject(self, obj): with self._lock: self._clearDead() oldSize = len(self._objs) matches = [ ho for ho in self._objs if ho.obj() is obj and obj is not None ] self._objs = [ ho for ho in self._objs if ho.obj() is not obj and obj is not None ] sizeDiff = oldSize - len(self._objs) if sizeDiff == 0: raise ValueError( "Object {!s} is not registered.".format(obj)) elif sizeDiff > 1: raise Exception("Internal error: duplicate objects {!s} " "detected in event registry.".format(obj)) elif len(matches) != 1: raise Exception("Internal error: More than one match " "for object {!s}.".format(obj)) o = matches[0] with o.lock: o.stop.set() def _initialize(self): for _i in range(self._n): newThread = HeartbeatSubsystem._HeartbeatTaskThread(self.queue) newThread.start() self._threads.append(newThread) def _checkThreadExceptions(self): for thread_ in self._threads: if thread_.exception.is_set(): # this will cause an exception thread_.join() def _clearDead(self): with self._lock: self._objs = [o for o in self._objs if o.obj() is not None] def _enqueue(self, obj): self.queue.put_nowait(obj) def __init__(self, numThreads, period, stopEvent=threading.Event()): self._log = logging.getLogger("heartbeat") self._thread = self._HeartbeatMainThread(numThreads, period, stopEvent) self._thread.start() @property def exception(self): return self._thread.exception.is_set() def registerObject(self, obj, callback): self._thread.registerObject(obj, callback) def unregisterObject(self, obj): self._thread.unregisterObject(obj) def raiseException(self): if self.exception: self._thread.join() else: raise Exception("Tried to get heartbeat exception, but there " "is none.") def join(self): self._log.info("Joining heartbeat threads...") self._thread.join() self._log.info("All threads joined.")
class KmailLock(object): lock = threading.RLock()
class InventoryLock(object): lock = threading.RLock()
class FaxModule(BaseChatModule): """ A module that handles faxing, including fax lookup for unknown monster codes, reporting on incoming faxes, and reporting what's in the fax machine. Configuration options: announce - set to true to use announcements (uses lots of bandwidth!) allow_requests - allows !fax MONSTERNAME fax_check_interval - frequency of checking if a new fax arrived [def. = 15] faxbot_timeout - time to wait until giving up on a fax request [def. = 90] url_timeout - time to try to load forum page before timing out [def. = 15] faxbot_id_number - player id number of faxbot [default = 2194132] fax_list_url - url of kolspading faxbot list [def. = http://goo.gl/Q352Q] [[[[alias]]]] ALIASNAME = ALIAS (monster alias name) Configuration example: [[[[alias]]]] lobsterfrogman = lfm # now you can type '!fax lfm' """ requiredCapabilities = ['chat'] _name = "fax" __lock = threading.RLock() _faxWait = 60 _checkFrequency = None _abortTime = None timeout = None faxbot_uid = None fax_list_url = None def __init__(self, manager, identity, config): self._initialized = False self._announce = True self._allowRequests = True self._downloadedFaxList = False self._faxList = {} self._alias = None super(FaxModule, self).__init__(manager, identity, config) # last request the bot made to FaxBot self._lastRequest, self._lastRequestTime = None, None # last monster in the fax log self._lastFax, self._lastFaxTime = None, None # username of last faxer / last time bot got a message from faxbot self._lastFaxUname, self._lastFaxBotTime = None, None self._noMoreFaxesTime = None self._lastFaxCheck = 0 self.updateLastFax() def _configure(self, config): try: self._checkFrequency = int( config.setdefault('fax_check_interval', 15)) self._abortTime = int(config.setdefault('faxbot_timeout', 90)) self.timeout = int(config.setdefault('url_timeout', 15)) self.faxbot_uid = int( config.setdefault('faxbot_id_number',"2194132)) self.fax_lisw_url = config.setdefault('fax_list_url', "http://goo.gl/Q352Q") self._announce = stringToBool(config.setdefault('announce', 'false')) self._lite = stringToBool(config.setdefault('allow_requests', 'true')) except ValueError: raise Exception("Fax Module config error: " "fax_check_interval, faxbot_timeout, " "url_timeout, faxbot_id_number must be integral") self._alias = config.setdefault('alias', {'lobsterfrogman': 'lfm'}) def initialize(self, state, initData): newFaxList = map(FaxMonsterEntry.fromDict, state['faxes']) self._faxList = dict((e.code, e) for e in newFaxList) self._noMoreFaxesTime = None @property def state(self): return {'faxes': map(FaxMonsterEntry.toDict, self._faxList.values())} @property def initialState(self): return {'faxes': []} def initializeFaxList(self): """ download and parse the list of fax monsters from the thread on kolspading.com """ self.log("Initializing fax list...") numTries = 3 success = False for i in range(numTries): try: # download and interpret the page txt = urllib2.urlopen(self.fax_list_url, timeout=self.timeout).read() for c in range(32,128): htmlCode = "&#{};".format(c) txt = txt.replace(htmlCode, chr(c)) matches = re.findall(r'>([^>/]+): /w FaxBot ([^<]+)<', txt) self._faxList = dict((b.strip().lower(), FaxMonsterEntry(a,b)) for a,b in matches) self.log("Found {} available faxes." .format(len(self._faxList))) success = True break except (HTTPError, URLError, socket.timeout, socket.error) as e: self.log("Error loading webpage for fax list: {}: {}" .format(e.__class__.__name__, e.args[0])) if i + 1 != numTries: time.sleep(1) if not success: self.log("Failed to initialize fax list; using backup ({} entries)" .format(len(self._faxList))) for code,alias in self._alias.items(): code = code.strip().lower() if code in self._faxList: self._faxList[code].addAlias(alias) elif len(self._faxList) > 0: raise Exception("Invalid fax alias (no such fax code): " "{} -> {}".format(alias, code)) def checkAbort(self): """ Check if a request is too old and should be aborted """ with self.__lock: if (self._lastRequest is not None and self._noMoreFaxesTime is None): timeDiff = utcTime() - self._lastRequestTime if timeDiff > self._abortTime: self.chat("FaxBot has not replied to my request. " "Please try again.") self.log("Aborting fax request for '{}'" .format(self._lastRequest)) self._lastRequest = None self._lastRequestTime = None def checkForNewFax(self, announceInChat=True): """ See if a new fax has arrived, and possibly announce it if it has. """ with self.__lock: self._lastFaxCheck = utcTime() replyStr = None lastFaxTime = self._lastFaxTime lastFaxUname = self._lastFaxUname lastFaxMonster = self._lastFax event = self.updateLastFax() if (self._lastFax is not None and (lastFaxTime != self._lastFaxTime or lastFaxUname != self._lastFaxUname or lastFaxMonster != self._lastFax)): self.log("Received new fax {}".format(event)) replyStr = ("{} has copied a {} into the fax machine." .format(self._lastFaxUname, self._lastFax)) if announceInChat: self.chat(replyStr) if event['userId'] == self.faxbot_uid: self._lastRequest = None self._lastRequestTime = None if self._noMoreFaxesTime is not None: if utcTime() - self._noMoreFaxesTime > 3600 * 3: self._noMoreFaxesTime = False self._lastRequestTime = None self.checkAbort() return replyStr def faxMonster(self, args, isPM): """Send a request to FaxBot, if not waiting on another request. This function is a wrapper for fax() and handles the fax lookup and faxbot delay. """ with self.__lock: self.checkAbort() last = self._lastFaxBotTime if last is None: last = utcTime() - self._faxWait - 1 if self._lastRequest is not None: return ("I am still waiting for FaxBot to reply " "to my last request.") timeSinceLast = utcTime() - last if timeSinceLast >= self._faxWait or isPM: if len(self._faxList) > 0: return self.faxFromList(args, isPM) monstername = args.split()[0] return self.fax(monstername, monstername, "(fax lookup unavailable) ", isPM) else: return ("Please wait {} more seconds to request a fax." .format(int(self._faxWait - timeSinceLast))) def fax(self, monstercode, monstername, prependText="", isPM=False, force=False): ''' This function actually performs the fax request ''' stripname = re.search(r'(?:Some )?(.*)', monstername).group(1) if isPM: return ("After asking in chat, you can manually request a {} " "with /w FaxBot {}".format(monstername, monstercode)) elif not self._allowRequests: return ("Code to request a {}: " "/w FaxBot {}".format(monstername, monstercode)) with self.__lock: self.log("{}Requesting {}...".format(prependText, monstername)) self.checkForNewFax(self._announce) if (self._lastFax.lower().strip() == stripname.lower().strip() and not force): self.log("{} already in fax.".format(monstercode)) return ("{} There is already a(n) {} in the fax machine. " "Use '!fax {} force' to fax one anyway." .format(prependText, monstername, monstercode)) if self._noMoreFaxesTime is None: self.whisper(self.faxbot_uid, monstercode) self._lastRequest = monstercode self._lastRequestTime = utcTime() return ("{}Requested {}, waiting for reply..." .format(prependText, monstername)) return ("You can manually request a {} with /w FaxBot {}" .format(monstername, monstercode)) def faxFromList(self, args, isPM): '''Look up the monster code in the list using fuzzy matching, then fax it. (Or, if in quiet mode, display its code) ''' splitArgs = args.split() if any(s for s in splitArgs if s.strip().lower() == "force"): return self.fax(splitArgs[0], splitArgs[0], "(forcing) ", isPM, force=True) # first, check for exact code/name/alias matches matches = [entry.code for entry in self._faxList.values() if entry.contains(args)] if len(matches) == 1: return self.fax(matches[0], self._faxList[matches[0]].name, "", isPM) # next, check for "close" matches simplify = (lambda x: x.replace("'", "") .replace("_", " ") .replace("-", " ").lower()) sArgs = simplify(args) scoreDiff = 15 scores = defaultdict(list) # make list of all possible names/codes/aliases allNames = [name for entry in self._faxList.values() for name in entry.nameList] for s in allNames: score1 = fuzz.partial_token_set_ratio(simplify(s), sArgs) scores[score1].append(s) allScores = scores.keys() maxScore = max(allScores) for score in allScores: if score < maxScore - scoreDiff: del scores[score] matches = [] for match in scores.values(): matches.extend(match) fuzzyMatchKeys = set(entry.code for entry in self._faxList.values() for match in matches if entry.contains(match)) # also check for args as a subset of string or code detokenize = lambda x: ''.join(re.split(r"'|_|-| ", x)).lower() dArgs = detokenize(args) matches = [name for name in allNames if dArgs in detokenize(name)] subsetMatchKeys = set(entry.code for entry in self._faxList.values() for match in matches if entry.contains(match)) ls = len(subsetMatchKeys) lf = len(fuzzyMatchKeys) matchKeys = subsetMatchKeys | fuzzyMatchKeys lm = len(matchKeys) if ls == 0 and lf == 1: m = matchKeys.pop() return self.fax(m, self._faxList[m].name, "(fuzzy match) ", isPM) elif lm == 1: m = matchKeys.pop() return self.fax(m, self._faxList[m].name, "(subset match) ", isPM) elif lm > 1 and lm < 6: possibleMatchStr = ", ".join( ("{} ({})".format(self._faxList[k].name,k)) for k in matchKeys) return "Did you mean one of: {}?".format(possibleMatchStr) elif lm > 1: return ("Matched {} monster names/codes; please be more specific." .format(ls + lf)) return ("No known monster with name/code matching '{0}'. " "Use '!fax {0} force' to force, or check the monster list " "at {1} .".format(args, self.fax_list_url)) def updateLastFax(self): """ Update what's in the Fax machine """ with self.__lock: # suppress annoying output from pyKol kol.util.Report.removeOutputSection("*") try: r = ClanLogPartialRequest(self.session) log = self.tryRequest(r, numTries=5, initialDelay=0.25, scaleFactor=1.5) finally: kol.util.Report.addOutputSection("*") faxEvents = [event for event in log['entries'] if event['type'] == CLAN_LOG_FAX] lastEvent = None if len(faxEvents) == 0 else faxEvents[0] if lastEvent is None: self._lastFax = None self._lastFaxTime = None self._lastFaxUname = None else: self._lastFax = lastEvent['monster'] lastFaxTimeAz = tz.localize(lastEvent['date']) lastFaxTimeUtc = lastFaxTimeAz.astimezone(utc) self._lastFaxTime = calendar.timegm(lastFaxTimeUtc.timetuple()) self._lastFaxUname = lastEvent['userName'] return lastEvent def processFaxbotMessage(self, txt): """ Process a PM from FaxBot """ with self.__lock: if "I do not understand your request" in txt: replyTxt = ("FaxBot does not have the requested monster '{}'. " "(Check the list at {} )" .format(self._lastRequest, self.fax_list_url)) self._lastRequest = None self._lastRequestTime = None return replyTxt if "just delivered a fax" in txt: self._lastRequest = None self._lastRequestTime = None return ("FaxBot received the request too early. " "Please try again.") if "try again tomorrow" in txt: self._noMoreFaxesTime = utcTime() txt = ("I'm not allowed to request any more faxes today. " "Request manually with /w FaxBot {}" .format(self._lastRequest)) self._lastRequest = None self._lastRequestTime = utcTime() return txt m = re.search(r'has copied', txt) if m is not None: self._lastRequest = None self._lastRequestTime = None self._lastFaxBotTime = utcTime() # suppress output from checkForNewFax since we are returning # the text, to be output later return self.checkForNewFax(False) self._lastRequest = None self._lastRequestTime = None return "Received message from FaxBot: {}".format(txt) def printLastFax(self): """ Get the chat text to represent what's in the Fax machine. """ if self._lite: if utcTime() - self._lastFaxCheck >= self._checkFrequency: self.checkForNewFax(False) if self._lastFax is None: return "I can't tell what's in the fax machine." elapsed = utcTime() - self._lastFaxTime timeStr = "{} minutes".format(int((elapsed+59) // 60)) return ("The fax has held a(n) {} for the last {}. " "(List of monsters {} )" .format(self._lastFax, timeStr, self.fax_list_url)) def _processCommand(self, message, cmd, args): if cmd == "fax": if args != "": isPM = (message['type'] == "private") return self.faxMonster(args, isPM) else: return self.printLastFax() elif message.get('userId', 0) == self.faxbot_uid: self.log("Received FaxBot PM: {}".format(message['text'])) msg = self.processFaxbotMessage(message['text']) if msg is not None: self.chat(msg) return None def _heartbeat(self): if self._initialized and not self._downloadedFaxList: with self.__lock: self.initializeFaxList() self._downloadedFaxList = True if utcTime() - self._lastFaxCheck >= self._checkFrequency: if self._announce: self.checkForNewFax(True) def _eventCallback(self, eData): s = eData.subject if s == "state": if eData.to is None: self._eventReply({ 'warning': '(omitted for general state inquiry)'}) else: self._eventReply(self.state) elif s == "startup" and eData.fromIdentity == "__system__": self._initialized = True def _availableCommands(self): if self._allowRequests: return {'fax': "!fax: check the contents of the fax machine. " "'!fax MONSTERNAME' requests a fax from FaxBot."} else: return {'fax': "!fax: check the contents of the fax machine. " "'!fax MONSTBRNAME' shows the code to request " "MONSTERNAME from FaxBot."}
def __init__(self, *args, **kwargs): self._exc = None self._exc_info = sys.exc_info self.exception = threading.Event() super(ExceptionThread, self).__init__(*args, **kwargs)