Merge branch 'master' into develop

This commit is contained in:
Griatch 2018-01-27 14:20:13 +01:00
commit 4376058ee9
11 changed files with 201 additions and 111 deletions

View file

@ -457,7 +457,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
callertype="account", session=session, **kwargs) callertype="account", session=session, **kwargs)
def search(self, searchdata, return_puppet=False, search_object=False, def search(self, searchdata, return_puppet=False, search_object=False,
typeclass=None, nofound_string=None, multimatch_string=None, **kwargs): typeclass=None, nofound_string=None, multimatch_string=None, use_nicks=True, **kwargs):
""" """
This is similar to `DefaultObject.search` but defaults to searching This is similar to `DefaultObject.search` but defaults to searching
for Accounts only. for Accounts only.
@ -481,6 +481,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
multimatch_string (str, optional): A one-time error multimatch_string (str, optional): A one-time error
message to echo if `searchdata` leads to multiple matches. message to echo if `searchdata` leads to multiple matches.
If not given, will fall back to the default handler. If not given, will fall back to the default handler.
use_nicks (bool, optional): Use account-level nick replacement.
Return: Return:
match (Account, Object or None): A single Account or Object match. match (Account, Object or None): A single Account or Object match.
@ -496,8 +497,10 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
if searchdata.lower() in ("me", "*me", "self", "*self",): if searchdata.lower() in ("me", "*me", "self", "*self",):
return self return self
if search_object: if search_object:
matches = ObjectDB.objects.object_search(searchdata, typeclass=typeclass) matches = ObjectDB.objects.object_search(searchdata, typeclass=typeclass, use_nicks=use_nicks)
else: else:
searchdata = self.nicks.nickreplace(searchdata, categories=("account", ), include_account=False)
matches = AccountDB.objects.account_search(searchdata, typeclass=typeclass) matches = AccountDB.objects.account_search(searchdata, typeclass=typeclass)
matches = _AT_SEARCH_RESULT(matches, self, query=searchdata, matches = _AT_SEARCH_RESULT(matches, self, query=searchdata,
nofound_string=nofound_string, nofound_string=nofound_string,

View file

@ -544,7 +544,8 @@ class CmdWall(COMMAND_DEFAULT_CLASS):
Usage: Usage:
@wall <message> @wall <message>
Announces a message to all connected accounts. Announces a message to all connected sessions
including all currently unlogged in.
""" """
key = "@wall" key = "@wall"
locks = "cmd:perm(wall) or perm(Admin)" locks = "cmd:perm(wall) or perm(Admin)"
@ -556,5 +557,5 @@ class CmdWall(COMMAND_DEFAULT_CLASS):
self.caller.msg("Usage: @wall <message>") self.caller.msg("Usage: @wall <message>")
return return
message = "%s shouts \"%s\"" % (self.caller.name, self.args) message = "%s shouts \"%s\"" % (self.caller.name, self.args)
self.msg("Announcing to all connected accounts ...") self.msg("Announcing to all connected sessions ...")
SESSIONS.announce_all(message) SESSIONS.announce_all(message)

View file

@ -1,6 +1,7 @@
""" """
General Character commands usually available to all characters General Character commands usually available to all characters
""" """
import re
from django.conf import settings from django.conf import settings
from evennia.utils import utils, evtable from evennia.utils import utils, evtable
from evennia.typeclasses.attributes import NickTemplateInvalid from evennia.typeclasses.attributes import NickTemplateInvalid
@ -75,37 +76,41 @@ class CmdLook(COMMAND_DEFAULT_CLASS):
class CmdNick(COMMAND_DEFAULT_CLASS): class CmdNick(COMMAND_DEFAULT_CLASS):
""" """
define a personal alias/nick define a personal alias/nick by defining a string to
match and replace it with another on the fly
Usage: Usage:
nick[/switches] <string> [= [replacement_string]] nick[/switches] <string> [= [replacement_string]]
nick[/switches] <template> = <replacement_template> nick[/switches] <template> = <replacement_template>
nick/delete <string> or number nick/delete <string> or number
nick/test <test string> nicks
Switches: Switches:
inputline - replace on the inputline (default) inputline - replace on the inputline (default)
object - replace on object-lookup object - replace on object-lookup
account - replace on account-lookup account - replace on account-lookup
delete - remove nick by name or by index given by /list
clearall - clear all nicks
list - show all defined aliases (also "nicks" works) list - show all defined aliases (also "nicks" works)
test - test input to see what it matches with delete - remove nick by index in /list
clearall - clear all nicks
Examples: Examples:
nick hi = say Hello, I'm Sarah! nick hi = say Hello, I'm Sarah!
nick/object tom = the tall man nick/object tom = the tall man
nick build $1 $2 = @create/drop $1;$2 - (template) nick build $1 $2 = @create/drop $1;$2
nick tell $1 $2=@page $1=$2 - (template) nick tell $1 $2=@page $1=$2
nick tm?$1=@page tallman=$1
nick tm\=$1=@page tallman=$1
A 'nick' is a personal string replacement. Use $1, $2, ... to catch arguments. A 'nick' is a personal string replacement. Use $1, $2, ... to catch arguments.
Put the last $-marker without an ending space to catch all remaining text. You Put the last $-marker without an ending space to catch all remaining text. You
can also use unix-glob matching: can also use unix-glob matching for the left-hand side <string>:
* - matches everything * - matches everything
? - matches a single character ? - matches 0 or 1 single characters
[seq] - matches all chars in sequence [abcd] - matches these chars in any order
[!seq] - matches everything not in sequence [!abcd] - matches everything not among these chars
\= - escape literal '=' you want in your <string>
Note that no objects are actually renamed or changed by this command - your nicks Note that no objects are actually renamed or changed by this command - your nicks
are only available to you. If you want to permanently add keywords to an object are only available to you. If you want to permanently add keywords to an object
@ -116,14 +121,36 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
aliases = ["nickname", "nicks"] aliases = ["nickname", "nicks"]
locks = "cmd:all()" locks = "cmd:all()"
def parse(self):
"""
Support escaping of = with \=
"""
super(CmdNick, self).parse()
args = (self.lhs or "") + (" = %s" % self.rhs if self.rhs else "")
parts = re.split(r"(?<!\\)=", args, 1)
self.rhs = None
if len(parts) < 2:
self.lhs = parts[0].strip()
else:
self.lhs, self.rhs = [part.strip() for part in parts]
self.lhs = self.lhs.replace("\=", "=")
def func(self): def func(self):
"""Create the nickname""" """Create the nickname"""
caller = self.caller def _cy(string):
switches = self.switches "add color to the special markers"
nicktypes = [switch for switch in switches if switch in ("object", "account", "inputline")] or ["inputline"] return re.sub(r"(\$[0-9]+|\*|\?|\[.+?\])", r"|Y\1|n", string)
nicklist = utils.make_iter(caller.nicks.get(return_obj=True) or []) caller = self.caller
account = self.caller.account or caller
switches = self.switches
nicktypes = [switch for switch in switches if switch in (
"object", "account", "inputline")] or ["inputline"]
nicklist = (utils.make_iter(caller.nicks.get(category="inputline", return_obj=True) or []) +
utils.make_iter(caller.nicks.get(category="object", return_obj=True) or []) +
utils.make_iter(account.nicks.get(category="account", return_obj=True) or []))
if 'list' in switches or self.cmdstring in ("nicks", "@nicks"): if 'list' in switches or self.cmdstring in ("nicks", "@nicks"):
@ -133,24 +160,51 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
table = evtable.EvTable("#", "Type", "Nick match", "Replacement") table = evtable.EvTable("#", "Type", "Nick match", "Replacement")
for inum, nickobj in enumerate(nicklist): for inum, nickobj in enumerate(nicklist):
_, _, nickvalue, replacement = nickobj.value _, _, nickvalue, replacement = nickobj.value
table.add_row(str(inum + 1), nickobj.db_category, nickvalue, replacement) table.add_row(str(inum + 1), nickobj.db_category, _cy(nickvalue), _cy(replacement))
string = "|wDefined Nicks:|n\n%s" % table string = "|wDefined Nicks:|n\n%s" % table
caller.msg(string) caller.msg(string)
return return
if 'clearall' in switches: if 'clearall' in switches:
caller.nicks.clear() caller.nicks.clear()
caller.account.nicks.clear()
caller.msg("Cleared all nicks.") caller.msg("Cleared all nicks.")
return return
if 'delete' in switches or 'del' in switches:
if not self.args or not self.lhs:
caller.msg("usage nick/delete #num ('nicks' for list)")
return
# see if a number was given
arg = self.args.lstrip("#")
if arg.isdigit():
# we are given a index in nicklist
delindex = int(arg)
if 0 < delindex <= len(nicklist):
oldnick = nicklist[delindex - 1]
_, _, old_nickstring, old_replstring = oldnick.value
else:
caller.msg("Not a valid nick index. See 'nicks' for a list.")
return
nicktype = oldnick.category
nicktypestr = "%s-nick" % nicktype.capitalize()
if nicktype == "account":
account.nicks.remove(old_nickstring, category=nicktype)
else:
caller.nicks.remove(old_nickstring, category=nicktype)
caller.msg("%s removed: '|w%s|n' -> |w%s|n." % (
nicktypestr, old_nickstring, old_replstring))
return
if not self.args or not self.lhs: if not self.args or not self.lhs:
caller.msg("Usage: nick[/switches] nickname = [realname]") caller.msg("Usage: nick[/switches] nickname = [realname]")
return return
# setting new nicks
nickstring = self.lhs nickstring = self.lhs
replstring = self.rhs replstring = self.rhs
old_nickstring = None
old_replstring = None
if replstring == nickstring: if replstring == nickstring:
caller.msg("No point in setting nick same as the string to replace...") caller.msg("No point in setting nick same as the string to replace...")
@ -160,47 +214,40 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
errstring = "" errstring = ""
string = "" string = ""
for nicktype in nicktypes: for nicktype in nicktypes:
oldnick = caller.nicks.get(key=nickstring, category=nicktype, return_obj=True) if nicktype == "account":
obj = account
else:
obj = caller
nicktypestr = "%s-nick" % nicktype.capitalize()
old_nickstring = None
old_replstring = None
oldnick = obj.nicks.get(key=nickstring, category=nicktype, return_obj=True)
if oldnick: if oldnick:
_, _, old_nickstring, old_replstring = oldnick.value _, _, old_nickstring, old_replstring = oldnick.value
else: if replstring:
# no old nick, see if a number was given
arg = self.args.lstrip("#")
if arg.isdigit():
# we are given a index in nicklist
delindex = int(arg)
if 0 < delindex <= len(nicklist):
oldnick = nicklist[delindex - 1]
_, _, old_nickstring, old_replstring = oldnick.value
else:
errstring += "Not a valid nick index."
else:
errstring += "Nick not found."
if "delete" in switches or "del" in switches:
# clear the nick
if old_nickstring and caller.nicks.has(old_nickstring, category=nicktype):
caller.nicks.remove(old_nickstring, category=nicktype)
string += "\nNick removed: '|w%s|n' -> |w%s|n." % (old_nickstring, old_replstring)
else:
errstring += "\nNick '|w%s|n' was not deleted." % old_nickstring
elif replstring:
# creating new nick # creating new nick
errstring = "" errstring = ""
if oldnick: if oldnick:
string += "\nNick '|w%s|n' updated to map to '|w%s|n'." % (old_nickstring, replstring) if replstring == old_replstring:
string += "\nIdentical %s already set." % nicktypestr.lower()
else: else:
string += "\nNick '|w%s|n' mapped to '|w%s|n'." % (nickstring, replstring) string += "\n%s '|w%s|n' updated to map to '|w%s|n'." % (
nicktypestr, old_nickstring, replstring)
else:
string += "\n%s '|w%s|n' mapped to '|w%s|n'." % (nicktypestr, nickstring, replstring)
try: try:
caller.nicks.add(nickstring, replstring, category=nicktype) obj.nicks.add(nickstring, replstring, category=nicktype)
except NickTemplateInvalid: except NickTemplateInvalid:
caller.msg("You must use the same $-markers both in the nick and in the replacement.") caller.msg("You must use the same $-markers both in the nick and in the replacement.")
return return
elif old_nickstring and old_replstring: elif old_nickstring and old_replstring:
# just looking at the nick # just looking at the nick
string += "\nNick '|w%s|n' maps to '|w%s|n'." % (old_nickstring, old_replstring) string += "\n%s '|w%s|n' maps to '|w%s|n'." % (nicktypestr, old_nickstring, old_replstring)
errstring = "" errstring = ""
string = errstring if errstring else string string = errstring if errstring else string
caller.msg(string) caller.msg(_cy(string))
class CmdInventory(COMMAND_DEFAULT_CLASS): class CmdInventory(COMMAND_DEFAULT_CLASS):

View file

@ -126,11 +126,12 @@ class TestGeneral(CommandTest):
self.call(general.CmdPose(), "looks around", "Char looks around") self.call(general.CmdPose(), "looks around", "Char looks around")
def test_nick(self): def test_nick(self):
self.call(general.CmdNick(), "testalias = testaliasedstring1", "Nick 'testalias' mapped to 'testaliasedstring1'.") self.call(general.CmdNick(), "testalias = testaliasedstring1", "Inputlinenick 'testalias' mapped to 'testaliasedstring1'.")
self.call(general.CmdNick(), "/account testalias = testaliasedstring2", "Nick 'testalias' mapped to 'testaliasedstring2'.") self.call(general.CmdNick(), "/account testalias = testaliasedstring2", "Accountnick 'testalias' mapped to 'testaliasedstring2'.")
self.call(general.CmdNick(), "/object testalias = testaliasedstring3", "Nick 'testalias' mapped to 'testaliasedstring3'.") self.call(general.CmdNick(), "/object testalias = testaliasedstring3", "Objectnick 'testalias' mapped to 'testaliasedstring3'.")
self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias")) self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias"))
self.assertEqual(u"testaliasedstring2", self.char1.nicks.get("testalias", category="account")) self.assertEqual(None, self.char1.nicks.get("testalias", category="account"))
self.assertEqual(u"testaliasedstring2", self.char1.account.nicks.get("testalias", category="account"))
self.assertEqual(u"testaliasedstring3", self.char1.nicks.get("testalias", category="object")) self.assertEqual(u"testaliasedstring3", self.char1.nicks.get("testalias", category="object"))
def test_get_and_drop(self): def test_get_and_drop(self):
@ -184,7 +185,7 @@ class TestAdmin(CommandTest):
self.call(admin.CmdPerm(), "Char2 = Builder", "Permission 'Builder' given to Char2 (the Object/Character).") self.call(admin.CmdPerm(), "Char2 = Builder", "Permission 'Builder' given to Char2 (the Object/Character).")
def test_wall(self): def test_wall(self):
self.call(admin.CmdWall(), "Test", "Announcing to all connected accounts ...") self.call(admin.CmdWall(), "Test", "Announcing to all connected sessions ...")
def test_ban(self): def test_ban(self):
self.call(admin.CmdBan(), "Char", "NameBan char was added.") self.call(admin.CmdBan(), "Char", "NameBan char was added.")

View file

@ -15,12 +15,15 @@ from evennia.utils.logger import log_trace
# module import # module import
_MOD_IMPORT = None _MOD_IMPORT = None
# throttles # global throttles
_MAX_CONNECTION_RATE = float(settings.MAX_CONNECTION_RATE) _MAX_CONNECTION_RATE = float(settings.MAX_CONNECTION_RATE)
# per-session throttles
_MAX_COMMAND_RATE = float(settings.MAX_COMMAND_RATE) _MAX_COMMAND_RATE = float(settings.MAX_COMMAND_RATE)
_MAX_CHAR_LIMIT = int(settings.MAX_CHAR_LIMIT) _MAX_CHAR_LIMIT = int(settings.MAX_CHAR_LIMIT)
_MIN_TIME_BETWEEN_CONNECTS = 1.0 / float(settings.MAX_CONNECTION_RATE) _MIN_TIME_BETWEEN_CONNECTS = 1.0 / float(_MAX_CONNECTION_RATE)
_MIN_TIME_BETWEEN_COMMANDS = 1.0 / float(_MAX_COMMAND_RATE)
_ERROR_COMMAND_OVERFLOW = settings.COMMAND_RATE_WARNING _ERROR_COMMAND_OVERFLOW = settings.COMMAND_RATE_WARNING
_ERROR_MAX_CHAR = settings.MAX_CHAR_LIMIT_WARNING _ERROR_MAX_CHAR = settings.MAX_CHAR_LIMIT_WARNING
@ -58,9 +61,6 @@ class PortalSessionHandler(SessionHandler):
self.connection_last = self.uptime self.connection_last = self.uptime
self.connection_task = None self.connection_task = None
self.command_counter = 0
self.command_counter_reset = self.uptime
self.command_overflow = False
def at_server_connection(self): def at_server_connection(self):
""" """
@ -354,8 +354,6 @@ class PortalSessionHandler(SessionHandler):
Data is serialized before passed on. Data is serialized before passed on.
""" """
# from evennia.server.profiling.timetrace import timetrace # DEBUG
# text = timetrace(text, "portalsessionhandler.data_in") # DEBUG
try: try:
text = kwargs['text'] text = kwargs['text']
if (_MAX_CHAR_LIMIT > 0) and len(text) > _MAX_CHAR_LIMIT: if (_MAX_CHAR_LIMIT > 0) and len(text) > _MAX_CHAR_LIMIT:
@ -367,30 +365,38 @@ class PortalSessionHandler(SessionHandler):
pass pass
if session: if session:
now = time.time() now = time.time()
if self.command_counter > _MAX_COMMAND_RATE > 0:
# data throttle (anti DoS measure) try:
delta_time = now - self.command_counter_reset command_counter_reset = session.command_counter_reset
self.command_counter = 0 except AttributeError:
self.command_counter_reset = now command_counter_reset = session.command_counter_reset = now
self.command_overflow = delta_time < 1.0 session.command_counter = 0
if self.command_overflow:
reactor.callLater(1.0, self.data_in, None) # global command-rate limit
if self.command_overflow: if max(0, now - command_counter_reset) > 1.0:
# more than a second since resetting the counter. Refresh.
session.command_counter_reset = now
session.command_counter = 0
session.command_counter += 1
if session.command_counter * _MIN_TIME_BETWEEN_COMMANDS > 1.0:
self.data_out(session, text=[[_ERROR_COMMAND_OVERFLOW], {}]) self.data_out(session, text=[[_ERROR_COMMAND_OVERFLOW], {}])
return return
if not self.portal.amp_protocol:
# this can happen if someone connects before AMP connection
# was established (usually on first start)
reactor.callLater(1.0, self.data_in, session, **kwargs)
return
# scrub data # scrub data
kwargs = self.clean_senddata(session, kwargs) kwargs = self.clean_senddata(session, kwargs)
# relay data to Server # relay data to Server
self.command_counter += 1
session.cmd_last = now session.cmd_last = now
self.portal.amp_protocol.send_MsgPortal2Server(session, self.portal.amp_protocol.send_MsgPortal2Server(session,
**kwargs) **kwargs)
else:
# called by the callLater callback
if self.command_overflow:
self.command_overflow = False
reactor.callLater(1.0, self.data_in, None)
def data_out(self, session, **kwargs): def data_out(self, session, **kwargs):
""" """

View file

@ -227,26 +227,45 @@ class TelnetOOB(object):
GMCP messages will be outgoing on the following GMCP messages will be outgoing on the following
form (the non-JSON cmdname at the start is what form (the non-JSON cmdname at the start is what
IRE games use, supposedly, and what clients appear IRE games use, supposedly, and what clients appear
to have adopted): to have adopted). A cmdname without Package will end
up in the Core package, while Core package names will
be stripped on the Evennia side.
[cmdname, [], {}] -> cmdname [cmd.name, [], {}] -> Cmd.Name
[cmdname, [arg], {}] -> cmdname arg [cmd.name, [arg], {}] -> Cmd.Name arg
[cmdname, [args],{}] -> cmdname [args] [cmd.name, [args],{}] -> Cmd.Name [args]
[cmdname, [], {kwargs}] -> cmdname {kwargs} [cmd.name, [], {kwargs}] -> Cmd.Name {kwargs}
[cmdname, [args, {kwargs}] -> cmdname [[args],{kwargs}] [cmdname, [args, {kwargs}] -> Core.Cmdname [[args],{kwargs}]
Notes:
There are also a few default mappings between evennia outputcmds and
GMCP:
client_options -> Core.Supports.Get
get_inputfuncs -> Core.Commands.Get
get_value -> Char.Value.Get
repeat -> Char.Repeat.Update
monitor -> Char.Monitor.Update
""" """
if cmdname in EVENNIA_TO_GMCP:
gmcp_cmdname = EVENNIA_TO_GMCP[cmdname]
elif "_" in cmdname:
gmcp_cmdname = ".".join(word.capitalize() for word in cmdname.split("_"))
else:
gmcp_cmdname = "Core.%s" % cmdname.capitalize()
if not (args or kwargs): if not (args or kwargs):
gmcp_string = cmdname gmcp_string = gmcp_cmdname
elif args: elif args:
if len(args) == 1: if len(args) == 1:
args = args[0] args = args[0]
if kwargs: if kwargs:
gmcp_string = "%s %s" % (cmdname, json.dumps([args, kwargs])) gmcp_string = "%s %s" % (gmcp_cmdname, json.dumps([args, kwargs]))
else: else:
gmcp_string = "%s %s" % (cmdname, json.dumps(args)) gmcp_string = "%s %s" % (gmcp_cmdname, json.dumps(args))
else: # only kwargs else: # only kwargs
gmcp_string = "%s %s" % (cmdname, json.dumps(kwargs)) gmcp_string = "%s %s" % (gmcp_cmdname, json.dumps(kwargs))
# print("gmcp string", gmcp_string) # DEBUG # print("gmcp string", gmcp_string) # DEBUG
return gmcp_string return gmcp_string
@ -398,14 +417,9 @@ class TelnetOOB(object):
kwargs.pop("options", None) kwargs.pop("options", None)
if self.MSDP: if self.MSDP:
msdp_cmdname = cmdname encoded_oob = self.encode_msdp(cmdname, *args, **kwargs)
encoded_oob = self.encode_msdp(msdp_cmdname, *args, **kwargs)
self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE) self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE)
if self.GMCP: if self.GMCP:
if cmdname in EVENNIA_TO_GMCP: encoded_oob = self.encode_gmcp(cmdname, *args, **kwargs)
gmcp_cmdname = EVENNIA_TO_GMCP[cmdname]
else:
gmcp_cmdname = "Custom.Cmd"
encoded_oob = self.encode_gmcp(gmcp_cmdname, *args, **kwargs)
self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE) self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE)

