-1

What to do if I want to see clock icon during processing?

def refresh():
    window.config(cursor="clock")
    p = subprocess.Popen("ping 8.8.8.8 -c 3", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    while p.stdout is not None:
        line = p.stdout.readline()
        # Add line in list and remove carriage return
        if not line:
            p.stdout.flush()
            break
        text.insert(INSERT, "test\n")


btn_refresh = Button(frame2, text="Refresh Data", command=refresh)
Alex
  • 11
  • 4
  • 1
    Why not do what you have shown? – mkrieger1 Jul 16 '21 at 14:33
  • 2
    Instead of `time.sleep(3)` and `window.config(cursor="")` use `window.after(3000, lambda: window.config(cursor=""))`. That will call `window.config(cursor="")` after 3000 milliseconds (which is 3 seconds). You shouldn't use `time.sleep` when using `tkinter`. If it's just a lot of calculations that don't use `tkinter`, put them in another thread. – TheLizzard Jul 16 '21 at 14:35
  • Can you give an example what to do if I have subprocesses instead of time.sleep(3) ? @TheLizzard – Alex Jul 19 '21 at 09:16
  • @Alex That becomes way more complicated. It will also depend on how you start the process. If you start it using `proc = subprocess.Popen(...)`, you can use threading and `proc.poll() != None` to check when the process ends. Can you please provide a minimal example? For the command you can use something like: `ping 8.8.8.8 -n 3` – TheLizzard Jul 19 '21 at 09:28
  • updated @TheLizzard Can you explain ? – Alex Jul 19 '21 at 11:22
  • Shouldn't the `text.insert(INSERT, "test\n")` be `text.insert(INSERT, line)`? – TheLizzard Jul 19 '21 at 11:24

1 Answers1

0

Try this:

from subprocess import Popen, PIPE
from threading import Thread, Lock
import tkinter as tk

# Global variables
proc = None
running = True
stdout_buffer = ""
stdout_buffer_lock = Lock()


def stdout_loop():
    global stdout_buffer
    with stdout_buffer_lock:
        # Get the data and clear the buffer:
        data, stdout_buffer = stdout_buffer, ""
    text.config(state="normal")
    text.insert("end", data)
    text.see("end")
    text.config(state="disabled")
    if proc is None:
        if len(commands) == 0:
            # If we are done with all of the commands:
            text.config(cursor="")
            root.config(cursor="")
        else:
            # If we have more commands to do call `start_next_proc`
            start_next_proc()
    else:
        root.after(100, stdout_loop)

def start_next_proc():
    global proc, commands
    command = commands.pop(0) # Take the first one from the list
    proc = Popen(command, shell=True, stdout=PIPE)
    new_thread = Thread(target=read_stdout, daemon=True)
    new_thread.start()
    stdout_loop()

def start_new_proc():
    global commands
    # For linux use "-c 5". For windows use "-n 5"
    commands = ["ping 8.8.8.8 -c 5",
                "ping 1.1.1.1 -c 5",
                "ping 5.5.5.5 -c 5"]
    start_next_proc()

    text.config(cursor="clock")
    root.config(cursor="clock")

def read_stdout():
    global proc, stdout_buffer
    while proc.poll() is None:
        line = proc.stdout.readline()
        with stdout_buffer_lock:
            stdout_buffer += line.decode()
    proc = None


root = tk.Tk()

text = tk.Text(root, state="disabled")
text.pack()

button = tk.Button(root, text="Run", command=start_new_proc)
button.pack()

root.mainloop()

In the code, I start the process in the main thread and then I start 2 loops. One of the loops is a while loop that's inside read_stdout. It reads all of the data out of the process and writes it into stdout_buffer. The second loops is a tkinter loop (inside stdout_loop) that reads stdout_buffer and writes it inside the Text widget.

I also use a lock to make sure that the buffer doesn't get corrupted.

If tkinter was thread safe, the code would have been much shorter but it isn't.

If you want it as a class:

from subprocess import Popen, PIPE
from threading import Thread, Lock
import tkinter as tk


class TkinterPopen(tk.Text):
    def __init__(self, master, state="disabled", **kwargs):
        super().__init__(master, state=state, **kwargs)
        self.commands = []
        self.proc = None
        self.running = True
        self.stdout_buffer = ""
        self.stdout_buffer_lock = Lock()

    def stdout_loop(self) -> None:
        with self.stdout_buffer_lock:
            # Get the data and clear the buffer:
            data, self.stdout_buffer = self.stdout_buffer, ""
        state = super().cget("state")
        super().config(state="normal")
        super().insert("end", data)
        super().see("end")
        super().config(state=state)
        if self.proc is None:
            if len(self.commands) == 0:
                # If we are done with all of the commands:
                super().config(cursor="")
            else:
                # If we have more commands to do call `start_next_proc`
                self.start_next_proc()
        else:
            super().after(100, self.stdout_loop)

    def start_next_proc(self) -> None:
        command = self.commands.pop(0) # Take the first one from the list
        self.proc = Popen(command, shell=True, stdout=PIPE)
        new_thread = Thread(target=self.read_stdout, daemon=True)
        new_thread.start()
        self.stdout_loop()

    def run_commands(self, commands:list) -> None:
        self.commands = commands
        self.start_next_proc()
        super().config(cursor="clock")

    def read_stdout(self):
        while self.proc.poll() is None:
            line = self.proc.stdout.readline()
            with self.stdout_buffer_lock:
                self.stdout_buffer += line.decode()
        self.proc = None


if __name__ == "__main__":
    def start_commands():
        # For linux use "-c 5". For windows use "-n 5"
        commands = ["echo hi",
                    "ping 1.1.1.1 -n 3",
                    "ping 5.5.5.5 -n 3"]
        tkinter_popen.run_commands(commands)

    root = tk.Tk()

    tkinter_popen = TkinterPopen(root)
    tkinter_popen.pack()

    button = tk.Button(root, text="Run", command=start_commands)
    button.pack()

    root.mainloop()
TheLizzard
  • 7,248
  • 2
  • 11
  • 31
  • it's run forever @TheLizzard Can you fix ? – Alex Jul 19 '21 at 14:52
  • @Alex For me it doesn't it runs for about 5 sec and then it stops. What do you see? Did you change the command? – TheLizzard Jul 19 '21 at 14:54
  • The cursor doesn't change after hour... Do you have any idea ? I run it from linux @TheLizzard – Alex Jul 19 '21 at 16:38
  • @Alex Found the problem. On windows it's `"ping 8.8.8.8 -n 5"` but on linux it's `"ping 8.8.8.8 -c 5"`. That small change fixed the problem. – TheLizzard Jul 19 '21 at 16:49
  • If I have 10 processes, how can you recommend to do ? Can you give me an example with 2-3 process ?@TheLizzard – Alex Jul 20 '21 at 08:37
  • @Alex I updated my answer. I just used a queue named `commands`. When `start_next_proc` is called it resets the queue with 3 commands. Each time `start_next_proc` is called, it takes the first item out of the `commands` list and starts a new process executing that command. – TheLizzard Jul 20 '21 at 09:03
  • But if I need different processes ( different popen) then how can I do it ? @TheLizzard – Alex Jul 20 '21 at 09:51
  • @Alex Not sure what you mean. Right now it's using different `Popen`s for each command that it needs to run. – TheLizzard Jul 20 '21 at 09:53
  • I meant: different commands with different outputs. I need to do different calculation, How can I know if it's the same prop ? @TheLizzard – Alex Jul 20 '21 at 10:09
  • If you want different output boxes, just copy paste the code multiple times and just change the variable names – TheLizzard Jul 20 '21 at 10:11
  • I want only one text box. Can I use popen list instead of command list ? if yes, how ? @TheLizzard – Alex Jul 20 '21 at 10:14
  • Do you want to use something like `["ping", "8.8.8.8", "-c 5"]` instead of `"ping 8.8.8.8 -c 5"`? – TheLizzard Jul 20 '21 at 10:19
  • No. I meant: different commands. not the same. different commands -> different output ->Different behavior -> different conditions-> different prints. understand ? @TheLizzard – Alex Jul 20 '21 at 10:42
  • You want different commands but the same text box? Just change `commands` to the different commands that you want to run. For example: `commands = ['echo "hi"', "ping 8.8.8.8 -c 3", ...]`. If this isn't what you are looking for, leave the question open so someone else can answer it. – TheLizzard Jul 20 '21 at 10:48