3

I've written a Python 3 TkInter-based GUI application that launches a worker thread in the background. After the worker thread has finished, it waits two seconds (this is to avoid a possible race condition), and then sends a KeyboardInterrupt to tell the main thread it can close down.

Expected behaviour: running the program launches a GUI window, prints some text to the console after which the program closes down automatically.

Actual behaviour: instead of closing automatically, it only does so after the user either hovers the mouse over the GUI window area, or presses a key on the keyboard! Apart from that the program runs without reporting any errors.

Anyone have any idea why this is happening, and how to fix this? I already tried to wrap the KeyboardInterrupt into a separate function and then call that through a timer object, but this results in the same behaviour.

I've been able to reproduce this issue on 2 different Linux machines that run Python 3.5.2. and 3.6.6 respectively.

#! /usr/bin/env python3

import os
import threading
import _thread as thread
import time
import tkinter as tk
import tkinter.scrolledtext as ScrolledText

class myGUI(tk.Frame):

    # This class defines the graphical user interface 

    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.root = parent
        self.build_gui()

    def build_gui(self):                    
        # Build GUI
        self.root.title('TEST')
        self.root.option_add('*tearOff', 'FALSE')
        self.grid(column=0, row=0, sticky='ew')
        self.grid_columnconfigure(0, weight=1, uniform='a')

        # Add text widget to display logging info
        st = ScrolledText.ScrolledText(self, state='disabled')
        st.configure(font='TkFixedFont')
        st.grid(column=0, row=1, sticky='w', columnspan=4)

def worker():
    """Skeleton worker function, runs in separate thread (see below)"""  

    # Print some text to console
    print("Working!")
    # Wait 2 seconds to avoid race condition
    time.sleep(2)
    # This triggers a KeyboardInterrupt in the main thread
    thread.interrupt_main()

def main():
    try:
        root = tk.Tk()
        myGUI(root)
        t1 = threading.Thread(target=worker, args=[])
        t1.start()
        root.mainloop()
        t1.join()
    except KeyboardInterrupt:
        # Close program if subthread issues KeyboardInterrupt
        os._exit(0)

main()

(Github Gist link to the above script here)

johan
  • 764
  • 7
  • 17

1 Answers1

2

root.mainloop() mainloop is blocking and pending (interceptable) signals in Python are only examined in between execution of bytecode instructions. t1.join() in your code actually never gets executed.

Since mainloop block-waits for forwarded hardware-interrupts, for unblocking you have to provide them by e.g. hovering over the window like you saw. Only then the interpreter detects the pending KeyboardInterrupt. That's just how signal processing in Python works.

Solving the general problem could mean finding ways to unblock blocking I/O-calls by externally injecting what's needed to unblock them, or just not using blocking calls in the first place.

For your concrete setup, you could kill the whole process with an unhandled SIGTERM, but of course, that would be very, very ugly to do and is also unnecessary here. If you just search for a way to timeout your window, you can timeout with the tkinter.Tk.after method (shown here and here), or you get rid of mainloop and run your loop yourself (here).

The latter could look like:

def main():
    root = tk.Tk()
    myGUI(root)

    t1 = threading.Thread(target=worker, args=[])
    t1.start()

    while True:
        try:
            root.update_idletasks()
            root.update()
            time.sleep(0.1)
        except KeyboardInterrupt:
            print('got interrupt')
            break

    t1.join()
Darkonaut
  • 20,186
  • 7
  • 54
  • 65
  • Thanks, getting rid of `mainloop` as per your suggestion does the trick. Didn't know about the `tkinter.TK.after`method, will check that out as well. – johan Nov 19 '18 at 23:35