def initialize(self, logger=None): #setup log options if not self.console: logger.stop() if self.logfile: log.startLogging(open(join(self.botdir, self.logfile), 'a'), setStdout=False) # setup global database and databasemanager self.databasemanager = DBManager(self.datadir, self.datafile) self.reloadStage2() #start dbcommittimer # TODO: figure out if actually need this, and what SQLite transaction/journaling mode we should be using Timers._addTimer("_dbcommit", 60*60, self.databasemanager.dbcommit, reps=-1) #every hour (60*60)
class SettingsBase(object): nick = "BurlyBot" altnicks = None # [] nicksuffix = "_" nickservpass = None commandprefix = "!" datadir = "data" debug = False datafile = "BurlyBot.db" enablestate = False encoding = "utf-8" cert = None verify = False console = True logfile = None modules = None # OrderedSet(["core"]) _admins = None servers = None # {} botdir = None configfile = None moduleopts = None # {} databasemanager = None @property def admins(self): return self._admins @admins.setter def admins(self, value): # When we reset defaults, we grab values from SettingsBase... But 'admins' tries to get the property. if isinstance(value, property): return # TODO: Don't know how to handle this more cleanly self._admins = [x.lower() for x in value] #TODO: not sure if the following is needed or not. Class.dict seems to behave strangely def _setDefaults(self): self.altnicks = [] self.modules = OrderedSet(["core"]) self._admins = [] self.moduleopts = {} def __init__(self): self.servers = {} self._setDefaults() def _loadsettings(self): # TODO: need some exception handling for loading JSON try: newsets = load(open(self.configfile, "r")) except ValueError as e: raise ConfigException("Config file (%s) contains errors: %s" "\nTry http://jsonlint.com/ and make sure no trailing commas." % (self.configfile, e)) self.newservers = newservers = [] self.oldservers = oldservers = [] # Only look for options we care about for opt in KEYS_MAIN: if opt in newsets: if opt == "servers": # calculate difference to know which servers to disconnect: oldservers = set(self.servers.iterkeys()) # Create servers and put them in the server map for serveropts in newsets["servers"]: if "serverlabel" not in serveropts: # TODO: instead of raise, create warning and continue loading. print "Missing serverlabel in config. Skipping server" continue label = serveropts["serverlabel"] if label in self.servers: #refresh server settings try: self.servers[label].setup(serveropts) except Exception as e: print "Error in server setup for (%s), server settings may be in inconsistent state. %s" % (label, e) continue else: try: s = Server(serveropts) except Exception as e: print "Error in server setup for (%s), skipping. %s" % (label, e) continue self.servers[label] = s newservers.append(s) try: oldservers.remove(label) #remove new server from old set except KeyError: pass elif opt == "modules": setattr(self, opt, OrderedSet(newsets[opt])) else: setattr(self, opt, newsets[opt]) # store servers for connection/disconnection at a latter time self.newservers = newservers self.oldservers = oldservers def _connect(self, servers): for server in servers: if server.ssl: if not SSL: print "Error: Cannot connect to '%s', pyOpenSSL not installed" % server.serverlabel self.databasemanager.delServer(server.serverlabel) continue try: reactor.connectSSL(server.host, server.port, server._factory, createCertOptions(server)) except Exception as e: print "SSL Error: Cannot connect to '%s' (%s)" % (server.serverlabel, e) self.databasemanager.delServer(server.serverlabel) else: reactor.connectTCP(server.host, server.port, server._factory) def createDatabases(self, servers): for server in servers: self.databasemanager.addServer(server.serverlabel, server.datafile) def _disconnect(self, servers): #NOTE: this is serverlabel for server in servers: print "DISCONNECTING: %s" % server server = self.servers[server] if server.container._botinst: server.container._botinst.quit() server._factory.stopTrying() #callLater delserver so that just incase some modules catch quit or error event, and use DB for it # May cause race condition when connecting to new server that uses same name but different DBfile # Hope someone doesn't do that... reactor.callLater(1.0, self.databasemanager.delServer, server.serverlabel) #remove oldservers from servers dict try: del self.servers[server.serverlabel] except KeyError: print "Warning: tried to remove server that didn't exist" def load(self): self.reloadStage1() def reloadStage1(self): #restore "defaults" for key in KEYS_MAIN: if key == "servers": continue #never nuke servers setattr(self, key, getattr(SettingsBase, key)) self._setDefaults() if self.configfile: #attempt to load user options self._loadsettings() def reloadStage2(self): #disconnect before reloading dispatchers self._disconnect(self.oldservers) # Reset Dispatcher loaded modules # get a list of previously loaded modules so can track stale modules oldmodules = set(Dispatcher.MODULEDICT.keys()) Dispatcher.reset() #create databases so init() can do database things. self.createDatabases(self.newservers) for server in self.servers.itervalues(): server.initializeReload() Dispatcher.showLoadErrors() #compare currently loaded modules to oldmodules oldmodules = oldmodules.difference(set(Dispatcher.MODULEDICT.keys())) #remove oldmodules from sys.modules for module in oldmodules: print "Removing module: %s" % module try: del modules[module] except KeyError: print "WARNING: module was never in modules %s" % module # connect after load dispatchers self._connect(self.newservers) self.oldservers = self.newservers = [] # TODO: when twisted supports good logger, consider allowing per-server logfile # NOTE: logfile is not chat logging # This must be called only once def initialize(self, logger=None): #setup log options if not self.console: logger.stop() if self.logfile: log.startLogging(open(join(self.botdir, self.logfile), 'a'), setStdout=False) # setup global database and databasemanager self.databasemanager = DBManager(self.datadir, self.datafile) self.reloadStage2() #start dbcommittimer # TODO: figure out if actually need this, and what SQLite transaction/journaling mode we should be using Timers._addTimer("_dbcommit", 60*60, self.databasemanager.dbcommit, reps=-1) #every hour (60*60) def saveOptions(self): d = OrderedDict() for key in KEYS_MAIN: if key == "servers": continue val = getattr(self, key) if val: d[key] = val if self.servers: d["servers"] = [serv._getDict() for serv in self.servers.itervalues()] else: EXAMPLE_SERVER = DummyServer(EXAMPLE_OPTS) EXAMPLE_SERVER2 = DummyServer(EXAMPLE_OPTS2) d["servers"] = [EXAMPLE_SERVER._getDict(), EXAMPLE_SERVER2._getDict()] dump(d, open(self.configfile, "wb"), indent=4, separators=(',', ': '), cls=ConfigEncoder) def shutdown(self, relaunch=False): self._disconnect(self.servers.keys()) #stop timers or just not care... Timers._stopall() reactor.callLater(2.0, self.databasemanager.shutdown) # to give time for individual shutdown Dispatcher.unloadModules() reactor.callLater(2.5, reactor.stop) # to give time for individual shutdown # TODO: make sure this works properly # it may act odd on Windows due to execv not replacing current process. if relaunch: register(relaunchfunc, executable, argv) def hardshutdown(self): Timers._stopall() Dispatcher.unloadModules() self.databasemanager.shutdown()