evennia/src/server/sessionhandler.py

537 lines
19 KiB
Python
Raw Normal View History

2006-12-22 01:40:40 +00:00
"""
2012-03-30 23:57:04 +02:00
This module defines handlers for storing sessions when handles
sessions of users connecting to the server.
There are two similar but separate stores of sessions:
2012-03-30 23:57:04 +02:00
ServerSessionHandler - this stores generic game sessions
for the game. These sessions has no knowledge about
2012-03-30 23:57:04 +02:00
how they are connected to the world.
PortalSessionHandler - this stores sessions created by
twisted protocols. These are dumb connectors that
handle network communication but holds no game info.
2012-03-30 23:57:04 +02:00
2006-12-22 01:40:40 +00:00
"""
import time
from django.conf import settings
from src.commands.cmdhandler import CMD_LOGINSTART
# delayed imports
_PlayerDB = None
_ServerSession = None
_ServerConfig = None
_ScriptDB = None
2012-04-29 21:17:14 +02:00
2012-03-30 23:57:04 +02:00
# AMP signals
PCONN = chr(1) # portal session connect
PDISCONN = chr(2) # portal session disconnect
PSYNC = chr(3) # portal session sync
SLOGIN = chr(4) # server session login
2012-03-30 23:57:04 +02:00
SDISCONN = chr(5) # server session disconnect
SDISCONNALL = chr(6) # server session disconnect all
2012-03-30 23:57:04 +02:00
SSHUTD = chr(7) # server shutdown
SSYNC = chr(8) # server session sync
# i18n
from django.utils.translation import ugettext as _
2012-04-29 21:17:14 +02:00
SERVERNAME = settings.SERVERNAME
MULTISESSION_MODE = settings.MULTISESSION_MODE
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
def delayed_import():
"Helper method for delayed import of all needed entities"
global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB
if not _ServerSession:
from src.server.serversession import ServerSession as _ServerSession
if not _PlayerDB:
from src.players.models import PlayerDB as _PlayerDB
if not _ServerConfig:
from src.server.models import ServerConfig as _ServerConfig
if not _ScriptDB:
from src.scripts.models import ScriptDB as _ScriptDB
# including once to avoid warnings in Python syntax checkers
_ServerSession, _PlayerDB, _ServerConfig, _ScriptDB
2012-04-29 21:17:14 +02:00
#-----------------------------------------------------------
# SessionHandler base class
#------------------------------------------------------------
class SessionHandler(object):
"""
This handler holds a stack of sessions.
"""
def __init__(self):
"""
2012-03-30 23:57:04 +02:00
Init the handler.
"""
self.sessions = {}
def get_sessions(self, include_unloggedin=False):
"""
Returns the connected session objects.
"""
if include_unloggedin:
return self.sessions.values()
else:
return [session for session in self.sessions.values() if session.logged_in]
def get_session(self, sessid):
"""
Get session by sessid
"""
return self.sessions.get(sessid, None)
def get_all_sync_data(self):
"""
2012-03-30 23:57:04 +02:00
Create a dictionary of sessdata dicts representing all
sessions in store.
"""
return dict((sessid, sess.get_sync_data()) for sessid, sess in self.sessions.items())
#------------------------------------------------------------
# Server-SessionHandler class
#------------------------------------------------------------
class ServerSessionHandler(SessionHandler):
"""
This object holds the stack of sessions active in the game at
2012-03-30 23:57:04 +02:00
any time.
A session register with the handler in two steps, first by
registering itself with the connect() method. This indicates an
non-authenticated session. Whenever the session is authenticated
the session together with the related player is sent to the login()
2012-03-30 23:57:04 +02:00
method.
"""
# AMP communication methods
def __init__(self):
"""
2012-03-30 23:57:04 +02:00
Init the handler.
"""
self.sessions = {}
2012-03-30 23:57:04 +02:00
self.server = None
2012-04-29 21:17:14 +02:00
self.server_data = {"servername":SERVERNAME}
def portal_connect(self, portalsession):
"""
2012-03-30 23:57:04 +02:00
Called by Portal when a new session has connected.
Creates a new, unlogged-in game session.
portalsession is a dictionary of all property:value keys
defining the session and which is marked to
be synced.
"""
delayed_import()
global _ServerSession, _PlayerDB, _ScriptDB
sess = _ServerSession()
sess.sessionhandler = self
sess.load_sync_data(portalsession)
if sess.logged_in and sess.uid:
# this can happen in the case of auto-authenticating protocols like SSH
sess.player = _PlayerDB.objects.get_player_from_uid(sess.uid)
sess.at_sync()
# validate all script
_ScriptDB.objects.validate()
self.sessions[sess.sessid] = sess
sess.data_in(CMD_LOGINSTART)
def portal_disconnect(self, sessid):
"""
Called by Portal when portal reports a closing of a session
from the portal side.
"""
session = self.sessions.get(sessid, None)
if session:
session.disconnect()
del self.sessions[session.sessid]
self.session_count(-1)
def portal_session_sync(self, portalsessions):
"""
Syncing all session ids of the portal with the ones of the server. This is instantiated
by the portal when reconnecting.
2012-03-30 23:57:04 +02:00
portalsessions is a dictionary {sessid: {property:value},...} defining
each session and the properties in it which should be synced.
2012-03-30 23:57:04 +02:00
"""
delayed_import()
global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB
for sess in self.sessions.values():
# we delete the old session to make sure to catch eventual lingering references.
del sess
for sessid, sessdict in portalsessions.items():
sess = _ServerSession()
sess.sessionhandler = self
sess.load_sync_data(sessdict)
if sess.uid:
sess.player = _PlayerDB.objects.get_player_from_uid(sess.uid)
self.sessions[sessid] = sess
sess.at_sync()
# after sync is complete we force-validate all scripts (this also starts them)
init_mode = _ServerConfig.objects.conf("server_restart_mode", default=None)
_ScriptDB.objects.validate(init_mode=init_mode)
_ServerConfig.objects.conf("server_restart_mode", delete=True)
# announce the reconnection
self.announce_all(_(" ... Server restarted."))
def portal_shutdown(self):
"""
Called by server when shutting down the portal.
2012-03-30 23:57:04 +02:00
"""
self.server.amp_protocol.call_remote_PortalAdmin(0,
operation=SSHUTD,
2012-03-30 23:57:04 +02:00
data="")
# server-side access methods
def login(self, session, player):
"""
Log in the previously unloggedin session and the player we by
now should know is connected to it. After this point we
assume the session to be logged in one way or another.
"""
2012-03-30 23:57:04 +02:00
# we have to check this first before uid has been assigned
# this session.
if not self.sessions_from_player(player):
player.is_connected = True
# sets up and assigns all properties on the session
session.at_login(player)
# player init
player.at_init()
# Check if this is the first time the *player* logs in
if player.db.FIRST_LOGIN:
player.at_first_login()
del player.db.FIRST_LOGIN
player.at_pre_login()
if MULTISESSION_MODE == 0:
# disconnect all previous sessions.
self.disconnect_duplicate_sessions(session)
nsess = len(self.sessions_from_player(player))
totalstring = "%i session%s total" % (nsess, nsess > 1 and "s" or "")
session.log(_('Logged in: %s %s (%s)' % (player, session.address, totalstring)))
2012-03-30 23:57:04 +02:00
session.logged_in = True
# sync the portal to the session
sessdata = session.get_sync_data()
self.server.amp_protocol.call_remote_PortalAdmin(session.sessid,
operation=SLOGIN,
data=sessdata)
player.at_post_login()
2012-03-30 23:57:04 +02:00
def disconnect(self, session, reason=""):
"""
Called from server side to remove session and inform portal
of this fact.
"""
session = self.sessions.get(session.sessid)
if not session:
return
if hasattr(session, "player") and session.player:
# only log accounts logging off
nsess = len(self.sessions_from_player(session.player)) - 1
remaintext = nsess and "%i session%s remaining" % (nsess, nsess > 1 and "s" or "") or "no more sessions"
session.log(_('Logged out: %s %s (%s)' % (session.player, session.address, remaintext)))
session.at_disconnect()
sessid = session.sessid
del self.sessions[sessid]
# inform portal that session should be closed.
self.server.amp_protocol.call_remote_PortalAdmin(sessid,
operation=SDISCONN,
data=reason)
def all_sessions_portal_sync(self):
"""
This is called by the server when it reboots. It syncs all session data
to the portal. Returns a deferred!
"""
sessdata = self.get_all_sync_data()
return self.server.amp_protocol.call_remote_PortalAdmin(0,
operation=SSYNC,
data=sessdata)
def disconnect_all_sessions(self, reason=_("You have been disconnected.")):
"""
Cleanly disconnect all of the connected sessions.
"""
2012-03-30 23:57:04 +02:00
for session in self.sessions:
del session
# tell portal to disconnect all sessions
self.server.amp_protocol.call_remote_PortalAdmin(0,
operation=SDISCONNALL,
data=reason)
def disconnect_duplicate_sessions(self, curr_session, reason = _("Logged in from elsewhere. Disconnecting.") ):
"""
Disconnects any existing sessions with the same user.
"""
uid = curr_session.uid
doublet_sessions = [sess for sess in self.sessions.values()
2012-03-30 23:57:04 +02:00
if sess.logged_in
and sess.uid == uid
and sess != curr_session]
for session in doublet_sessions:
2012-03-30 23:57:04 +02:00
self.disconnect(session, reason)
def validate_sessions(self):
"""
2012-03-30 23:57:04 +02:00
Check all currently connected sessions (logged in and not)
and see if any are dead.
"""
tcurr = time.time()
2012-02-25 21:15:58 +01:00
reason= _("Idle timeout exceeded, disconnecting.")
2012-03-30 23:57:04 +02:00
for session in (session for session in self.sessions.values()
if session.logged_in and IDLE_TIMEOUT > 0
and (tcurr - session.cmd_last) > IDLE_TIMEOUT):
self.disconnect(session, reason=reason)
def player_count(self):
"""
Get the number of connected players (not sessions since a
player may have more than one session depending on settings).
Only logged-in players are counted here.
"""
return len(set(session.uid for session in self.sessions.values() if session.logged_in))
2012-03-30 23:57:04 +02:00
def session_from_player(self, player, sessid):
"""
Given a player and a session id, return the actual session object
"""
session = self.sessions.get(sessid)
return session and session.logged_in and player.uid == session.uid and session or None
def sessions_from_player(self, player):
"""
Given a player, return all matching sessions.
"""
uid = player.uid
return [session for session in self.sessions.values() if session.logged_in and session.uid == uid]
def sessions_from_character(self, character):
"""
Given a game character, return any matching sessions.
"""
sessid = character.sessid
if sessid:
return self.sessions.get(sessid)
2012-03-30 23:57:04 +02:00
return None
def announce_all(self, message):
"""
Send message to all connected sessions
"""
for sess in self.sessions.values():
self.data_out(sess, message)
def data_out(self, session, string="", data=""):
"""
Sending data Server -> Portal
"""
self.server.amp_protocol.call_remote_MsgServer2Portal(sessid=session.sessid,
msg=string,
data=data)
def data_in(self, sessid, string="", data=""):
"""
Data Portal -> Server
2012-03-30 23:57:04 +02:00
"""
session = self.sessions.get(sessid, None)
if session:
session.data_in(string)
# ignore 'data' argument for now; this is otherwise the place
# to put custom effects on the server due to data input, e.g.
2012-03-30 23:57:04 +02:00
# from a custom client.
def oob_data_in(self, sessid, data):
"""
OOB (Out-of-band) Data Portal -> Server
"""
session = self.sessions.get(sessid, None)
if session:
session.oob_data_in(data)
def oob_data_out(self, session, data):
"""
OOB (Out-of-band) Data Server -> Portal
"""
self.server.amp_protocol.call_remote_OOBServer2Portal(session.sessid,
data=data)
#------------------------------------------------------------
# Portal-SessionHandler class
#------------------------------------------------------------
class PortalSessionHandler(SessionHandler):
"""
This object holds the sessions connected to the portal at any time.
It is synced with the server's equivalent SessionHandler over the AMP
2012-03-30 23:57:04 +02:00
connection.
2012-03-30 23:57:04 +02:00
Sessions register with the handler using the connect() method. This
will assign a new unique sessionid to the session and send that sessid
2012-03-30 23:57:04 +02:00
to the server using the AMP connection.
"""
def __init__(self):
"""
Init the handler
"""
2012-03-30 23:57:04 +02:00
self.portal = None
self.sessions = {}
self.latest_sessid = 0
self.uptime = time.time()
self.connection_time = 0
def at_server_connection(self):
"""
Called when the Portal establishes connection with the
Server. At this point, the AMP connection is already
established.
"""
2012-03-30 23:57:04 +02:00
self.connection_time = time.time()
def connect(self, session):
"""
Called by protocol at first connect. This adds a not-yet authenticated session
2012-03-30 23:57:04 +02:00
using an ever-increasing counter for sessid.
"""
self.latest_sessid += 1
sessid = self.latest_sessid
session.sessid = sessid
sessdata = session.get_sync_data()
self.sessions[sessid] = session
2012-03-30 23:57:04 +02:00
# sync with server-side
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
operation=PCONN,
data=sessdata)
def disconnect(self, session):
"""
Called from portal side when the connection is closed from the portal side.
"""
sessid = session.sessid
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
operation=PDISCONN)
2012-03-30 23:57:04 +02:00
def server_disconnect(self, sessid, reason=""):
"""
Called by server to force a disconnect by sessid
"""
session = self.sessions.get(sessid, None)
if session:
2012-03-30 23:57:04 +02:00
session.disconnect(reason)
if sessid in self.sessions:
# in case sess.disconnect doesn't delete it
del self.sessions[sessid]
2012-03-30 23:57:04 +02:00
del session
def server_disconnect_all(self, reason=""):
"""
Called by server when forcing a clean disconnect for everyone.
"""
2012-03-30 23:57:04 +02:00
for session in self.sessions.values():
session.disconnect(reason)
2012-03-30 23:57:04 +02:00
del session
self.sessions = {}
def server_logged_in(self, sessid, data):
"The server tells us that the session has been authenticated. Updated it."
sess = self.get_session(sessid)
sess.load_sync_data(data)
def server_session_sync(self, serversessions):
"""
Server wants to save data to the portal, maybe because it's about to shut down.
We don't overwrite any sessions here, just update them in-place and remove
any that are out of sync (which should normally not be the case)
serversessions - dictionary {sessid:{property:value},...} describing the properties
to sync on all sessions
"""
to_save = [sessid for sessid in serversessions if sessid in self.sessions]
to_delete = [sessid for sessid in serversessions if sessid not in to_save]
# save protocols
for sessid in to_save:
self.sessions[sessid].load_sync_data(serversessions[sessid])
# disconnect out-of-sync missing protocols
for sessid in to_delete:
self.server_disconnect(sessid)
def count_loggedin(self, include_unloggedin=False):
"""
Count loggedin connections, alternatively count all connections.
"""
return len(self.get_sessions(include_unloggedin=include_unloggedin))
2012-03-30 23:57:04 +02:00
def session_from_suid(self, suid):
"""
Given a session id, retrieve the session (this is primarily
intended to be called by web clients)
"""
2012-03-30 23:57:04 +02:00
return [sess for sess in self.get_sessions(include_unloggedin=True)
if hasattr(sess, 'suid') and sess.suid == suid]
def data_in(self, session, string="", data=""):
"""
2012-03-30 23:57:04 +02:00
Called by portal sessions for relaying data coming
in from the protocol to the server. data is
serialized before passed on.
"""
2012-04-29 21:17:14 +02:00
#print "portal_data_in:", string
self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid,
msg=string,
data=data)
def announce_all(self, message):
"""
Send message to all connection sessions
"""
for session in self.sessions.values():
session.data_out(message)
def data_out(self, sessid, string="", data=""):
"""
2012-03-30 23:57:04 +02:00
Called by server for having the portal relay messages and data
to the correct session protocol.
"""
session = self.sessions.get(sessid, None)
if session:
2012-03-30 23:57:04 +02:00
session.data_out(string, data=data)
def oob_data_in(self, session, data):
"""
OOB (Out-of-band) data Portal -> Server
"""
print "portal_oob_data_in:", data
self.portal.amp_protocol.call_remote_OOBPortal2Server(session.sessid,
data=data)
def oob_data_out(self, sessid, data):
"""
OOB (Out-of-band) data Server -> Portal
"""
print "portal_oob_data_out:", data
session = self.sessions.get(sessid, None)
if session:
session.oob_data_out(data)
SESSIONS = ServerSessionHandler()
PORTAL_SESSIONS = PortalSessionHandler()