174

I've seen several of answers about using Handle or Process Monitor, but I would like to be able to find out in my own code (C#) which process is locking a file.

I have a nasty feeling that I'm going to have to spelunk around in the win32 API, but if anyone has already done this and can put me on the right track, I'd really appreciate the help.

Update

Links to similar questions

Community
  • 1
  • 1
AJ.
  • 12,991
  • 18
  • 49
  • 62

6 Answers6

159

Long ago it was impossible to reliably get the list of processes locking a file because Windows simply did not track that information. To support the Restart Manager API, that information is now tracked.

I put together code that takes the path of a file and returns a List<Process> of all processes that are locking that file.

using System.Runtime.InteropServices;
using System.Diagnostics;
using System;
using System.Collections.Generic;

static public class FileUtil
{
    [StructLayout(LayoutKind.Sequential)]
    struct RM_UNIQUE_PROCESS
    {
        public int dwProcessId;
        public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
    }

    const int RmRebootReasonNone = 0;
    const int CCH_RM_MAX_APP_NAME = 255;
    const int CCH_RM_MAX_SVC_NAME = 63;

    enum RM_APP_TYPE
    {
        RmUnknownApp = 0,
        RmMainWindow = 1,
        RmOtherWindow = 2,
        RmService = 3,
        RmExplorer = 4,
        RmConsole = 5,
        RmCritical = 1000
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    struct RM_PROCESS_INFO
    {
        public RM_UNIQUE_PROCESS Process;

        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)]
        public string strAppName;

        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)]
        public string strServiceShortName;

        public RM_APP_TYPE ApplicationType;
        public uint AppStatus;
        public uint TSSessionId;
        [MarshalAs(UnmanagedType.Bool)]
        public bool bRestartable;
    }

    [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
    static extern int RmRegisterResources(uint pSessionHandle,
                                          UInt32 nFiles,
                                          string[] rgsFilenames,
                                          UInt32 nApplications,
                                          [In] RM_UNIQUE_PROCESS[] rgApplications,
                                          UInt32 nServices,
                                          string[] rgsServiceNames);

    [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto)]
    static extern int RmStartSession(out uint pSessionHandle, int dwSessionFlags, string strSessionKey);

    [DllImport("rstrtmgr.dll")]
    static extern int RmEndSession(uint pSessionHandle);

    [DllImport("rstrtmgr.dll")]
    static extern int RmGetList(uint dwSessionHandle,
                                out uint pnProcInfoNeeded,
                                ref uint pnProcInfo,
                                [In, Out] RM_PROCESS_INFO[] rgAffectedApps,
                                ref uint lpdwRebootReasons);

    /// <summary>
    /// Find out what process(es) have a lock on the specified file.
    /// </summary>
    /// <param name="path">Path of the file.</param>
    /// <returns>Processes locking the file</returns>
    /// <remarks>See also:
    /// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373661(v=vs.85).aspx
    /// http://wyupdate.googlecode.com/svn-history/r401/trunk/frmFilesInUse.cs (no copyright in code at time of viewing)
    /// 
    /// </remarks>
    static public List<Process> WhoIsLocking(string path)
    {
        uint handle;
        string key = Guid.NewGuid().ToString();
        List<Process> processes = new List<Process>();

        int res = RmStartSession(out handle, 0, key);
        if (res != 0) throw new Exception("Could not begin restart session.  Unable to determine file locker.");

        try
        {
            const int ERROR_MORE_DATA = 234;
            uint pnProcInfoNeeded = 0,
                 pnProcInfo = 0,
                 lpdwRebootReasons = RmRebootReasonNone;

            string[] resources = new string[] { path }; // Just checking on one resource.

            res = RmRegisterResources(handle, (uint)resources.Length, resources, 0, null, 0, null);

            if (res != 0) throw new Exception("Could not register resource.");                                    

            //Note: there's a race condition here -- the first call to RmGetList() returns
            //      the total number of process. However, when we call RmGetList() again to get
            //      the actual processes this number may have increased.
            res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, null, ref lpdwRebootReasons);

            if (res == ERROR_MORE_DATA)
            {
                // Create an array to store the process results
                RM_PROCESS_INFO[] processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded];
                pnProcInfo = pnProcInfoNeeded;

                // Get the list
                res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons);
                if (res == 0)
                {
                    processes = new List<Process>((int)pnProcInfo);

                    // Enumerate all of the results and add them to the 
                    // list to be returned
                    for (int i = 0; i < pnProcInfo; i++)
                    {
                        try
                        {
                            processes.Add(Process.GetProcessById(processInfo[i].Process.dwProcessId));
                        }
                        // catch the error -- in case the process is no longer running
                        catch (ArgumentException) { }
                    }
                }
                else throw new Exception("Could not list processes locking resource.");                    
            }
            else if (res != 0) throw new Exception("Could not list processes locking resource. Failed to get size of result.");                    
        }
        finally
        {
            RmEndSession(handle);
        }

        return processes;
    }
}

