Search code examples
pythonmultithreadingtkintertwisted

Twisted TCP server with a Tkinter GUI


recently I've been experimenting with Twisted (python library) in an attempt to make a TCP chat server/client. I had the server running nicely but when I tried to add a Tkinter-based GUI to the server, things got weird. As soon as a user connects to the server, a message is sent to the GUI. However, somewhere along the way something goes wrong and a long-winded error comes up, the gist of which is that Tkinter ran out of stack space because of an infinite loop. I've put my code below. The function that I am having trouble with is App.write(text) and User.connectionMade(*args) as well as any other function in the User class that attempts to print text to the GUI.

from twisted.internet.protocol import ServerFactory, Protocol
from twisted.internet import reactor
from os import path
import yaml
import threading
from Tkinter import *

__version__ = ''
__author__ = ''

class User(Protocol):
    def connectionMade(self,*args):
        self.gui.write('New connection from %s' % (self.addr.host))
        self.transport.write('Username: ')

    def connectionLost(self,reason):
        self.gui.write('Connection lost with %s' % (self.addr.host))
        if not self.name == None:
            msg = '%s has disconnected\r\n' % (self.name)
            self.gui.write(msg.rstrip())
            self.toAll(msg)
            del self.users[self.name]

    def dataReceived(self,data):
        if data == '\x08':
            if len(self.text) > 0:
                self.text = self.text[:-1]
            return 
        elif not data.endswith('\r\n'):
            self.text += data
            return
        if self.name == None:
            self.setName(self.text)
        else:
            self.handle(self.text)
        self.text = ''

    def handle(self,data):
        if not data.startswith('/'):
            self.chat(data)
        else:
            self.gui.write('%s executed command %s' % (self.name, data))
            if data in ['/help','/h']: self.cmdHelp()
            elif data in ['/list','/l']: self.userList()
            elif data in ['/motd','/m']: self.sendMotd()
            elif data in ['/ping','/p']: self.transport.write('Pong!\r\n')
            else: self.transport.write('Unrecognized command %s\r\n' % (data))

    def cmdHelp(self):
        x = ['\r\nCOMMANDS:',\
             '/motd,/m - Display the MOTD',\
             '/list,/l - Display a list of online users',\
             '/help,/h - Display this list\r\n']
        for item in x:
            self.transport.write(item+'\r\n')

    def sendMotd(self):
        self.transport.write('\r\nMOTD: %s\r\n\r\n' % (self.motd))

    def userList(self):
        self.transport.write('\r\nCURRENTLY ONLINE: server,%s\r\n\r\n' % (','.join(item for item in self.users)))

    def setName(self,name):
        if self.users.has_key(name) or name.lower() == 'server':
            self.transport.write('That username is in use!\r\nUsername: ')
        elif ' ' in name:
            self.transport.write('No spaces are allowed in usernames!\r\nUsername: ')
        elif name == '':
            self.transport.write('You must enter a username!\r\nUsername: ')
        else:
            self.users[name] = self
            self.name = name
            self.gui.write('New user registered as %s' % (name))
            self.toAll('%s has connected' % (self.name))
            self.transport.write('\nSuccessfully logged in as %s\r\n\r\n' % (name))
            self.sendMotd()

    def toAll(self,msg):
        for name,protocol in self.users.iteritems():
            if not protocol == self:
                protocol.transport.write(msg)

    def chat(self,data):
        to_self = '<%s (you)> %s\r\n' % (self.name, data)
        to_else = '<%s> %s\r\n' % (self.name, data)
        self.gui.write('[CHAT] - %s' % (to_else.rstrip()))
        self.transport.write(to_self)
        self.toAll(to_else)

    def __init__(self,addr=None,users=None,motd=None,master=None):
        self.name = None
        self.addr = addr
        self.users = users
        self.motd = motd
        self.text = ''
        self.factory = master
        self.gui = self.factory.app
        self.kicked = False

class App(Frame):
    def write(self,text):
        self.display.insert(END,text+'\n')

    def clear(self,event=None):
        self.display.delete(1.0,END)

    def userList(self):
        self.write('Currently online: server,%s' % (','.join(item for item in self.factory.users)))

    def handle(self,event=None):
        msg = self.entry.get()
        self.entry.delete(0,END)
        if not msg.startswith('/'): self.send(msg)
        elif msg in ['/cls','/clear','/clr','/c']: self.clear()
        elif msg in ['/list','/l']: self.userList()
        elif msg in ['/exit']: self.kill()
        else: self.write('Unrecognized command \'%s\'' % (msg))

    def send(self,msg,event=None):
        for item in self.factory.users: self.factory.users[item].transport.write('<server> %s\r\n' % (msg))
        self.write('[CHAT] - <server> %s' % (msg))

    def kill(self):
        self.write('Stopping server...')
        reactor.stop()
        self.write('GUI says guidbye! :(')
        self.quit()

    def __init__(self,master,factory):
        Frame.__init__(self,master)
        self.grid(row=0,sticky=N+E+S+W)
        self.columnconfigure(0,weight=1)
        self.rowconfigure(0,weight=1)

        self.display = Text(self)
        self.display.grid(row=0,sticky=N+E+S+W)
        self.yscroll = Scrollbar(self,command=self.display.yview)
        self.yscroll.grid(row=0,column=1,sticky=N+S)
        self.display.config(yscrollcommand=self.yscroll.set)
        self.entry = Entry(self)
        self.entry.grid(row=1,sticky=E+W)
        self.master = master

        self.master.wm_title('TCP Chat Server v%s' % (__version__))
        self.factory = factory

        self.motd = ''
        self.port = 0

        self.entry.bind('<Return>',self.handle)
        self.master.protocol('WM_DELETE_WINDOW',self.kill)

        self.write('TCP Chat Server v%s' % (__version__))
        self.write('by %s\n' % (__author__))
        self.write('Server currently running on port %s' % (self.factory.port))

class Main(ServerFactory):
    def buildProtocol(self,addr):
        return User(addr=addr,users=self.users,motd=self.motd,master=self)

    def start(self):
        self.root = Tk()
        self.root.columnconfigure(0,weight=1)
        self.root.rowconfigure(0,weight=1)

        self.app = App(self.root,self)
        self.app.mainloop()

    def __init__(self,motd,port):
        self.users = {}
        self.motd = motd
        self.port = port

        self.tk_thread = threading.Thread(target=self.start)
        self.tk_thread.start()


if not path.isfile('config.yml'):
    open('config.yml','w').write('port: 4444\nmotd: No motd set!')

with open('config.yml','r') as f:
    dump = yaml.load(f.read())
    motd = dump['motd']
    port = dump['port']

reactor.listenTCP(port,Main(motd,port))
reactor.run()

Everything else is running as expected, and when I comment out the App.write('') statements, the program runs as expected (sans GUI and server-side messages). I've been using Windows to test the program so I use

telnet localhost 4444

to run the client.


Solution

  • Twisted has some specialised support for Tkinter.

    from Tkinter import *
    from twisted.internet import tksupport, reactor
    
    root = Tk()
    
    # Install the Reactor support
    tksupport.install(root)
    
    # at this point build Tk app as usual using the root object,
    # and start the program with "reactor.run()", and stop it
    # with "reactor.stop()".