Resolved issue122. Also clarified the functional sequence for disconnecting a session cleanly (avoiding circular calls that might happen if disconnection happens manually or automatically due to server shutdown). Removed the specific stopping callback from the webclient and put it in the mai

n server process instead, so all protocols can get a message when server shuts down with Ctrl-C.
This commit is contained in:
Griatch 2010-12-12 10:54:33 +00:00
parent 6ecbda03ea
commit 939307a5c1
6 changed files with 118 additions and 68 deletions

View file

@ -58,15 +58,14 @@ class CmdBoot(MuxCommand):
# Boot by player object
pobj = caller.search("*%s" % args, global_search=True)
if not pobj:
return
pobj = pobj
if pobj.has_player:
return
if pobj.character.has_player:
if not has_perm(caller, pobj, 'can_boot'):
string = "You don't have the permission to boot %s."
pobj.msg(string)
return
# we have a bootable object with a connected user
matches = SESSIONS.sessions_from_object(pobj)
matches = SESSIONS.sessions_from_player(pobj)
for match in matches:
boot_list.append(match)
else:
@ -87,10 +86,8 @@ class CmdBoot(MuxCommand):
for session in boot_list:
name = session.name
if feedback:
session.msg(feedback)
session.disconnectClient()
SESSIONS.remove_session(session)
session.msg(feedback)
session.disconnect()
caller.msg("You booted %s." % name)
@ -120,7 +117,7 @@ class CmdDelPlayer(MuxCommand):
args = self.args
if not args:
caller.msg("Usage: @delplayer[/delobj] <player/user name or #id>")
caller.msg("Usage: @delplayer[/delobj] <player/user name or #id> [: reason]")
return
reason = ""
@ -129,7 +126,7 @@ class CmdDelPlayer(MuxCommand):
# We use player_search since we want to be sure to find also players
# that lack characters.
players = PlayerDB.objects.filter(db_key=args)
players = caller.search("*%s" % args)
if not players:
try:
players = PlayerDB.objects.filter(id=args)
@ -150,12 +147,13 @@ class CmdDelPlayer(MuxCommand):
try:
player = user.get_profile()
except Exception:
player = None
player = None
if not has_perm_string(caller, 'manage_players'):
string = "You don't have the permissions to delete this player."
caller.msg(string)
return
string = ""
name = user.username
user.delete()
@ -164,7 +162,7 @@ class CmdDelPlayer(MuxCommand):
player.delete()
string = "Player %s was deleted." % name
else:
string += "The User %s was deleted, but had no Player associated with it." % name
string += "The User %s was deleted. It had no Player associated with it." % name
caller.msg(string)
return

View file

@ -16,7 +16,7 @@ if os.name == 'nt':
os.path.dirname(os.path.abspath(__file__)))))
from twisted.application import internet, service
from twisted.internet import protocol, reactor
from twisted.internet import protocol, reactor, defer
from twisted.web import server, static
from django.db import connection
from django.conf import settings
@ -92,6 +92,10 @@ class Evennia(object):
print '-'*50
# set a callback if the server is killed abruptly,
# by Ctrl-C, reboot etc.
reactor.addSystemEventTrigger('before', 'shutdown',self.shutdown, _abrupt=True)
self.game_running = True
# Server startup methods
@ -145,16 +149,21 @@ class Evennia(object):
if WEBCLIENT_ENABLED:
clientstring = '/client'
print " webserver%s: " % clientstring + ", ".join([str(port) for port in WEBSERVER_PORTS])
def shutdown(self, message=None):
def shutdown(self, message="{rThe server has been shutdown. Disconnecting.{n", _abrupt=False):
"""
Gracefully disconnect everyone and kill the reactor.
If called directly, this disconnects everyone cleanly and shuts down the
reactor. If the server is killed by other means (Ctrl-C, reboot etc), this
might be called as a callback, at which point the reactor is already dead
and should not be tried to stop again (_abrupt=True).
message - message to send to all connected sessions
_abrupt - only to be used by internal callback_mechanism.
"""
if not message:
message = 'The server has been shutdown. Please check back soon.'
SESSIONS.disconnect_all_sessions(reason=message)
reactor.callLater(0, reactor.stop)
if not _abrupt:
reactor.callLater(0, reactor.stop)
#------------------------------------------------------------
#
@ -200,13 +209,14 @@ if WEBSERVER_ENABLED:
if WEBCLIENT_ENABLED:
# create ajax client processes at /webclientdata
from src.server.webclient import WebClient
from src.server.webclient import WebClient
web_root.putChild("webclientdata", WebClient())
web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
for port in WEBSERVER_PORTS:
# create the webserver
webserver = internet.TCPServer(port, web_site)
#webserver = internet.SSLServer(port, web_site)
webserver.setName('EvenniaWebServer%s' % port)
EVENNIA.services.addService(webserver)

