Make IDLE_TIMEOUT avoidable with account-lock.

Resolves #701. Check the Account-lock 'no_idle_disconnect before kicking an idle
session. This also means superusers will never be kicked. Move the
idle task to the Server to avoid lock imports in Portal. Make the
'lock' command also able to target Accounts. Also some other fixes.
This commit is contained in:
Griatch 2017-08-20 23:11:33 +02:00
parent cdac9678b9
commit 8a2e362b7c
11 changed files with 63 additions and 52 deletions

View file

@ -190,7 +190,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# session-related methods
def disconnect_session_from_account(self, session):
def disconnect_session_from_account(self, session, reason=None):
"""
Access method for disconnecting a given session from the
account (connection happens automatically in the
@ -198,12 +198,13 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
Args:
session (Session): Session to disconnect.
reason (str, optional): Eventual reason for the disconnect.
"""
global _SESSIONS
if not _SESSIONS:
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
_SESSIONS.disconnect(session)
_SESSIONS.disconnect(session, reason)
# puppeting operations
@ -789,8 +790,8 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
"""
reason = reason and "(%s)" % reason or ""
self._send_to_connect_channel("|R%s disconnected %s|n" % (self.key, reason))
reason = " (%s)" % reason if reason else ""
self._send_to_connect_channel("|R%s disconnected%s|n" % (self.key, reason))
def at_post_disconnect(self, **kwargs):
"""

View file

@ -657,10 +657,12 @@ class CmdQuit(COMMAND_DEFAULT_CLASS):
if 'all' in self.switches:
account.msg("|RQuitting|n all sessions. Hope to see you soon again.", session=self.session)
reason = "quit/all"
for session in account.sessions.all():
account.disconnect_session_from_account(session)
account.disconnect_session_from_account(session, reason)
else:
nsess = len(account.sessions.all())
reason = "quit"
if nsess == 2:
account.msg("|RQuitting|n. One session is still connected.", session=self.session)
elif nsess > 2:
@ -668,7 +670,7 @@ class CmdQuit(COMMAND_DEFAULT_CLASS):
else:
# we are quitting the last available session
account.msg("|RQuitting|n. Hope to see you again, soon.", session=self.session)
account.disconnect_session_from_account(self.session)
account.disconnect_session_from_account(self.session, reason)
class CmdColorTest(COMMAND_DEFAULT_CLASS):

View file

@ -1735,9 +1735,9 @@ class CmdLock(ObjManipCommand):
assign a lock definition to an object
Usage:
@lock <object>[ = <lockstring>]
@lock <object or *account>[ = <lockstring>]
or
@lock[/switch] <object>/<access_type>
@lock[/switch] <object or *account>/<access_type>
Switch:
del - delete given access type
@ -1779,16 +1779,19 @@ class CmdLock(ObjManipCommand):
if '/' in self.lhs:
# call on the form @lock obj/access_type
objname, access_type = [p.strip() for p in self.lhs.split('/', 1)]
obj = caller.search(objname)
if not obj:
if objname.startswith("*"):
obj = caller.search_account(objname.lstrip('*'))
if not obj:
obj = caller.search(objname)
if not obj:
return
if not (obj.access(caller, 'control') or obj.access(caller, "edit")):
caller.msg("You are not allowed to do that.")
return
lockdef = obj.locks.get(access_type)
string = ""
if lockdef:
if 'del' in self.switches:
if not (obj.access(caller, 'control') or obj.access(caller, "edit")):
caller.msg("You are not allowed to do that.")
return
obj.locks.delete(access_type)
string = "deleted lock %s" % lockdef
else:
@ -1808,9 +1811,12 @@ class CmdLock(ObjManipCommand):
return
objname, lockdef = self.lhs, self.rhs
obj = caller.search(objname)
if not obj:
return
if objname.startswith("*"):
obj = caller.search_account(objname.lstrip('*'))
if not obj:
obj = caller.search(objname)
if not obj:
return
if not (obj.access(caller, 'control') or obj.access(caller, "edit")):
caller.msg("You are not allowed to do that.")
return

View file

@ -73,33 +73,6 @@ AMP_INTERFACE = settings.AMP_INTERFACE
AMP_ENABLED = AMP_HOST and AMP_PORT and AMP_INTERFACE
# Maintenance function - this is called repeatedly by the portal.
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
def _portal_maintenance():
"""
The maintenance function handles repeated checks and updates that
the server needs to do. It is called every minute.
"""
# check for idle sessions
now = time.time()
reason = "Idle timeout exceeded, disconnecting."
for session in [sess for sess in PORTAL_SESSIONS.values()
if (now - sess.cmd_last) > _IDLE_TIMEOUT]:
session.disconnect(reason=reason)
PORTAL_SESSIONS.disconnect(session)
if _IDLE_TIMEOUT > 0:
# only start the maintenance task if we care about idling.
_maintenance_task = LoopingCall(_portal_maintenance)
_maintenance_task.start(60) # called every minute
# -------------------------------------------------------------
# Portal Service object
# -------------------------------------------------------------

