From 939307a5c1bd039ce74bf826faaf9a436ece57e1 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 12 Dec 2010 10:54:33 +0000 Subject: [PATCH] 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. --- src/commands/default/admin.py | 22 ++++++-------- src/server/server.py | 28 +++++++++++------ src/server/session.py | 57 +++++++++++++++++++++++++++++------ src/server/sessionhandler.py | 7 +++-- src/server/telnet.py | 28 ++++++++++------- src/server/webclient.py | 44 +++++++++++++-------------- 6 files changed, 118 insertions(+), 68 deletions(-) diff --git a/src/commands/default/admin.py b/src/commands/default/admin.py index 8149e788e8..2340317102 100644 --- a/src/commands/default/admin.py +++ b/src/commands/default/admin.py @@ -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] ") + caller.msg("Usage: @delplayer[/delobj] [: 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 diff --git a/src/server/server.py b/src/server/server.py index d90bb2099f..3d384419cc 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -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) diff --git a/src/server/session.py b/src/server/session.py index 75d8923904..3206afa923 100644 --- a/src/server/session.py +++ b/src/server/session.py @@ -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) diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index 4f4a56c0c2..ddf66b3e05 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -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): diff --git a/src/server/telnet.py b/src/server/telnet.py index 897b8cdd9d..897380307a 100644 --- a/src/server/telnet.py +++ b/src/server/telnet.py @@ -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): """ diff --git a/src/server/webclient.py b/src/server/webclient.py index 39ff48e552..1ccd496836 100644 --- a/src/server/webclient.py +++ b/src/server/webclient.py @@ -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): """