Cleaned up the webclient and changed how it handles sessions and identifies with the server. Fixed some reported bugs caused by the changed layout of sessionhandler.

This commit is contained in:
Griatch 2010-12-11 13:37:26 +00:00
parent b1682f78c9
commit 0eb5d29560
16 changed files with 191 additions and 107 deletions

View file

@ -7,7 +7,7 @@ Admin commands
from django.conf import settings
from django.contrib.auth.models import User
from src.players.models import PlayerDB
from src.server import sessionhandler
from src.server.sessionhandler import SESSIONS
from src.permissions.permissions import has_perm, has_perm_string
from src.permissions.models import PermissionGroup
from src.utils import utils
@ -48,7 +48,7 @@ class CmdBoot(MuxCommand):
if 'port' in self.switches:
# Boot a particular port.
sessions = sessionhandler.get_session_list(True)
sessions = SESSIONS.get_session_list(True)
for sess in sessions:
# Find the session with the matching port number.
if sess.getClientAddress()[1] == int(args):
@ -66,7 +66,7 @@ class CmdBoot(MuxCommand):
pobj.msg(string)
return
# we have a bootable object with a connected user
matches = sessionhandler.sessions_from_object(pobj)
matches = SESSIONS.sessions_from_object(pobj)
for match in matches:
boot_list.append(match)
else:
@ -90,7 +90,7 @@ class CmdBoot(MuxCommand):
if feedback:
session.msg(feedback)
session.disconnectClient()
sessionhandler.remove_session(session)
SESSIONS.remove_session(session)
caller.msg("You booted %s." % name)
@ -479,4 +479,4 @@ class CmdWall(MuxCommand):
self.caller.msg("Usage: @wall <message>")
return
message = "%s shouts \"%s\"" % (self.caller.name, self.args)
sessionhandler.announce_all(message)
SESSIONS.announce_all(message)

View file