View file

@ -129,6 +129,10 @@ def _server_maintenance():
if _MAINTENANCE_COUNT % 3700 == 0: if _MAINTENANCE_COUNT % 3700 == 0:
# validate channels off-sync with scripts # validate channels off-sync with scripts
evennia.CHANNEL_HANDLER.update() evennia.CHANNEL_HANDLER.update()
if _MAINTENANCE_COUNT % (3600 * 7) == 0:
# drop database connection every 7 hrs to avoid default timeouts on MySQL
# (see https://github.com/evennia/evennia/issues/1376)
connection.close()
# handle idle timeouts # handle idle timeouts
if _IDLE_TIMEOUT > 0: if _IDLE_TIMEOUT > 0:
@ -139,11 +143,6 @@ def _server_maintenance():
session.account.access(session.account, "noidletimeout", default=False): session.account.access(session.account, "noidletimeout", default=False):
SESSIONS.disconnect(session, reason=reason) SESSIONS.disconnect(session, reason=reason)
# Commenting this out, it is probably not needed
# with CONN_MAX_AGE set. Keeping it as a reminder
# if database-gone-away errors appears again /Griatch
# if _MAINTENANCE_COUNT % 18000 == 0:
# connection.close()
maintenance_task = LoopingCall(_server_maintenance) maintenance_task = LoopingCall(_server_maintenance)
maintenance_task.start(60, now=True) # call every minute maintenance_task.start(60, now=True) # call every minute

