Summary: Guest blogger, Adam Driscoll, talks about using Windows PowerShell to determine which process is locking a file.
Microsoft Scripting Guy, Ed Wilson, is here. Today we have a guest blogger, Adam Driscoll...
It’s quite common to run into the issue where a process is locking a particular file. Attempting to delete or open the file can result in an error message when another process has exclusive access to the file. Unfortunately, Windows PowerShell nor the .NET Framework expose a simple way of determining which process is locking the file. Determine which process is locking the file requires quite a few steps to interrogate the system. This can be accomplished by using a set of Windows API calls and Platform Invoke (PInvoke) functionality. In this post, we’ll take a tour of what it takes to identify the process that is locking a file.
The first step is to understand how Windows manages files and other objects. When opening a file in Windows, the kernel provides a handle to the calling process to identify the opened file. A handle is a kernel-level identifier to an open file or other Windows object. There are handles for all types of Windows objects, including files, events, and mutexes.
To view the open handles on the system, we need to utilize the NtQuerySystemInformation function. This function is largely undocumented, and it requires some intimate knowledge to use correctly. The first step is to call several Win32 functions and structures that we will need for this operation. This can be accomplished by using the Add-Type cmdlet. The functions involved include:
NtQuerySystemInformation: Amongst other things, can query the handles open on the system.
NtQueryObject: Queries additional information about a handle.
OpenProcess: Allows us to get more information about the process that owns the handle.
DuplicateHandle: Creates a copy of a handle in the current process (for example, ISE) so we can perform additional operations on it.
QueryDosDevice: Converts a logical drive (such as drive C) into the DOS device identifier.
SystemHandleEntry: Describes the memory structure of handle information that is returned by NtQuerySystemInformation.
For more information about the signatures that are required to use these functions, refer to PInvoke.net.
Now we need to define the PInvoke functions. This has been eliminated in this document for brevity, but it is included in the script posted in the Script Center Repository: Find-LockingProcess.
The next step is to query the handles that are currently open on the system. The following snippet, which is part of a larger function, queries the open handles on the system. We pass 16 into the NtQuerySystemInformation function to signify that we want it to return open handles. We then pass in IntPtr, which will hold the information return by the system.
The IntPtr object is used to hold the address of a chunk of memory that has been provided or allocated to us. The first time we fall through the while loop, it returns the size of the data that we need and then we allocate it by using the AllocHGlobal function. This returns a segment of memory large enough to hold the data.
The IntPtr object points to the beginning of that segment of memory. The next time we cycle through the loop, we have enough memory available to store all the handle data and NtQuerySystemInformation fills our memory segment with that data.
while ($true)
{
$ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($length)
$wantedLength = 0
$SystemHandleInformation = 16
$result = [NtDll]::NtQuerySystemInformation($SystemHandleInformation, $ptr, $length, [ref] $wantedLength)
if ($result -eq [NT_STATUS]::STATUS_INFO_LENGTH_MISMATCH)
{
$length = [Math]::Max($length, $wantedLength)
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)
$ptr = [IntPtr]::Zero
}
elseif ($result -eq [NT_STATUS]::STATUS_SUCCESS)
{
break
}
else
{
throw (New-Object System.ComponentModel.Win32Exception)
}
}
From here, we can convert the handle information into a .NET structure that can be used easily in Windows PowerShell—much like any other object. We need to loop numerous times because we have multiple SystemHandleEntry structures to create—one for each open handle. We actually create the structures by using the Marshal.PtrToStructure method in the .NET Framework.
You may have noticed that we are incrementing an offset by using the size of the SystemHandleEntry. This is because we have a big blob of data, and we need to move sequentially down it to get each SystemHandleEntry. You can think of this as a big row of Lego blocks. We know the size of one block and instruct the PtrToStructure method to where the beginning of the each block is. It breaks that block off and returns it as a SystemHandleEntry.
Note that I am evaluating whether the ObjectTypeNumber is 31. This is the file object type number for Windows 8. Other operating systems have different identifying numbers. To reduce the size of this post, I’ve eliminated the logic to determine that number, but it is possible to do so dynamically.
$offset = [IntPtr]::Size
$size = [System.Runtime.InteropServices.Marshal]::SizeOf([SystemHandleEntry])
for ($i = 0; $i -lt $handleCount; $i++)
{
[SystemHandleEntry]$FileHandle = [System.Runtime.InteropServices.Marshal]::PtrToStructure([IntPtr]([long]$ptr + $offset), [SystemHandleEntry])
if ($FileHandle.ObjectTypeNumber -eq 31)
{
$FileHandle | ConvertTo-HandleHashTable
}
$offset += $size
}
We have also defined a ConvertTo-HandleHashTable function. The purpose of this function is to convert the handle information into a useful hash table that will contain the path to the file and a System.Diagnostics.Process object. This is much more useful than the memory addresses and process IDs that currently populate the SystemHandleEntry structures that we just created.
Inside this function, we need to duplicate the handle returned by NtQuerySystemInformation. If we do not create a copy of the handle, we’ll be unable to query additional information about it. We use OpenProcess to get a handle to the process that owns the original handle. This is necessary for the DuplicateHandle function. The DuplicateHandle function is what provides us with a malleable handle inside our process.
$sourceProcessHandle = [IntPtr]::Zero
$handleDuplicate = [IntPtr]::Zero
$currentProcessHandle = (Get-Process -Id $Pid).Handle
$sourceProcessHandle = [Kernel32]::OpenProcess(0x40, $true, $HandleEntry.OwnerProcessId)
if (-not [Kernel32]::DuplicateHandle($sourceProcessHandle,
[IntPtr]$HandleEntry.Handle,
$currentProcessHandle,
[ref]$handleDuplicate,
0,
$false,
2))
{
return
}
Next, we use the NtQueryObject function to get additional information about the handle. We use the duplicate handle because we will not have the proper rights on the original handle. In this case, we are calling NtQueryObject with the ObjectNameInformation flag. This tells NtQueryObject that we want the name of the handle. In regards to a file handle, this is the path to the file.
$length = 0
[NtDll]::NtQueryObject($handleDuplicate,
[OBJECT_INFORMATION_CLASS]::ObjectNameInformation,
[IntPtr]::Zero,
0,
[ref]$length) | Out-Null
$ptr = [IntPtr]::Zero
$ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($length)
if ([NtDll]::NtQueryObject($handleDuplicate,
[OBJECT_INFORMATION_CLASS]::ObjectNameInformation,
$ptr,
$length,
[ref]$length) -ne [NT_STATUS]::STATUS_SUCCESS)
{
return
}
When we have the name as an IntPtr object, we need to convert it into a .NET string. To do this, we use the Marshal.PtrToStringUni method. It converts a memory address into a useable .NET string.
$Path = [System.Runtime.InteropServices.Marshal]::PtrToStringUni([IntPtr]([long]$ptr+ 2 * [IntPtr]::Size))
Finally, we create a new PSCustomObject that contains the path to the file and the Process object that owns the handle in question. You can see that we are using a ConvertTo-RegularFileName function. This is due to the fact that the name returned by NtQueryObject will be in the DOS-style format. This format looks similar to: \Device\HarddiskVolume1\myfile.txt.
Performing a comparison on a path like this would likely fail later. The result of ConvertTo-RegularFileName will instead be in this format: C:\MyFile.txt.
[PSCustomObject]@{
Path=(ConvertTo-RegularFileName $Path);
Process=(Get-Process -Id $HandleEntry.OwnerProcessId);
}
The ConvertTo-RegularFileName function uses the QueryDosDevice function to convert a logical drive (such as drive C) into a DOS style drive (for example, \Device\HarddiskVolume1\). We use Environment::GetLogicalDrives to return all the drives on the current machine. Then we convert them to DOS style and fix up the raw path that is returned by NtQueryObject.
foreach ($logicalDrive in [Environment]::GetLogicalDrives())
{
$targetPath = New-Object System.Text.StringBuilder 256
if ([Kernel32]::QueryDosDevice($logicalDrive.Substring(0, 2), $targetPath, 256) -eq 0)
{
return $targetPath
}
$targetPathString = $targetPath.ToString()
if ($RawFileName.StartsWith($targetPathString))
{
$RawFileName = $RawFileName.Replace($targetPathString,
$logicalDrive.Substring(0, 2))
break
}
}
$RawFileName
This all comes together in the most useful of the functions, Find-LockingProcess. This advanced function accepts a file info object (as returned by Get-ChildItem), or a raw path, and it finds handles that are open for that path. When it finds the handle, it returns the Process object for the locking file.
function Find-LockingProcess
{
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline=$true,ParameterSetName="Pipeline",Mandatory)]
[System.IO.FileInfo]$InputObject,
[Parameter(ValueFromPipeline=$true,ParameterSetName="Path",Mandatory)]
[String]$Path
)
Begin {
$Handles = Get-FileHandle
}
Process
{
if ($InputObject)
{
$Handles | Where-Object { $_.Path -eq $InputObject.FullName } | Select-Object -ExpandProperty Process
}
if ($Path)
{
$Handles| Where-Object { $_.Path -contains $Path } | Select-Object -ExpandProperty Process
}
}
}
To use this function, we could then specify a full file path. We will get output that references all the processes that have a handle open to the file. It’s worth noting that not all the process may be locking the file, but only a process that currently has a handle can be locking it. In the following example, Steam is locking the file and devenv is not.
Find-LockingProcess -Path "E:\Program Files (x86)\Steam\steam.log"
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
685 85 218692 37356 536 116.84 4420 Steam
73285 248 279136 366676 887 550.61 3344 devenv
74622 249 267796 359196 904 550.68 3344 devenv
75959 246 282880 369672 881 550.76 3344 devenv
88123 247 307540 389004 897 551.17 3344 devenv
The full script can be found in the Script Center Repository: Find-LockingProcess. You will likely need to run your Windows PowerShell host as an administrator to successfully execute this script.
~Adam
Thank you, Adam, for taking your time to share with our readers.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy