""" System commands """ import traceback import os import datetime import sys import django import twisted from time import time as timemeasure from django.conf import settings from src.server.caches import get_cache_sizes from src.server.sessionhandler import SESSIONS from src.scripts.models import ScriptDB from src.objects.models import ObjectDB from src.players.models import PlayerDB from src.utils import logger, utils, gametime, create, is_pypy, prettytable from src.utils.utils import crop from src.commands.default.muxcommand import MuxCommand # delayed imports _resource = None _idmapper = None _attribute_cache = None # limit symbol import for API __all__ = ("CmdReload", "CmdReset", "CmdShutdown", "CmdPy", "CmdScripts", "CmdObjects", "CmdService", "CmdAbout", "CmdTime", "CmdServerLoad") class CmdReload(MuxCommand): """ reload the server Usage: @reload [reason] This restarts the server. The Portal is not affected. Non-persistent scripts will survive a @reload (use @reset to purge) and at_reload() hooks will be called. """ key = "@reload" locks = "cmd:perm(reload) or perm(Immortals)" help_category = "System" def func(self): """ Reload the system. """ reason = "" if self.args: reason = "(Reason: %s) " % self.args.rstrip(".") SESSIONS.announce_all(" Server restarting %s..." % reason) SESSIONS.server.shutdown(mode='reload') class CmdReset(MuxCommand): """ reset and reboot the server Usage: @reset A cold reboot. This works like a mixture of @reload and @shutdown, - all shutdown hooks will be called and non-persistent scrips will be purged. But the Portal will not be affected and the server will automatically restart again. """ key = "@reset" aliases = ['@reboot'] locks = "cmd:perm(reload) or perm(Immortals)" help_category = "System" def func(self): """ Reload the system. """ SESSIONS.announce_all(" Server restarting ...") SESSIONS.server.shutdown(mode='reset') class CmdShutdown(MuxCommand): """ stop the server completely Usage: @shutdown [announcement] Gracefully shut down both Server and Portal. """ key = "@shutdown" locks = "cmd:perm(shutdown) or perm(Immortals)" help_category = "System" def func(self): "Define function" try: # Only allow shutdown if caller has session self.caller.sessions[0] except Exception: return self.msg('Shutting down server ...') announcement = "\nServer is being SHUT DOWN!\n" if self.args: announcement += "%s\n" % self.args logger.log_infomsg('Server shutdown by %s.' % self.caller.name) SESSIONS.announce_all(announcement) SESSIONS.portal_shutdown() SESSIONS.server.shutdown(mode='shutdown') class CmdPy(MuxCommand): """ execute a snippet of python code Usage: @py Switch: time - output an approximate execution time for Separate multiple commands by ';'. A few variables are made available for convenience in order to offer access to the system (you can import more at execution time). Available variables in @py environment: self, me : caller here : caller.location ev : the evennia API inherits_from(obj, parent) : check object inheritance You can explore The evennia API from inside the game by calling ev.help(), ev.managers.help() etc. {rNote: In the wrong hands this command is a severe security risk. It should only be accessible by trusted server admins/superusers.{n """ key = "@py" aliases = ["!"] locks = "cmd:perm(py) or perm(Immortals)" help_category = "System" def func(self): "hook function" caller = self.caller pycode = self.args if not pycode: string = "Usage: @py " self.msg(string) return # check if caller is a player # import useful variables import ev available_vars = {'self': caller, 'me': caller, 'here': hasattr(caller, "location") and caller.location or None, 'ev': ev, 'inherits_from': utils.inherits_from} try: self.msg(">>> %s" % pycode, raw=True, sessid=self.sessid) except TypeError: self.msg(">>> %s" % pycode, raw=True) mode = "eval" try: try: pycode_compiled = compile(pycode, "", mode) except Exception: mode = "exec" pycode_compiled = compile(pycode, "", mode) duration = "" if "time" in self.switches: t0 = timemeasure() ret = eval(pycode_compiled, {}, available_vars) t1 = timemeasure() duration = " (%.4f ms)" % ((t1 - t0) * 1000) else: ret = eval(pycode_compiled, {}, available_vars) if mode == "eval": ret = "{n<<< %s%s" % (str(ret), duration) else: ret = "{n<<< Done.%s" % duration except Exception: errlist = traceback.format_exc().split('\n') if len(errlist) > 4: errlist = errlist[4:] ret = "\n".join("{n<<< %s" % line for line in errlist if line) if ret is not None: try: self.msg(ret, sessid=self.sessid) except TypeError: self.msg(ret) # helper function. Kept outside so it can be imported and run # by other commands. def format_script_list(scripts): "Takes a list of scripts and formats the output." if not scripts: return "" table = prettytable.PrettyTable(["{wdbref", "{wobj", "{wkey", "{wintval", "{wnext", "{wrept", "{wdb", "{wtypeclass", "{wdesc"], align='r') table.align = 'r' for script in scripts: nextrep = script.time_until_next_repeat() table.add_row([script.id, script.obj.key if (hasattr(script, 'obj') and script.obj) else "", script.key, script.interval if script.interval > 0 else "--", "%ss" % nextrep if nextrep else "--", "%i" % script.repeats if script.repeats else "--", "*" if script.persistent else "-", script.typeclass_path.rsplit('.', 1)[-1], crop(script.desc, width=20)]) return "%s" % table class CmdScripts(MuxCommand): """ list and manage all running scripts Usage: @scripts[/switches] [#dbref, key, script.path or ] Switches: start - start a script (must supply a script path) stop - stops an existing script kill - kills a script - without running its cleanup hooks validate - run a validation on the script(s) If no switches are given, this command just views all active scripts. The argument can be either an object, at which point it will be searched for all scripts defined on it, or an script name or #dbref. For using the /stop switch, a unique script #dbref is required since whole classes of scripts often have the same name. Use @script for managing commands on objects. """ key = "@scripts" aliases = ["@globalscript", "@listscripts"] locks = "cmd:perm(listscripts) or perm(Wizards)" help_category = "System" def func(self): "implement method" caller = self.caller args = self.args string = "" if args: if "start" in self.switches: # global script-start mode new_script = create.create_script(args) if new_script: caller.msg("Global script %s was started successfully." % args) else: caller.msg("Global script %s could not start correctly. See logs." % args) return # test first if this is a script match scripts = ScriptDB.objects.get_all_scripts(key=args) if not scripts: # try to find an object instead. objects = ObjectDB.objects.object_search(args) if objects: scripts = [] for obj in objects: # get all scripts on the object(s) scripts.extend(ScriptDB.objects.get_all_scripts_on_obj(obj)) else: # we want all scripts. scripts = ScriptDB.objects.get_all_scripts() if not scripts: caller.msg("No scripts are running.") return if not scripts: string = "No scripts found with a key '%s', or on an object named '%s'." % (args, args) caller.msg(string) return if self.switches and self.switches[0] in ('stop', 'del', 'delete', 'kill'): # we want to delete something if not scripts: string = "No scripts/objects matching '%s'. " % args string += "Be more specific." elif len(scripts) == 1: # we have a unique match! if 'kill' in self.switches: string = "Killing script '%s'" % scripts[0].key scripts[0].stop(kill=True) else: string = "Stopping script '%s'." % scripts[0].key scripts[0].stop() #import pdb #pdb.set_trace() ScriptDB.objects.validate() #just to be sure all is synced else: # multiple matches. string = "Multiple script matches. Please refine your search:\n" string += format_script_list(scripts) elif self.switches and self.switches[0] in ("validate", "valid", "val"): # run validation on all found scripts nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts) string = "Validated %s scripts. " % ScriptDB.objects.all().count() string += "Started %s and stopped %s scripts." % (nr_started, nr_stopped) else: # No stopping or validation. We just want to view things. string = format_script_list(scripts) caller.msg(string) class CmdObjects(MuxCommand): """ statistics on objects in the database Usage: @objects [] Gives statictics on objects in database as well as a list of latest objects in database. If not given, defaults to 10. """ key = "@objects" aliases = ["@listobjects", "@listobjs", '@stats', '@db'] locks = "cmd:perm(listobjects) or perm(Builders)" help_category = "System" def func(self): "Implement the command" caller = self.caller if self.args and self.args.isdigit(): nlim = int(self.args) else: nlim = 10 nobjs = ObjectDB.objects.count() base_char_typeclass = settings.BASE_CHARACTER_TYPECLASS nchars = ObjectDB.objects.filter(db_typeclass_path=base_char_typeclass).count() nrooms = ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=base_char_typeclass).count() nexits = ObjectDB.objects.filter(db_location__isnull=False, db_destination__isnull=False).count() nother = nobjs - nchars - nrooms - nexits # total object sum table totaltable = prettytable.PrettyTable(["{wtype", "{wcomment", "{wcount", "{w%%"]) totaltable.align = 'l' totaltable.add_row(["Characters", "(BASE_CHARACTER_TYPECLASS)", nchars, "%.2f" % ((float(nchars) / nobjs) * 100)]) totaltable.add_row(["Rooms", "(location=None)", nrooms, "%.2f" % ((float(nrooms) / nobjs) * 100)]) totaltable.add_row(["Exits", "(destination!=None)", nexits, "%.2f" % ((float(nexits) / nobjs) * 100)]) totaltable.add_row(["Other", "", nother, "%.2f" % ((float(nother) / nobjs) * 100)]) # typeclass table typetable = prettytable.PrettyTable(["{wtypeclass", "{wcount", "{w%%"]) typetable.align = 'l' dbtotals = ObjectDB.objects.object_totals() for path, count in dbtotals.items(): typetable.add_row([path, count, "%.2f" % ((float(count) / nobjs) * 100)]) # last N table objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim):] latesttable = prettytable.PrettyTable(["{wcreated", "{wdbref", "{wname", "{wtypeclass"]) latesttable.align = 'l' for obj in objs: latesttable.add_row([utils.datetime_format(obj.date_created), obj.dbref, obj.key, obj.typeclass.path]) string = "\n{wObject subtype totals (out of %i Objects):{n\n%s" % (nobjs, totaltable) string += "\n{wObject typeclass distribution:{n\n%s" % typetable string += "\n{wLast %s Objects created:{n\n%s" % (min(nobjs, nlim), latesttable) caller.msg(string) class CmdPlayers(MuxCommand): """ list all registered players Usage: @players [nr] Lists statistics about the Players registered with the game. It will list the amount of latest registered players If not given, defaults to 10. """ key = "@players" aliases = ["@listplayers"] locks = "cmd:perm(listplayers) or perm(Wizards)" def func(self): "List the players" caller = self.caller if self.args and self.args.is_digit(): nlim = int(self.args) else: nlim = 10 nplayers = PlayerDB.objects.count() # typeclass table dbtotals = PlayerDB.objects.object_totals() typetable = prettytable.PrettyTable(["{wtypeclass", "{wcount", "{w%%"]) typetable.align = 'l' for path, count in dbtotals.items(): typetable.add_row([path, count, "%.2f" % ((float(count) / nplayers) * 100)]) # last N table plyrs = PlayerDB.objects.all().order_by("db_date_created")[max(0, nplayers - nlim):] latesttable = prettytable.PrettyTable(["{wcreated", "{wdbref", "{wname", "{wtypeclass"]) latesttable.align = 'l' for ply in plyrs: latesttable.add_row([utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.typeclass.path]) string = "\n{wPlayer typeclass distribution:{n\n%s" % typetable string += "\n{wLast %s Players created:{n\n%s" % (min(nplayers, nlim), latesttable) caller.msg(string) class CmdService(MuxCommand): """ manage system services Usage: @service[/switch] Switches: list - shows all available services (default) start - activates or reactivate a service stop - stops/inactivate a service (can often be restarted) delete - tries to permanently remove a service Service management system. Allows for the listing, starting, and stopping of services. If no switches are given, services will be listed. Note that to operate on the service you have to supply the full (green or red) name as given in the list. """ key = "@service" aliases = ["@services"] locks = "cmd:perm(service) or perm(Immortals)" help_category = "System" def func(self): "Implement command" caller = self.caller switches = self.switches if switches and switches[0] not in ("list", "start", "stop", "delete"): caller.msg("Usage: @service/ [servicename]") return # get all services sessions = caller.sessions if not sessions: return service_collection = SESSIONS.server.services if not switches or switches[0] == "list": # Just display the list of installed services and their # status, then exit. table = prettytable.PrettyTable(["{wService{n (use @services/start|stop|delete)", "{wstatus"]) table.align = 'l' for service in service_collection.services: table.add_row([service.name, service.running and "{gRunning" or "{rNot Running"]) caller.msg(str(table)) return # Get the service to start / stop try: service = service_collection.getServiceNamed(self.args) except Exception: string = 'Invalid service name. This command is case-sensitive. ' string += 'See @service/list for valid service name (enter the full name exactly).' caller.msg(string) return if switches[0] in ("stop", "delete"): # Stopping/killing a service gracefully closes it and disconnects # any connections (if applicable). delmode = switches[0] == "delete" if not service.running: caller.msg('That service is not currently running.') return if service.name[:7] == 'Evennia': if delmode: caller.msg("You cannot remove a core Evennia service (named 'Evennia***').") return string = "You seem to be shutting down a core Evennia service (named 'Evennia***'). Note that" string += "stopping some TCP port services will *not* disconnect users *already*" string += "connected on those ports, but *may* instead cause spurious errors for them. To " string += "safely and permanently remove ports, change settings file and restart the server." caller.msg(string) if delmode: service.stopService() service_collection.removeService(service) caller.msg("Stopped and removed service '%s'." % self.args) else: service.stopService() caller.msg("Stopped service '%s'." % self.args) return if switches[0] == "start": #Starts a service. if service.running: caller.msg('That service is already running.') return caller.msg("Starting service '%s'." % self.args) service.startService() class CmdAbout(MuxCommand): """ show Evennia info Usage: @about Display info about the game engine. """ key = "@about" aliases = "@version" locks = "cmd:all()" help_category = "System" def func(self): "Show the version" try: import south sversion = "{wSouth{n %s" % south.__version__ except ImportError: sversion = "{wSouth{n " string = """ {cEvennia{n %s{n MUD/MUX/MU* development system {wLicence{n BSD 3-Clause Licence {wWeb{n http://www.evennia.com {wIrc{n #evennia on FreeNode {wForum{n http://www.evennia.com/discussions {wMaintainer{n (2010-) Griatch (griatch AT gmail DOT com) {wMaintainer{n (2006-10) Greg Taylor {wOS{n %s {wPython{n %s {wDjango{n %s {wTwisted{n %s %s """ % (utils.get_evennia_version(), os.name, sys.version.split()[0], django.get_version(), twisted.version.short(), sversion) self.caller.msg(string) class CmdTime(MuxCommand): """ show server time statistics Usage: @time List Server time statistics such as uptime and the current time stamp. """ key = "@time" aliases = "@uptime" locks = "cmd:perm(time) or perm(Players)" help_category = "System" def func(self): "Show server time data in a table." table = prettytable.PrettyTable(["{wserver time statistic","{wtime"]) table.align = 'l' table.add_row(["Current server uptime", utils.time_format(gametime.uptime(), 3)]) table.add_row(["Total server running time", utils.time_format(gametime.runtime(), 2)]) table.add_row(["Total in-game time (realtime x %g" % (gametime.TIMEFACTOR), utils.time_format(gametime.gametime(), 2)]) table.add_row(["Server time stamp", datetime.datetime.now()]) self.caller.msg(str(table)) class CmdServerLoad(MuxCommand): """ show server load and memory statistics Usage: @serverload This command shows server load statistics and dynamic memory usage. Some Important statistics in the table: {wServer load{n is an average of processor usage. It's usually between 0 (no usage) and 1 (100% usage), but may also be temporarily higher if your computer has multiple CPU cores. The {wResident/Virtual memory{n displays the total memory used by the server process. Evennia {wcaches{n all retrieved database entities when they are loaded by use of the idmapper functionality. This allows Evennia to maintain the same instances of an entity and allowing non-persistent storage schemes. The total amount of cached objects are displayed plus a breakdown of database object types. Finally, {wAttributes{n are cached on-demand for speed. The total amount of memory used for this type of cache is also displayed. """ key = "@server" aliases = ["@serverload", "@serverprocess"] locks = "cmd:perm(list) or perm(Immortals)" help_category = "System" def func(self): "Show list." caller = self.caller # display active processes if not utils.host_os_is('posix'): string = "Process listings are only available under Linux/Unix." caller.msg(string) return global _resource, _idmapper if not _resource: import resource as _resource if not _idmapper: from src.utils.idmapper import base as _idmapper import resource loadavg = os.getloadavg() psize = _resource.getpagesize() pid = os.getpid() rmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "rss")).read()) / 1024.0 # resident memory vmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "vsz")).read()) / 1024.0 # virtual memory pmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "%mem")).read()) # percent of resident memory to total rusage = resource.getrusage(resource.RUSAGE_SELF) # load table loadtable = prettytable.PrettyTable(["property", "statistic"]) loadtable.align = 'l' loadtable.add_row(["Server load (1 min)", "%g" % loadavg[0]]) loadtable.add_row(["Process ID", "%g" % pid]), loadtable.add_row(["Bytes per page", "%g " % psize]) loadtable.add_row(["CPU time used (total)", "%s (%gs)" % (utils.time_format(rusage.ru_utime), rusage.ru_utime)]) loadtable.add_row(["CPU time used (user)", "%s (%gs)" % (utils.time_format(rusage.ru_stime), rusage.ru_stime)]) loadtable.add_row(["Memory usage","%g MB (%g%%)" % (rmem, pmem)]) loadtable.add_row(["Virtual address space\n {x(resident+swap+caching){n", "%g MB" % vmem]) loadtable.add_row(["Page faults", "%g hard, %g soft, %g swapouts" % (rusage.ru_majflt, rusage.ru_minflt, rusage.ru_nswap)]) loadtable.add_row(["Disk I/O", "%g reads, %g writes" % (rusage.ru_inblock, rusage.ru_oublock)]) loadtable.add_row(["Network I/O", "%g in, %g out" % (rusage.ru_msgrcv, rusage.ru_msgsnd)]) loadtable.add_row(["Context switching", "%g vol, %g forced, %g signals" % (rusage.ru_nvcsw, rusage.ru_nivcsw, rusage.ru_nsignals)]) string = "{wServer CPU and Memory load:{n\n%s" % loadtable if not is_pypy: # Cache size measurements are not available on PyPy # because it lacks sys.getsizeof # object cache size cachedict = _idmapper.cache_size() totcache = cachedict["_total"] sorted_cache = sorted([(key, tup[0], tup[1]) for key, tup in cachedict.items() if key !="_total" and tup[0] > 0], key=lambda tup: tup[2], reverse=True) memtable = prettytable.PrettyTable(["entity name", "number", "cache (MB)", "idmapper %%"]) memtable.align = 'l' for tup in sorted_cache: memtable.add_row([tup[0], "%i" % tup[1], "%5.2f" % tup[2], "%.2f" % (float(tup[2] / totcache[1]) * 100)]) # get sizes of other caches attr_cache_info, prop_cache_info = get_cache_sizes() string += "\n{w Entity idmapper cache usage:{n %5.2f MB (%i items)\n%s" % (totcache[1], totcache[0], memtable) string += "\n{w On-entity Attribute cache usage:{n %5.2f MB (%i attrs)" % (attr_cache_info[1], attr_cache_info[0]) string += "\n{w On-entity Property cache usage:{n %5.2f MB (%i props)" % (prop_cache_info[1], prop_cache_info[0]) base_mem = vmem - totcache[1] - attr_cache_info[1] - prop_cache_info[1] string += "\n{w Base Server usage (virtmem-idmapper-attrcache-propcache):{n %5.2f MB" % base_mem caller.msg(string)