View file

@ -188,6 +188,7 @@ class ServerSession(Session):
if not _ObjectDB: if not _ObjectDB:
from evennia.objects.models import ObjectDB as _ObjectDB from evennia.objects.models import ObjectDB as _ObjectDB
super(ServerSession, self).at_sync()
if not self.logged_in: if not self.logged_in:
# assign the unloggedin-command set. # assign the unloggedin-command set.
self.cmdset_storage = settings.CMDSET_UNLOGGEDIN self.cmdset_storage = settings.CMDSET_UNLOGGEDIN

View file

@ -132,7 +132,8 @@ class Session(object):
on uid etc). on uid etc).
""" """
self.protocol_flags.update(self.account.attributs.get("_saved_protocol_flags"), {}) if self.account:
self.protocol_flags.update(self.account.attributes.get("_saved_protocol_flags", {}))
# access hooks # access hooks

View file

@ -614,10 +614,14 @@ class ServerSessionHandler(SessionHandler):
""" """
uid = curr_session.uid uid = curr_session.uid
# we can't compare sessions directly since this will compare addresses and
# mean connecting from the same host would not catch duplicates
sid = id(curr_session)
doublet_sessions = [sess for sess in self.values() doublet_sessions = [sess for sess in self.values()
if sess.logged_in and if sess.logged_in and
sess.uid == uid and sess.uid == uid and
sess != curr_session] id(sess) != sid]
for session in doublet_sessions: for session in doublet_sessions:
self.disconnect(session, reason) self.disconnect(session, reason)