@ -4,7 +4,7 @@ now.
"""
import time
from django.conf import settings
from src.server import sessionhandler
from src.server.sessionhandler import SESSIONS
from src.permissions.models import PermissionGroup
from src.permissions.permissions import has_perm, has_perm_string
from src.objects.models import HANDLE_SEARCH_ERRORS
@ -342,7 +342,7 @@ class CmdWho(MuxCommand):
"""
caller = self.caller
session_list = sessionhandler.get_sessions()
session_list = SESSIONS.get_sessions()
if self.cmdstring == "doing":
show_session_data = False
@ -350,44 +350,44 @@ class CmdWho(MuxCommand):
show_session_data = has_perm_string(caller, "Immortals,Wizards")
if show_session_data:
retval = "Player Name On For Idle Room Cmds Host\n\r"
table = [["Player Name"], ["On for"], ["Idle"], ["Room"], ["Cmds"], ["Host"]]
else:
retval = "Player Name On For Idle\n\r"
table = [["Player Name"], ["On for"], ["Idle"]]
for session in session_list:
if not session.logged_in:
continue
delta_cmd = time.time() - session.cmd_last_visible
delta_conn = time.time() - session.conn_time
plr_pobject = session.get_character()
if show_session_data:
retval += '%-31s%9s %4s%-3s#%-6d%5d%3s%-25s\r\n' % \
(plr_pobject.name[:25], \
# On-time
utils.time_format(delta_conn,0), \
# Idle time
utils.time_format(delta_cmd,1), \
# Flags
'', \
# Location
plr_pobject.location.id, \
session.cmd_total, \
# More flags?
'', \
session.address[0])
table[0].append(plr_pobject.name[:25])
table[1].append(utils.time_format(delta_conn,0))
table[2].append(utils.time_format(delta_cmd,1))
table[3].append(plr_pobject.location.id)
table[4].append(session.cmd_total)
table[5].append(session.address[0])
else:
retval += '%-31s%9s %4s%-3s\r\n' % \
(plr_pobject.name[:25], \
# On-time
utils.time_format(delta_conn,0), \
# Idle time
utils.time_format(delta_cmd,1), \
# Flags
'')
retval += '%d Players logged in.' % (len(session_list),)
table[0].append(plr_pobject.name[:25])
table[1].append(utils.time_format(delta_conn,0))
table[2].append(utils.time_format(delta_cmd,1))
stable = []
for row in table: # prettify values
stable.append([str(val).strip() for val in row])
ftable = utils.format_table(stable, 5)
string = ""
for ir, row in enumerate(ftable):
if ir == 0:
string += "\n" + "{w%s{n" % ("".join(row))
else:
string += "\n" + "".join(row)
nplayers = (SESSIONS.player_count())
if nplayers == 1:
string += '\nOne player logged in.'
else:
string += '\n%d players logged in.' % nplayers
caller.msg(retval)
caller.msg(string)
class CmdSay(MuxCommand):
"""

View file

@ -9,13 +9,15 @@ import os, datetime
import django, twisted
from django.contrib.auth.models import User
from src.server import sessionhandler
from src.server.server import EVENNIA
from src.server.sessionhandler import SESSIONS
from src.scripts.models import ScriptDB
from src.objects.models import ObjectDB
from src.config.models import ConfigValue
from src.utils import reloads, create, logger, utils, gametime
from src.commands.default.muxcommand import MuxCommand
class CmdReload(MuxCommand):
"""
Reload the system
@ -429,13 +431,9 @@ class CmdShutdown(MuxCommand):
announcement = "\nServer is being SHUT DOWN!\n"
if self.args:
announcement += "%s\n" % self.args
sessionhandler.announce_all(announcement)
logger.log_infomsg('Server shutdown by %s.' % self.caller.name)
# access server through session so we don't call server directly
# (importing it directly would restart it...)
session.server.shutdown()
SESSIONS.announce_all(announcement)
EVENNIA.shutdown()
class CmdVersion(MuxCommand):
"""

View file

@ -17,7 +17,7 @@ be able to delete connections on the fly).
from django.db import models
from src.utils.idmapper.models import SharedMemoryModel
from src.players.models import PlayerDB
from src.server import sessionhandler
from src.server.sessionhandler import SESSIONS
from src.comms import managers
from src.permissions.permissions import has_perm
from src.utils.utils import is_iter
@ -509,7 +509,7 @@ class Channel(SharedMemoryModel):
# send message to all connected players
for conn in conns:
for session in \
sessionhandler.SESSIONS.sessions_from_player(conn.player):
SESSIONS.sessions_from_player(conn.player):
session.msg(msg)
return True

View file

@ -44,7 +44,7 @@ from django.conf import settings
from django.db import models
from django.contrib.auth.models import User
from django.utils.encoding import smart_str
from src.server import sessionhandler
from src.server.sessionhandler import SESSIONS
from src.players import manager
from src.typeclasses.models import Attribute, TypedObject
from src.permissions import permissions
@ -220,7 +220,7 @@ class PlayerDB(TypedObject):
#@property
def sessions_get(self):
"Getter. Retrieve sessions related to this player/user"
return sessionhandler.SESSIONS.sessions_from_player(self)
return SESSIONS.sessions_from_player(self)
#@sessions.setter
def sessions_set(self, value):
"Setter. Protects the sessions property from adding things"

View file

@ -7,7 +7,7 @@ It also defines a few common scripts.
from time import time
from twisted.internet import task
from src.server import sessionhandler
from src.server.sessionhandler import SESSIONS
from src.typeclasses.typeclass import TypeClass
from src.scripts.models import ScriptDB
from src.comms import channelhandler
@ -234,7 +234,7 @@ class CheckSessions(Script):
"called every 60 seconds"
#print "session check!"
#print "ValidateSessions run"
sessionhandler.validate_sessions()
SESSIONS.validate_sessions()
class ValidateScripts(Script):
"Check script validation regularly"

View file

@ -22,7 +22,7 @@ from django.db import connection
from django.conf import settings
from src.utils import reloads
from src.config.models import ConfigValue
from src.server import sessionhandler
from src.server.sessionhandler import SESSIONS
from src.server import initial_setup
from src.utils.utils import get_evennia_version
@ -77,7 +77,7 @@ class Evennia(object):
self.run_initial_setup()
# we have to null this here.
sessionhandler.SESSIONS.session_count(0)
SESSIONS.session_count(0)
self.start_time = time.time()
@ -152,7 +152,7 @@ class Evennia(object):
"""
if not message:
message = 'The server has been shutdown. Please check back soon.'
sessionhandler.SESSIONS.disconnect_all_sessions(reason=message)
SESSIONS.disconnect_all_sessions(reason=message)
reactor.callLater(0, reactor.stop)

View file

@ -12,7 +12,7 @@ from django.conf import settings
from src.comms.models import Channel
from src.utils import logger, reloads
from src.commands import cmdhandler
from src.server import sessionhandler
from src.server.sessionhandler import SESSIONS
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
IDLE_COMMAND = settings.IDLE_COMMAND
@ -75,7 +75,7 @@ class SessionBase(object):
# Total number of commands issued.
self.cmd_total = 0
#self.channels_subscribed = {}
sessionhandler.SESSIONS.add_unloggedin_session(self)
SESSIONS.add_unloggedin_session(self)
# call hook method
self.at_connect()
@ -101,7 +101,7 @@ class SessionBase(object):
reloads.reload_scripts(obj=self.player.character, init_mode=True)
#add session to connected list
sessionhandler.SESSIONS.add_loggedin_session(self)
SESSIONS.add_loggedin_session(self)
#call hook
self.at_login()
@ -123,7 +123,7 @@ class SessionBase(object):
uaccount.last_login = datetime.now()
uaccount.save()
self.logged_in = False
sessionhandler.SESSIONS.remove_session(self)
SESSIONS.remove_session(self)
def session_validate(self):
"""

View file

@ -151,6 +151,14 @@ class SessionHandler(object):
num = max(0, num + add)
ConfigValue.objects.conf('nr_sessions', str(num))
def player_count(self):
"""
Get the number of connected players (not sessions since a player
may have more than one session connected if ALLOW_MULTISESSION is True)
Only logged-in players are counted here.
"""
return len(set(sess.uid for sess in self.get_sessions()))
def sessions_from_player(self, player):
"""
Given a player, return any matching sessions.
@ -179,6 +187,13 @@ class SessionHandler(object):
"""
return [sess for sess in self.get_sessions(include_unloggedin=True) if sess.suid and sess.suid == suid]
def announce_all(self, message):
"""
Send message to all connected sessions
"""
for sess in self.get_sessions(include_unloggedin=True):
sess.msg(message)
SESSIONS = SessionHandler()

View file

@ -16,6 +16,8 @@ found on http://localhost:8020/webclient.)
handle these requests and act as a gateway
to sessions connected over the webclient.
"""
import time
from hashlib import md5
from twisted.web import server, resource
from twisted.internet import defer
@ -27,7 +29,8 @@ from django.conf import settings
from src.utils import utils
from src.utils.text2html import parse_html
from src.config.models import ConnectScreen
from src.server import session, sessionhandler
from src.server import session
from src.server.sessionhandler import SESSIONS
SERVERNAME = settings.SERVERNAME
ENCODINGS = settings.ENCODINGS
@ -94,8 +97,8 @@ class WebClient(resource.Resource):
self.databuffer[suid] = dataentries
def disconnect(self, suid):
"Disconnect session"
sess = sessionhandler.SESSIONS.session_from_suid(suid)
"Disconnect session with given suid."
sess = SESSIONS.session_from_suid(suid)
if sess:
sess[0].session_disconnect()
if self.requests.has_key(suid):
@ -110,32 +113,35 @@ class WebClient(resource.Resource):
This is called by render_POST when the client
requests an init mode operation (at startup)
"""
csess = request.getSession() # obs, this is a cookie, not an evennia session!
#csess = request.getSession() # obs, this is a cookie, not an evennia session!
#csees.expireCallbacks.append(lambda : )
suid = csess.uid
suid = request.args.get('suid', ['0'])[0]
remote_addr = request.getClientIP()
host_string = "%s (%s:%s)" % (SERVERNAME, request.getHost().host, request.getHost().port)
self.requests[suid] = []
self.databuffer[suid] = []
if suid == '0':
# creating a unique id hash string
suid = md5(str(time.time())).hexdigest()
self.requests[suid] = []
self.databuffer[suid] = []
sess = sessionhandler.SESSIONS.session_from_suid(suid)
if not sess:
sess = WebClientSession()
sess.client = self
sess.session_connect(remote_addr, suid)
sessionhandler.SESSIONS.add_unloggedin_session(sess)
return jsonify({'msg':host_string})
sess.client = self
sess.session_connect(remote_addr, suid)
return jsonify({'msg':host_string, 'suid':suid})
def mode_input(self, request):
"""
This is called by render_POST when the client
is sending data to the server.
"""
string = request.args.get('msg', [''])[0]
data = request.args.get('data', [None])[0]
suid = request.getSession().uid
sess = sessionhandler.SESSIONS.session_from_suid(suid)
suid = request.args.get('suid', ['0'])[0]
if suid == '0':
return ''
sess = SESSIONS.session_from_suid(suid)
if sess:
string = request.args.get('msg', [''])[0]
data = request.args.get('data', [None])[0]
sess[0].at_data_in(string, data)
return ''
@ -147,16 +153,29 @@ class WebClient(resource.Resource):
mechanism: the server will wait to reply until data is
available.
"""
suid = request.getSession().uid
suid = request.args.get('suid', ['0'])[0]
if suid == '0':
return ''
dataentries = self.databuffer.get(suid, [])
if dataentries:
return dataentries.pop(0)
return dataentries.pop(0)
reqlist = self.requests.get(suid, [])
request.notifyFinish().addErrback(self._responseFailed, suid, request)
reqlist.append(request)
self.requests[suid] = reqlist
return server.NOT_DONE_YET
def mode_close(self, request):
"""
This is called by render_POST when the client is signalling
that it is about to be closed.
"""
suid = request.args.get('suid', ['0'])[0]
if suid == '0':
self.disconnect(suid)
return ''
def render_POST(self, request):
"""
This function is what Twisted calls with POST requests coming
@ -176,6 +195,9 @@ class WebClient(resource.Resource):
elif dmode == 'receive':
# the client is waiting to receive data.
return self.mode_receive(request)
elif dmode == 'close':
# the client is closing
return self.mode_close(request)
else:
# this should not happen if client sends valid data.
return ''
@ -234,8 +256,7 @@ class WebClientSession(session.Session):
# string handling is similar to telnet
if self.encoding:
try:
string = utils.to_str(string, encoding=self.encoding)
#self.client.lineSend(self.suid, ansi.parse_ansi(string, strip_ansi=True))
string = utils.to_str(string, encoding=self.encoding)
self.client.lineSend(self.suid, parse_html(string))
return
except Exception:
@ -256,7 +277,7 @@ class WebClientSession(session.Session):
self.client.lineSend(self.suid, parse_html(string))
def at_data_in(self, string, data=None):
"""
Input from Player -> Evennia (called by client).
Input from Player -> Evennia (called by client protocol).
Use of 'data' is up to the client - server implementation.
"""

View file

@ -33,7 +33,7 @@ TELNET_PORTS = [4000]
# in the section 'Config for Django web features')
WEBSERVER_ENABLED = True
# A list of ports the Evennia webserver listens on
WEBSERVER_PORTS = [8020]
WEBSERVER_PORTS = [8000]
# Start the evennia ajax client on /webclient
# (the webserver must also be running)
WEBCLIENT_ENABLED = True

View file

@ -28,8 +28,10 @@ def reload_modules():
# We protect e.g. src/ from reload since reloading it in a running
# server can create unexpected results (and besides, non-evennia devs
# should never need to do that anyway). Updating src requires a server
# reboot.
# reboot. Modules in except_dirs are considered ok to reload despite being
# inside src/
protected_dirs = ('src.',)
except_dirs = ('src.commands.default.')
# flag 'dangerous' typeclasses (those which retain a memory
# reference, notably Scripts with a timer component) for
@ -43,8 +45,8 @@ def reload_modules():
unsafe_modules = list(set(unsafe_modules))
def safe_dir_to_reload(modpath):
"Check so modpath is not a subdir of a protected dir"
return not any(modpath.startswith(pdir) for pdir in protected_dirs)
"Check so modpath is not a subdir of a protected dir, and not an ok exception"
return not any(modpath.startswith(pdir) and not any(modpath.startswith(pdir) for pdir in except_dirs) for pdir in protected_dirs)
def safe_mod_to_reload(modpath):
"Check so modpath is not in an unsafe module"
return not any(mpath.startswith(modpath) for mpath in unsafe_modules)

View file

@ -37,7 +37,7 @@ class TextToHTMLparser(object):
normalcode = '\033[0m'
tabstop = 4
re_string = re.compile(r'(?P<htmlchars>[<&>])|(?P<space>^[ \t]+)|(?P<lineend>\r\n|\r|\n)|(?P<protocal>(^|\s)((http|ftp)://.*?))(\s|$)',
re_string = re.compile(r'(?P<htmlchars>[<&>])|(?P<space>^[ \t]+)|(?P<lineend>\r\n|\r|\n)|(?P<protocol>(^|\s)((http|ftp)://.*?))(\s|$)',
re.S|re.M|re.I)
def re_color(self, text):
@ -95,7 +95,7 @@ class TextToHTMLparser(object):
elif c['space'] == '\t':
return ' '*self.tabstop
else:
url = m.group('protocal')
url = m.group('protocol')
if url.startswith(' '):
prefix = ' '
url = url[1:]

View file

@ -320,12 +320,21 @@ def format_table(table, extra_space=1):
collumns must have the same number of rows (some positions may be
empty though).
The function formats the columns to be as wide as the wides member
The function formats the columns to be as wide as the widest member
of each column.
extra_space defines how much extra padding should minimum be left between
collumns.
first_row_ansi defines an evential colour string for the first row.
print the resulting list e.g. with
for ir, row in enumarate(ftable):
if ir == 0:
# make first row white
string += "\n{w" + ""join(row) + "{n"
else:
string += "\n" + "".join(row)
print string
"""
if not table:

View file

@ -28,15 +28,18 @@ contain the 'mode' of the request to be handled by the protocol:
should happen at this point. The server returns a data object
with the 'msg' property containing the server address.
mode 'close' - closes the connection. The server closes the session and does
cleanup at this point.
*/
// jQuery must be imported by the calling html page before this script
// (it comes with Evennia, in media/javascript/jquery-<version>.js)
// There are plenty of help on using the jQuery library on http://jquery.com/
// Server communications
var CLIENT_HASH = '0'; // variable holding the client id
function webclient_receive(){
// This starts an asynchronous long-polling request. It will either timeout
// or receive data from the 'receivedata' url. In both cases a new request will
@ -49,7 +52,7 @@ function webclient_receive(){
cache: false, // Forces browser reload independent of cache
timeout:30000, // Timeout in ms. After this time a new long-poll will be started.
dataType:"json",
data: {mode:'receive'},
data: {mode:'receive', 'suid':CLIENT_HASH},
// callback methods
@ -66,7 +69,7 @@ function webclient_receive(){
function webclient_input(){
// Send an input from the player to the server
var outmsg = $("#inputfield").val() // get data from form
var outmsg = $("#inputfield").val(); // get data from form
$.ajax({
type: "POST",
@ -74,7 +77,7 @@ function webclient_input(){
async: true,
cache: false,
timeout: 30000,
data: {mode:'input', msg:outmsg, data:'NoData'},
data: {mode:'input', msg:outmsg, data:'NoData', 'suid':CLIENT_HASH},
//callback methods
@ -82,7 +85,7 @@ function webclient_input(){
//if (outmsg.length > 0 ) msg_display("inp", outmsg) // echo input on command line
history_add(outmsg);
HISTORY_POS = 0;
$('#inputform')[0].reset() // clear input field
$('#inputform')[0].reset(); // clear input field
},
error: function(XMLHttpRequest, textStatus, errorThrown){
msg_display("err", "Error: Server returned an error or timed out. Try resending.");
@ -100,19 +103,20 @@ function webclient_init(){
cache: false,
timeout: 50000,
dataType:"json",
data: {mode:'init'},
data: {mode:'init', 'suid':CLIENT_HASH},
// callback methods
success: function(data){ // called when request to initdata completes
$("#connecting").remove() // remove the "connecting ..." message.
CLIENT_HASH = data.suid // unique id hash given from server
setTimeout(function () { // a small timeout to stop 'loading' indicator in Chrome
$("#playercount").fadeOut('slow');
}, 10000);
// Report success
msg_display('sys',"Connected to " + data.msg + ".")
msg_display('sys',"Connected to " + data.msg + ".");
// Wait for input
webclient_receive();
@ -124,6 +128,29 @@ function webclient_init(){
});
}
function webclient_close(){
// Kill the connection and do house cleaning on the server.
$.ajax({
type: "POST",
url: "/webclientdata",
async: false,
cache: false,
timeout: 50000,
dataType: "json",
data: {mode: 'close', 'suid': CLIENT_HASH},
success: function(data){
CLIENT_HASH = '0';
alert("Mud client connection was closed cleanly.");
},
error: function(XMLHttpRequest, textStatus, errorThrown){
CLIENT_HASH = '0';
alert("There was an error disconnecting from the mud server.");
}
});
}
// Display messages
function msg_display(type, msg){
@ -156,17 +183,17 @@ function history_add(input) {
// add an entry to history
if (input != HISTORY[HISTORY.length-1]) {
if (HISTORY.length >= HISTORY_MAX_LENGTH) {
HISTORY.shift() // kill oldest history entry
HISTORY.shift(); // kill oldest history entry
}
HISTORY[HISTORY.length-1] = input
HISTORY[HISTORY.length] = ''
HISTORY[HISTORY.length-1] = input;
HISTORY[HISTORY.length] = '';
}
}
// Catching keyboard shortcuts
$(document).keypress( function(event) {
var code = event.keyCode ? event.keyCode : event.which
var code = event.keyCode ? event.keyCode : event.which;
// always focus input field
$("#inputfield")[0].focus();
@ -192,21 +219,34 @@ $(document).keypress( function(event) {
// handler to avoid double-clicks until the ajax request finishes
$("#inputsend").one("click", webclient_input)
// Callback function - called when page has finished loading (gets things going)
$(document).ready(function(){
// remove the "no javascript" warning, since we obviously have javascript
$('#noscript').remove()
// set sizes of elements and reposition them
function webclient_set_sizes() {
// Sets the size of the message window
var win_h = $(document).height();
var win_w = $('#wrapper').width();
var inp_h = $('#inputform').height();
var inp_w = $('#inputsend').width()
$("#messagewindow").css({'height':win_h-inp_h - 20});
$("#inputfield").css({'width':win_w-inp_w - 20});
}
setTimeout(function () { // a small timeout to stop 'loading' indicator in Chrome
// Callback function - called when page has finished loading (gets things going)
$(document).ready(function(){
// remove the "no javascript" warning, since we obviously have javascript
$('#noscript').remove();
// set sizes of elements and reposition them
webclient_set_sizes();
// a small timeout to stop 'loading' indicator in Chrome
setTimeout(function () {
webclient_init();
}, 500);
});
// Callback function - called when the browser window resizes
$(window).resize(function() {
webclient_set_sizes();
});
// Callback function - called when page is closed or moved away from.
$(window).unload(function() {
webclient_close();
});

View file

@ -8,8 +8,7 @@ page and serve it eventual static content.
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.conf import settings
from src.server import sessionhandler
from src.config.models import ConfigValue
from src.server.sessionhandler import SESSIONS
def webclient(request):
"""
@ -17,7 +16,7 @@ def webclient(request):
"""
# as an example we send the number of connected players to the template
pagevars = {'num_players_connected': ConfigValue.objects.conf('nr_sessions')}
pagevars = {'num_players_connected': SESSIONS.player_count()}
context_instance = RequestContext(request)
return render_to_response('webclient.html', pagevars, context_instance)