powershellfile-lockingsysinternals

Determinig process having write locks on files using PowerShell


I need to write a PowerShell script to move a folder with large amount of files(around 1k to 10k) before attempting to move i want to check if there are any process having write locks in the file and should ask user confirmation to kill the process with write locks and move the files. I have already tries the solutions proposed in

Identify locks on dll files -- Doesn't work for folders

Determine process that locks a file -- Too complex and error prone

Unlock files using handle.exe -- Closes only handle and closes all handles.

One option would be to use Handle.exe from SysInternals and kill the process which has write locks. It's easy if we do it manually as by using process name we can identify which process has lock on the file and kill it. Is there any way to determine if it's a write lock based on handle type(highlighted in red in below image)? Is there any mapping for the handle type and it's permissions? so that i can parse the handle.exe output and extract process which have write locks.

Handle output


Solution

  • I couldn't find an only PowerShell way to get this task done. I have used the .net code from this Answer. One problem with this code is it's returning a list of process objects which was too much unnecessary info for me so I made some modifications to the function WhoisLocking to return Dictionary of process id and process name. Below is the modified code for the function

    static public Dictionary<int, String> WhoIsLocking(string path)
    {
        uint handle;
        string key = Guid.NewGuid().ToString();
        List<Process> processes = new List<Process>();
        Dictionary<int, String> processDict = new Dictionary<int, String>(); 
    
        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);
        }
        //Returning a dictionary to keep it simple
        foreach(Process p in processes){
            processDict.Add(p.Id, p.ProcessName);
        }
        return processDict;
    }
    

    Another problem with this code is it works for only files hence i have created a wrapper in PoweShell to call this function for each file in the folder. This class parses all handles for each call hence it's inefficient to call this method for each file. To optimize it i am calling this function only for files with write locks. I have also looked for some ways to add process owner to the information but unfortunately .net Process object dosen't contain owner information and i didn't find any easy way to do it in .net. To get the owner information i am using PowerShell Get-CimInstance Win32_Process and Invoke-CimMethod. Below is my PowerShell method.

    function Get-LockingProcess {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        # Specifies a path to the file to be modified
        [Parameter(Mandatory = $true)]
        $path ,
        [switch]$Raw
    )
    #adding using reflection to avoid file being locked
    $bytes = [System.IO.File]::ReadAllBytes($($PSScriptRoot + "\lib\LockedProcess.dll"))
    [System.Reflection.Assembly]::Load($bytes) | Out-Null
    $process = @{}
    $path = Get-Item -Path $path
    if ($path.PSIsContainer) {
        Get-ChildItem -Path $path -Recurse | % {
            $TPath = $_
            try { [IO.File]::OpenWrite($TPath.FullName).close()}
            catch {
                if (! $TPath.PSIsContainer) {
                    $resp = $([FileUtil]::WhoIsLocking($TPath.FullName))
                }
                foreach ($k in $resp.Keys) {
                    if (! $process.ContainsKey($k)) {
                        $process.$k = $resp.$k
                    }
                }
            }
        }
    }
    else {
        $process = $([FileUtil]::WhoIsLocking($path.FullName))
    }
    #adding additional details to the hash
    $processList=@()
    foreach($id in $process.Keys){
        $temp=@{}
        $temp.Name=$process.$id
        $proc=Get-CimInstance Win32_Process -Filter $("ProcessId="+$id)
        $proc=Invoke-CimMethod -InputObject $proc -MethodName GetOwner
        if($proc.Domain -ne ""){
            $temp.Owner=$proc.Domain+"\"+$proc.User
        }else{
            $temp.Owner=$proc.User
        }
        $temp.PID=$id
        $processList+=$temp
    }
    if($Raw){
        $processList
    }else{
        $processList.ForEach({[PSCustomObject]$_}) | Format-Table -AutoSize
    }
    

    By default this prints the result in a tabular form and if it needs to be used in a script caller could pass Raw which will return it as an array. This might not be the most efficient solution but gets the task done well. If you find something could be optimized please feel free to comment.