From 97cf1213e6a4cfe032a9d017e416cf237cd45451 Mon Sep 17 00:00:00 2001 From: Greg Taylor Date: Mon, 21 May 2007 20:52:05 +0000 Subject: [PATCH] Converted to Twisted from asyncore. Not positive if this is just my local machine, but it seems like this backend is a bit faster. --- INSTALL | 1 + events.py | 10 +++-- functions_general.py | 2 +- scheduler.py | 22 +++-------- server.py | 90 +++++++++++++++++--------------------------- session.py | 87 +++++++++++++++++++++++------------------- session_mgr.py | 24 ++++++------ startup.sh | 22 +++-------- 8 files changed, 113 insertions(+), 145 deletions(-) diff --git a/INSTALL b/INSTALL index 64da6cae86..e5b30dcb05 100644 --- a/INSTALL +++ b/INSTALL @@ -1,6 +1,7 @@ Requirements ------------ * Python 2.5 strongly recommended, although 2.3 or 2.4 may work just fine. +* Twisted -- http://twistedmatrix.com/ * PySqlite2 (If you're using the default SQLite driver) * Django (Latest trunk from Subversion recommended) * Optional: Apache2 or equivalent webserver with a Python interpreter diff --git a/events.py b/events.py index 958fd3b0eb..c83cd29a87 100644 --- a/events.py +++ b/events.py @@ -1,17 +1,21 @@ +import time +from twisted.internet import protocol, reactor, defer import session_mgr """ Holds the events scheduled in scheduler.py. """ +# Dictionary of events with a list in the form of: [, ] schedule = { - 'check_sessions': 60, + 'check_sessions': [60, None] } -lastrun = {} def check_sessions(): """ - Check all of the connected sessions. + Event: Check all of the connected sessions. """ session_mgr.check_all_sessions() + schedule['check_sessions'][1] = time.time() + reactor.callLater(schedule['check_sessions'][0], check_sessions) diff --git a/functions_general.py b/functions_general.py index e67783cbac..fc2c870bdf 100644 --- a/functions_general.py +++ b/functions_general.py @@ -108,7 +108,7 @@ def announce_all(message, with_ann_prefix=True, with_nl=True): newline = '' for session in session_mgr.get_session_list(): - session.msg_no_nl('%s %s%s' % (prefix, message,newline,)) + session.msg('%s %s%s' % (prefix, message,newline,)) def word_wrap(text, width=78): """ diff --git a/scheduler.py b/scheduler.py index 4dfdd895ec..c8db5d1804 100644 --- a/scheduler.py +++ b/scheduler.py @@ -1,5 +1,5 @@ -import time import events +from twisted.internet import protocol, reactor, defer """ A really simple scheduler. We can probably get a lot fancier with this in the future, but it'll do for now. @@ -11,24 +11,12 @@ ADDING AN EVENT: """ # The timer method to be triggered by the main server loop. -def heartbeat(): +def start_events(): """ Handle one tic/heartbeat. """ - tictime = time.time() for event in events.schedule: - try: - events.lastrun[event] - except: - events.lastrun[event] = time.time() - - diff = tictime - events.lastrun[event] + event_func = getattr(events, event) - if diff >= events.schedule[event]: - event_func = getattr(events, event) - - if callable(event_func): - event_func() - - # We'll get a new reading for time for accuracy. - events.lastrun[event] = time.time() + if callable(event_func): + reactor.callLater(events.schedule[event][0], event_func) diff --git a/server.py b/server.py index 8eaf3951da..96f0b1c1a0 100755 --- a/server.py +++ b/server.py @@ -1,12 +1,15 @@ from traceback import format_exc -from asyncore import dispatcher -from asynchat import async_chat -import socket, asyncore, time +import time +import sys + +from twisted.application import internet, service +from twisted.internet import protocol, reactor, defer + from django.db import models from django.db import connection from apps.config.models import CommandAlias -import sys +from session import SessionProtocol import scheduler import functions_general import session_mgr @@ -15,22 +18,20 @@ import settings import cmdtable import initial_setup -class Server(dispatcher): - """ - The main server class from which everything branches. - """ - def __init__(self): +class EvenniaService(service.Service): + + def __init__(self, filename="blah"): self.cmd_alias_list = {} self.game_running = True - + # Database-specific startup optimizations. if settings.DATABASE_ENGINE == "sqlite3": self.sqlite3_prep() - + # Wipe our temporary flags on all of the objects. cursor = connection.cursor() cursor.execute("UPDATE objects_object SET nosave_flags=''") - + print '-'*50 # Load command aliases into memory for easy/quick access. self.load_cmd_aliases() @@ -40,21 +41,15 @@ class Server(dispatcher): print ' Game started for the first time, setting defaults.' initial_setup.handle_setup() - # Start accepting connections. - dispatcher.__init__(self) - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - self.set_reuse_addr() - self.bind(('', int(self.port))) - self.listen(100) self.start_time = time.time() print ' %s started on port %s.' % (gameconf.get_configvalue('site_name'), self.port,) print '-'*50 - + scheduler.start_events() + """ BEGIN SERVER STARTUP METHODS """ - def load_cmd_aliases(self): """ Load up our command aliases. @@ -63,17 +58,8 @@ class Server(dispatcher): for alias in alias_list: self.cmd_alias_list[alias.user_input] = alias.equiv_command print ' Command Aliases Loaded: %i' % (len(self.cmd_alias_list),) - - def handle_accept(self): - """ - What to do when we get a connection. - """ - conn, addr = self.accept() - session = session_mgr.new_session(self, conn, addr) - session.game_connect_screen(session) - print 'Connection:', str(session) - print 'Sessions active:', len(session_mgr.get_session_list()) - + pass + def sqlite3_prep(self): """ Optimize some SQLite stuff at startup since we can't save it to the @@ -84,14 +70,14 @@ class Server(dispatcher): cursor.execute("PRAGMA synchronous=OFF") cursor.execute("PRAGMA count_changes=OFF") cursor.execute("PRAGMA temp_store=2") - + """ BEGIN GENERAL METHODS - """ + """ def shutdown(self, message='The server has been shutdown. Please check back soon.'): functions_general.announce_all(message) - self.game_running = False - + session_mgr.disconnect_all_sessions() + reactor.callLater(0, reactor.stop) def command_list(self): """ @@ -112,32 +98,26 @@ class Server(dispatcher): 'events', 'functions_db', 'functions_general', 'functions_comsys', 'functions_help', 'gameconf', 'session', 'apps.objects.models', 'apps.helpsys.models', 'apps.config.models'] - + for mod in reload_list: reload(sys.modules[mod]) session.msg("Modules reloaded.") functions_general.log_infomsg("Modules reloaded by %s." % (session,)) + + def getEvenniaServiceFactory(self): + f = protocol.ServerFactory() + f.protocol = SessionProtocol + f.server = self + return f + """ END Server CLASS - """ + """ -""" -BEGIN MAIN APPLICATION LOGIC -""" -if __name__ == '__main__': - server = Server() - - try: - while server.game_running: - asyncore.loop(timeout=5, count=1) - scheduler.heartbeat() - - except KeyboardInterrupt: - server.shutdown() - print '--> Server killed by keystroke.' +application = service.Application('Evennia') +mud_service = EvenniaService('Evennia Server') - except: - server.shutdown(message="The server has encountered a fatal error and has been shut down. Please check back soon.") - functions_general.log_errmsg("Untrapped error: %s" % - (format_exc())) +# Sheet sheet, fire ze missiles! +serviceCollection = service.IServiceCollection(application) +internet.TCPServer(4000, mud_service.getEvenniaServiceFactory()).setServiceParent(serviceCollection) \ No newline at end of file diff --git a/session.py b/session.py index ea2ca1a278..8f1c67e9aa 100755 --- a/session.py +++ b/session.py @@ -1,7 +1,8 @@ -from asyncore import dispatcher -from asynchat import async_chat -import socket, asyncore, time, sys +import time, sys import cPickle as pickle + +from twisted.conch.telnet import StatefulTelnetProtocol + import cmdhandler from apps.objects.models import Object from django.contrib.auth.models import User @@ -10,20 +11,34 @@ import functions_db import functions_general import session_mgr -class PlayerSession(async_chat): +class SessionProtocol(StatefulTelnetProtocol): """ This class represents a player's sesssion. From here we branch down into other various classes, please try to keep this one tidy! """ - def __init__(self, server, sock, addr): - async_chat.__init__(self, sock) - self.server = server - self.address = addr - self.set_terminator("\n") + + def connectionMade(self): + """ + What to do when we get a connection. + """ + session_mgr.add_session(self) + self.game_connect_screen() + self.prep_session() + print 'Connection:', self + print 'Sessions active:', len(session_mgr.get_session_list()) + + def getClientAddress(self): + """ + Returns the client's address and port in a tuple. For example + ('127.0.0.1', 41917) + """ + return self.transport.client + + def prep_session(self): + #self.server = server + self.address = self.getClientAddress() self.name = None - self.data = [] self.uid = None - self.sock = sock self.logged_in = False # The time the user last issued a command. self.cmd_last = time.time() @@ -35,6 +50,18 @@ class PlayerSession(async_chat): self.conn_time = time.time() self.channels_subscribed = {} + def disconnectClient(self): + """ + Manually disconnect the client. + """ + self.transport.loseConnection() + + def connectionLost(self, reason): + """ + Execute this when a client abruplty loses their connection. + """ + print "DISCONNECT:", reason.getErrorMessage() + def has_user_channel(self, cname, alias_search=False, return_muted=False): """ Is this session subscribed to the named channel? @@ -93,24 +120,17 @@ class PlayerSession(async_chat): if chan_list: self.channels_subscribed = pickle.loads(chan_list) - def collect_incoming_data(self, data): - """ - Stuff any incoming data into our buffer, self.data - """ - self.data.append(data) - - def found_terminator(self): + def lineReceived(self, data): """ Any line return indicates a command for the purpose of a MUD. So we take the user input and pass it to our command handler. """ - line = (''.join(self.data)) + line = (''.join(data)) line = line.strip('\r') uinput = line - self.data = [] # Stuff anything we need to pass in this dictionary. - cdat = {"server": self.server, "uinput": uinput, "session": self} + cdat = {"server": self.factory.server, "uinput": uinput, "session": self} cmdhandler.handle(cdat) def handle_close(self): @@ -122,7 +142,7 @@ class PlayerSession(async_chat): pobject.set_flag("CONNECTED", False) pobject.get_location().emit_to_contents("%s has disconnected." % (pobject.get_name(show_dbref=False),), exclude=pobject) - async_chat.handle_close(self) + self.disconnectClient() self.logged_in = False session_mgr.remove_session(self) print 'Sessions active:', len(session_mgr.get_session_list()) @@ -137,7 +157,7 @@ class PlayerSession(async_chat): except: return False - def game_connect_screen(self, session): + def game_connect_screen(self): """ Show the banner screen. """ @@ -148,7 +168,7 @@ class PlayerSession(async_chat): connect \n\r create \"\" \n\r""" buffer += '-'*50 - session.msg(buffer) + self.msg(buffer) def login(self, user): """ @@ -163,27 +183,19 @@ class PlayerSession(async_chat): self.msg("You are now logged in as %s." % (self.name,)) pobject.get_location().emit_to_contents("%s has connected." % (pobject.get_name(),), exclude=pobject) - cdat = {"session": self, "uinput":'look', "server": self.server} + cdat = {"session": self, "uinput":'look', "server": self.factory.server} cmdhandler.handle(cdat) functions_general.log_infomsg("Login: %s" % (self,)) pobject.set_attribute("Last", "%s" % (time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),)) - pobject.set_attribute("Lastsite", "%s" % (self.address[0],)) + pobject.set_attribute("Lastsite", "%s" % (self.address,)) self.load_user_channels() def msg(self, message): """ - Sends a message with the newline/return included. Use this instead of - directly calling push(). + Sends a message to the session. """ - self.push("%s\n\r" % (message,)) + self.sendLine("%s" % (message,)) - def msg_no_nl(self, message): - """ - Sends a message without the newline/return included. Use this instead of - directly calling push(). - """ - self.push("%s" % (message,)) - def __str__(self): """ String representation of the user session class. We use @@ -194,6 +206,3 @@ class PlayerSession(async_chat): else: symbol = '?' return "<%s> %s@%s" % (symbol, self.name, self.address,) - -# def handle_error(self): -# self.handle_close() diff --git a/session_mgr.py b/session_mgr.py index 450e63d99f..6496375084 100644 --- a/session_mgr.py +++ b/session_mgr.py @@ -1,5 +1,4 @@ import time -from session import PlayerSession import gameconf """ @@ -8,13 +7,12 @@ Session manager, handles connected players. # Our list of connected sessions. session_list = [] -def new_session(server, conn, addr): +def add_session(session): """ - Create and return a new session. + Adds a session to the session list. """ - session = PlayerSession(server, conn, addr) session_list.insert(0, session) - return session + print 'Sessions active:', len(get_session_list()) def get_session_list(): """ @@ -22,6 +20,13 @@ def get_session_list(): """ return session_list +def disconnect_all_sessions(): + """ + Cleanly disconnect all of the connected sessions. + """ + for sess in get_session_list(): + sess.handle_close() + def check_all_sessions(): """ Check all currently connected sessions and see if any are dead. @@ -38,14 +43,7 @@ def check_all_sessions(): if (time.time() - sess.cmd_last) > idle_timeout: sess.msg("Idle timeout exceeded, disconnecting.") sess.handle_close() - ## This doesn't seem to provide an accurate indication of timed out - ## sessions. - #if not sess.writable() or not sess.readable(): - # print 'Problematic Session:' - # print 'Readable ', sess.readable() - # print 'Writable ', sess.writable() - - + def remove_session(session): """ Removes a session from the session list. diff --git a/startup.sh b/startup.sh index f1cd0820dd..39106c5905 100755 --- a/startup.sh +++ b/startup.sh @@ -1,25 +1,13 @@ #!/bin/bash export DJANGO_SETTINGS_MODULE="settings" -## Uncomment whichever python binary you'd like to use to run the game. -## Evennia is developed on 2.5 but should be compatible with 2.4. -# PYTHON_BIN="python" -# PYTHON_BIN="python2.4" -PYTHON_BIN="python2.5" - -## The name of your logfile. -LOGNAME="logs/evennia.log" -## Where to put the last log file from the game's last running -## on next startup. -LOGNAME_OLD="logs/evennia.log.old" -mv $LOGNAME $LOGNAME_OLD - ## There are several different ways you can run the server, read the ## description for each and uncomment the desired mode. -## Generate profile data for use with cProfile. -# $PYTHON_BIN -m cProfile -o profiler.log -s time server.py +## TODO: Make this accept a command line argument to use interactive +## mode instead of having to uncomment crap. + ## Interactive mode. Good for development and debugging. -#$PYTHON_BIN server.py +#twistd -noy twistd -ny server.py ## Stand-alone mode. Good for running games. -nohup $PYTHON_BIN server.py > $LOGNAME & +twistd -ny server.py