Using from Limited Permission (e.g. IIS)

This call accesses the registry. If the process does not have permission to do so, you will get ERROR_WRITE_FAULT, meaning An operation was unable to read or write to the registry. You could selectively grant permission to your restricted account to the necessary part of the registry. It is more secure though to have your limited access process set a flag (e.g. in the database or the file system, or by using an interprocess communication mechanism such as queue or named pipe) and have a second process call the Restart Manager API.

Granting other-than-minimal permissions to the IIS user is a security risk.

Eric J.
  • 143,945
  • 62
  • 324
  • 540
  • Has anyone tried that, it looks that it could really work (for windows above Vista and srv 2008) – Daniel Mošmondor Dec 18 '13 at 10:11
  • 1
    Is there a way to make this work for remote processes? – Carlos Muñoz Jan 24 '14 at 23:00
  • Confirmed here as well. Thanks much for posting this! +1 – ninehundreds Jun 25 '14 at 19:28
  • Is there a way to do this in WinXP? – Blagoh Aug 13 '14 at 17:53
  • 1
    @Blagoh: I do not believe the Restart Manager is available on Windows XP. You would need to resort to one of the other, less accurate methods posted here. – Eric J. Aug 13 '14 at 18:16
  • You're right its not availalbe in xp. Thanks for prompt reply. Is there any you would recommend. I like/trust your experience. Do you know about using "Runtime dynamic linking" to identify the process? Could you comment on its accuracy please. – Blagoh Aug 13 '14 at 18:51
  • 5
    @Blagoh: If you just want to know who is locking a specific DLL, you can use `tasklist /m YourDllName.dll` and parse the output. See http://stackoverflow.com/questions/152506/what-does-this-do-tasklist-m-mscor – Eric J. Aug 13 '14 at 18:58
  • Thanks for that. However it's a not a dll, its a text file. :( – Blagoh Aug 13 '14 at 19:02
  • 23
    Only solution that doesn't require 3rd party tools or undocumented API calls. Should well be the accepted answer. – IInspectable Mar 23 '15 at 15:53
  • @MichaelRayLovett: I have not seen anything about Restart Manager being depreciated. Perhaps post a simple demonstration case as a new question on Stack Overflow. Reference this question and ask what changes need to be made to get your code to work on that .NET version and Windows version. – Eric J. Mar 28 '16 at 17:30
  • 4
    I've tried this out (and it works) on Windows 2008R2, Windows 2012R2, Windows 7 and Windows 10. I found that it had to be run with elevated privileges in a lot of circumstances otherwise it fails when trying to get the list of processes locking a file. – Jay Apr 08 '16 at 12:33
  • This is fantastic. It's working on Windows 7. Thank you so much for this code. – JohnC1 Apr 26 '16 at 05:52
  • This solution works on Windows 10 however keep in mind if the program accessing said file is not putting a lock on it it'll return false. i.e: you can't tell if a .txt file is being accessed. I'm looking for a more consistent solution to this in this question: http://stackoverflow.com/questions/37276962/monitor-a-folder-and-find-if-a-file-is-open-in-windows-app – Tequilalime May 17 '16 at 15:55
  • @MichalHainc: ERROR_WRITE_FAULT means `An operation was unable to read or write to the registry.` https://msdn.microsoft.com/en-us/library/windows/desktop/aa373661(v=vs.85).aspx You *could* selectively grant permission to your IIS user to the necessary part of the registry. More secure though to have your IIS process set a flag (e.g. in the database or the file system, or by using an interprocess communication mechanism such as queue or named pipe) and have a second process call the Restart Manager API. Granting other-than-minimal permissions to the IIS user is a security risk. – Eric J. Jun 20 '16 at 17:43
  • It works on Win10 (x64) but throws exception "System.ComponentModel.Win32Exception: A 32 bit processes cannot access modules of a 64 bit process." on Win2008R2 (x64) – Alexander Selishchev Aug 16 '16 at 08:03
  • I don't understand why `RmGetList()` needs to return `ERROR_MORE_DATA` in order for it to work. It's returning 5 for me and throwing an error. – Kyle Delaney Sep 06 '17 at 21:49
  • Can't get it to work with web browsers. I need to detect if a file is open using a web browser. Is it possible ? – Anis Tissaoui Oct 16 '18 at 16:58
  • @AnisTissaoui If you mean a file on the client machine where the browser is running, not without some sort of plugin that has native access. It would be a huge security flaw if browsers gave direct file system access like that. – Eric J. Oct 17 '18 at 16:47
  • @EricJ. When it didn't work i was like "What kind of special access browsers have to the file system ?". I need to study browsers a bit more i assume. I would appreciate sharing any resources on how browsers access client machine file system :) . – Anis Tissaoui Oct 18 '18 at 11:24
  • 1
    Thank you so much. This really works for my problem. – Lake_Lagunita May 26 '21 at 09:56
  • This doesn't work for me. It shows nothing is locking the file but the I see the file being locked with lockhunter. – Gaspa79 Dec 02 '21 at 17:45
  • Does this work for 64bit? RmGetList always returns 0 for me. – Chizl Mar 20 '22 at 03:22
