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 ...@@ -4,6 +4,10 @@ import re
import random import random
from asyncbase import AsyncBase from asyncbase import AsyncBase
# Socket error "Resource temporarily unavailable": try again ;)
EAGAIN = 11
class ClientConnection(object, AsyncBase, asyncore.dispatcher): class ClientConnection(object, AsyncBase, asyncore.dispatcher):
def __init__(self): def __init__(self):
...@@ -51,6 +55,29 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher): ...@@ -51,6 +55,29 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
if 'connect' in self.event_list: if 'connect' in self.event_list:
self.event_list['connect'](self) 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): def handle_close(self):
self.close() self.close()
...@@ -61,7 +88,18 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher): ...@@ -61,7 +88,18 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
# Receive a (multiline) message. # Receive a (multiline) message.
while True: while True:
try:
chunk = self.recv(1) 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: if not chunk:
raise RuntimeError('socket connection broken') raise RuntimeError('socket connection broken')
if chunk in ['-','+']: if chunk in ['-','+']:
...@@ -78,7 +116,7 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher): ...@@ -78,7 +116,7 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
buf += chunk buf += chunk
buf = buf[:-1] buf = buf[:-1]
self.debug_log('< %s' % buf) self.debug_log('< %s' % buf.replace('\n','\\n').replace('\r','\\r'))
# Invoke the proper callback function. # Invoke the proper callback function.
if not self.verified: if not self.verified:
...@@ -112,7 +150,7 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher): ...@@ -112,7 +150,7 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
if not self.authentification_sent: if not self.authentification_sent:
self.send_queue.put('USER %s' % self.user) self.send_queue.put('USER %s' % self.user)
self.authentification_sent = True self.authentification_sent = True
elif buf[1:9] == 'Username': elif buf[0] == '+':
# TODO: handle 'username is taken'. # TODO: handle 'username is taken'.
self.authenticated = True self.authenticated = True
if 'authenticated' in self.event_list: if 'authenticated' in self.event_list:
...@@ -145,5 +183,5 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher): ...@@ -145,5 +183,5 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
if __name__ == '__main__': if __name__ == '__main__':
client = ClientConnection() client = ClientConnection()
client.connect(('ow150.science.uva.nl', 16897)) client.connect(('localhost', 16897))
client.init_loop() client.init_loop()
...@@ -16,15 +16,15 @@ class BaseBar(object): ...@@ -16,15 +16,15 @@ class BaseBar(object):
self.color_pair = curses.color_pair(0) self.color_pair = curses.color_pair(0)
self._prefix = '' self._prefix = ''
def prefix(self, value=''): def prefix(self, value=None):
if not value: if value == None: # Note: an empty string can also be set.
return self._prefix return self._prefix
self._prefix = value self._prefix = value
self.display(self._msg) self.display(self._msg)
def display(self, msg): def display(self, msg):
self._msg = msg self._msg = msg
if self.prefix: if self.prefix():
msg = self.prefix() + ' ' + msg msg = self.prefix() + ' ' + msg
# Curses will raise a 'curses.error' when the last possible character is # Curses will raise a 'curses.error' when the last possible character is
# written. This exception should therefore always be catched. The raised # written. This exception should therefore always be catched. The raised
......
import curses import curses
import sys import sys
import threading import threading
from time import sleep
from async import ClientConnection from async import ClientConnection
from chat_window import ChatWindow from chat_window import ChatWindow
...@@ -102,6 +103,8 @@ class CLI: ...@@ -102,6 +103,8 @@ class CLI:
c = self.stdscr.getch() c = self.stdscr.getch()
if c != curses.ERR and self.command_bar.handle_input(c): if c != curses.ERR and self.command_bar.handle_input(c):
break break
# wait 1 milliseconds
sleep(.001)
except KeyboardInterrupt: except KeyboardInterrupt:
self.execute('quit') self.execute('quit')
...@@ -141,6 +144,7 @@ class CLI: ...@@ -141,6 +144,7 @@ class CLI:
# The chat connection is ran in a separate thread. # The chat connection is ran in a separate thread.
self.connection_thread = threading.Thread() self.connection_thread = threading.Thread()
self.connection_thread.daemon = True
self.connection_thread.run = main.connection.init_loop self.connection_thread.run = main.connection.init_loop
self.connection_thread.start() self.connection_thread.start()
...@@ -152,10 +156,13 @@ class CLI: ...@@ -152,10 +156,13 @@ class CLI:
and the 'Offline' message is set to the info bar. and the 'Offline' message is set to the info bar.
""" """
self.connection.close()
del self.connection_thread
self.connection = None self.connection = None
self.display_info('Offline. Type "/connect HOST" to connect' \ self.display_info('Offline. Type "/connect HOST" to connect' \
+ ' to another chat server.') + ' to another chat server.')
self.debug_window.clear() self.info_bar.prefix('')
self.refresh()
def help(main): def help(main):
if main.chat_window.displayed_help: if main.chat_window.displayed_help:
...@@ -204,13 +211,6 @@ All commands listed below should be preceded by a slash: ...@@ -204,13 +211,6 @@ All commands listed below should be preceded by a slash:
main.chat_window.window.refresh() main.chat_window.window.refresh()
def quit(main): def quit(main):
# Disconnect the connection
e = None
try:
del self.connection
except Exception, e:
pass
# Reverse the curses-friendly terminal settings. # Reverse the curses-friendly terminal settings.
curses.nocbreak(); curses.nocbreak();
self.stdscr.keypad(0); self.stdscr.keypad(0);
...@@ -219,9 +219,10 @@ All commands listed below should be preceded by a slash: ...@@ -219,9 +219,10 @@ All commands listed below should be preceded by a slash:
# Restore the terminal to its original operating mode. # Restore the terminal to its original operating mode.
curses.endwin() curses.endwin()
if e: # Disconnect the connection
raise e if hasattr(self, 'connection_thread'):
del self.connection_thread
self.connection = None
sys.exit(0) sys.exit(0)
def raw(main, cmd): def raw(main, cmd):
......
...@@ -4,16 +4,25 @@ import asyncore ...@@ -4,16 +4,25 @@ import asyncore
import logging import logging
import logging.config import logging.config
import os import os
import re
import socket import socket
import sys import sys
from asyncbase import AsyncBase from asyncbase import AsyncBase
# Greeting message sent to a connected client.
GREETING_MSG = 'Hi there!' GREETING_MSG = 'Hi there!'
# Major and minor version of this server.
MAJOR_VERSION = 1 MAJOR_VERSION = 1
MINOR_VERSION = 0 MINOR_VERSION = 0
class SocketError(RuntimeError):
pass
class ClientData(object):
pass
class Server(asyncore.dispatcher): class Server(asyncore.dispatcher):
""" """
Basic server which will listen on an host address and port. The given Basic server which will listen on an host address and port. The given
...@@ -27,18 +36,18 @@ class Server(asyncore.dispatcher): ...@@ -27,18 +36,18 @@ class Server(asyncore.dispatcher):
self.port = port self.port = port
self.handler = handler 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') logging.config.fileConfig('logging.conf')
self.log = logging.getLogger('Server') 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.log.info('waiting for incoming connections on port %s.' % port)
self.listen(5) self.listen(5)
self.active_clients = {} # Dictonary which maps ip/port tuple to client data object.
self.clients = {}
def handle_accept(self): def handle_accept(self):
""" """
...@@ -48,53 +57,160 @@ class Server(asyncore.dispatcher): ...@@ -48,53 +57,160 @@ class Server(asyncore.dispatcher):
try: try:
conn, addr = self.accept() conn, addr = self.accept()
self.log.info('accepted client %s:%d' % addr) self.log.info('accepted client %s:%d' % addr)
client = self.connect_client(conn, addr)
except socket.error: except socket.error:
self.log.warning('warning: server accept() threw an exception.') self.log.warning('warning: server accept() threw an exception.')
return return
except TypeError: except TypeError:
self.log.warning('warning: server accept() threw EWOULDBLOCK.') self.log.warning('warning: server accept() threw EWOULDBLOCK.')
return 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 # creates an instance of the handler class to handle the
# request/response on the incoming connection. # 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): class RequestHandler(AsyncBase, asyncore.dispatcher):
def __init__(self, conn, address, server): def __init__(self, conn, address, client, server):
AsyncBase.__init__(self) AsyncBase.__init__(self)
asyncore.dispatcher.__init__(self, conn) asyncore.dispatcher.__init__(self, conn)
self.address = address self.address = address
self.client = client
self.server = server self.server = server
self.username = ''
self.log = self.server.log self.log = self.server.log
self.send_welcome_message() self.send_welcome_message()
def send_welcome_message(self): 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" \ self.send_raw("CHAT/%d.%d/%s" \
% (MAJOR_VERSION, MINOR_VERSION, GREETING_MSG)) % (MAJOR_VERSION, MINOR_VERSION, GREETING_MSG))
def handle_read(self): 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 = '' buf = ''
# Receive a message from the client. try:
while True: while True:
chunk = self.recv(1) chunk = self.recv(1)
if not chunk: if not chunk:
raise RuntimeError('socket connection broken') raise SocketError('socket connection broken')
elif chunk == '\n' and buf[-1] == '\r': elif chunk == '\n' and buf[-1] == '\r':
break break
buf += chunk 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] buf = buf[:-1]
self.debug_log('< %s' % buf) self.debug_log('< %s' % buf)
self.parse_response(buf)
def handle_error(self):
self.server.disconnect_client(self.address)
self.send_positive('Ok') def parse_response(self, buf):
#self.send_negative('Ok') """
>>> 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): def send_positive(self, msg):
self.send_raw('+%s' % msg) self.send_raw('+%s' % msg)
return True
def send_negative(self, msg): def send_negative(self, msg):
self.send_raw('-%s' % 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 __name__ == '__main__':
if len(sys.argv) != 3: 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