Question

Summary

I have wxPython GUI which allows the user to open files to view. Currently I do this with os.startfile(). However, I've come to learn that this is not the best method, so I'm looking to improve. The main drawback of startfile() is that I have no control over the file once it has been launched. This means that a user could leave a file open so it would be unavailable for another user.

What I'm Looking For

In my GUI, it is possible to have children windows. I keep track of all of these by storing the GUI objects in a list, then when the parent is closed, I just run through the list and close all the children. I would like to do the same with any file the user selects. How can I launch a file and retain a python object such that I can close it on command? Thanks in advance

My Dreams of a Solution

  • Launch the file in such a way that there is a Python object which I can pass between functions
  • Some way to launch a file in its default program and return the PID
  • A way to retrieve the PID with the file name

Progress So Far

Here's the frame I plan on using. The important bits are the run() and end() functions of the FileThread class as this is where the solution will go.

import wx
from wx.lib.scrolledpanel import ScrolledPanel 
import threading
import os

class GUI(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, -1, 'Hey, a GUI!', size=(300,300))
        self.panel = ScrolledPanel(parent=self, id=-1) 
        self.panel.SetupScrolling()  
        self.Bind(wx.EVT_CLOSE, self.OnClose)

        self.openFiles = []

        self.openBtn = wx.Button(self.panel, -1, "Open a File")
        self.pollBtn = wx.Button(self.panel, -1, "Poll")
        self.Bind(wx.EVT_BUTTON, self.OnOpen, self.openBtn)
        self.Bind(wx.EVT_BUTTON, self.OnPoll, self.pollBtn)

        vbox = wx.BoxSizer(wx.VERTICAL)

        vbox.Add((20,20), 1)
        vbox.Add(self.openBtn)
        vbox.Add((20,20), 1)
        vbox.Add(self.pollBtn)
        vbox.Add((20,20), 1)

        hbox = wx.BoxSizer(wx.HORIZONTAL)
        hbox.Add(vbox, flag=wx.TOP|wx.BOTTOM|wx.LEFT|wx.RIGHT|wx.EXPAND, border = 10)

        self.panel.SetSizer(hbox)
        self.panel.Layout()

    def OnOpen(self, event):
        fileName = "AFileIWantToOpenWithTheFullPath.txt"
        self.openFiles.append(FileThread(fileName))

    def OnPoll(self, event):
        self.openFiles[0].Poll()

    def OnClose(self, event):
        for file in self.openFiles:
            file.end()
            self.openFiles.remove(file)

        self.Destroy()

class FileThread(threading.Thread):
    def __init__(self, file):
        threading.Thread.__init__(self)
        self.file = file
        self.start()

    def run(self):
        doc = subprocess.Popen(["start", " /MAX", "/WAIT", self.file], shell=True)
        return doc

    def Poll(self):
        print "polling"
        print self.doc.poll()
        print self.doc.pid

    def end(self):
        try:
            print "killing file {}".format(self.file)
        except:
            print "file has already been killed"

def main():
    app = wx.PySimpleApp()
    gui = GUI()
    gui.Show()
    app.MainLoop()


if __name__ == "__main__": main()

Some Extra Notes

  • I'm not concerned with portability, this will only be run on a few controlled computers around the office
  • I don't think this matters, but I'm running the pythonw executable through a batch file

Update

I played around a bit with subprocess.Popen(), but ran into the same issue. I can make the Popen object using

doc = subprocess.Popen(["start", "Full\\Path\\to\\File.txt"], shell=True)

but when I poll() the object, it always returns 0. The docs say that A None value indicates that the process hasn’t terminated yet so the 0 means that my process has terminated. Thus, attempting to kill() it does nothing.

I suspect this is because the process completes when the start command finishes and the file is launched. I want something that will keep going even after the file is launched, can this be done with Popen()?

Was it helpful?

Solution

The problem lies within the fact, that the process being handled by the Popen class in your case is the start shell command process, which terminates just after it runs the application associated with given file type. The solution is to use the /WAIT flag for the start command, so the start process waits for its child to terminate. Then, using for example the psutil module you can achieve what you want with the following sequence:

>>> import psutil
>>> import subprocess
>>> doc = subprocess.Popen(["start", "/WAIT", "file.pdf"], shell=True)
>>> doc.poll()
>>> psutil.Process(doc.pid).get_children()[0].kill()
>>> doc.poll()
0
>>> 

After the third line Adobe Reader appears with the file opened. poll returns None as long as the window is open thanks to the /WAIT flag. After killing start's child Adobe Reader window disappears.

Other probably possible solution would be to find the application associated with given file type in the registry and run it without using start and shell=True.

I've tested this on 32 bit Python 2.7.5 on 64 bit Windows Vista, and 32 bit Python 2.7.2 on 64 bit Windows 7. Below is an example run on the latter. I've made some simple adjustments to your code - marked with a freehand red circles (!).

step1 step2 step3 step4

Also, possibly it is worth to consider this comment from the documentation:

The shell argument (which defaults to False) specifies whether to use the shell as the program to execute. If shell is True, it is recommended to pass args as a string rather than as a sequence.

and run the process as follows:

subprocess.Popen("start /WAIT " + self.file, shell=True)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top