Search code examples
pythonstdinpopen

How to properly use subprocess.Popen thread's in python?


I am writing a python script which will execute some another script from bash terminals. I need two bash terminal and execute some script in that bash terminal one by one. After executing the script, I will read the out put messages to a Tkinter text area. I have designed simple python script which will create 2 process. I am using thread to monitor the stdout and stderror for each processes. I am using while loop to hold the process.

Here is the my script

import os
import tkinter as tk
from tkinter import *
import tkinter.scrolledtext as tkst
import subprocess
from subprocess import Popen
import threading

class TKValidator:
    def __init__(self): 
        #create TK
        self.ws = Tk()
        self.ws.state('zoomed')
        
        self.flag = IntVar()
        
        #process for bash 1
        self.process1 = subprocess.Popen(["bash"], stderr=subprocess.PIPE,shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
        #process for bash 2
        self.process2 = subprocess.Popen(["bash"], stderr=subprocess.PIPE,shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
        exit = False
        
        #start threads (stdout and stderror monitor)
        threading.Thread(target=self.read_stdout_1).start()
        threading.Thread(target=self.read_stderror_1).start()
        threading.Thread(target=self.read_stdout_2).start()
        threading.Thread(target=self.read_stderror_2).start()
        
        self.frame2 = tk.Frame(self.ws, bg="#777474")
        self.frame2.grid(column=0, row=0, padx=(100,100), pady=(10, 10), sticky="W")
        
        #Start button
        self.btnVerify = Button(self.ws, text='Start', command=self.ButtonClick)
        self.btnVerify.grid(row=5, columnspan=3, pady=10)
        
        #output text area
        self.logArea = tkst.ScrolledText(self.ws, wrap= tk.WORD, width=80, height=20, name="logArea")
        self.logArea.grid(padx=(100,100), pady=(10, 10) ,row=10, sticky="W")


    def read_stdout_1(self):
        while not exit:
            msg = self.process1.stdout.readline()
            self.logArea.insert(INSERT, "process_1 logging start")
            self.logArea.insert(INSERT, msg.decode())
            self.logArea.insert(END, "\n logging end")
            
    def read_stderror_1(self):
        while not exit:
            msg = self.process1.stderr.readline()
            self.logArea.insert(INSERT, "Error")
            self.logArea.insert(INSERT, msg.decode())
            self.logArea.insert(END, "\n Error end")


    def read_stdout_2(self):
        while not exit:
            msg = self.process2.stdout.readline()
            self.logArea.insert(INSERT, "process_2 logging start")
            self.logArea.insert(INSERT, msg.decode())
            self.logArea.insert(END, "\logging end")

            
    def read_stderror_2(self):
        while not exit:
            msg = self.process2.stderr.readline()
            self.logArea.insert(INSERT, "Error")
            self.logArea.insert(INSERT, msg.decode())
            self.logArea.insert(END, "\n Error end")


    def ButtonClick(self):
        print("Button clicked")
        
        #set flag to 1
        self.flag = 1
        
        while not exit:
            if self.flag == 1:
                print("Case 1")
                self.process1.stdin.write(r'python C:\path\test1.py'.encode())
                self.process1.stdin.flush()
                self.flag = 2
                
            elif self.flag == 2:
                print("Case 2")
                self.process1.stdin.write(r'python C:\path\test2.py'.encode())
                self.process1.stdin.flush()
                self.flag = 3
            
            elif self.flag == 3:
                print("Case 3")
                self.process2.stdin.write(r'python C:\path\test3.py'.encode())
                self.process2.stdin.flush()
                self.flag = 4
           
            elif self.flag == 4:
                print("Case 3")
                self.process2.stdin.write(r'python C:\path\test4.py'.encode())
                self.process2.stdin.flush()
                self.flag = 5
                
            else:
                break


    def run(self):
        self.ws.title('Test Script')
        self.ws.mainloop() 
        
validator = TKValidator()
validator.run() 

But after exectuing this script, I can see only the output like

Button clicked

I want to run the while loop one by one but it is not working. Can you please suggest, what is the problem here and how can I fix this to work as I expected ?


Solution

  • The short answer here is this line:

    while not exit:
    

    The value of exit here is always True. Try printing exit to find out what it actually is:

    print(exit, bool(exit))
    while not exit:
    

    Incidentally exit was a bad variable name for this reason.

    The longer answer is: why are you doing things this way? As far as I can tell (I admit I only skimmed the code and the control flow manages to be sufficiently convoluted that I'm not sure I followed it) your requirements are:

    • when a button is clicked, two scripts should be launched
    • when they are finished (or whilst running?), their outputs should be printed to the textarea

    There is no reason to mess about with starting shells, feeding to their stdin, polling their stdout, etc, for any of this. Just launch a process, join it and drop the output into the textarea or poll it whist running and output to the textarea. If you want to output from two subprocesses at once you need deconfliction logic: probably a queue of some description.

    If you need to interact with the scripts I strongly recommend you use pexpect.