View file

@ -268,7 +268,6 @@ class PortalSessionHandler(SessionHandler):
data (dict): The session sync data.
"""
print("server_logged_in: %s: %s" % (session, data))
session.load_sync_data(data)
session.at_login()

View file

@ -213,6 +213,12 @@ class SshProtocol(Manhole, session.Session):
# session-general method hooks
def at_login(self):
"""
Called when this session gets authenticated by the server.
"""
pass
def disconnect(self, reason="Connection closed. Goodbye for now."):
"""
Disconnect from server.

View file

@ -116,6 +116,12 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
# do the sync
self.sessionhandler.sync(self)
def at_login(self):
"""
Called when this session gets authenticated by the server.
"""
pass
def enableRemote(self, option):
"""
This sets up the remote-activated options we allow for this protocol.

View file

@ -107,6 +107,12 @@ class WebClient(resource.Resource):
self.keep_alive.stop()
self.keep_alive = None
def at_login(self):
"""
Called when this session gets authenticated by the server.
"""
pass
def lineSend(self, csessid, data):
"""
This adds the data to the buffer and/or sends it to the client

View file

@ -16,7 +16,7 @@ import os
from twisted.web import static
from twisted.application import internet, service
from twisted.internet import reactor, defer
from twisted.internet.task import LoopingCall, deferLater
from twisted.internet.task import LoopingCall
import django
django.setup()
@ -36,6 +36,8 @@ from evennia.utils.utils import get_evennia_version, mod_import, make_iter
from evennia.comms import channelhandler
from evennia.server.sessionhandler import SESSIONS
from django.utils.translation import ugettext as _
_SA = object.__setattr__
SERVER_PIDFILE = ""
@ -89,11 +91,13 @@ _FLUSH_CACHE = None
_IDMAPPER_CACHE_MAXSIZE = settings.IDMAPPER_CACHE_MAXSIZE
_GAMETIME_MODULE = None
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
def _server_maintenance():
"""
This maintenance function handles repeated checks and updates that
the server needs to do. It is called every 5 minutes.
the server needs to do. It is called every minute.
"""
global EVENNIA, _MAINTENANCE_COUNT, _FLUSH_CACHE, _GAMETIME_MODULE
if not _FLUSH_CACHE:
@ -124,6 +128,14 @@ def _server_maintenance():
# validate channels off-sync with scripts
evennia.CHANNEL_HANDLER.update()
# handle idle timeouts
reason = _("idle timeout exceeded")
for session in (sess for sess in SESSIONS.values()
if (now - sess.cmd_last) > _IDLE_TIMEOUT):
if not session.account or not \
session.account.access(session.account, "no_idle_disconnect", default=False):
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

View file

@ -9,7 +9,6 @@ are stored on the Portal side)
from builtins import object
import weakref
import importlib
import time
from django.utils import timezone
from django.conf import settings
@ -232,7 +231,7 @@ class ServerSession(Session):
# add the session-level cmdset
self.cmdset = CmdSetHandler(self, True)
def at_disconnect(self):
def at_disconnect(self, reason=None):
"""
Hook called by sessionhandler when disconnecting this session.
@ -245,7 +244,7 @@ class ServerSession(Session):
uaccount.last_login = timezone.now()
uaccount.save()
# calling account hook
account.at_disconnect()
account.at_disconnect(reason)
self.logged_in = False
if not self.sessionhandler.sessions_from_account(account):
# no more sessions connected to this account

View file

@ -500,11 +500,12 @@ class ServerSessionHandler(SessionHandler):
if hasattr(session, "account") and session.account:
# only log accounts logging off
nsess = len(self.sessions_from_account(session.account)) - 1
string = "Logged out: {account} {address} ({nsessions} sessions(s) remaining)"
string = string.format(account=session.account, address=session.address, nsessions=nsess)
sreason = " ({})".format(reason) if reason else ""
string = "Logged out: {account} {address} ({nsessions} sessions(s) remaining){reason}"
string = string.format(reason=sreason, account=session.account, address=session.address, nsessions=nsess)
session.log(string)
session.at_disconnect()
session.at_disconnect(reason)
sessid = session.sessid
if sessid in self and not hasattr(self, "_disconnect_all"):
del self[sessid]