mirror of
https://github.com/evennia/evennia.git
synced 2026-04-07 00:45:22 +02:00
CmdTime has two table outputs that are not aligned in the same width. This change aligns the tables to the same width of 78.
1182 lines
40 KiB
Python
1182 lines
40 KiB
Python
"""
|
|
|
|
System commands
|
|
|
|
"""
|
|
|
|
|
|
import code
|
|
import traceback
|
|
import os
|
|
import io
|
|
import datetime
|
|
import sys
|
|
import django
|
|
import twisted
|
|
import time
|
|
|
|
from django.conf import settings
|
|
from django.core.paginator import Paginator
|
|
from evennia.server.sessionhandler import SESSIONS
|
|
from evennia.scripts.models import ScriptDB
|
|
from evennia.objects.models import ObjectDB
|
|
from evennia.accounts.models import AccountDB
|
|
from evennia.utils import logger, utils, gametime, create, search
|
|
from evennia.utils.eveditor import EvEditor
|
|
from evennia.utils.evtable import EvTable
|
|
from evennia.utils.evmore import EvMore
|
|
from evennia.utils.utils import crop, class_from_module
|
|
|
|
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
|
|
|
# delayed imports
|
|
_RESOURCE = None
|
|
_IDMAPPER = None
|
|
|
|
# limit symbol import for API
|
|
__all__ = (
|
|
"CmdReload",
|
|
"CmdReset",
|
|
"CmdShutdown",
|
|
"CmdPy",
|
|
"CmdScripts",
|
|
"CmdObjects",
|
|
"CmdService",
|
|
"CmdAbout",
|
|
"CmdTime",
|
|
"CmdServerLoad",
|
|
)
|
|
|
|
|
|
class CmdReload(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
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"
|
|
aliases = ["restart"]
|
|
locks = "cmd:perm(reload) or perm(Developer)"
|
|
help_category = "System"
|
|
|
|
def func(self):
|
|
"""
|
|
Reload the system.
|
|
"""
|
|
reason = ""
|
|
if self.args:
|
|
reason = "(Reason: %s) " % self.args.rstrip(".")
|
|
SESSIONS.announce_all(" Server restart initiated %s..." % reason)
|
|
SESSIONS.portal_restart_server()
|
|
|
|
|
|
class CmdReset(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
reset and reboot the server
|
|
|
|
Usage:
|
|
reset
|
|
|
|
Notes:
|
|
For normal updating you are recommended to use reload rather
|
|
than this command. Use shutdown for a complete stop of
|
|
everything.
|
|
|
|
This emulates a cold reboot of the Server component of Evennia.
|
|
The difference to shutdown is that the Server will auto-reboot
|
|
and that it does not affect the Portal, so no users will be
|
|
disconnected. Contrary to reload however, all shutdown hooks will
|
|
be called and any non-database saved scripts, ndb-attributes,
|
|
cmdsets etc will be wiped.
|
|
|
|
"""
|
|
|
|
key = "reset"
|
|
aliases = ["reboot"]
|
|
locks = "cmd:perm(reload) or perm(Developer)"
|
|
help_category = "System"
|
|
|
|
def func(self):
|
|
"""
|
|
Reload the system.
|
|
"""
|
|
SESSIONS.announce_all(" Server resetting/restarting ...")
|
|
SESSIONS.portal_reset_server()
|
|
|
|
|
|
class CmdShutdown(COMMAND_DEFAULT_CLASS):
|
|
|
|
"""
|
|
stop the server completely
|
|
|
|
Usage:
|
|
shutdown [announcement]
|
|
|
|
Gracefully shut down both Server and Portal.
|
|
"""
|
|
|
|
key = "shutdown"
|
|
locks = "cmd:perm(shutdown) or perm(Developer)"
|
|
help_category = "System"
|
|
|
|
def func(self):
|
|
"""Define function"""
|
|
# Only allow shutdown if caller has session
|
|
if not self.caller.sessions.get():
|
|
return
|
|
self.msg("Shutting down server ...")
|
|
announcement = "\nServer is being SHUT DOWN!\n"
|
|
if self.args:
|
|
announcement += "%s\n" % self.args
|
|
logger.log_info("Server shutdown by %s." % self.caller.name)
|
|
SESSIONS.announce_all(announcement)
|
|
SESSIONS.portal_shutdown()
|
|
|
|
|
|
def _py_load(caller):
|
|
return ""
|
|
|
|
|
|
def _py_code(caller, buf):
|
|
"""
|
|
Execute the buffer.
|
|
"""
|
|
measure_time = caller.db._py_measure_time
|
|
client_raw = caller.db._py_clientraw
|
|
string = "Executing code%s ..." % (" (measure timing)" if measure_time else "")
|
|
caller.msg(string)
|
|
_run_code_snippet(
|
|
caller, buf, mode="exec", measure_time=measure_time, client_raw=client_raw, show_input=False
|
|
)
|
|
return True
|
|
|
|
|
|
def _py_quit(caller):
|
|
del caller.db._py_measure_time
|
|
caller.msg("Exited the code editor.")
|
|
|
|
|
|
def _run_code_snippet(
|
|
caller, pycode, mode="eval", measure_time=False, client_raw=False, show_input=True
|
|
):
|
|
"""
|
|
Run code and try to display information to the caller.
|
|
|
|
Args:
|
|
caller (Object): The caller.
|
|
pycode (str): The Python code to run.
|
|
measure_time (bool, optional): Should we measure the time of execution?
|
|
client_raw (bool, optional): Should we turn off all client-specific escaping?
|
|
show_input (bookl, optional): Should we display the input?
|
|
|
|
"""
|
|
# Try to retrieve the session
|
|
session = caller
|
|
if hasattr(caller, "sessions"):
|
|
sessions = caller.sessions.all()
|
|
|
|
available_vars = evennia_local_vars(caller)
|
|
|
|
if show_input:
|
|
for session in sessions:
|
|
try:
|
|
caller.msg(">>> %s" % pycode, session=session, options={"raw": True})
|
|
except TypeError:
|
|
caller.msg(">>> %s" % pycode, options={"raw": True})
|
|
|
|
try:
|
|
# reroute standard output to game client console
|
|
old_stdout = sys.stdout
|
|
old_stderr = sys.stderr
|
|
|
|
class FakeStd:
|
|
def __init__(self, caller):
|
|
self.caller = caller
|
|
|
|
def write(self, string):
|
|
self.caller.msg(string.rsplit("\n", 1)[0])
|
|
|
|
fake_std = FakeStd(caller)
|
|
sys.stdout = fake_std
|
|
sys.stderr = fake_std
|
|
|
|
try:
|
|
pycode_compiled = compile(pycode, "", mode)
|
|
except Exception:
|
|
mode = "exec"
|
|
pycode_compiled = compile(pycode, "", mode)
|
|
|
|
duration = ""
|
|
if measure_time:
|
|
t0 = time.time()
|
|
ret = eval(pycode_compiled, {}, available_vars)
|
|
t1 = time.time()
|
|
duration = " (runtime ~ %.4f ms)" % ((t1 - t0) * 1000)
|
|
caller.msg(duration)
|
|
else:
|
|
ret = eval(pycode_compiled, {}, available_vars)
|
|
|
|
except Exception:
|
|
errlist = traceback.format_exc().split("\n")
|
|
if len(errlist) > 4:
|
|
errlist = errlist[4:]
|
|
ret = "\n".join("%s" % line for line in errlist if line)
|
|
finally:
|
|
# return to old stdout
|
|
sys.stdout = old_stdout
|
|
sys.stderr = old_stderr
|
|
|
|
if ret is None:
|
|
return
|
|
elif isinstance(ret, tuple):
|
|
# we must convert here to allow msg to pass it (a tuple is confused
|
|
# with a outputfunc structure)
|
|
ret = str(ret)
|
|
|
|
for session in sessions:
|
|
try:
|
|
caller.msg(ret, session=session, options={"raw": True, "client_raw": client_raw})
|
|
except TypeError:
|
|
caller.msg(ret, options={"raw": True, "client_raw": client_raw})
|
|
|
|
|
|
def evennia_local_vars(caller):
|
|
"""Return Evennia local variables usable in the py command as a dictionary."""
|
|
import evennia
|
|
|
|
return {
|
|
"self": caller,
|
|
"me": caller,
|
|
"here": getattr(caller, "location", None),
|
|
"evennia": evennia,
|
|
"ev": evennia,
|
|
"inherits_from": utils.inherits_from,
|
|
}
|
|
|
|
|
|
class EvenniaPythonConsole(code.InteractiveConsole):
|
|
|
|
"""Evennia wrapper around a Python interactive console."""
|
|
|
|
def __init__(self, caller):
|
|
super().__init__(evennia_local_vars(caller))
|
|
self.caller = caller
|
|
|
|
def write(self, string):
|
|
"""Don't send to stderr, send to self.caller."""
|
|
self.caller.msg(string)
|
|
|
|
def push(self, line):
|
|
"""Push some code, whether complete or not."""
|
|
old_stdout = sys.stdout
|
|
old_stderr = sys.stderr
|
|
|
|
class FakeStd:
|
|
def __init__(self, caller):
|
|
self.caller = caller
|
|
|
|
def write(self, string):
|
|
self.caller.msg(string.split("\n", 1)[0])
|
|
|
|
fake_std = FakeStd(self.caller)
|
|
sys.stdout = fake_std
|
|
sys.stderr = fake_std
|
|
result = None
|
|
try:
|
|
result = super().push(line)
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
sys.stderr = old_stderr
|
|
return result
|
|
|
|
|
|
class CmdPy(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
execute a snippet of python code
|
|
|
|
Usage:
|
|
py [cmd]
|
|
py/edit
|
|
py/time <cmd>
|
|
py/clientraw <cmd>
|
|
py/noecho
|
|
|
|
Switches:
|
|
time - output an approximate execution time for <cmd>
|
|
edit - open a code editor for multi-line code experimentation
|
|
clientraw - turn off all client-specific escaping. Note that this may
|
|
lead to different output depending on prototocol (such as angular brackets
|
|
being parsed as HTML in the webclient but not in telnet clients)
|
|
noecho - in Python console mode, turn off the input echo (e.g. if your client
|
|
does this for you already)
|
|
|
|
Without argument, open a Python console in-game. This is a full console,
|
|
accepting multi-line Python code for testing and debugging. Type `exit()` to
|
|
return to the game. If Evennia is reloaded, the console will be closed.
|
|
|
|
Enter a line of instruction after the 'py' command to execute it
|
|
immediately. Separate multiple commands by ';' or open the code editor
|
|
using the /edit switch (all lines added in editor will be executed
|
|
immediately when closing or using the execute command in the editor).
|
|
|
|
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
|
|
evennia : the evennia API
|
|
inherits_from(obj, parent) : check object inheritance
|
|
|
|
You can explore The evennia API from inside the game by calling
|
|
the `__doc__` property on entities:
|
|
py evennia.__doc__
|
|
py evennia.managers.__doc__
|
|
|
|
|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 = ["!"]
|
|
switch_options = ("time", "edit", "clientraw", "noecho")
|
|
locks = "cmd:perm(py) or perm(Developer)"
|
|
help_category = "System"
|
|
|
|
def func(self):
|
|
"""hook function"""
|
|
|
|
caller = self.caller
|
|
pycode = self.args
|
|
|
|
noecho = "noecho" in self.switches
|
|
|
|
if "edit" in self.switches:
|
|
caller.db._py_measure_time = "time" in self.switches
|
|
caller.db._py_clientraw = "clientraw" in self.switches
|
|
EvEditor(
|
|
self.caller,
|
|
loadfunc=_py_load,
|
|
savefunc=_py_code,
|
|
quitfunc=_py_quit,
|
|
key="Python exec: :w or :!",
|
|
persistent=True,
|
|
codefunc=_py_code,
|
|
)
|
|
return
|
|
|
|
if not pycode:
|
|
# Run in interactive mode
|
|
console = EvenniaPythonConsole(self.caller)
|
|
banner = (
|
|
"|gEvennia Interactive Python mode{echomode}\n"
|
|
"Python {version} on {platform}".format(
|
|
echomode=" (no echoing of prompts)" if noecho else "",
|
|
version=sys.version,
|
|
platform=sys.platform,
|
|
)
|
|
)
|
|
self.msg(banner)
|
|
line = ""
|
|
main_prompt = "|x[py mode - quit() to exit]|n"
|
|
prompt = main_prompt
|
|
while line.lower() not in ("exit", "exit()"):
|
|
try:
|
|
line = yield (prompt)
|
|
if noecho:
|
|
prompt = "..." if console.push(line) else main_prompt
|
|
else:
|
|
if line:
|
|
self.caller.msg(f">>> {line}")
|
|
prompt = line if console.push(line) else main_prompt
|
|
except SystemExit:
|
|
break
|
|
self.msg("|gClosing the Python console.|n")
|
|
return
|
|
|
|
_run_code_snippet(
|
|
caller,
|
|
self.args,
|
|
measure_time="time" in self.switches,
|
|
client_raw="clientraw" in self.switches,
|
|
)
|
|
|
|
|
|
class ScriptEvMore(EvMore):
|
|
"""
|
|
Listing 1000+ Scripts can be very slow and memory-consuming. So
|
|
we use this custom EvMore child to build en EvTable only for
|
|
each page of the list.
|
|
|
|
"""
|
|
|
|
def init_pages(self, scripts):
|
|
"""Prepare the script list pagination"""
|
|
script_pages = Paginator(scripts, max(1, int(self.height / 2)))
|
|
super().init_pages(script_pages)
|
|
|
|
def page_formatter(self, scripts):
|
|
"""Takes a page of scripts and formats the output
|
|
into an EvTable."""
|
|
|
|
if not scripts:
|
|
return "<No scripts>"
|
|
|
|
table = EvTable(
|
|
"|wdbref|n",
|
|
"|wobj|n",
|
|
"|wkey|n",
|
|
"|wintval|n",
|
|
"|wnext|n",
|
|
"|wrept|n",
|
|
"|wdb",
|
|
"|wtypeclass|n",
|
|
"|wdesc|n",
|
|
align="r",
|
|
border="tablecols",
|
|
width=self.width,
|
|
)
|
|
|
|
for script in scripts:
|
|
|
|
nextrep = script.time_until_next_repeat()
|
|
if nextrep is None:
|
|
nextrep = "PAUSED" if script.db._paused_time else "--"
|
|
else:
|
|
nextrep = "%ss" % nextrep
|
|
|
|
maxrepeat = script.repeats
|
|
remaining = script.remaining_repeats() or 0
|
|
if maxrepeat:
|
|
rept = "%i/%i" % (maxrepeat - remaining, maxrepeat)
|
|
else:
|
|
rept = "-/-"
|
|
|
|
table.add_row(
|
|
script.id,
|
|
f"{script.obj.key}({script.obj.dbref})"
|
|
if (hasattr(script, "obj") and script.obj)
|
|
else "<Global>",
|
|
script.key,
|
|
script.interval if script.interval > 0 else "--",
|
|
nextrep,
|
|
rept,
|
|
"*" if script.persistent else "-",
|
|
script.typeclass_path.rsplit(".", 1)[-1],
|
|
crop(script.desc, width=20),
|
|
)
|
|
|
|
return str(table)
|
|
|
|
|
|
class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
list and manage all running scripts
|
|
|
|
Usage:
|
|
scripts[/switches] [#dbref, key, script.path or <obj>]
|
|
|
|
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 a 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"]
|
|
switch_options = ("start", "stop", "kill", "validate")
|
|
locks = "cmd:perm(listscripts) or perm(Admin)"
|
|
help_category = "System"
|
|
|
|
excluded_typeclass_paths = ["evennia.prototypes.prototypes.DbPrototype"]
|
|
|
|
def func(self):
|
|
"""implement method"""
|
|
|
|
caller = self.caller
|
|
args = self.args
|
|
|
|
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
|
|
# filter any found scripts by tag category.
|
|
scripts = scripts.exclude(db_typeclass_path__in=self.excluded_typeclass_paths)
|
|
|
|
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 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 # DEBUG
|
|
# pdb.set_trace() # DEBUG
|
|
ScriptDB.objects.validate() # just to be sure all is synced
|
|
caller.msg(string)
|
|
else:
|
|
# multiple matches.
|
|
ScriptEvMore(caller, scripts, session=self.session)
|
|
caller.msg("Multiple script matches. Please refine your search")
|
|
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)
|
|
caller.msg(string)
|
|
else:
|
|
# No stopping or validation. We just want to view things.
|
|
ScriptEvMore(caller, scripts.order_by("id"), session=self.session)
|
|
|
|
|
|
class CmdObjects(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
statistics on objects in the database
|
|
|
|
Usage:
|
|
objects [<nr>]
|
|
|
|
Gives statictics on objects in database as well as
|
|
a list of <nr> latest objects in database. If not
|
|
given, <nr> defaults to 10.
|
|
"""
|
|
|
|
key = "objects"
|
|
aliases = ["listobjects", "listobjs", "stats", "db"]
|
|
locks = "cmd:perm(listobjects) or perm(Builder)"
|
|
help_category = "System"
|
|
|
|
def func(self):
|
|
"""Implement the command"""
|
|
|
|
caller = self.caller
|
|
nlim = int(self.args) if self.args and self.args.isdigit() else 10
|
|
nobjs = ObjectDB.objects.count()
|
|
Character = class_from_module(settings.BASE_CHARACTER_TYPECLASS)
|
|
nchars = Character.objects.all_family().count()
|
|
Room = class_from_module(settings.BASE_ROOM_TYPECLASS)
|
|
nrooms = Room.objects.all_family().count()
|
|
Exit = class_from_module(settings.BASE_EXIT_TYPECLASS)
|
|
nexits = Exit.objects.all_family().count()
|
|
nother = nobjs - nchars - nrooms - nexits
|
|
nobjs = nobjs or 1 # fix zero-div error with empty database
|
|
|
|
# total object sum table
|
|
totaltable = self.styled_table(
|
|
"|wtype|n", "|wcomment|n", "|wcount|n", "|w%|n", border="table", align="l"
|
|
)
|
|
totaltable.align = "l"
|
|
totaltable.add_row(
|
|
"Characters",
|
|
"(BASE_CHARACTER_TYPECLASS + children)",
|
|
nchars,
|
|
"%.2f" % ((float(nchars) / nobjs) * 100),
|
|
)
|
|
totaltable.add_row(
|
|
"Rooms",
|
|
"(BASE_ROOM_TYPECLASS + children)",
|
|
nrooms,
|
|
"%.2f" % ((float(nrooms) / nobjs) * 100),
|
|
)
|
|
totaltable.add_row(
|
|
"Exits",
|
|
"(BASE_EXIT_TYPECLASS + children)",
|
|
nexits,
|
|
"%.2f" % ((float(nexits) / nobjs) * 100),
|
|
)
|
|
totaltable.add_row("Other", "", nother, "%.2f" % ((float(nother) / nobjs) * 100))
|
|
|
|
# typeclass table
|
|
typetable = self.styled_table(
|
|
"|wtypeclass|n", "|wcount|n", "|w%|n", border="table", align="l"
|
|
)
|
|
typetable.align = "l"
|
|
dbtotals = ObjectDB.objects.get_typeclass_totals()
|
|
for stat in dbtotals:
|
|
typetable.add_row(
|
|
stat.get("typeclass", "<error>"),
|
|
stat.get("count", -1),
|
|
"%.2f" % stat.get("percent", -1),
|
|
)
|
|
|
|
# last N table
|
|
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim) :]
|
|
latesttable = self.styled_table(
|
|
"|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="table"
|
|
)
|
|
latesttable.align = "l"
|
|
for obj in objs:
|
|
latesttable.add_row(
|
|
utils.datetime_format(obj.date_created), obj.dbref, obj.key, obj.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 CmdAccounts(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
Manage registered accounts
|
|
|
|
Usage:
|
|
accounts [nr]
|
|
accounts/delete <name or #id> [: reason]
|
|
|
|
Switches:
|
|
delete - delete an account from the server
|
|
|
|
By default, lists statistics about the Accounts registered with the game.
|
|
It will list the <nr> amount of latest registered accounts
|
|
If not given, <nr> defaults to 10.
|
|
"""
|
|
|
|
key = "accounts"
|
|
aliases = ["account", "listaccounts"]
|
|
switch_options = ("delete",)
|
|
locks = "cmd:perm(listaccounts) or perm(Admin)"
|
|
help_category = "System"
|
|
|
|
def func(self):
|
|
"""List the accounts"""
|
|
|
|
caller = self.caller
|
|
args = self.args
|
|
|
|
if "delete" in self.switches:
|
|
account = getattr(caller, "account")
|
|
if not account or not account.check_permstring("Developer"):
|
|
caller.msg("You are not allowed to delete accounts.")
|
|
return
|
|
if not args:
|
|
caller.msg("Usage: accounts/delete <name or #id> [: reason]")
|
|
return
|
|
reason = ""
|
|
if ":" in args:
|
|
args, reason = [arg.strip() for arg in args.split(":", 1)]
|
|
# We use account_search since we want to be sure to find also accounts
|
|
# that lack characters.
|
|
accounts = search.account_search(args)
|
|
if not accounts:
|
|
self.msg("Could not find an account by that name.")
|
|
return
|
|
if len(accounts) > 1:
|
|
string = "There were multiple matches:\n"
|
|
string += "\n".join(" %s %s" % (account.id, account.key) for account in accounts)
|
|
self.msg(string)
|
|
return
|
|
account = accounts.first()
|
|
if not account.access(caller, "delete"):
|
|
self.msg("You don't have the permissions to delete that account.")
|
|
return
|
|
username = account.username
|
|
# ask for confirmation
|
|
confirm = (
|
|
"It is often better to block access to an account rather than to delete it. "
|
|
"|yAre you sure you want to permanently delete "
|
|
"account '|n{}|y'|n yes/[no]?".format(username)
|
|
)
|
|
answer = yield (confirm)
|
|
if answer.lower() not in ("y", "yes"):
|
|
caller.msg("Canceled deletion.")
|
|
return
|
|
|
|
# Boot the account then delete it.
|
|
self.msg("Informing and disconnecting account ...")
|
|
string = "\nYour account '%s' is being *permanently* deleted.\n" % username
|
|
if reason:
|
|
string += " Reason given:\n '%s'" % reason
|
|
account.msg(string)
|
|
logger.log_sec(
|
|
"Account Deleted: %s (Reason: %s, Caller: %s, IP: %s)."
|
|
% (account, reason, caller, self.session.address)
|
|
)
|
|
account.delete()
|
|
self.msg("Account %s was successfully deleted." % username)
|
|
return
|
|
|
|
# No switches, default to displaying a list of accounts.
|
|
if self.args and self.args.isdigit():
|
|
nlim = int(self.args)
|
|
else:
|
|
nlim = 10
|
|
|
|
naccounts = AccountDB.objects.count()
|
|
|
|
# typeclass table
|
|
dbtotals = AccountDB.objects.object_totals()
|
|
typetable = self.styled_table(
|
|
"|wtypeclass|n", "|wcount|n", "|w%%|n", border="cells", align="l"
|
|
)
|
|
for path, count in dbtotals.items():
|
|
typetable.add_row(path, count, "%.2f" % ((float(count) / naccounts) * 100))
|
|
# last N table
|
|
plyrs = AccountDB.objects.all().order_by("db_date_created")[max(0, naccounts - nlim) :]
|
|
latesttable = self.styled_table(
|
|
"|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", border="cells", align="l"
|
|
)
|
|
for ply in plyrs:
|
|
latesttable.add_row(
|
|
utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.path
|
|
)
|
|
|
|
string = "\n|wAccount typeclass distribution:|n\n%s" % typetable
|
|
string += "\n|wLast %s Accounts created:|n\n%s" % (min(naccounts, nlim), latesttable)
|
|
caller.msg(string)
|
|
|
|
|
|
class CmdService(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
manage system services
|
|
|
|
Usage:
|
|
service[/switch] <service>
|
|
|
|
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"]
|
|
switch_options = ("list", "start", "stop", "delete")
|
|
locks = "cmd:perm(service) or perm(Developer)"
|
|
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/<list|start|stop|delete> [servicename]")
|
|
return
|
|
|
|
# get all services
|
|
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 = self.styled_table(
|
|
"|wService|n (use services/start|stop|delete)", "|wstatus", 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 = ("|RYou seem to be shutting down a core Evennia "
|
|
"service (named 'Evennia*').\nNote that stopping "
|
|
"some TCP port services will *not* disconnect users "
|
|
"*already* connected on those ports, but *may* "
|
|
"instead cause spurious errors for them.\nTo safely "
|
|
"and permanently remove ports, change settings file "
|
|
"and restart the server.|n\n")
|
|
caller.msg(string)
|
|
|
|
if delmode:
|
|
service.stopService()
|
|
service_collection.removeService(service)
|
|
caller.msg("|gStopped and removed service '%s'.|n" % self.args)
|
|
else:
|
|
caller.msg(f"Stopping service '{self.args}'...")
|
|
try:
|
|
service.stopService()
|
|
except Exception as err:
|
|
caller.msg(f"|rErrors were reported when stopping this service{err}.\n"
|
|
"If there are remaining problems, try reloading "
|
|
"or rebooting the server.")
|
|
caller.msg("|g... Stopped service '%s'.|n" % self.args)
|
|
return
|
|
|
|
if switches[0] == "start":
|
|
# Attempt to start a service.
|
|
if service.running:
|
|
caller.msg("That service is already running.")
|
|
return
|
|
caller.msg(f"Starting service '{self.args}' ...")
|
|
try:
|
|
service.startService()
|
|
except Exception as err:
|
|
caller.msg(f"|rErrors were reported when starting this service{err}.\n"
|
|
"If there are remaining problems, try reloading the server, changing the "
|
|
"settings if it's a non-standard service.|n")
|
|
caller.msg("|gService started.|n")
|
|
|
|
|
|
class CmdAbout(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
show Evennia info
|
|
|
|
Usage:
|
|
about
|
|
|
|
Display info about the game engine.
|
|
"""
|
|
|
|
key = "about"
|
|
aliases = "version"
|
|
locks = "cmd:all()"
|
|
help_category = "System"
|
|
|
|
def func(self):
|
|
"""Display information about server or target"""
|
|
|
|
string = """
|
|
|cEvennia|n MU* development system
|
|
|
|
|wEvennia version|n: {version}
|
|
|wOS|n: {os}
|
|
|wPython|n: {python}
|
|
|wTwisted|n: {twisted}
|
|
|wDjango|n: {django}
|
|
|
|
|wLicence|n https://opensource.org/licenses/BSD-3-Clause
|
|
|wWeb|n http://www.evennia.com
|
|
|wIrc|n #evennia on irc.freenode.net:6667
|
|
|wForum|n http://www.evennia.com/discussions
|
|
|wMaintainer|n (2010-) Griatch (griatch AT gmail DOT com)
|
|
|wMaintainer|n (2006-10) Greg Taylor
|
|
|
|
""".format(
|
|
version=utils.get_evennia_version(),
|
|
os=os.name,
|
|
python=sys.version.split()[0],
|
|
twisted=twisted.version.short(),
|
|
django=django.get_version(),
|
|
)
|
|
self.caller.msg(string)
|
|
|
|
|
|
class CmdTime(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
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(Player)"
|
|
help_category = "System"
|
|
|
|
def func(self):
|
|
"""Show server time data in a table."""
|
|
table1 = self.styled_table("|wServer time", "", align="l", width=78)
|
|
table1.add_row("Current uptime", utils.time_format(gametime.uptime(), 3))
|
|
table1.add_row("Portal uptime", utils.time_format(gametime.portal_uptime(), 3))
|
|
table1.add_row("Total runtime", utils.time_format(gametime.runtime(), 2))
|
|
table1.add_row("First start", datetime.datetime.fromtimestamp(gametime.server_epoch()))
|
|
table1.add_row("Current time", datetime.datetime.now())
|
|
table1.reformat_column(0, width=30)
|
|
table2 = self.styled_table(
|
|
"|wIn-Game time",
|
|
"|wReal time x %g" % gametime.TIMEFACTOR,
|
|
align="l",
|
|
width=78,
|
|
border_top=0,
|
|
)
|
|
epochtxt = "Epoch (%s)" % ("from settings" if settings.TIME_GAME_EPOCH else "server start")
|
|
table2.add_row(epochtxt, datetime.datetime.fromtimestamp(gametime.game_epoch()))
|
|
table2.add_row("Total time passed:", utils.time_format(gametime.gametime(), 2))
|
|
table2.add_row(
|
|
"Current time ", datetime.datetime.fromtimestamp(gametime.gametime(absolute=True))
|
|
)
|
|
table2.reformat_column(0, width=30)
|
|
self.caller.msg(str(table1) + "\n" + str(table2))
|
|
|
|
|
|
class CmdServerLoad(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
show server load and memory statistics
|
|
|
|
Usage:
|
|
server[/mem]
|
|
|
|
Switches:
|
|
mem - return only a string of the current memory usage
|
|
flushmem - flush the idmapper cache
|
|
|
|
This command shows server load statistics and dynamic memory
|
|
usage. It also allows to flush the cache of accessed database
|
|
objects.
|
|
|
|
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.
|
|
|
|
The |wflushmem|n switch allows to flush the object cache. Please
|
|
note that due to how Python's memory management works, releasing
|
|
caches may not show you a lower Residual/Virtual memory footprint,
|
|
the released memory will instead be re-used by the program.
|
|
|
|
"""
|
|
|
|
key = "server"
|
|
aliases = ["serverload", "serverprocess"]
|
|
switch_options = ("mem", "flushmem")
|
|
locks = "cmd:perm(list) or perm(Developer)"
|
|
help_category = "System"
|
|
|
|
def func(self):
|
|
"""Show list."""
|
|
|
|
global _IDMAPPER
|
|
if not _IDMAPPER:
|
|
from evennia.utils.idmapper import models as _IDMAPPER
|
|
|
|
if "flushmem" in self.switches:
|
|
# flush the cache
|
|
prev, _ = _IDMAPPER.cache_size()
|
|
nflushed = _IDMAPPER.flush_cache()
|
|
now, _ = _IDMAPPER.cache_size()
|
|
string = (
|
|
"The Idmapper cache freed |w{idmapper}|n database objects.\n"
|
|
"The Python garbage collector freed |w{gc}|n Python instances total."
|
|
)
|
|
self.caller.msg(string.format(idmapper=(prev - now), gc=nflushed))
|
|
return
|
|
|
|
# display active processes
|
|
|
|
os_windows = os.name == "nt"
|
|
pid = os.getpid()
|
|
|
|
if os_windows:
|
|
# Windows requires the psutil module to even get paltry
|
|
# statistics like this (it's pretty much worthless,
|
|
# unfortunately, since it's not specific to the process) /rant
|
|
try:
|
|
import psutil
|
|
|
|
has_psutil = True
|
|
except ImportError:
|
|
has_psutil = False
|
|
|
|
if has_psutil:
|
|
loadavg = psutil.cpu_percent()
|
|
_mem = psutil.virtual_memory()
|
|
rmem = _mem.used / (1000.0 * 1000)
|
|
pmem = _mem.percent
|
|
|
|
if "mem" in self.switches:
|
|
string = "Total computer memory usage: |w%g|n MB (%g%%)"
|
|
self.caller.msg(string % (rmem, pmem))
|
|
return
|
|
# Display table
|
|
loadtable = self.styled_table("property", "statistic", align="l")
|
|
loadtable.add_row("Total CPU load", "%g %%" % loadavg)
|
|
loadtable.add_row("Total computer memory usage", "%g MB (%g%%)" % (rmem, pmem))
|
|
loadtable.add_row("Process ID", "%g" % pid),
|
|
else:
|
|
loadtable = (
|
|
"Not available on Windows without 'psutil' library "
|
|
"(install with |wpip install psutil|n)."
|
|
)
|
|
|
|
else:
|
|
# Linux / BSD (OSX) - proper pid-based statistics
|
|
|
|
global _RESOURCE
|
|
if not _RESOURCE:
|
|
import resource as _RESOURCE
|
|
|
|
loadavg = os.getloadavg()[0]
|
|
rmem = (
|
|
float(os.popen("ps -p %d -o %s | tail -1" % (pid, "rss")).read()) / 1000.0
|
|
) # resident memory
|
|
vmem = (
|
|
float(os.popen("ps -p %d -o %s | tail -1" % (pid, "vsz")).read()) / 1000.0
|
|
) # virtual memory
|
|
pmem = float(
|
|
os.popen("ps -p %d -o %s | tail -1" % (pid, "%mem")).read()
|
|
) # % of resident memory to total
|
|
rusage = _RESOURCE.getrusage(_RESOURCE.RUSAGE_SELF)
|
|
|
|
if "mem" in self.switches:
|
|
string = "Memory usage: RMEM: |w%g|n MB (%g%%), VMEM (res+swap+cache): |w%g|n MB."
|
|
self.caller.msg(string % (rmem, pmem, vmem))
|
|
return
|
|
|
|
loadtable = self.styled_table("property", "statistic", align="l")
|
|
loadtable.add_row("Server load (1 min)", "%g" % loadavg)
|
|
loadtable.add_row("Process ID", "%g" % pid),
|
|
loadtable.add_row("Memory usage", "%g MB (%g%%)" % (rmem, pmem))
|
|
loadtable.add_row("Virtual address space", "")
|
|
loadtable.add_row("|x(resident+swap+caching)|n", "%g MB" % vmem)
|
|
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(
|
|
"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),
|
|
)
|
|
|
|
# os-generic
|
|
|
|
string = "|wServer CPU and Memory load:|n\n%s" % loadtable
|
|
|
|
# object cache count (note that sys.getsiseof is not called so this works for pypy too.
|
|
total_num, cachedict = _IDMAPPER.cache_size()
|
|
sorted_cache = sorted(
|
|
[(key, num) for key, num in cachedict.items() if num > 0],
|
|
key=lambda tup: tup[1],
|
|
reverse=True,
|
|
)
|
|
memtable = self.styled_table("entity name", "number", "idmapper %", align="l")
|
|
for tup in sorted_cache:
|
|
memtable.add_row(tup[0], "%i" % tup[1], "%.2f" % (float(tup[1]) / total_num * 100))
|
|
|
|
string += "\n|w Entity idmapper cache:|n %i items\n%s" % (total_num, memtable)
|
|
|
|
# return to caller
|
|
self.caller.msg(string)
|
|
|
|
|
|
class CmdTickers(COMMAND_DEFAULT_CLASS):
|
|
"""
|
|
View running tickers
|
|
|
|
Usage:
|
|
tickers
|
|
|
|
Note: Tickers are created, stopped and manipulated in Python code
|
|
using the TickerHandler. This is merely a convenience function for
|
|
inspecting the current status.
|
|
|
|
"""
|
|
|
|
key = "tickers"
|
|
help_category = "System"
|
|
locks = "cmd:perm(tickers) or perm(Builder)"
|
|
|
|
def func(self):
|
|
from evennia import TICKER_HANDLER
|
|
|
|
all_subs = TICKER_HANDLER.all_display()
|
|
if not all_subs:
|
|
self.caller.msg("No tickers are currently active.")
|
|
return
|
|
table = self.styled_table("interval (s)", "object", "path/methodname", "idstring", "db")
|
|
for sub in all_subs:
|
|
table.add_row(
|
|
sub[3],
|
|
"%s%s"
|
|
% (
|
|
sub[0] or "[None]",
|
|
sub[0] and " (#%s)" % (sub[0].id if hasattr(sub[0], "id") else "") or "",
|
|
),
|
|
sub[1] if sub[1] else sub[2],
|
|
sub[4] or "[Unset]",
|
|
"*" if sub[5] else "-",
|
|
)
|
|
self.caller.msg("|wActive tickers|n:\n" + str(table))
|