View file

@ -2,6 +2,35 @@
This defines a generic session class.
All protocols should implement this class and its hook methods.
The process of first connect:
- The custom connection-handler for the respective
protocol should be called by the transport connection itself.
- The connect-handler handles whatever internal settings are needed
- The connection-handler calls session_connect()
- session_connect() setups sessions then calls session.at_connect()
Disconnecting is a bit more complex in order to avoid circular calls
depending on if the disconnect happens automatically or manually from
a command.
The process at automatic disconnect:
- The custom disconnect-handler for the respective protocol
should be called by the transport connection itself. This handler
should be defined with a keyword argument 'step' defaulting to 1.
- since step=1, the disconnect-handler calls session_disconnect()
- session_disconnect() removes session, then calls session.at_disconnect()
- session.at_disconnect() calls the custom disconnect-handler with
step=2 as argument
- since step=2, the disconnect-handler closes the connection and
performs all needed protocol cleanup.
The process of manual disconnect:
- The command/outside function calls session.session_disconnect().
- from here the process proceeds as the automatic disconnect above.
"""
import time
@ -76,9 +105,9 @@ class SessionBase(object):
self.cmd_total = 0
#self.channels_subscribed = {}
SESSIONS.add_unloggedin_session(self)
# call hook method
# calling hook
self.at_connect()
def session_login(self, player):
"""
Private startup mechanisms that need to run at login
@ -106,7 +135,7 @@ class SessionBase(object):
#call hook
self.at_login()
def session_disconnect(self, reason=None):
def session_disconnect(self):
"""
Clean up the session, removing it from the game and doing some
accounting. This method is used also for non-loggedin
@ -118,12 +147,12 @@ class SessionBase(object):
if self.logged_in:
character = self.get_character()
if character:
character.player.at_disconnect(reason)
uaccount = character.player.user
uaccount.last_login = datetime.now()
uaccount.save()
self.logged_in = False
SESSIONS.remove_session(self)
self.at_disconnect()
self.logged_in = False
SESSIONS.remove_session(self)
def session_validate(self):
"""
@ -275,11 +304,19 @@ class Session(SessionBase):
"""
pass
def at_disconnect(self, reason=None):
def at_disconnect(self):
"""
This method is called just before cleaning up the session
(so still logged_in=True at this point).
This method should not be called from commands, instead it
is called automatically by session_disconnect() as part of
the cleanup.
This method MUST call the protocol-dependant disconnect-handler
with step=2 to finalize the closing of the connection!
"""
# self.my-disconnect-handler(step=2)
pass
def at_data_in(self, string="", data=None):
@ -302,9 +339,9 @@ class Session(SessionBase):
def login(self, player):
"alias for at_login"
self.at_login(player)
def logout(self):
"alias for at_logout"
self.at_disconnect()
def disconnect(self):
"alias for session_disconnect"
self.session_disconnect()
def msg(self, string='', data=None):
"alias for at_data_out"
self.at_data_out(string, data)

View file

@ -97,13 +97,14 @@ class SessionHandler(object):
else:
return self.loggedin
def disconnect_all_sessions(self, reason=None):
def disconnect_all_sessions(self, reason="You have been disconnected."):
"""
Cleanly disconnect all of the connected sessions.
"""
sessions = self.get_sessions(include_unloggedin=True)
sessions = self.get_sessions(include_unloggedin=True)
for session in sessions:
session.session_disconnect(reason)
session.at_data_out(reason)
session.session_disconnect()
self.session_count(0)
def disconnect_duplicate_sessions(self, session):

View file

@ -32,16 +32,23 @@ class TelnetProtocol(StatefulTelnetProtocol, session.Session):
# initialize the session
self.session_connect(self.getClientAddress())
def connectionLost(self, reason="Disconnecting. Goodbye for now."):
def connectionLost(self, reason=None, step=1):
"""
This is executed when the connection is lost for
whatever reason. It should also be called from
self.at_disconnect() so one can close the connection
manually without having to know the name of this specific
method.
whatever reason.
Closing the connection takes two steps
step 1 - is the default and is used when this method is
called automatically. The method should then call self.session_disconnect().
Step 2 - means this method is called from at_disconnect(). At this point
the sessions are assumed to have been handled, and so the transport can close
without further ado.
"""
self.session_disconnect(reason)
self.transport.loseConnection()
if step == 1:
self.session_disconnect()
else:
self.transport.loseConnection()
def getClientAddress(self):
"""
@ -81,7 +88,7 @@ class TelnetProtocol(StatefulTelnetProtocol, session.Session):
# show screen
screen = ConnectScreen.objects.get_random_connect_screen()
string = ansi.parse_ansi(screen.text)
self.lineSend(string)
self.at_data_out(string)
def at_login(self):
"""
@ -96,9 +103,8 @@ class TelnetProtocol(StatefulTelnetProtocol, session.Session):
"""
Disconnect from server
"""
if reason:
self.lineSend(reason)
self.connectionLost(reason)
self.at_data_out(reason)
self.connectionLost(step=2)
def at_data_out(self, string, data=None):
"""

View file

@ -64,7 +64,6 @@ class WebClient(resource.Resource):
def __init__(self):
self.requests = {}
self.databuffer = {}
reactor.addSystemEventTrigger('before', 'shutdown',self._forced_disconnect)
def getChild(self, path, request):
"""
@ -78,15 +77,7 @@ class WebClient(resource.Resource):
self.requests.get(suid, []).remove(request)
except ValueError:
pass
def _forced_disconnect(self):
"""
Callback launched when webserver is closing forcefully (Ctrl-C, reboot etc)
All we do is make sure the connected clients are notitifed.
"""
for suid in self.requests.keys():
self.lineSend(suid, parse_html("{rThe MUD server shut down. You were disconnected.{n"))
def lineSend(self, suid, string, data=None):
"""
This adds the data to the buffer and/or sends it to
@ -105,17 +96,24 @@ class WebClient(resource.Resource):
dataentries.append(jsonify({'msg':string, 'data':data}))
self.databuffer[suid] = dataentries
def disconnect(self, suid):
"Disconnect session with given suid."
sess = SESSIONS.session_from_suid(suid)
if sess:
def disconnect(self, suid, step=1):
"""
Disconnect session with given suid.
step 1 : call session_disconnect()
step 2 : finalize disconnection
"""
if step == 1:
sess = SESSIONS.session_from_suid(suid)
sess[0].session_disconnect()
if self.requests.has_key(suid):
for request in self.requests.get(suid, []):
request.finish()
del self.requests[suid]
if self.databuffer.has_key(suid):
del self.databuffer[suid]
else:
if self.requests.has_key(suid):
for request in self.requests.get(suid, []):
request.finish()
del self.requests[suid]
if self.databuffer.has_key(suid):
del self.databuffer[suid]
def mode_init(self, request):
"""
@ -230,8 +228,8 @@ class WebClientSession(session.Session):
"""
# show screen
screen = ConnectScreen.objects.get_random_connect_screen()
string = parse_html(screen.text)
self.client.lineSend(self.suid, string)
#string = parse_html(screen.text)
self.at_data_out(screen.text)
def at_login(self):
"""
@ -248,7 +246,7 @@ class WebClientSession(session.Session):
"""
if reason:
self.lineSend(self.suid, reason)
self.client.disconnect(self.suid)
self.client.disconnect(self.suid, step=2)
def at_data_out(self, string='', data=None):
"""