Commit bbd17ad8 authored by Taddeüs Kroes's avatar Taddeüs Kroes

Merge branch 'master' of ssh://vo20.nl/home/git/repos/uva

parents 8b945169 b0ef31ea
......@@ -4,6 +4,10 @@ import re
import random
from asyncbase import AsyncBase
# Socket error "Resource temporarily unavailable": try again ;)
EAGAIN = 11
class ClientConnection(object, AsyncBase, asyncore.dispatcher):
def __init__(self):
......@@ -51,6 +55,29 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
if 'connect' in self.event_list:
self.event_list['connect'](self)
def handle_error(self):
if hasattr(self, 'main'):
import sys
t, v, tb = sys.exc_info()
tbinfo = []
while tb:
tbinfo.append((
tb.tb_frame.f_code.co_filename,
tb.tb_frame.f_code.co_name,
str(tb.tb_lineno)
))
tb = tb.tb_next
file, function, line = tbinfo[-1]
info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo])
self.debug_log('Exception raised in "%s" (%s line %s). Traceback:' \
% (function, file, line))
self.debug_log(info)
self.main.display_info(str(v))
#self.main.execute('disconnect')
#self.main.display_info('Error raised due to a broken or refused'\
# + ' connection')
def handle_close(self):
self.close()
......@@ -61,7 +88,18 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
# Receive a (multiline) message.
while True:
try:
chunk = self.recv(1)
except socket.error, e:
# Suppress "Resource temporarily unavailable" exceptions.
if e.errno == EAGAIN:
# Wait 5 ms
import time
time.sleep(0.005)
continue
else:
raise
if not chunk:
raise RuntimeError('socket connection broken')
if chunk in ['-','+']:
......@@ -78,7 +116,7 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
buf += chunk
buf = buf[:-1]
self.debug_log('< %s' % buf)
self.debug_log('< %s' % buf.replace('\n','\\n').replace('\r','\\r'))
# Invoke the proper callback function.
if not self.verified:
......@@ -112,7 +150,7 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
if not self.authentification_sent:
self.send_queue.put('USER %s' % self.user)
self.authentification_sent = True
elif buf[1:9] == 'Username':
elif buf[0] == '+':
# TODO: handle 'username is taken'.
self.authenticated = True
if 'authenticated' in self.event_list:
......@@ -145,5 +183,5 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
if __name__ == '__main__':
client = ClientConnection()
client.connect(('ow150.science.uva.nl', 16897))
client.connect(('localhost', 16897))
client.init_loop()
......@@ -16,15 +16,15 @@ class BaseBar(object):
self.color_pair = curses.color_pair(0)
self._prefix = ''
def prefix(self, value=''):
if not value:
def prefix(self, value=None):
if value == None: # Note: an empty string can also be set.
return self._prefix
self._prefix = value
self.display(self._msg)
def display(self, msg):
self._msg = msg
if self.prefix:
if self.prefix():
msg = self.prefix() + ' ' + msg
# Curses will raise a 'curses.error' when the last possible character is
# written. This exception should therefore always be catched. The raised
......
import curses
import sys
import threading
from time import sleep
from async import ClientConnection
from chat_window import ChatWindow
......@@ -102,6 +103,8 @@ class CLI:
c = self.stdscr.getch()
if c != curses.ERR and self.command_bar.handle_input(c):
break
# wait 1 milliseconds
sleep(.001)
except KeyboardInterrupt:
self.execute('quit')
......@@ -141,6 +144,7 @@ class CLI:
# The chat connection is ran in a separate thread.
self.connection_thread = threading.Thread()
self.connection_thread.daemon = True
self.connection_thread.run = main.connection.init_loop
self.connection_thread.start()
......@@ -152,10 +156,13 @@ class CLI:
and the 'Offline' message is set to the info bar.
"""
self.connection.close()
del self.connection_thread
self.connection = None
self.display_info('Offline. Type "/connect HOST" to connect' \
+ ' to another chat server.')
self.debug_window.clear()
self.info_bar.prefix('')
self.refresh()
def help(main):
if main.chat_window.displayed_help:
......@@ -204,13 +211,6 @@ All commands listed below should be preceded by a slash:
main.chat_window.window.refresh()
def quit(main):
# Disconnect the connection
e = None
try:
del self.connection
except Exception, e:
pass
# Reverse the curses-friendly terminal settings.
curses.nocbreak();
self.stdscr.keypad(0);
......@@ -219,9 +219,10 @@ All commands listed below should be preceded by a slash:
# Restore the terminal to its original operating mode.
curses.endwin()
if e:
raise e
# Disconnect the connection
if hasattr(self, 'connection_thread'):
del self.connection_thread
self.connection = None
sys.exit(0)
def raw(main, cmd):
......
......@@ -4,16 +4,25 @@ import asyncore
import logging
import logging.config
import os
import re
import socket
import sys
from asyncbase import AsyncBase
# Greeting message sent to a connected client.
GREETING_MSG = 'Hi there!'
# Major and minor version of this server.
MAJOR_VERSION = 1
MINOR_VERSION = 0
class SocketError(RuntimeError):
pass
class ClientData(object):
pass
class Server(asyncore.dispatcher):
"""
Basic server which will listen on an host address and port. The given
......@@ -27,18 +36,18 @@ class Server(asyncore.dispatcher):
self.port = port
self.handler = handler
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind((ip, port))
logging.config.fileConfig('logging.conf')
self.log = logging.getLogger('Server')
# Listen on given ip-address and port
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind((ip, port))
self.log.info('waiting for incoming connections on port %s.' % port)
self.listen(5)
self.active_clients = {}
# Dictonary which maps ip/port tuple to client data object.
self.clients = {}
def handle_accept(self):
"""
......@@ -48,53 +57,160 @@ class Server(asyncore.dispatcher):
try:
conn, addr = self.accept()
self.log.info('accepted client %s:%d' % addr)
client = self.connect_client(conn, addr)
except socket.error:
self.log.warning('warning: server accept() threw an exception.')
return
except TypeError:
self.log.warning('warning: server accept() threw EWOULDBLOCK.')
return
def connect_client(self, conn, addr):
"""
Initialise a client connection after the server accepted an incoming
connection. This will set the connection handler.
"""
client = ClientData()
# creates an instance of the handler class to handle the
# request/response on the incoming connection.
self.handler(conn, addr, self)
client.handler = self.handler(conn, addr, client, self)
client.username = ''
self.clients['%s:%d' % addr] = client
return client
def disconnect_client(self, addr):
"""
Client leaves the chat server. This function is called when the socket
is broken or the client closes the connection gracefully.
"""
# Suppress error if a client is already disconnected.
try:
client = self.clients['%s:%d' % addr]
self.send_all('LEAVE %s' % client.username, True)
del self.clients['%s:%d' % addr]
except KeyError:
pass
def change_username(self, addr, username):
"""
Update the username of the client.
"""
self.clients['%s:%d' % addr].username = username
def send_all(self, msg, sender=None):
"""
Send a message or notification from a client to all connected clients
(optionally including to the sender).
"""
for c in self.clients:
if self.clients[c].handler != sender:
self.clients[c].handler.send_raw(msg)
class RequestHandler(AsyncBase, asyncore.dispatcher):
def __init__(self, conn, address, server):
def __init__(self, conn, address, client, server):
AsyncBase.__init__(self)
asyncore.dispatcher.__init__(self, conn)
self.address = address
self.client = client
self.server = server
self.username = ''
self.log = self.server.log
self.send_welcome_message()
def send_welcome_message(self):
"""
Welcome our new client by sending a welcome message. This message
contains the server version number and a greetings message.
"""
self.send_raw("CHAT/%d.%d/%s" \
% (MAJOR_VERSION, MINOR_VERSION, GREETING_MSG))
def handle_read(self):
"""
Receive a message from the client. If the connection is somehow broken,
disconnect the client and clean the corresponding data.
"""
buf = ''
# Receive a message from the client.
try:
while True:
chunk = self.recv(1)
if not chunk:
raise RuntimeError('socket connection broken')
raise SocketError('socket connection broken')
elif chunk == '\n' and buf[-1] == '\r':
break
buf += chunk
except SocketError, socket.error:
self.log.info('client %s:%d disconnected or socket is broken.' \
% self.address)
self.server.disconnect_client(self.address)
return
# Received a message, so it's time to parse the message.
buf = buf[:-1]
self.debug_log('< %s' % buf)
self.parse_response(buf)
def handle_error(self):
self.server.disconnect_client(self.address)
self.send_positive('Ok')
#self.send_negative('Ok')
def parse_response(self, buf):
"""
>>> class DummyServer(object):
... def __init__(self): self.log = None
... def change_username(self, addr, username): pass
... def send_all(self, msg, sender): pass
>>> req = RequestHandler(None, None, None, DummyServer())
>>> assert req.parse_response('CHAT')
>>> assert req.parse_response('USER foo')
>>> # Some error handling
>>> assert not req.parse_response('CHAT ') # Client must send CHAT
>>> assert not req.parse_response('USER') # No username given.
>>> assert not req.parse_response('USER j/k') # username has a /
"""
cmd = buf.split(' ')[0]
if buf == 'CHAT':
# Client wants to chat.
return self.send_positive('Ok')
if cmd == 'USER':
# User changes/sets its username.
if re.match(ur'^[^\u0000-\u001f\u007f-\u009f/:]+$', buf[5:]):
if not self.username:
self.send_all('JOIN %s' % buf[5:], True)
else:
self.send_all('RENAME %s/%s' \
% (self.username, buf[5:]), True)
self.set_username(buf[5:])
return self.send_positive('Ok')
return self.send_negative('Invalid username.')
# TODO: user can't send a message if he didn't send his username first.
if cmd == 'SAY':
return self.send_all('SAY %s/%s' % (self.username, buf[4:]))
if cmd == 'NAMES':
clients = self.server.clients
self.send_raw('+Ok:\r\n' \
+ ''.join([clients[c].username + '\r\n' for c in clients]))
return True
return self.send_negative('Unsupported command.')
def send_positive(self, msg):
self.send_raw('+%s' % msg)
return True
def send_negative(self, msg):
self.send_raw('-%s' % msg)
return False
def send_all(self, msg, except_sender=False):
self.server.send_all(msg, except_sender and self)
return True
def set_username(self, username):
self.username = username
self.server.change_username(self.address, username)
if __name__ == '__main__':
if len(sys.argv) != 3:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment