evennia/src/server/amp.py

451 lines
16 KiB
Python

"""
Contains the protocols, commands, and client factory needed for the server
to service the MUD portal proxy.
The separation works like this:
Portal - (AMP client) handles protocols. It contains a list of connected sessions in a
dictionary for identifying the respective player connected. If it looses the AMP connection
it will automatically try to reconnect.
Server - (AMP server) Handles all mud operations. The server holds its own list
of sessions tied to player objects. This is synced against the portal at startup
and when a session connects/disconnects
"""
import os
try:
import cPickle as pickle
except ImportError:
import pickle
from twisted.protocols import amp
from twisted.internet import protocol
from django.conf import settings
from src.utils.utils import to_str
from src.server.models import ServerConfig
from src.scripts.models import ScriptDB
from src.players.models import PlayerDB
from src.server.serversession import ServerSession
PORTAL_RESTART = os.path.join(settings.GAME_DIR, "portal.restart")
SERVER_RESTART = os.path.join(settings.GAME_DIR, "server.restart")
# communication bits
PCONN = chr(1) # portal session connect
PDISCONN = chr(2) # portal session disconnect
PSYNC = chr(3) # portal session sync
SLOGIN = chr(4) # server session login
SDISCONN = chr(5) # server session disconnect
SDISCONNALL = chr(6) # server session disconnect all
SSHUTD = chr(7) # server shutdown
SSYNC = chr(8) # server session sync
# i18n
from django.utils.translation import ugettext as _
def get_restart_mode(restart_file):
"""
Parse the server/portal restart status
"""
if os.path.exists(restart_file):
flag = open(restart_file, 'r').read()
return flag == "True"
return False
class AmpServerFactory(protocol.ServerFactory):
"""
This factory creates new AMPProtocol protocol instances to use for accepting
connections from TCPServer.
"""
def __init__(self, server):
"""
server: The Evennia server service instance
protocol: The protocol the factory creates instances of.
"""
self.server = server
self.protocol = AMPProtocol
def buildProtocol(self, addr):
"""
Start a new connection, and store it on the service object
"""
#print "Evennia Server connected to Portal at %s." % addr
self.server.amp_protocol = AMPProtocol()
self.server.amp_protocol.factory = self
return self.server.amp_protocol
class AmpClientFactory(protocol.ReconnectingClientFactory):
"""
This factory creates new AMPProtocol protocol instances to use to connect
to the MUD server. It also maintains the portal attribute
on the ProxyService instance, which is used for piping input
from Telnet to the MUD server.
"""
# Initial reconnect delay in seconds.
initialDelay = 1
#factor = 1.5
maxDelay = 1
def __init__(self, portal):
self.portal = portal
self.protocol = AMPProtocol
def startedConnecting(self, connector):
"""
Called when starting to try to connect to the MUD server.
"""
pass
#print 'AMP started to connect:', connector
def buildProtocol(self, addr):
"""
Creates an AMPProtocol instance when connecting to the server.
"""
#print "Portal connected to Evennia server at %s." % addr
self.resetDelay()
self.portal.amp_protocol = AMPProtocol()
self.portal.amp_protocol.factory = self
return self.portal.amp_protocol
def clientConnectionLost(self, connector, reason):
"""
Called when the AMP connection to the MUD server is lost.
"""
if not get_restart_mode(SERVER_RESTART):
self.portal.sessions.announce_all(_(" Portal lost connection to Server."))
protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
def clientConnectionFailed(self, connector, reason):
"""
Called when an AMP connection attempt to the MUD server fails.
"""
self.portal.sessions.announce_all(" ...")
protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
class MsgPortal2Server(amp.Command):
"""
Message portal -> server
"""
arguments = [('sessid', amp.Integer()),
('msg', amp.String()),
('data', amp.String())]
errors = [(Exception, 'EXCEPTION')]
response = []
class MsgServer2Portal(amp.Command):
"""
Message server -> portal
"""
arguments = [('sessid', amp.Integer()),
('msg', amp.String()),
('data', amp.String())]
errors = [(Exception, 'EXCEPTION')]
response = []
class OOBPortal2Server(amp.Command):
"""
OOB data portal -> server
"""
arguments = [('sessid', amp.Integer()),
('data', amp.String())]
errors = [(Exception, "EXCEPTION")]
response = []
class OOBServer2Portal(amp.Command):
"""
OOB data server -> portal
"""
arguments = [('sessid', amp.Integer()),
('data', amp.String())]
errors = [(Exception, "EXCEPTION")]
response = []
class ServerAdmin(amp.Command):
"""
Portal -> Server
Sent when the portal needs to perform admin
operations on the server, such as when a new
session connects or resyncs
"""
arguments = [('sessid', amp.Integer()),
('operation', amp.String()),
('data', amp.String())]
errors = [(Exception, 'EXCEPTION')]
response = []
class PortalAdmin(amp.Command):
"""
Server -> Portal
Sent when the server needs to perform admin
operations on the portal.
"""
arguments = [('sessid', amp.Integer()),
('operation', amp.String()),
('data', amp.String())]
errors = [(Exception, 'EXCEPTION')]
response = []
dumps = lambda data: to_str(pickle.dumps(data, pickle.HIGHEST_PROTOCOL))
loads = lambda data: pickle.loads(to_str(data))
#------------------------------------------------------------
# Core AMP protocol for communication Server <-> Portal
#------------------------------------------------------------
class AMPProtocol(amp.AMP):
"""
This is the protocol that the MUD server and the proxy server
communicate to each other with. AMP is a bi-directional protocol, so
both the proxy and the MUD use the same commands and protocol.
AMP specifies responder methods here and connect them to amp.Command
subclasses that specify the datatypes of the input/output of these methods.
"""
# helper methods
def connectionMade(self):
"""
This is called when a connection is established
between server and portal. It is called on both sides,
so we need to make sure to only trigger resync from the
server side.
"""
if hasattr(self.factory, "portal"):
sessdata = self.factory.portal.sessions.get_all_sync_data()
#print sessdata
self.call_remote_ServerAdmin(0,
PSYNC,
data=sessdata)
if get_restart_mode(SERVER_RESTART):
msg = _(" ... Server restarted.")
self.factory.portal.sessions.announce_all(msg)
self.factory.portal.sessions.at_server_connection()
# Error handling
def errback(self, e, info):
"error handler, to avoid dropping connections on server tracebacks."
e.trap(Exception)
print _("AMP Error for %(info)s: %(e)s") % {'info': info, 'e': e.getErrorMessage()}
# Message definition + helper methods to call/create each message type
# Portal -> Server Msg
def amp_msg_portal2server(self, sessid, msg, data):
"""
Relays message to server. This method is executed on the Server.
"""
#print "msg portal -> server (server side):", sessid, msg
self.factory.server.sessions.data_in(sessid, msg, loads(data))
return {}
MsgPortal2Server.responder(amp_msg_portal2server)
def call_remote_MsgPortal2Server(self, sessid, msg, data=""):
"""
Access method called by the Portal and executed on the Portal.
"""
#print "msg portal->server (portal side):", sessid, msg
self.callRemote(MsgPortal2Server,
sessid=sessid,
msg=msg,
data=dumps(data)).addErrback(self.errback, "MsgPortal2Server")
# Server -> Portal message
def amp_msg_server2portal(self, sessid, msg, data):
"""
Relays message to Portal. This method is executed on the Portal.
"""
#print "msg server->portal (portal side):", sessid, msg
self.factory.portal.sessions.data_out(sessid, msg, loads(data))
return {}
MsgServer2Portal.responder(amp_msg_server2portal)
def call_remote_MsgServer2Portal(self, sessid, msg, data=""):
"""
Access method called by the Server and executed on the Server.
"""
#print "msg server->portal (server side):", sessid, msg, data
self.callRemote(MsgServer2Portal,
sessid=sessid,
msg=to_str(msg),
data=dumps(data)).addErrback(self.errback, "OOBServer2Portal")
# OOB Portal -> Server
# Portal -> Server Msg
def amp_oob_portal2server(self, sessid, data):
"""
Relays out-of-band data to server. This method is executed on the Server.
"""
#print "oob portal -> server (server side):", sessid, loads(data)
self.factory.server.sessions.oob_data_in(sessid, loads(data))
return {}
OOBPortal2Server.responder(amp_oob_portal2server)
def call_remote_OOBPortal2Server(self, sessid, data=""):
"""
Access method called by the Portal and executed on the Portal.
"""
#print "oob portal->server (portal side):", sessid, data
self.callRemote(OOBPortal2Server,
sessid=sessid,
data=dumps(data)).addErrback(self.errback, "OOBPortal2Server")
# OOB Server -> Portal message
def amp_oob_server2portal(self, sessid, data):
"""
Relays out-of-band data to Portal. This method is executed on the Portal.
"""
#print "oob server->portal (portal side):", sessid, data
self.factory.portal.sessions.oob_data_out(sessid, loads(data))
return {}
OOBServer2Portal.responder(amp_oob_server2portal)
def call_remote_OOBServer2Portal(self, sessid, data=""):
"""
Access method called by the Server and executed on the Portal.
"""
#print "oob server->portal (server side):", sessid, data
self.callRemote(OOBServer2Portal,
sessid=sessid,
data=dumps(data)).addErrback(self.errback, "OOBServer2Portal")
# Server administration from the Portal side
def amp_server_admin(self, sessid, operation, data):
"""
This allows the portal to perform admin
operations on the server. This is executed on the Server.
"""
data = loads(data)
#print "serveradmin (server side):", sessid, operation, data
if operation == PCONN: #portal_session_connect
# create a new session and sync it
sess = ServerSession()
sess.sessionhandler = self.factory.server.sessions
sess.load_sync_data(data)
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() # this runs initialization without acr
self.factory.server.sessions.portal_connect(sessid, sess)
elif operation == PDISCONN: #'portal_session_disconnect'
# session closed from portal side
self.factory.server.sessions.portal_disconnect(sessid)
elif operation == PSYNC: #'portal_session_sync'
# force a resync of sessions when portal reconnects to server (e.g. after a server reboot)
# the data kwarg contains a dict {sessid: {arg1:val1,...}} representing the attributes
# to sync for each session.
sesslist = []
server_sessionhandler = self.factory.server.sessions
for sessid, sessdict in data.items():
sess = ServerSession()
sess.sessionhandler = server_sessionhandler
sess.load_sync_data(sessdict)
if sess.uid:
sess.player = PlayerDB.objects.get_player_from_uid(sess.uid)
sesslist.append(sess)
# replace sessions on server
server_sessionhandler.portal_session_sync(sesslist)
# after sync is complete we force-validate all scripts (this starts everything)
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)
else:
raise Exception(_("operation %(op)s not recognized.") % {'op': operation})
return {}
ServerAdmin.responder(amp_server_admin)
def call_remote_ServerAdmin(self, sessid, operation="", data=""):
"""
Access method called by the Portal and Executed on the Portal.
"""
#print "serveradmin (portal side):", sessid, operation, data
data = dumps(data)
self.callRemote(ServerAdmin,
sessid=sessid,
operation=operation,
data=data).addErrback(self.errback, "ServerAdmin")
# Portal administraton from the Server side
def amp_portal_admin(self, sessid, operation, data):
"""
This allows the server to perform admin
operations on the portal. This is executed on the Portal.
"""
data = loads(data)
#print "portaladmin (portal side):", sessid, operation, data
if operation == SLOGIN: # 'server_session_login'
# a session has authenticated; sync it.
sess = self.factory.portal.sessions.get_session(sessid)
sess.load_sync_data(data)
elif operation == SDISCONN: #'server_session_disconnect'
# the server is ordering to disconnect the session
self.factory.portal.sessions.server_disconnect(sessid, reason=data)
elif operation == SDISCONNALL: #'server_session_disconnect_all'
# server orders all sessions to disconnect
self.factory.portal.sessions.server_disconnect_all(reason=data)
elif operation == SSHUTD: #server_shutdown'
# the server orders the portal to shut down
self.factory.portal.shutdown(restart=False)
elif operation == SSYNC: #'server_session_sync'
# server wants to save session data to the portal, maybe because
# it's about to shut down. We don't overwrite any sessions,
# just update data on them and remove eventual ones that are
# out of sync (shouldn't happen normally).
portal_sessionhandler = self.factory.portal.sessions.sessions
to_save = [sessid for sessid in data if sessid in portal_sessionhandler.sessions]
to_delete = [sessid for sessid in data if sessid not in to_save]
# save protocols
for sessid in to_save:
portal_sessionhandler.sessions[sessid].load_sync_data(data[sessid])
# disconnect missing protocols
for sessid in to_delete:
portal_sessionhandler.server_disconnect(sessid)
else:
raise Exception(_("operation %(op)s not recognized.") % {'op': operation})
return {}
PortalAdmin.responder(amp_portal_admin)
def call_remote_PortalAdmin(self, sessid, operation="", data=""):
"""
Access method called by the server side.
"""
#print "portaladmin (server side):", sessid, operation, data
data = dumps(data)
self.callRemote(PortalAdmin,
sessid=sessid,
operation=operation,
data=data).addErrback(self.errback, "PortalAdmin")