6

I have a simple chat client that I was attempting to get working with Tkinter as the interface. My problem is that when running the mainloop with .after for the chat input/output, the window freezes and blocks until another message is received.

class Client(Frame):

    def __init__(self, **kwargs):
        Frame.__init__(self, Tk())
        self.pack()

        self.lb = Listbox(self, width=100, height=30)
        self.lb.pack()

        self.show_data = self.lb.after(1000, self.chat_handle)

        self.entry = Entry(self)
        self.entry.bind('<Return>', self.input_handle)
        self.entry.pack(side=BOTTOM, fill=X)

    def input_handle(self, event):
        msg = self.entry.get()
        self.entry.delete(0, 'end')
        new_msg = 'privmsg %s :' % self.channel + msg + '\r\n'
        self.client.sendall(new_msg)
        self.lb.insert(END, self.nick + ' | ' + msg)

    def chat_handle(self):
        try:
            self.data = self.client.recvfrom(1024)
        except socket.error:
            self.lb.insert(END, "Bad Connection!")
            return
        if self.data and len(self.data[0]) > 0:
            self.lb.insert(END, self.data[0])
        elif self.data and len(self.data[0]) == 0:
            self.lb.insert(END, "Connection Dropped!")
            return
        self.show_data = self.lb.after(1000, self.chat_handle)

This block of code is shortened but, shows the relavent parts involved. The Entry widget will become unresponsive for extended periods while .after is called and won't respond until a message is received.

When the Entry widget becomes responsive again, the entry field has all the data that was typed in but, I won't see the changes during the "frozen" time. The same goes for the Listbox widget.

If anyone could shed some light on why this is exactly or point out if I'm miss using a method here, it would be greatly appreciated.

EDIT: after some more research, its looking like the socket data is blocking whenever its called and window is getting frozen during this time.

tijko
  • 7,599
  • 11
  • 44
  • 64

2 Answers2

6

after executes the callback function after the given time; however, this method also runs in the main thread. So if there is an operation that takes more time than usual (in this case, the blocking recvfrom), the GUI will be unresponsive until the complete callback is executed.

To solve this, a common recipe is to spawn a new thread and communicate it with your Tkinter code with a synchronized object like a Queue. Thus, you put the data in the queue when you receive it from the socket, and then check periodically in the main thread inside the after callback.

This is a question whose answer can be adapted to use the same approach: Tkinter: How to use threads to preventing main event loop from "freezing"

Community
  • 1
  • 1
A. Rodas
  • 20,171
  • 8
  • 62
  • 72
  • thanks for the response and link. I'm reading up on non-blocking sockets right now too. – tijko Jun 10 '13 at 02:31
  • I went with `select` here but, I'm going to make a version that uses the `Queue` module with threads too. – tijko Jun 11 '13 at 15:30
  • To be completely clear: it's not the `after()` method *itself* that is freezing things up, it's the callback. E.g. you can execute an `after()` with a delay of (say) 10 seconds, and for those 10 seconds everything will continue normally. But if the callback causes blocking or consumes excess resources, *then* the GUI will freeze. (Just wanted to note this as I was temporarily confused, thinking that if I put an `after()` call in my code, no lines following that `after()` would execute until the time expired.) – JDM Aug 28 '18 at 15:15
0

I learned about using select to make system calls to check if a socket file is ready to be read.

How to set timeout on python's socket recv method?

class Client(Frame):

    def __init__(self, **kwargs):
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.connect(("host", port))
        self.client.setblocking(0)
        Frame.__init__(self, Tk())
        self.pack()

        self.lb = Listbox(self, width=100, height=30)
        self.lb.pack()

        self.show_data = self.lb.after(1000, self.chat_handle)

        self.entry = Entry(self)
        self.entry.bind('<Return>', self.input_handle)
        self.entry.pack(side=BOTTOM, fill=X)

    def input_handle(self, event):
        msg = self.entry.get()
        self.entry.delete(0, 'end')
        new_msg = 'privmsg %s :' % self.channel + msg + '\r\n'
        self.client.sendall(new_msg)
        self.lb.insert(END, self.nick + ' | ' + msg)

    def chat_handle(self):
        socket_data = select.select([self.client], [], [], 0.3) # set timeout on last arg
        if socket_data[0]:
            try:
                self.data = self.client.recvfrom(1024)
            except socket.error:
                self.lb.insert(END, "Bad Connection!")
                return
            if self.data and len(self.data[0]) > 0:
                self.lb.insert(END, self.data[0])
            elif self.data and len(self.data[0]) == 0:
                self.lb.insert(END, "Connection Dropped!")
                return
            self.show_data = self.lb.after(1000, self.chat_hand
Community
  • 1
  • 1
tijko
  • 7,599
  • 11
  • 44
  • 64