Search code examples
pythonmultithreadinguser-interfacetkinterpython-multithreading

Threading with Tkinter


I need to be able to send control-c to the new thread that I start. How would I go about this. I can start the thread, but I need to stop like I do in the command line using control-c.

from Tkinter import *
import threading # should use the threading module instead!
import Queue
import os


def show1():
    os.system('ola_recorder -p MyRecord -i 0')

def show2 ():
    os.system('ola_recorder -p MyRecord ')

def stop ():
    os.system('^c')


t = threading.Thread(name='Show2', target=show2)

root = Tk()
b = Button(root, text="Show 1", command=lambda: thread.start_new(show1, ()))
b.pack()

b2 = Button(root, text="Show 2", command=lambda: t.start())
b2.pack()

root.mainloop()

Solution

  • First, the reason what you're doing doesn't work:

    Each call to os.system just calls your platform's system (or _system, on some Windows platforms) function, which creates a brand-new shell process each time. So, there's no way you can do anything with os.system to affect another call.


    If you want to send a ^C to an arbitrary process, you can do that with os.kill. To make it portable, you have to do something like this:

    def send_ctrl_c(pid):
        try:
            sig = signal.CTRL_C_EVENT
        except AttributeError:
            sig = signal.SIGINT
        else:
            os.signal(pid, sig)
    

    However, you need the other process's pid to do that, and os.system doesn't give you that.


    So, the right thing to do is to use the subprocess module:

    proc2 = None
    
    def show2():
        global proc2
        proc2 = subprocess.Popen('ola_recorder -p MyRecord', shell=True)
        proc2.wait()
    

    Since there's no good reason to use the shell here, you'd probably be better off passing Popen a list of args, instead of a string, and leaving off the shell=True. And of course it would be a lot cleaner to not stick proc2 in a global, but I'll ignore that for this toy example.

    Anyway, now you can get the pid and use it:

    def stop():
        send_ctrl_c(proc2.pid)
    

    However, if you're going to do that, you might as well just use the Popen object directly. See the docs for full details of what you can do with it, but here's a quick version:

    def stop():
        global proc2
        try:
            sig = signal.CTRL_C_EVENT
        except AttributeError:
            sig = signal.SIGINT
        proc.send_signal(sig)
    

    When you call stop, the process will get killed exactly as if it had received a ^C (POSIX), or as close as possible to as if it had received a ^C (Windows), the wait call will return (with a -2, at least on POSIX), and your thread will finish as well.


    One last note: You almost never want to use the thread module directly, and you do not want to reuse threading.Thread objects. So, instead of this:

    b = Button(root, text="Show 1", command=lambda: thread.start_new(show1, ()))
    

    … or this:

    b2 = Button(root, text="Show 2", command=lambda: t.start())
    

    … do this:

    def start2():
        global t2
        t2 = threading.Thread(name='Show2', target=show2)
        t2.start()
    

    Of course, a non-toy program should avoid using a global, and will want to keep track of all existing threads instead of just the last one (if you're allowed to click the button while a background thread is already running), and will probably want to join all of its threads at quit.