0

I have a need from a Tkinter GUI to be able to start a long-running Linux script but at the same time I want to be able to have a stop-button enable so I can stop the process. Neither Tkinter nor popen are threadsafe. I thought of simply either placing the popen function in a thread or possibly just enabling a button in a thread. I am currently using Red Hat Linux 5.9 which uses Python 2.4.3, but I have later versions available online that I could use. In the program, below note that I reconfigure the start button to a stop button, but that does not work because the start button function is active, but it indicates my intent. Note that the stop function simply does an os.kill() on the child.

#!/usr/bin/python
import subprocess
import sys
import Tkinter
import tkMessageBox
import signal
import os
import time

class popentest:

    def __init__(self):
        self.mainWindow = Tkinter.Tk()

    def __createUI(self, mainWindow):
        mainWindow.protocol("WM_DELETE_WINDOW", self.OnExitWindow)
        ## Local variables. 
        sleep=5
        iter=5
        self.pid=0
        self.mainWindow.title("Test popen")
        self.start=Tkinter.Button(mainWindow, text=u"Start", command=self.onStart)
        self.start.grid()
        self.kwit = Tkinter.Button(mainWindow,text=u"Quit !",
                                command=self.onQuit)
        self.kwit.grid()
        self.lIter=Tkinter.Label(mainWindow, text="Iterations: ")
        self.iterE=Tkinter.Entry(mainWindow, width=2)
        self.lSleep = Tkinter.Label(mainWindow, text="Sleep time")
        self.sleepEntry = Tkinter.Entry(mainWindow, width=3)
        self.lIter.grid()
        self.iterE.grid()
        self.lSleep.grid()
        self.sleepEntry.grid()
        self.iterE.insert(0, str(iter))
        self.sleepEntry.insert(0,str(sleep))

    def startPopen(self):
        self.__createUI(self.mainWindow)
        self.mainWindow.mainloop()

    def execute(self, numIters, sleep):
        self.p = subprocess.Popen(['./testpopen.sh',str(numIters), str(sleep)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
        self.pid=self.p.pid
        print str(self.p.pid)+" started"
        for line in iter(self.p.stdout.readline, ''):
            print line
        self.p.stdout.close()
        self.pid=0
        self.start.configure(text=u"Start", command=self.onStart)

    def onStart(self):
        numIters=self.iterE.get()
        sleep=self.sleepEntry.get()
        if not numIters.isdigit():
            tkMessageBox.showerror(
            "invalid entry",
            "Iteration (%s)must be numeric\n" % numIters)
            return
        elif not sleep.isdigit():
            tkMessageBox.showerror(
            "invalid entry",
            "Sleep(%s) is not numeric\n" % sleep)
            return
        numIters=int(numIters)
        sleep=int(sleep)
        if numIters <= 0 or sleep <=0 :
            tkMessageBox.showerror(
            "invalid entry",
            "Either iteration (%d) or sleep(%d) is <= 0\n" % (numIters, sleep))
        else:
            print "configuring start to stop"
            self.start.configure(text=u"Stop", command=self.onStop)
            time.sleep(1)
            self.execute(numIters, sleep)

    def onStop(self):
        print "configuring stop to start"

        os.kill(p.pid, signal.SIGTERM)
        self.start.configure(text=u"Start", command=self.onStart)

    def OnExitWindow(self):
        if self.pid != 0 :
            os.kill(self.pid, signal.SIGKILL)
        self.mainWindow.destroy()

    def onQuit(self):
        if self.pid != 0 :
            os.kill(self.pid, signal.SIGKILL)
        self.mainWindow.destroy()

if __name__ == "__main__":  
    remote = popentest()
    remote.startPopen()    
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
Jerry
  • 55
  • 10
  • to avoid blocking the GUI; you could [read subprocess' output in a different thread](https://gist.github.com/zed/42324397516310c86288) – jfs Feb 19 '14 at 21:15

1 Answers1

1

You can start your process using Popen, using a non-blocking pipe to communicate with the process - this way, you can receive its output asynchronously. I already used an enhanced version of Popen, code was from an ActiveState Python cookbook recipe. I could not find the recipe on the web anymore, but as I still have the code I pasted it here:

https://gist.github.com/mguijarr/6874724

Then, in your Tkinter code, you can use a timer to check periodically for the state of the process (terminated, or not) and to get output:

self.p = EnhancedPopen.Popen(['./testpopen.sh',str(numIters), str(sleep)],
                             stdin=subprocess.PIPE,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             shell=True,universal_newlines=True)
self.mainWindow.after(100, self.check_process)

def check_process(self):
  # get stdout output
  output = EnhancedPopen.recv_some(self.p, e=0, stderr=0)
  ...
  if self.p.poll() is not None:
    # process terminated
    ...
    return
  # set timer again (until process exits)
  self.mainWindow.after(100, self.check_process_output)
mguijarr
  • 7,641
  • 6
  • 45
  • 72
  • Thanks. I'll see if I can try it. I am limited by corporate regulations not to install unapproved, packages, but this should be ok. The issue is that I need a button active so the user can kill the process. This may suffice. I am posting another solution below also. – Jerry Oct 08 '13 at 12:13
  • I incorporated EnhancedPopen and the above code into my test program, and it works perfectly. I should be able to incorporate this in the 2 production programs with no problem. Again, my thanks for posting this. – Jerry Oct 08 '13 at 14:27
  • Great :) I am happy for you. – mguijarr Oct 08 '13 at 15:30