Search code examples
pythonpython-3.xtkinterpexpect

GUI freezes on cmd run but not through Pycharm


So I've been looking at this for quite some time and can't figure this problem out.

The goal here is to have it display realtime results line by line from the cmd/ terminal into the tkinter ScrolledText/Text widget, which it does except when ran through the cmd on Windows.

I am running the code on both Linux (Kali) and Windows 10. If I run it using Pycharm on Windows 10 then it will run smoothly and the GUI won't freeze waiting for the code run using Pexpect. If I run the code through the cmd (python tmp_stack.py) then it will freeze the GUI until Pexpect finishes running the file I asked it to.
On Linux terminal the code runs fine and doesn't freeze (after adjusting it for Linux).
I combined multiple files into tmp_stack.py to prevent the need to create more files for no real reason.
I have tested that the configuration runs the same on both Windows and Linux.

Side note: I don't mind changing to subprocess.Popen and I don't mind using threading if it won't complain about main loop and will work.

requirements.txt - pexpect==4.6.0

op.py:

import time

print('This is op.py')
for i in range(10):
    time.sleep(1)
    print(i)

oscheck.py:

import platform

MYPLATFORM = platform.system()

tmp_stack.py:

from tkinter import *
from tkinter.ttk import *
from tkinter.scrolledtext import ScrolledText

import oscheck

if oscheck.MYPLATFORM == 'Windows':
    from pexpect import popen_spawn
elif oscheck.MYPLATFORM == 'Linux':
    from pexpect import spawn


class TextFrame(Frame):
    def __init__(self, master=None, **kwargs):
        super().__init__(master, **kwargs)
        self.textarea = ScrolledText(master=self, wrap=WORD, bg='black', fg='white')
        self.textarea.pack(side=TOP, fill=BOTH, expand=True)

    def insert(self, text):
        self.textarea['state'] = 'normal'
        # Insert at the end of the TextArea.
        self.textarea.insert(END, text)
        self.textarea['state'] = 'disabled'
        self.update()


class TheFrame(TextFrame):
    def __init__(self, master=None, **kwargs):
        super().__init__(master, **kwargs)
        self.btn = Button(master=self, text="Run op", command=self.run_op)
        self.btn.pack(fill=X)
        # Ignore first child of pexpect on Linux because it is the bin bash prefix.
        self.linux_flag = True

    def run_op(self):
        filename = 'op.py'
        cmd = ['python', filename]

        self.cmdstdout = RunCommand(cmd)

        # Get root.
        root_name = self._nametowidget(self.winfo_parent()).winfo_parent()
        root = self._nametowidget(root_name)

        root.after(0, self.updateLines())

    def updateLines(self):
        for line in self.cmdstdout.get_child():
            if oscheck.MYPLATFORM == 'Linux' and self.linux_flag:
                self.linux_flag = False
                continue
            try:
                self.insert(line.decode('utf-8'))
            except TclError as e:
                print("Window has been closed.\n", e)
                self.close()
                break

    def close(self):
        self.cmdstdout.close()


class RunCommand:
    def __init__(self, command):
        command = ' '.join(command)
        if oscheck.MYPLATFORM == 'Windows':
            print('You are on Windows.')
            self.child = popen_spawn.PopenSpawn(command, timeout=None)
        elif oscheck.MYPLATFORM == 'Linux':
            print('You are on Linux.')
            self.child = spawn("/bin/bash", timeout=None)
            self.child.sendline(command)
        else:
            print('Not Linux or Windows, probably Mac')
            self.child = spawn(command, timeout=None)

    def get_child(self):
        return self.child

    def close(self):
        if oscheck.MYPLATFORM == 'Linux':
            self.child.terminate(True)


def on_close(root):
    root.quit()
    root.destroy()


root = Tk()

if oscheck.MYPLATFORM == 'Windows':
    root.state('zoomed')
elif oscheck.MYPLATFORM == 'Linux':
    root.attributes('-zoomed', True)

the_frame = TheFrame(root, padding="1")
the_frame.grid(column=0, row=0, sticky=N+W+S+E)

root.protocol("WM_DELETE_WINDOW", lambda: on_close(root))
mainloop()

Solution

  • Try running the child Python process with output buffering disabled.

    In other words, try replacing the line

            cmd = ['python', filename]
    

    with

            cmd = ['python', '-u', filename]
    

    See also the Python documentation for the -u option.