Converted to Twisted from asyncore. Not positive if this is just my local machine, but it seems like this backend is a bit faster.

This commit is contained in:
Greg Taylor 2007-05-21 20:52:05 +00:00
parent 82f46a2b69
commit 97cf1213e6
8 changed files with 113 additions and 145 deletions

View file

@ -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

View file

@ -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: [<interval>, <lastrantime>]
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)

View file

@ -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):
"""

View file

@ -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)

View file

@ -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)

View file

@ -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 <email> <password>\n\r
create \"<username>\" <email> <password>\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()

View file

@ -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.

View file

@ -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