62

It is very complex to invoke Win32 from C#.

You should use the tool Handle.exe.

After that your C# code have to be the following:

string fileName = @"c:\aaa.doc";//Path to locked file

Process tool = new Process();
tool.StartInfo.FileName = "handle.exe";
tool.StartInfo.Arguments = fileName+" /accepteula";
tool.StartInfo.UseShellExecute = false;
tool.StartInfo.RedirectStandardOutput = true;
tool.Start();           
tool.WaitForExit();
string outputTool = tool.StandardOutput.ReadToEnd();

string matchPattern = @"(?<=\s+pid:\s+)\b(\d+)\b(?=\s+)";
foreach(Match match in Regex.Matches(outputTool, matchPattern))
{
    Process.GetProcessById(int.Parse(match.Value)).Kill();
}
Uwe Keim
  • 38,279
  • 56
  • 171
  • 280
  • 1
    nice example, but to my knowledge, handle.exe now shows a nasty prompt to accept some conditions when you run it on a client machine for the first time, which in my opinion, disqualifies it – Arsen Zahray Jun 20 '12 at 18:01
  • 14
    @Arsen Zahray: You can accept the eula automatically by passing in a command line option of `/accepteula`. I've updated Gennady's answer with the change. – Jon Cage Oct 18 '12 at 15:08
  • What version of Handle.exe did you use? The newest one V4 seems to be changed in a Broken way. /accepteula and Filename is not longer supported – Venson Sep 24 '14 at 13:41
  • @JPVenson: I just tried the latest version available for download (v4.0 Published: September 11, 2014) and /accepteula and filename both work fine. – Mr. Bungle Oct 10 '14 at 02:04
  • 3
    You can't redistribute `handle.exe` – Basic Mar 05 '15 at 03:14
  • You can download it on the fly, tough. Also, you can write the `EulaAccepted` value inside `HKCU\Software\Sysinternals\` in the windows registry prior to running, thus avoiding an unnecessary program run. – beppe9000 Aug 24 '15 at 19:13
  • 4
    I disagree - it has no complexity when invoking win32 api from c#. – Idan Oct 13 '15 at 16:53
  • add this line: fileName = "\"" + fileName + "\""; this will allow spaces in the dir, – DarkPh03n1X Jan 17 '17 at 08:53
  • add this line: tool.StartInfo.CreateNoWindow = true; this will hide the black box – DarkPh03n1X Jan 17 '17 at 08:53
  • 1
    This is really slow for me. Did anyone find a faster way of doing this? Especially for multiple files? – DeveloperExceptionError Mar 03 '20 at 15:29
37

One of the good things about handle.exe is that you can run it as a subprocess and parse the output.

We do this in our deployment script - works like a charm.

orip
  • 69,626
  • 21
  • 116
  • 145
8

I had issues with stefan's solution. Below is a modified version which seems to work well.

using System;
using System.Collections;
using System.Diagnostics;
using System.Management;
using System.IO;

static class Module1
{
    static internal ArrayList myProcessArray = new ArrayList();
    private static Process myProcess;

    public static void Main()
    {
        string strFile = "c:\\windows\\system32\\msi.dll";
        ArrayList a = getFileProcesses(strFile);
        foreach (Process p in a)
        {
            Debug.Print(p.ProcessName);
        }
    }

    private static ArrayList getFileProcesses(string strFile)
    {
        myProcessArray.Clear();
        Process[] processes = Process.GetProcesses();
        int i = 0;
        for (i = 0; i <= processes.GetUpperBound(0) - 1; i++)
        {
            myProcess = processes[i];
            //if (!myProcess.HasExited) //This will cause an "Access is denied" error
            if (myProcess.Threads.Count > 0)
            {
                try
                {
                    ProcessModuleCollection modules = myProcess.Modules;
                    int j = 0;
                    for (j = 0; j <= modules.Count - 1; j++)
                    {
                        if ((modules[j].FileName.ToLower().CompareTo(strFile.ToLower()) == 0))
                        {
                            myProcessArray.Add(myProcess);
                            break;
                            // TODO: might not be correct. Was : Exit For
                        }
                    }
                }
                catch (Exception exception)
                {
                    //MsgBox(("Error : " & exception.Message)) 
                }
            }
        }

        return myProcessArray;
    }
}

UPDATE

If you just want to know which process(es) are locking a particular DLL, you can execute and parse the output of tasklist /m YourDllName.dll. Works on Windows XP and later. See

What does this do? tasklist /m "mscor*"

Community
  • 1
  • 1
user137604
  • 113
  • 1
  • 2
  • I so very much fail to see why `myProcessArray` is a class member (but also actually returned from getFileProcesses()? Same goes for `myProcess`. – Oskar Berggren Nov 20 '17 at 08:31
6

This works for DLLs locked by other processes. This routine will not find out for example that a text file is locked by a word process.

C#:

using System.Management; 
using System.IO;   

static class Module1 
{ 
static internal ArrayList myProcessArray = new ArrayList(); 
private static Process myProcess; 

public static void Main() 
{ 

    string strFile = "c:\\windows\\system32\\msi.dll"; 
    ArrayList a = getFileProcesses(strFile); 
    foreach (Process p in a) { 
        Debug.Print(p.ProcessName); 
    } 
} 


private static ArrayList getFileProcesses(string strFile) 
{ 
    myProcessArray.Clear(); 
    Process[] processes = Process.GetProcesses; 
    int i = 0; 
    for (i = 0; i <= processes.GetUpperBound(0) - 1; i++) { 
        myProcess = processes(i); 
        if (!myProcess.HasExited) { 
            try { 
                ProcessModuleCollection modules = myProcess.Modules; 
                int j = 0; 
                for (j = 0; j <= modules.Count - 1; j++) { 
                    if ((modules.Item(j).FileName.ToLower.CompareTo(strFile.ToLower) == 0)) { 
                        myProcessArray.Add(myProcess); 
                        break; // TODO: might not be correct. Was : Exit For 
                    } 
                } 
            } 
            catch (Exception exception) { 
            } 
            //MsgBox(("Error : " & exception.Message)) 
        } 
    } 
    return myProcessArray; 
} 
} 

VB.Net:

Imports System.Management
Imports System.IO

Module Module1
Friend myProcessArray As New ArrayList
Private myProcess As Process

Sub Main()

    Dim strFile As String = "c:\windows\system32\msi.dll"
    Dim a As ArrayList = getFileProcesses(strFile)
    For Each p As Process In a
        Debug.Print(p.ProcessName)
    Next
End Sub


Private Function getFileProcesses(ByVal strFile As String) As ArrayList
    myProcessArray.Clear()
    Dim processes As Process() = Process.GetProcesses
    Dim i As Integer
    For i = 0 To processes.GetUpperBound(0) - 1
        myProcess = processes(i)
        If Not myProcess.HasExited Then
            Try
                Dim modules As ProcessModuleCollection = myProcess.Modules
                Dim j As Integer
                For j = 0 To modules.Count - 1
                    If (modules.Item(j).FileName.ToLower.CompareTo(strFile.ToLower) = 0) Then
                        myProcessArray.Add(myProcess)
                        Exit For
                    End If
                Next j
            Catch exception As Exception
                'MsgBox(("Error : " & exception.Message))
            End Try
        End If
    Next i
    Return myProcessArray
End Function
End Module
phuclv
  • 32,499
  • 12
  • 130
  • 417
Stefan
  • 11,274
  • 7
  • 49
  • 75
-1

simpler with linq:

public void KillProcessesAssociatedToFile(string file)
    {
        GetProcessesAssociatedToFile(file).ForEach(x =>
        {
            x.Kill();
            x.WaitForExit(10000);
        });
    }

    public List<Process> GetProcessesAssociatedToFile(string file)
    {
        return Process.GetProcesses()
            .Where(x => !x.HasExited
                && x.Modules.Cast<ProcessModule>().ToList()
                    .Exists(y => y.FileName.ToLowerInvariant() == file.ToLowerInvariant())
                ).ToList();
    }
Aalok
  • 533
  • 8
  • 19