View file

@ -311,6 +311,8 @@ class EvenniaLogFile(logfile.LogFile):
_LOG_FILE_HANDLES = {} # holds open log handles _LOG_FILE_HANDLES = {} # holds open log handles
_LOG_FILE_HANDLE_COUNTS = {}
_LOG_FILE_HANDLE_RESET = 500
def _open_log_file(filename): def _open_log_file(filename):
@ -318,10 +320,15 @@ def _open_log_file(filename):
Helper to open the log file (always in the log dir) and cache its Helper to open the log file (always in the log dir) and cache its
handle. Will create a new file in the log dir if one didn't handle. Will create a new file in the log dir if one didn't
exist. exist.
To avoid keeping the filehandle open indefinitely we reset it every
_LOG_FILE_HANDLE_RESET accesses. This may help resolve issues for very
long uptimes and heavy log use.
""" """
# we delay import of settings to keep logger module as free # we delay import of settings to keep logger module as free
# from django as possible. # from django as possible.
global _LOG_FILE_HANDLES, _LOGDIR, _LOG_ROTATE_SIZE global _LOG_FILE_HANDLES, _LOG_FILE_HANDLE_COUNTS, _LOGDIR, _LOG_ROTATE_SIZE
if not _LOGDIR: if not _LOGDIR:
from django.conf import settings from django.conf import settings
_LOGDIR = settings.LOG_DIR _LOGDIR = settings.LOG_DIR
@ -329,13 +336,19 @@ def _open_log_file(filename):
filename = os.path.join(_LOGDIR, filename) filename = os.path.join(_LOGDIR, filename)
if filename in _LOG_FILE_HANDLES: if filename in _LOG_FILE_HANDLES:
# cache the handle _LOG_FILE_HANDLE_COUNTS[filename] += 1
return _LOG_FILE_HANDLES[filename] if _LOG_FILE_HANDLE_COUNTS[filename] > _LOG_FILE_HANDLE_RESET:
# close/refresh handle
_LOG_FILE_HANDLES[filename].close()
del _LOG_FILE_HANDLES[filename]
else: else:
# return cached handle
return _LOG_FILE_HANDLES[filename]
try: try:
filehandle = EvenniaLogFile.fromFullPath(filename, rotateLength=_LOG_ROTATE_SIZE) filehandle = EvenniaLogFile.fromFullPath(filename, rotateLength=_LOG_ROTATE_SIZE)
# filehandle = open(filename, "a+") # append mode + reading # filehandle = open(filename, "a+") # append mode + reading
_LOG_FILE_HANDLES[filename] = filehandle _LOG_FILE_HANDLES[filename] = filehandle
_LOG_FILE_HANDLE_COUNTS[filename] = 0
return filehandle return filehandle
except IOError: except IOError:
log_trace() log_trace()