Cleaned up dummyrunner and fixed a lot of issues

This commit is contained in:
Griatch 2021-06-02 00:24:21 +02:00
parent d7b66eecca
commit 677a34d06e
6 changed files with 408 additions and 142 deletions

View file

@ -37,7 +37,8 @@ DUMMYSESSION = namedtuple("DummySession", ["sessid"])(0)
# Portal-SessionHandler class # Portal-SessionHandler class
# ------------------------------------------------------------- # -------------------------------------------------------------
DOS_PROTECTION_MSG = _("{servername} DoS protection is active. You are queued to connect in {num} seconds ...") DOS_PROTECTION_MSG = _("{servername} DoS protection is active."
"You are queued to connect in {num} seconds ...")
class PortalSessionHandler(SessionHandler): class PortalSessionHandler(SessionHandler):

View file

@ -40,8 +40,16 @@ from twisted.conch import telnet
from twisted.internet import reactor, protocol from twisted.internet import reactor, protocol
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
from django.conf import settings import django
from evennia.utils import mod_import, time_format django.setup()
import evennia # noqa
evennia._init()
from django.conf import settings # noqa
from evennia.utils import mod_import, time_format # noqa
from evennia.commands.command import Command # noqa
from evennia.commands.cmdset import CmdSet # noqa
from evennia.utils.ansi import strip_ansi # noqa
# Load the dummyrunner settings module # Load the dummyrunner settings module
@ -51,8 +59,10 @@ if not DUMMYRUNNER_SETTINGS:
"Error: Dummyrunner could not find settings file at %s" "Error: Dummyrunner could not find settings file at %s"
% settings.DUMMYRUNNER_SETTINGS_MODULE % settings.DUMMYRUNNER_SETTINGS_MODULE
) )
IDMAPPER_CACHE_MAXSIZE = settings.IDMAPPER_CACHE_MAXSIZE
DATESTRING = "%Y%m%d%H%M%S" DATESTRING = "%Y%m%d%H%M%S"
CLIENTS = []
# Settings # Settings
@ -71,18 +81,37 @@ CHANCE_OF_LOGIN = DUMMYRUNNER_SETTINGS.CHANCE_OF_LOGIN
# Port to use, if not specified on command line # Port to use, if not specified on command line
TELNET_PORT = DUMMYRUNNER_SETTINGS.TELNET_PORT or settings.TELNET_PORTS[0] TELNET_PORT = DUMMYRUNNER_SETTINGS.TELNET_PORT or settings.TELNET_PORTS[0]
# #
NLOGGED_IN = 0 NCONNECTED = 0 # client has received a connection
NLOGIN_SCREEN = 0 # client has seen the login screen (server responded)
NLOGGING_IN = 0 # client starting login procedure
NLOGGED_IN = 0 # client has authenticated and logged in
# time when all clients have logged_in
# Messages TIME_ALL_LOGIN = 0
# actions since all logged in
TOTAL_ACTIONS = 0
TOTAL_LAG_MEASURES = 0
# lag per 30s for all logged in
TOTAL_LAG = 0
TOTAL_LAG_IN = 0
TOTAL_LAG_OUT = 0
INFO_STARTING = """ INFO_STARTING = """
Dummyrunner starting using {N} dummy account(s). If you don't see Dummyrunner starting using {nclients} dummy account(s). If you don't see
any connection messages, make sure that the Evennia server is any connection messages, make sure that the Evennia server is
running. running.
Use Ctrl-C to stop/disconnect clients. TELNET_PORT = {port}
IDMAPPER_CACHE_MAXSIZE = {idmapper_cache_size} MB
TIMESTEP = {timestep} (rate {rate}/s)
CHANCE_OF_LOGIN = {chance_of_login}% per time step
CHANCE_OF_ACTION = {chance_of_action}% per time step
-> avg rate (per client, after login): {avg_rate} cmds/s
-> total avg rate (after login): {avg_rate_total} cmds/s
Use Ctrl-C (or Cmd-C) to stop/disconnect all clients.
""" """
ERROR_NO_MIXIN = """ ERROR_NO_MIXIN = """
@ -97,6 +126,7 @@ ERROR_NO_MIXIN = """
to test all commands to test all commands
- change PASSWORD_HASHERS to use a faster (but less safe) algorithm - change PASSWORD_HASHERS to use a faster (but less safe) algorithm
when creating large numbers of accounts at the same time when creating large numbers of accounts at the same time
- set LOGIN_THROTTLE/CREATION_THROTTLE=None to disable it
If you don't want to use the custom settings of the mixin for some If you don't want to use the custom settings of the mixin for some
reason, you can change their values manually after the import, or reason, you can change their values manually after the import, or
@ -168,6 +198,39 @@ until you see the initial login slows things too much.
""" """
class CmdDummyRunnerEchoResponse(Command):
"""
Dummyrunner command measuring the round-about response time
from sending to receiving a result.
Usage:
dummyrunner_echo_response <timestamp>
Responds with
dummyrunner_echo_response:<timestamp>,<current_time>
The dummyrunner will send this and then compare the send time
with the receive time on both ends.
"""
key = "dummyrunner_echo_response"
def func(self):
# returns (dummy_client_timestamp,current_time)
self.msg(f"dummyrunner_echo_response:{self.args},{time.time()}")
if self.caller.account.is_superuser:
print(f"cmddummyrunner lag in: {time.time() - float(self.args)}s")
class DummyRunnerCmdSet(CmdSet):
"""
Dummyrunner injected cmdset.
"""
def at_cmdset_creation(self):
self.add(CmdDummyRunnerEchoResponse())
# ------------------------------------------------------------ # ------------------------------------------------------------
# Helper functions # Helper functions
# ------------------------------------------------------------ # ------------------------------------------------------------
@ -181,12 +244,12 @@ def idcounter():
Makes unique ids. Makes unique ids.
Returns: Returns:
count (int): A globally unique counter. str: A globally unique id.
""" """
global ICOUNT global ICOUNT
ICOUNT += 1 ICOUNT += 1
return str(ICOUNT) return str("{:03d}".format(ICOUNT))
GCOUNT = 0 GCOUNT = 0
@ -202,7 +265,7 @@ def gidcounter():
""" """
global GCOUNT global GCOUNT
GCOUNT += 1 GCOUNT += 1
return "%s-%s" % (time.strftime(DATESTRING), GCOUNT) return "%s_%s" % (time.strftime(DATESTRING), GCOUNT)
def makeiter(obj): def makeiter(obj):
@ -222,7 +285,6 @@ def makeiter(obj):
# Client classes # Client classes
# ------------------------------------------------------------ # ------------------------------------------------------------
class DummyClient(telnet.StatefulTelnetProtocol): class DummyClient(telnet.StatefulTelnetProtocol):
""" """
Handles connection to a running Evennia server, Handles connection to a running Evennia server,
@ -231,22 +293,36 @@ class DummyClient(telnet.StatefulTelnetProtocol):
""" """
def report(self, text, clientkey):
pad = " " * (25 - len(text))
tim = round(time.time() - self.connection_timestamp)
print(f"{text} {clientkey}{pad}\t"
f"conn: {NCONNECTED} -> "
f"welcome screen: {NLOGIN_SCREEN} -> "
f"authing: {NLOGGING_IN} -> "
f"loggedin/tot: {NLOGGED_IN}/{NCLIENTS} (after {tim}s)")
def connectionMade(self): def connectionMade(self):
""" """
Called when connection is first established. Called when connection is first established.
""" """
global NCONNECTED
# public properties # public properties
self.cid = idcounter() self.cid = idcounter()
self.key = "Dummy-%s" % self.cid self.key = f"Dummy-{self.cid}"
self.gid = "%s-%s" % (time.strftime(DATESTRING), self.cid) self.gid = f"{time.strftime(DATESTRING)}_{self.cid}"
self.istep = 0 self.istep = 0
self.exits = [] # exit names created self.exits = [] # exit names created
self.objs = [] # obj names created self.objs = [] # obj names created
self.connection_timestamp = time.time()
self.connection_attempt = 0
self.action_started = 0
self._connected = False self._connected = False
self._loggedin = False self._loggedin = False
self._logging_out = False self._logging_out = False
self._ready = False
self._report = "" self._report = ""
self._cmdlist = [] # already stepping in a cmd definition self._cmdlist = [] # already stepping in a cmd definition
self._login = self.factory.actions[0] self._login = self.factory.actions[0]
@ -255,6 +331,43 @@ class DummyClient(telnet.StatefulTelnetProtocol):
reactor.addSystemEventTrigger("before", "shutdown", self.logout) reactor.addSystemEventTrigger("before", "shutdown", self.logout)
NCONNECTED += 1
self.report("-> connected", self.key)
reactor.callLater(30, self._retry_welcome_screen)
def _retry_welcome_screen(self):
if not self._connected and not self._ready:
# we have connected but not received anything for 30s.
# (unclear why this would be - overload?)
# try sending a look to get something to start with
self.report("?? retrying welcome screen", self.key)
self.sendLine(bytes("look", 'utf-8'))
# make sure to check again later
reactor.callLater(30, self._retry_welcome_screen)
def _print_statistics(self):
global TIME_ALL_LOGIN, TOTAL_ACTIONS
global TOTAL_LAG, TOTAL_LAG_MEASURES, TOTAL_LAG_IN, TOTAL_LAG_OUT
tim = time.time() - TIME_ALL_LOGIN
avgrate = round(TOTAL_ACTIONS / tim)
lag = TOTAL_LAG / (TOTAL_LAG_MEASURES or 1)
lag_in = TOTAL_LAG_IN / (TOTAL_LAG_MEASURES or 1)
lag_out = TOTAL_LAG_OUT / (TOTAL_LAG_MEASURES or 1)
TOTAL_ACTIONS = 0
TOTAL_LAG = 0
TOTAL_LAG_IN = 0
TOTAL_LAG_OUT = 0
TOTAL_LAG_MEASURES = 0
TIME_ALL_LOGIN = time.time()
print(f".. running 30s average: ~{avgrate} actions/s "
f"lag: {lag:.2}s (in: {lag_in:.2}s, out: {lag_out:.2}s)")
reactor.callLater(30, self._print_statistics)
def dataReceived(self, data): def dataReceived(self, data):
""" """
Called when data comes in over the protocol. We wait to start Called when data comes in over the protocol. We wait to start
@ -264,15 +377,67 @@ class DummyClient(telnet.StatefulTelnetProtocol):
data (str): Incoming data. data (str): Incoming data.
""" """
if not self._connected and not data.startswith(chr(255)): global NLOGIN_SCREEN, NLOGGED_IN, NLOGGING_IN, NCONNECTED
# wait until we actually get text back (not just telnet global TOTAL_ACTIONS, TIME_ALL_LOGIN
# negotiation) global TOTAL_LAG, TOTAL_LAG_MEASURES, TOTAL_LAG_IN, TOTAL_LAG_OUT
self._connected = True
# start client tick if not data.startswith(b"\xff"):
d = LoopingCall(self.step) # regular text, not a telnet command
# dissipate exact step by up to +/- 0.5 second
timestep = TIMESTEP + (-0.5 + (random.random() * 1.0)) if NCLIENTS == 1:
d.start(timestep, now=True).addErrback(self.error) print("dummy-client sees:", str(data, "utf-8"))
if not self._connected:
# waiting for connection
# wait until we actually get text back (not just telnet
# negotiation)
# start client tick
d = LoopingCall(self.step)
df = max(abs(TIMESTEP * 0.001), min(TIMESTEP/10, 0.5))
# dither next attempt with random time
timestep = TIMESTEP + (-df + (random.random() * df))
d.start(timestep, now=True).addErrback(self.error)
self.connection_attempt += 1
self._connected = True
NLOGIN_SCREEN += 1
NCONNECTED -= 1
self.report("<- server sent login screen", self.key)
elif self._loggedin:
if not self._ready:
# logged in, ready to run
NLOGGED_IN += 1
NLOGGING_IN -= 1
self._ready = True
self.report("== logged in", self.key)
if NLOGGED_IN == NCLIENTS and not TIME_ALL_LOGIN:
# all are logged in! We can start collecting statistics
print(".. All clients connected and logged in!")
TIME_ALL_LOGIN = time.time()
reactor.callLater(30, self._print_statistics)
elif TIME_ALL_LOGIN:
TOTAL_ACTIONS += 1
try:
data = strip_ansi(str(data, "utf-8").strip())
if data.startswith("dummyrunner_echo_response:"):
# handle special lag-measuring command. This returns
# dummyrunner_echo_response:<starttime>,<midpointtime>
now = time.time()
_, data = data.split(":", 1)
start_time, mid_time = (float(part) for part in data.split(",", 1))
lag_in = mid_time - start_time
lag_out = now - mid_time
total_lag = now - start_time # full round-about time
TOTAL_LAG += total_lag
TOTAL_LAG_IN += lag_in
TOTAL_LAG_OUT += lag_out
TOTAL_LAG_MEASURES += 1
except Exception:
pass
def connectionLost(self, reason): def connectionLost(self, reason):
""" """
@ -283,7 +448,7 @@ class DummyClient(telnet.StatefulTelnetProtocol):
""" """
if not self._logging_out: if not self._logging_out:
print("client %s(%s) lost connection (%s)" % (self.key, self.cid, reason)) self.report("XX lost connection", self.key)
def error(self, err): def error(self, err):
""" """
@ -310,9 +475,9 @@ class DummyClient(telnet.StatefulTelnetProtocol):
""" """
self._logging_out = True self._logging_out = True
cmd = self._logout(self) cmd = self._logout(self)[0]
print("client %s(%s) logout (%s actions)" % (self.key, self.cid, self.istep)) self.report(f"-> logout/disconnect ({self.istep} actions)", self.key)
self.sendLine(cmd) self.sendLine(bytes(cmd, 'utf-8'))
def step(self): def step(self):
""" """
@ -321,7 +486,7 @@ class DummyClient(telnet.StatefulTelnetProtocol):
all "intelligence" of the dummy client. all "intelligence" of the dummy client.
""" """
global NLOGGED_IN global NLOGGING_IN, NLOGIN_SCREEN
rand = random.random() rand = random.random()
@ -329,11 +494,13 @@ class DummyClient(telnet.StatefulTelnetProtocol):
# no commands ready. Load some. # no commands ready. Load some.
if not self._loggedin: if not self._loggedin:
if rand < CHANCE_OF_LOGIN: if rand < CHANCE_OF_LOGIN or NLOGGING_IN < 10:
# lower rate of logins, but not below 1 / s
# get the login commands # get the login commands
self._cmdlist = list(makeiter(self._login(self))) self._cmdlist = list(makeiter(self._login(self)))
NLOGGED_IN += 1 # this is for book-keeping NLOGGING_IN += 1 # this is for book-keeping
print("connecting client %s (%i/%i)..." % (self.key, NLOGGED_IN, NCLIENTS)) NLOGIN_SCREEN -= 1
self.report("-> create/login", self.key)
self._loggedin = True self._loggedin = True
else: else:
# no login yet, so cmdlist not yet set # no login yet, so cmdlist not yet set
@ -347,12 +514,26 @@ class DummyClient(telnet.StatefulTelnetProtocol):
# at this point we always have a list of commands # at this point we always have a list of commands
if rand < CHANCE_OF_ACTION: if rand < CHANCE_OF_ACTION:
# send to the game # send to the game
self.sendLine(str(self._cmdlist.pop(0))) cmd = str(self._cmdlist.pop(0))
if cmd.startswith("dummyrunner_echo_response"):
# we need to set the timer element as close to
# the send as possible
cmd = cmd.format(timestamp=time.time())
self.sendLine(bytes(cmd, 'utf-8'))
self.action_started = time.time()
self.istep += 1 self.istep += 1
if NCLIENTS == 1:
print(f"dummy-client sent: {cmd}")
class DummyFactory(protocol.ClientFactory):
class DummyFactory(protocol.ReconnectingClientFactory):
protocol = DummyClient protocol = DummyClient
initialDelay = 1
maxDelay = 1
noisy = False
def __init__(self, actions): def __init__(self, actions):
"Setup the factory base (shared by all clients)" "Setup the factory base (shared by all clients)"
@ -397,7 +578,7 @@ def start_all_dummy_clients(nclients):
# setting up all clients (they are automatically started) # setting up all clients (they are automatically started)
factory = DummyFactory(actions) factory = DummyFactory(actions)
for i in range(NCLIENTS): for i in range(NCLIENTS):
reactor.connectTCP("localhost", TELNET_PORT, factory) reactor.connectTCP("127.0.0.1", TELNET_PORT, factory)
# start reactor # start reactor
reactor.run() reactor.run()
@ -422,12 +603,23 @@ if __name__ == "__main__":
) )
args = parser.parse_args() args = parser.parse_args()
nclients = int(args.nclients[0])
print(INFO_STARTING.format(N=args.nclients[0])) print(INFO_STARTING.format(
nclients=nclients,
port=TELNET_PORT,
idmapper_cache_size=IDMAPPER_CACHE_MAXSIZE,
timestep=TIMESTEP,
rate=1/TIMESTEP,
chance_of_login=CHANCE_OF_LOGIN * 100,
chance_of_action=CHANCE_OF_ACTION * 100,
avg_rate=(1 / TIMESTEP) * CHANCE_OF_ACTION,
avg_rate_total=(1 / TIMESTEP) * CHANCE_OF_ACTION * nclients
))
# run the dummyrunner # run the dummyrunner
t0 = time.time() TIME_START = t0 = time.time()
start_all_dummy_clients(nclients=args.nclients[0]) start_all_dummy_clients(nclients=nclients)
ttot = time.time() - t0 ttot = time.time() - t0
# output runtime # output runtime

View file

@ -6,9 +6,9 @@ the actions available to dummy accounts.
The settings are global variables: The settings are global variables:
- TIMESTEP - time in seconds between each 'tick' - TIMESTEP - time in seconds between each 'tick'. 1 is a good start.
- CHANCE_OF_ACTION - chance 0-1 of action happening - CHANCE_OF_ACTION - chance 0-1 of action happening. Default is 0.5.
- CHANCE_OF_LOGIN - chance 0-1 of login happening - CHANCE_OF_LOGIN - chance 0-1 of login happening. 0.01 is a good number.
- TELNET_PORT - port to use, defaults to settings.TELNET_PORT - TELNET_PORT - port to use, defaults to settings.TELNET_PORT
- ACTIONS - see below - ACTIONS - see below
@ -16,23 +16,25 @@ ACTIONS is a tuple
```python ```python
(login_func, logout_func, (0.3, func1), (0.1, func2) ... ) (login_func, logout_func, (0.3, func1), (0.1, func2) ... )
``` ```
where the first entry is the function to call on first connect, with a where the first entry is the function to call on first connect, with a
chance of occurring given by CHANCE_OF_LOGIN. This function is usually chance of occurring given by CHANCE_OF_LOGIN. This function is usually
responsible for logging in the account. The second entry is always responsible for logging in the account. The second entry is always
called when the dummyrunner disconnects from the server and should called when the dummyrunner disconnects from the server and should
thus issue a logout command. The other entries are tuples (chance, thus issue a logout command. The other entries are tuples (chance,
func). They are picked randomly, their commonality based on the func). They are picked randomly, their commonality based on the
cumulative chance given (the chance is normalized between all options cumulative chance given (the chance is normalized between all options
so if will still work also if the given chances don't add up to 1). so if will still work also if the given chances don't add up to 1).
Since each function can return a list of game-command strings, each
function may result in multiple operations. The PROFILE variable define pre-made ACTION tuples for convenience.
Each function should return an iterable of one or more command-call
strings (like "look here"), so each can group multiple command operations.
An action-function is called with a "client" argument which is a An action-function is called with a "client" argument which is a
reference to the dummy client currently performing the action. It reference to the dummy client currently performing the action.
returns a string or a list of command strings to execute. Use the
client object for optionally saving data between actions.
The client object has the following relevant properties and methods: The client object has the following relevant properties and methods:
@ -55,11 +57,15 @@ commands (such as creating an account and logging in).
---- ----
""" """
import random
import string
# Dummy runner settings # Dummy runner settings
# Time between each dummyrunner "tick", in seconds. Each dummy # Time between each dummyrunner "tick", in seconds. Each dummy
# will be called with this frequency. # will be called with this frequency.
TIMESTEP = 2 TIMESTEP = 1
# TIMESTEP = 0.025 # 40/s
# Chance of a dummy actually performing an action on a given tick. # Chance of a dummy actually performing an action on a given tick.
# This spreads out usage randomly, like it would be in reality. # This spreads out usage randomly, like it would be in reality.
@ -68,7 +74,7 @@ CHANCE_OF_ACTION = 0.5
# Chance of a currently unlogged-in dummy performing its login # Chance of a currently unlogged-in dummy performing its login
# action every tick. This emulates not all accounts logging in # action every tick. This emulates not all accounts logging in
# at exactly the same time. # at exactly the same time.
CHANCE_OF_LOGIN = 1.0 CHANCE_OF_LOGIN = 0.01
# Which telnet port to connect to. If set to None, uses the first # Which telnet port to connect to. If set to None, uses the first
# default telnet port of the running server. # default telnet port of the running server.
@ -79,9 +85,10 @@ TELNET_PORT = None
# some convenient templates # some convenient templates
DUMMY_NAME = "Dummy-%s" DUMMY_NAME = "Dummy_{gid}"
DUMMY_PWD = "password-%s" DUMMY_PWD = (''.join(random.choice(string.ascii_letters + string.digits)
START_ROOM = "testing_room_start_%s" for _ in range(20)) + "-{gid}")
START_ROOM = "testing_room_start_{gid}"
ROOM_TEMPLATE = "testing_room_%s" ROOM_TEMPLATE = "testing_room_%s"
EXIT_TEMPLATE = "exit_%s" EXIT_TEMPLATE = "exit_%s"
OBJ_TEMPLATE = "testing_obj_%s" OBJ_TEMPLATE = "testing_obj_%s"
@ -94,26 +101,27 @@ TOBJ_TYPECLASS = "contrib.tutorial_examples.red_button.RedButton"
# login/logout # login/logout
def c_login(client): def c_login(client):
"logins to the game" "logins to the game"
# we always use a new client name # we always use a new client name
cname = DUMMY_NAME % client.gid cname = DUMMY_NAME.format(gid=client.gid)
cpwd = DUMMY_PWD % client.gid cpwd = DUMMY_PWD.format(gid=client.gid)
room_name = START_ROOM.format(gid=client.gid)
# set up for digging a first room (to move to and keep the # we assign the dummyrunner cmdsert to ourselves so # we can use special commands
# login room clean) add_cmdset = (
roomname = ROOM_TEMPLATE % client.counter() "py from evennia.server.profiling.dummyrunner import DummyRunnerCmdSet;"
exitname1 = EXIT_TEMPLATE % client.counter() "self.cmdset.add(DummyRunnerCmdSet, persistent=False)"
exitname2 = EXIT_TEMPLATE % client.counter() )
client.exits.extend([exitname1, exitname2])
# create character, log in, then immediately dig a new location and
# teleport it (to keep the login room clean)
cmds = ( cmds = (
"create %s %s" % (cname, cpwd), f"create {cname} {cpwd}",
"connect %s %s" % (cname, cpwd), f"connect {cname} {cpwd}",
"@dig %s" % START_ROOM % client.gid, f"dig {room_name}",
"@teleport %s" % START_ROOM % client.gid, f"teleport {room_name}",
"@dig %s = %s, %s" % (roomname, exitname1, exitname2), add_cmdset,
) )
return cmds return cmds
@ -122,14 +130,16 @@ def c_login_nodig(client):
"logins, don't dig its own room" "logins, don't dig its own room"
cname = DUMMY_NAME % client.gid cname = DUMMY_NAME % client.gid
cpwd = DUMMY_PWD % client.gid cpwd = DUMMY_PWD % client.gid
cmds = (
cmds = ("create %s %s" % (cname, cpwd), "connect %s %s" % (cname, cpwd)) f"create {cname} {cpwd}",
f"connect {cname} {cpwd}"
)
return cmds return cmds
def c_logout(client): def c_logout(client):
"logouts of the game" "logouts of the game"
return "@quit" return ("quit",)
# random commands # random commands
@ -141,7 +151,7 @@ def c_looks(client):
if not cmds: if not cmds:
cmds = ["look %s" % exi for exi in client.exits] cmds = ["look %s" % exi for exi in client.exits]
if not cmds: if not cmds:
cmds = "look" cmds = ("look",)
return cmds return cmds
@ -151,7 +161,7 @@ def c_examines(client):
if not cmds: if not cmds:
cmds = ["examine %s" % exi for exi in client.exits] cmds = ["examine %s" % exi for exi in client.exits]
if not cmds: if not cmds:
cmds = "examine me" cmds = ("examine me",)
return cmds return cmds
@ -163,7 +173,7 @@ def c_idles(client):
def c_help(client): def c_help(client):
"reads help files" "reads help files"
cmds = ("help", "help @teleport", "help look", "help @tunnel", "help @dig") cmds = ("help", "dummyrunner_echo_response",)
return cmds return cmds
@ -173,7 +183,7 @@ def c_digs(client):
exitname1 = EXIT_TEMPLATE % client.counter() exitname1 = EXIT_TEMPLATE % client.counter()
exitname2 = EXIT_TEMPLATE % client.counter() exitname2 = EXIT_TEMPLATE % client.counter()
client.exits.extend([exitname1, exitname2]) client.exits.extend([exitname1, exitname2])
return "@dig/tel %s = %s, %s" % (roomname, exitname1, exitname2) return ("@dig/tel %s = %s, %s" % (roomname, exitname1, exitname2),)
def c_creates_obj(client): def c_creates_obj(client):
@ -200,9 +210,7 @@ def c_creates_button(client):
def c_socialize(client): def c_socialize(client):
"socializechats on channel" "socializechats on channel"
cmds = ( cmds = (
"ooc Hello!", "pub Hello!",
"ooc Testing ...",
"ooc Testing ... times 2",
"say Yo!", "say Yo!",
"emote stands looking around.", "emote stands looking around.",
) )
@ -212,81 +220,117 @@ def c_socialize(client):
def c_moves(client): def c_moves(client):
"moves to a previously created room, using the stored exits" "moves to a previously created room, using the stored exits"
cmds = client.exits # try all exits - finally one will work cmds = client.exits # try all exits - finally one will work
return "look" if not cmds else cmds return ("look",) if not cmds else cmds
def c_moves_n(client): def c_moves_n(client):
"move through north exit if available" "move through north exit if available"
return "north" return ("north",)
def c_moves_s(client): def c_moves_s(client):
"move through south exit if available" "move through south exit if available"
return "south" return ("south",)
# Action tuple (required) def c_measure_lag(client):
# """
# This is a tuple of client action functions. The first element is the Special dummyrunner command, injected in c_login. It measures
# function the client should use to log into the game and move to response time. Including this in the ACTION tuple will give more
# STARTROOM . The second element is the logout command, for cleanly dummyrunner output about just how fast commands are being processed.
# exiting the mud. The following elements are 2-tuples of (probability,
# action_function). The probablities should normally sum up to 1, The dummyrunner will treat this special and inject the
# otherwise the system will normalize them. {timestamp} just before sending.
"""
return ("dummyrunner_echo_response {timestamp}",)
# Action profile (required)
# Some pre-made profiles to test. To make your own, just assign a tuple to ACTIONS.
# #
# idler - does nothing after logging in
# looker - just looks around
# normal_player - moves around, reads help, looks around (digs rarely) (spammy)
# normal_builder - digs now and then, examines, creates objects, moves
# heavy_builder - digs and creates a lot, moves and examines
# socializing_builder - builds a lot, creates help entries, moves, chat (spammy)
# only_digger - extreme builder that only digs room after room
PROFILE = "normal_player"
# "normal builder" definitionj if PROFILE == 'idler':
# ACTIONS = ( c_login, ACTIONS = (
# c_logout, c_login,
# (0.5, c_looks), c_logout,
# (0.08, c_examines), (0.9, c_idles),
# (0.1, c_help), (0.1, c_measure_lag),
# (0.01, c_digs), )
# (0.01, c_creates_obj), elif PROFILE == 'looker':
# (0.3, c_moves)) ACTIONS = (
# "heavy" builder definition c_login,
# ACTIONS = ( c_login, c_logout,
# c_logout, (0.8, c_looks),
# (0.2, c_looks), (0.2, c_measure_lag)
# (0.1, c_examines), )
# (0.2, c_help), elif PROFILE == 'normal_player':
# (0.1, c_digs), ACTIONS = (
# (0.1, c_creates_obj), c_login,
# #(0.01, c_creates_button), c_logout,
# (0.2, c_moves)) (0.01, c_digs),
# "passive account" definition (0.29, c_looks),
# ACTIONS = ( c_login, (0.2, c_help),
# c_logout, (0.3, c_moves),
# (0.7, c_looks), (0.2, c_socialize),
# #(0.1, c_examines), (0.1, c_measure_lag)
# (0.3, c_help)) )
# #(0.1, c_digs), elif PROFILE == 'normal_builder':
# #(0.1, c_creates_obj), ACTIONS = (
# #(0.1, c_creates_button), c_login,
# #(0.4, c_moves)) c_logout,
# "inactive account" definition (0.5, c_looks),
# ACTIONS = (c_login_nodig, (0.08, c_examines),
# c_logout, (0.1, c_help),
# (1.0, c_idles)) (0.01, c_digs),
# "normal account" definition (0.01, c_creates_obj),
ACTIONS = (c_login, c_logout, (0.01, c_digs), (0.39, c_looks), (0.2, c_help), (0.4, c_moves)) (0.2, c_moves)
# walking tester. This requires a pre-made (0.1, c_measure_lag)
# "loop" of multiple rooms that ties back )
# to limbo (using @tunnel and @open) elif PROFILE == 'heavy_builder':
# ACTIONS = (c_login_nodig, ACTIONS = (
# c_logout, c_login,
# (1.0, c_moves_n)) c_logout,
# "socializing heavy builder" definition (0.1, c_looks),
# ACTIONS = (c_login, (0.1, c_examines),
# c_logout, (0.2, c_help),
# (0.1, c_socialize), (0.1, c_digs),
# (0.1, c_looks), (0.1, c_creates_obj),
# (0.2, c_help), (0.2, c_moves),
# (0.1, c_creates_obj), (0.1, c_measure_lag)
# (0.2, c_digs), )
# (0.3, c_moves)) elif PROFILE == 'socializing_builder':
# "heavy digger memory tester" definition ACTIONS = (
# ACTIONS = (c_login, c_login,
# c_logout, c_logout,
# (1.0, c_digs)) (0.1, c_socialize),
(0.1, c_looks),
(0.1, c_help),
(0.1, c_creates_obj),
(0.2, c_digs),
(0.3, c_moves),
(0.1, c_measure_lag)
)
elif PROFILE == 'only_digger':
ACTIONS = (
c_login,
c_logout,
(0.9, c_digs),
(0.1, c_measure_lag)
)
else:
print("No dummyrunner ACTION profile defined.")
import sys
sys.exit()

View file

@ -6,6 +6,7 @@ running dummyrunner, like this:
Note that these mixin-settings are not suitable for production Note that these mixin-settings are not suitable for production
servers! servers!
""" """
# the dummyrunner will check this variable to make sure # the dummyrunner will check this variable to make sure
@ -17,3 +18,25 @@ DUMMYRUNNER_MIXIN = True
PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",) PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",)
# make dummy clients able to test all commands # make dummy clients able to test all commands
PERMISSION_ACCOUNT_DEFAULT = "Developer" PERMISSION_ACCOUNT_DEFAULT = "Developer"
# disable throttles which would otherwise block the runner
CREATION_THROTTLE_LIMIT = None
CREATION_THROTTLE_TIMEOUT = None
LOGIN_THROTTLE_LIMIT = None
LOGIN_THROTTLE_TIMEOUT = None
MAX_COMMAND_RATE = 100000
MAX_CONNECTION_RATE = 100000
MAX_CHAR_LIMIT = 100000
print("""
Dummyrunner settings_mixin added (ONLY FOR PROFILING, NOT FOR PRODUCTION!)
PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",)
PERMISSION_ACCOUNT_DEFAULT = "Developer"
CREATION_THROTTLE_LIMIT = None
CREATION_THROTTLE_TIMEOUT = None
LOGIN_THROTTLE_LIMIT = None
LOGIN_THROTTLE_TIMEOUT = None
MAX_COMMAND_RATE = 100000
MAX_CONNECTION_RATE = 100000
MAX_CHAR_LIMIT = 100000
""")

View file

@ -26,7 +26,8 @@ class Throttle:
Keyword Args: Keyword Args:
name (str): Name of this throttle. name (str): Name of this throttle.
limit (int): Max number of failures before imposing limiter limit (int): Max number of failures before imposing limiter. If `None`,
the throttle is disabled.
timeout (int): number of timeout seconds after timeout (int): number of timeout seconds after
max number of tries has been reached. max number of tries has been reached.
cache_size (int): Max number of attempts to record per IP within a cache_size (int): Max number of attempts to record per IP within a
@ -197,6 +198,10 @@ class Throttle:
False otherwise. False otherwise.
""" """
if self.limit is None:
# throttle is disabled
return False
now = time.time() now = time.time()
ip = str(ip) ip = str(ip)

View file

@ -697,7 +697,8 @@ PERMISSION_ACCOUNT_DEFAULT = "Player"
CLIENT_DEFAULT_WIDTH = 78 CLIENT_DEFAULT_WIDTH = 78
# telnet standard height is 24; does anyone use such low-res displays anymore? # telnet standard height is 24; does anyone use such low-res displays anymore?
CLIENT_DEFAULT_HEIGHT = 45 CLIENT_DEFAULT_HEIGHT = 45
# Set rate limits per-IP on account creations and login attempts # Set rate limits per-IP on account creations and login attempts. Set limits
# to None to disable.
CREATION_THROTTLE_LIMIT = 2 CREATION_THROTTLE_LIMIT = 2
CREATION_THROTTLE_TIMEOUT = 10 * 60 CREATION_THROTTLE_TIMEOUT = 10 * 60
LOGIN_THROTTLE_LIMIT = 5 LOGIN_THROTTLE_LIMIT = 5