skip to Main Content

I am using the following function to allow the OS to open a third party application associated with the filetype in question. For example: If variable ‘fileToOpen’ links to a file (it’s full path of course) called flower.psd, this function would open up Photoshop in Windows and Gimp in Linux (typically).

def launchFile(fileToOpen):
    if platform.system() == 'Darwin':       # macOS
        subprocess.call(('open', fileToOpen))
    elif platform.system() == 'Windows':    # Windows
        os.startfile(fileToOpen)
    else:                                   # linux variants
        subprocess.call(('xdg-open', fileToOpen))

While it is running, I want to have the same python script monitor the use of that file and delete it once the third party app is done using it (meaning…the 3rd party app closed the psd file or the third party app itself closed and released the file from use).

I’ve tried using psutil and pywin32 but neither seem to work in Windows 10 with Python3.9. Does anyone have any success with this? If so, how did you go about getting the process of the third party app while not getting a permission error from Windows?

Ideally, I would like to get a solution that works across Windows, Macs, and Linux but I’ll take any help with Windows 10 for now since Mac and Linux can be found easier with commandline assistance with the ps -ax | grep %filename% command

Keep in mind, this would ideally track any file. TIA for your help.

Update by request:
I tried adding this code to mine (from a previous suggestion). Even this alone in a python test.py file spits out permission errors:

import psutil


for proc in psutil.process_iter():
    try:
        # this returns the list of opened files by the current process
        flist = proc.open_files()
        if flist:
            print(proc.pid,proc.name)
            for nt in flist:
                print("t",nt.path)

    # This catches a race condition where a process ends
    # before we can examine its files    
    except psutil.NoSuchProcess as err:
        print("****",err) 

The follow code below does not spit out an error but does not detect a file in use:

import psutil
from pathlib import Path

def has_handle(fpath):
    for proc in psutil.process_iter():
        try:
            for item in proc.open_files():
                if fpath == item.path:
                    return True
        except Exception:
            pass

    return False

thePath = Path("C:\Users\someUser\Downloads\Book1.xlsx")    
fileExists = has_handle(thePath)

if fileExists :
    print("This file is in use!")
else :
    print("This file is not in use")

2

Answers


  1. Chosen as BEST ANSWER

    Found it! The original recommendation from another post forgot one function..."Path" The item.path from the process list is returned as a string. This needs to convert to a Path object for comparison of your own path object.

    Therefore this line:

    if fpath == item.path:
    

    Should be:

    if fpath == Path(item.path):
    

    and here is the full code:

    import psutil
    from pathlib import Path
    
    def has_handle(fpath):
        for proc in psutil.process_iter():
            try:
                for item in proc.open_files():
                    print (item.path)
                    if fpath == Path(item.path):
                        return True
            except Exception:
                pass
    
        return False
    
    thePath = Path("C:\Users\someUser\Downloads\Book1.xlsx")    
    fileExists = has_handle(thePath)
    
    if fileExists :
        print("This file is in use!")
    else :
        print("This file is not in use")
    

    Note: The reason to use Path objects rather than a string is to stay OS independant.


  2. Based on @Frankie ‘s answer I put together this script. The script above took 16.1 seconds per file as proc.open_files() is quite slow.

    The script below checks all files in a directory and returns the pid related to each open file. 17 files only took 2.9s to check. This is due to only calling proc.open_files() if the files default app is open in memory.

    As this is used to check if a folder can be moved, the pid can be later used to force close the locking application but BEWARE that that application could have other documents open and all data would be lost.

    This does not detect open txt files or may not detect files that dont have a default application

    from pathlib import Path
    import psutil
    import os
    import shlex
    import winreg
    from pprint import pprint as pp
    from collections import defaultdict
    
    
    class CheckFiles():
        def check_locked_files(self, path: str):
            '''Check all files recursivly in a directory and return a dict with the
               locked files associated with each pid (proocess id) 
    
            Args:
                path (str): root directory
    
            Returns:
                dict: dict(pid:[filenames])
            '''
            fnames = []
            apps = set()
            for root, _, f_names in os.walk(path):
                for f in f_names:
                    f = Path(os.path.join(root, f))
                    if self.is_file_in_use(f):
                        default_app = Path(self.get_default_windows_app(f.suffix)).name
                        apps.add(default_app)
                        fnames.append(str(f))
            if apps:
                return self.find_process(fnames, apps)
    
        def find_process(self, fnames: list[str], apps: set[str]):
            '''find processes for each locked files
    
            Args:
                fnames (list[str]): list of filepaths
                apps (set[str]): set of default apps
    
            Returns:
                dict: dict(pid:[filenames])
            '''
            open_files = defaultdict(list)
            for p in psutil.process_iter(['name']):
                name = p.info['name']
                if name in apps:
                    try:
                        [open_files[p.pid].append(x.path) for x in p.open_files() if x.path in fnames]
                    except:
                        continue
            return dict(open_files)
            
    
        def is_file_in_use(self, file_path: str):
            '''Check if file is in use by trying to rename it to its own name (nothing changes) but if
               locked then this will fail
    
            Args:
                file_path (str): _description_
    
            Returns:
                bool: True is file is locked by a process
            '''
            path = Path(file_path)
            
            if not path.exists():
                raise FileNotFoundError
            try:
                path.rename(path)
            except PermissionError:
                return True
            else:
                return False
        
        def get_default_windows_app(self, suffix: str):
            '''Find the default app dedicated to a file extension (suffix)
    
            Args:
                suffix (str): ie ".jpg"
    
            Returns:
                None|str: default app exe
            '''
            try:
                class_root = winreg.QueryValue(winreg.HKEY_CLASSES_ROOT, suffix)
                with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r'{}shellopencommand'.format(class_root)) as key:
                    command = winreg.QueryValueEx(key, '')[0]
                    return shlex.split(command)[0]
            except:
                return None
            
    
    old_dir = r"C:path_to_dir"
    
    c = CheckFiles()
    r = c.check_locked_files(old_dir)
    pp(r)
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search