Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion FindOpenFileCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -42,7 +43,17 @@ protected override void ProcessRecord()
{
FilePath = GetUnresolvedProviderPathFromPSPath(FilePath);

WriteObject(WalkmanLib.RestartManager.GetLockingProcesses(FilePath), true);
// Check if the path is a directory
if (Directory.Exists(FilePath))
{
// For directories, use the GetAllHandles approach
WriteObject(WalkmanLib.GetFileLocks.GetAllHandles.GetProcessesLockingDirectory(FilePath), true);
}
else
{
// For files, use the RestartManager approach
WriteObject(WalkmanLib.RestartManager.GetLockingProcesses(FilePath), true);
}
}
else if (ParameterSetName == ProcessParameterSet)
{
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ PS C:\Users\adamr> Find-OpenFile -FilePath C:\Windows\System32\en-US\KernelBase.
27 14.69 48.04 3.91 11624 1 LockApp
```

Find processes accessing a folder or files within it

```
PS C:\Users\adamr> Find-OpenFile -FilePath C:\Test

NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
------ ----- ----- ------ -- -- -----------
56 50.62 74.23 10.42 15136 1 notepad
```

## Source

- C# Code Forked from [this repository](https://github.com/Walkman100/FileLocks)
48 changes: 48 additions & 0 deletions Tests/FindOpenFile.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,54 @@ Describe 'Find-OpenFile Module' {
}
}

Context 'Find-OpenFile - Directory Support' {
BeforeAll {
# Create a temporary directory with a file
$script:testDir = Join-Path $TestDrive 'testfolder'
New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
$script:testDirFile = Join-Path $script:testDir 'testfile.txt'
'Test content' | Out-File -FilePath $script:testDirFile -Force
}

It 'Should not throw error when checking a directory' {
# This is the main fix - previously this would throw:
# "Could not list processes locking resource. Failed to get size of result."
{ Find-OpenFile -FilePath $script:testDir } | Should -Not -Throw
}

It 'Should return Process objects when checking a directory with open handles' {
# Lock a file inside the directory
$fileStream = [System.IO.File]::Open($script:testDirFile, 'Open', 'Read', 'None')

try {
$result = Find-OpenFile -FilePath $script:testDir
# The result may or may not include the process depending on how the OS reports handles
# The key test is that it doesn't throw an error
if ($result) {
$result | ForEach-Object {
$_ | Should -BeOfType [System.Diagnostics.Process]
}
}
}
finally {
$fileStream.Close()
$fileStream.Dispose()
}
}

It 'Should accept directory path from pipeline' {
{ $script:testDir | Find-OpenFile } | Should -Not -Throw
}

It 'Should handle non-existent directory paths gracefully' {
$nonExistentDir = Join-Path $TestDrive 'nonexistentfolder'
# For non-existent paths, it falls back to the RestartManager approach
# which may or may not throw depending on Windows version
# The key is that it shouldn't throw the "Failed to get size of result" error for directories
{ Find-OpenFile -FilePath $nonExistentDir } | Should -Not -Throw
}
}

Context 'Platform Support' {
It 'Should only work on Windows' {
if ($IsLinux -or $IsMacOS) {
Expand Down
127 changes: 127 additions & 0 deletions WalkmanLib.GetFileLocks.GetAllHandles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,5 +343,132 @@ public static IEnumerable<HandleInfo> GetFileHandles()
yield return hi;
}
}

/// <summary>
/// Gets handles that match the specified directory path.
/// This method is used for finding processes that have a handle to a directory.
/// </summary>
/// <param name="directoryPath">The full path to the directory to search for.</param>
/// <returns>A list of HandleInfo objects for handles matching the directory.</returns>
public static IEnumerable<HandleInfo> GetDirectoryHandles(string directoryPath)
{
// Normalize the path for comparison
directoryPath = directoryPath.TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);

// Convert to device path format for comparison
// Windows handles use paths like \Device\HarddiskVolume3\path
// We need to match against the end portion of the path
string normalizedPath = directoryPath.Replace(System.IO.Path.AltDirectorySeparatorChar, System.IO.Path.DirectorySeparatorChar);

foreach (HandleInfo hi in GetFileHandles())
{
if (hi.Name != null)
{
// The handle name is in device path format (e.g., \Device\HarddiskVolume3\Users\test)
// We need to check if it ends with our directory path or starts with it (for files within)
string handlePath = hi.Name;

// Try to convert the device path to a DOS path for comparison
string dosPath = ConvertDevicePathToDosPath(handlePath);
if (dosPath != null)
{
dosPath = dosPath.TrimEnd(System.IO.Path.DirectorySeparatorChar);
if (string.Equals(dosPath, normalizedPath, StringComparison.OrdinalIgnoreCase) ||
dosPath.StartsWith(normalizedPath + System.IO.Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
{
yield return hi;
}
}
}
}
}

private static readonly object _deviceMapLock = new object();
private static Dictionary<string, string> _deviceToDriveMap;

/// <summary>
/// Converts a device path (e.g., \Device\HarddiskVolume3\path) to a DOS path (e.g., C:\path).
/// </summary>
private static string ConvertDevicePathToDosPath(string devicePath)
{
if (string.IsNullOrEmpty(devicePath))
return null;

// Thread-safe lazy initialization of device to drive map
if (_deviceToDriveMap == null)
{
lock (_deviceMapLock)
{
if (_deviceToDriveMap == null)
{
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (string drive in System.IO.Directory.GetLogicalDrives())
{
string driveLetter = drive.TrimEnd('\\');
string deviceName = QueryDosDevice(driveLetter);
if (deviceName != null)
{
map[deviceName] = driveLetter;
}
}
_deviceToDriveMap = map;
}
}
}

// Try to find a matching device prefix
foreach (var kvp in _deviceToDriveMap)
{
if (devicePath.StartsWith(kvp.Key, StringComparison.OrdinalIgnoreCase))
{
return kvp.Value + devicePath.Substring(kvp.Key.Length);
}
}

return null;
}

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern uint QueryDosDevice(string lpDeviceName, System.Text.StringBuilder lpTargetPath, uint ucchMax);

private static string QueryDosDevice(string driveLetter)
{
var buffer = new System.Text.StringBuilder(260);
if (QueryDosDevice(driveLetter, buffer, (uint)buffer.Capacity) != 0)
{
return buffer.ToString();
}
return null;
}

/// <summary>
/// Gets processes that have a handle to the specified directory or files within it.
/// </summary>
/// <param name="directoryPath">The full path to the directory.</param>
/// <returns>A list of unique Process objects.</returns>
public static List<System.Diagnostics.Process> GetProcessesLockingDirectory(string directoryPath)
{
var processIds = new HashSet<int>();
var processes = new List<System.Diagnostics.Process>();

foreach (HandleInfo hi in GetDirectoryHandles(directoryPath))
{
if (!processIds.Contains(hi.ProcessId))
{
processIds.Add(hi.ProcessId);
try
{
processes.Add(System.Diagnostics.Process.GetProcessById(hi.ProcessId));
}
catch (ArgumentException)
{
// Process no longer exists - this is expected as processes can terminate
// between the time we enumerate handles and try to get the process
}
}
}

return processes;
}
}
}