Search code examples
pythonpython-3.xmultithreadingtkintersubprocess

Freezing tkinter window via sending .communicate request in Popen process


I have this code, product of my imagination and ChatGPT help:

import subprocess
import threading
import tkinter as tk

class PingThread(threading.Thread):
    def __init__(self, text_widget):
        super().__init__()
        self.text_widget = text_widget
        self.process = None
        self.stop_event = threading.Event()

    def run(self):
        self.process = subprocess.Popen(['cmd'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
        self.process.stdin.write(b'ping -t google.com\n')
        self.process.stdin.flush()

        while not self.stop_event.is_set():
            line = self.process.stdout.readline().decode('cp866')
            if not line:
                break
            self.text_widget.insert(tk.END, line)
            self.text_widget.see(tk.END)

    def stop(self):
        if self.process:
            self.process.communicate(b'\x03')
            self.process.wait()                                #Window freezes on the .communicate line, so this and line below are not executing at all
        self.stop_event.set()

def ping():
    ping_thread = PingThread(text)
    ping_thread.start()

    def handle_ctrl_c(event):
        ping_thread.stop()
        text.insert(tk.END, '\nProcess terminated.\n')

    root.bind('<Control-c>', handle_ctrl_c)

root = tk.Tk()

text = tk.Text(root)
text.pack()

button = tk.Button(root, text='Ping', command=ping)
button.pack()

root.mainloop()

I'm trying to create a console simulation in Tkinter. I'm sending ping request and listening console for it's response. All works well, except Ctrl+C command, which should finishes ping execution and response the statistics from console. Window just freezing, when i try to send self.process.communicate(b'\x03')

What causes that? As i understand, this line should send Ctrl+C to the console and while loop should receive last lines from the console, with ping's statistics?


Solution

  • So now that I better understand your issue, here is another try. My answer is not working perfectly, because I use a workaround to get the last 4 lines after sending CTRL_BREAK_EVENT

    However, for your question the 2 most important things:

    1. Use creationflags=subprocess.CREATE_NEW_PROCESS_GROUP when you create your process. If I understand it correctly, this creates a different process for the terminal and the ping command, so you can still get the output from the terminal after passing the stop signal.
    2. To terminate the ping command, send signal.CTRL_BREAK_EVENT on Windows. In general, these signals seem to be not consistent for operating systems. (see also the comment from @chris_se)

    The workaround I use is, to only read the next 4 lines of STDOUT after setting the stop event (exiting the while loop). I know that the statistics summary of the ping command will print this number of lines. Of course we could increase it to be sure to not miss anything.

    Here is the code:

    import subprocess
    import threading
    import tkinter as tk
    import signal
    
    class PingThread(threading.Thread):
        def __init__(self, text_widget):
            super().__init__()
            self.text_widget = text_widget
            self.process = None
            self.stop_event = threading.Event()
    
        def run(self):
            self.process = subprocess.Popen(['cmd'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
            self.process.stdin.write(b'ping -t google.com\n')
            self.process.stdin.flush()
    
            while not self.stop_event.is_set():
                line = self.process.stdout.readline().decode('cp866')
                if not line:
                    break
                self.text_widget.insert(tk.END, line)
                self.text_widget.see(tk.END)
    
            self.text_widget.insert(tk.END, self.process.stdout.readline().decode('cp866'))
            for i in range(4):
                self.text_widget.insert(tk.END, self.process.stdout.readline().decode('cp866'))
                self.text_widget.see(tk.END)
                i += 1
    
            self.process.stdout.close()
            self.process.kill()
            self.text_widget.insert(tk.END, '\nProcess terminated.\n')
            root.unbind('<Control-c>')
    
        def stop(self):
            if self.process:
                self.stop_event.set()
                self.process.send_signal(signal.CTRL_BREAK_EVENT)
    
    
    def ping():
        ping_thread = PingThread(text)
        ping_thread.start()
    
        def handle_ctrl_c(event):
            ping_thread.stop()
    
        root.bind('<Control-c>', handle_ctrl_c)
    
    root = tk.Tk()
    
    text = tk.Text(root)
    text.pack()
    
    button = tk.Button(root, text='Ping', command=ping)
    button.pack()
    
    root.mainloop()