Skip to content

Commit 389fb1c

Browse files
authored
Merge pull request #4 from ironmansoftware/copilot/fix-open-folder-check-error
Add folder support for Find-OpenFile -FilePath
2 parents 0a63001 + 6b37aa2 commit 389fb1c

File tree

4 files changed

+197
-1
lines changed

4 files changed

+197
-1
lines changed

FindOpenFileCommand.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Diagnostics;
3+
using System.IO;
34
using System.Linq;
45
using System.Management.Automation;
56
using System.Runtime.InteropServices;
@@ -42,7 +43,17 @@ protected override void ProcessRecord()
4243
{
4344
FilePath = GetUnresolvedProviderPathFromPSPath(FilePath);
4445

45-
WriteObject(WalkmanLib.RestartManager.GetLockingProcesses(FilePath), true);
46+
// Check if the path is a directory
47+
if (Directory.Exists(FilePath))
48+
{
49+
// For directories, use the GetAllHandles approach
50+
WriteObject(WalkmanLib.GetFileLocks.GetAllHandles.GetProcessesLockingDirectory(FilePath), true);
51+
}
52+
else
53+
{
54+
// For files, use the RestartManager approach
55+
WriteObject(WalkmanLib.RestartManager.GetLockingProcesses(FilePath), true);
56+
}
4657
}
4758
else if (ParameterSetName == ProcessParameterSet)
4859
{

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@ PS C:\Users\adamr> Find-OpenFile -FilePath C:\Windows\System32\en-US\KernelBase.
8383
27 14.69 48.04 3.91 11624 1 LockApp
8484
```
8585

86+
Find processes accessing a folder or files within it
87+
88+
```
89+
PS C:\Users\adamr> Find-OpenFile -FilePath C:\Test
90+
91+
NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
92+
------ ----- ----- ------ -- -- -----------
93+
56 50.62 74.23 10.42 15136 1 notepad
94+
```
95+
8696
## Source
8797

8898
- C# Code Forked from [this repository](https://github.com/Walkman100/FileLocks)

Tests/FindOpenFile.Tests.ps1

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,54 @@ Describe 'Find-OpenFile Module' {
179179
}
180180
}
181181

182+
Context 'Find-OpenFile - Directory Support' {
183+
BeforeAll {
184+
# Create a temporary directory with a file
185+
$script:testDir = Join-Path $TestDrive 'testfolder'
186+
New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null
187+
$script:testDirFile = Join-Path $script:testDir 'testfile.txt'
188+
'Test content' | Out-File -FilePath $script:testDirFile -Force
189+
}
190+
191+
It 'Should not throw error when checking a directory' {
192+
# This is the main fix - previously this would throw:
193+
# "Could not list processes locking resource. Failed to get size of result."
194+
{ Find-OpenFile -FilePath $script:testDir } | Should -Not -Throw
195+
}
196+
197+
It 'Should return Process objects when checking a directory with open handles' {
198+
# Lock a file inside the directory
199+
$fileStream = [System.IO.File]::Open($script:testDirFile, 'Open', 'Read', 'None')
200+
201+
try {
202+
$result = Find-OpenFile -FilePath $script:testDir
203+
# The result may or may not include the process depending on how the OS reports handles
204+
# The key test is that it doesn't throw an error
205+
if ($result) {
206+
$result | ForEach-Object {
207+
$_ | Should -BeOfType [System.Diagnostics.Process]
208+
}
209+
}
210+
}
211+
finally {
212+
$fileStream.Close()
213+
$fileStream.Dispose()
214+
}
215+
}
216+
217+
It 'Should accept directory path from pipeline' {
218+
{ $script:testDir | Find-OpenFile } | Should -Not -Throw
219+
}
220+
221+
It 'Should handle non-existent directory paths gracefully' {
222+
$nonExistentDir = Join-Path $TestDrive 'nonexistentfolder'
223+
# For non-existent paths, it falls back to the RestartManager approach
224+
# which may or may not throw depending on Windows version
225+
# The key is that it shouldn't throw the "Failed to get size of result" error for directories
226+
{ Find-OpenFile -FilePath $nonExistentDir } | Should -Not -Throw
227+
}
228+
}
229+
182230
Context 'Platform Support' {
183231
It 'Should only work on Windows' {
184232
if ($IsLinux -or $IsMacOS) {

WalkmanLib.GetFileLocks.GetAllHandles.cs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,5 +343,132 @@ public static IEnumerable<HandleInfo> GetFileHandles()
343343
yield return hi;
344344
}
345345
}
346+
347+
/// <summary>
348+
/// Gets handles that match the specified directory path.
349+
/// This method is used for finding processes that have a handle to a directory.
350+
/// </summary>
351+
/// <param name="directoryPath">The full path to the directory to search for.</param>
352+
/// <returns>A list of HandleInfo objects for handles matching the directory.</returns>
353+
public static IEnumerable<HandleInfo> GetDirectoryHandles(string directoryPath)
354+
{
355+
// Normalize the path for comparison
356+
directoryPath = directoryPath.TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
357+
358+
// Convert to device path format for comparison
359+
// Windows handles use paths like \Device\HarddiskVolume3\path
360+
// We need to match against the end portion of the path
361+
string normalizedPath = directoryPath.Replace(System.IO.Path.AltDirectorySeparatorChar, System.IO.Path.DirectorySeparatorChar);
362+
363+
foreach (HandleInfo hi in GetFileHandles())
364+
{
365+
if (hi.Name != null)
366+
{
367+
// The handle name is in device path format (e.g., \Device\HarddiskVolume3\Users\test)
368+
// We need to check if it ends with our directory path or starts with it (for files within)
369+
string handlePath = hi.Name;
370+
371+
// Try to convert the device path to a DOS path for comparison
372+
string dosPath = ConvertDevicePathToDosPath(handlePath);
373+
if (dosPath != null)
374+
{
375+
dosPath = dosPath.TrimEnd(System.IO.Path.DirectorySeparatorChar);
376+
if (string.Equals(dosPath, normalizedPath, StringComparison.OrdinalIgnoreCase) ||
377+
dosPath.StartsWith(normalizedPath + System.IO.Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
378+
{
379+
yield return hi;
380+
}
381+
}
382+
}
383+
}
384+
}
385+
386+
private static readonly object _deviceMapLock = new object();
387+
private static Dictionary<string, string> _deviceToDriveMap;
388+
389+
/// <summary>
390+
/// Converts a device path (e.g., \Device\HarddiskVolume3\path) to a DOS path (e.g., C:\path).
391+
/// </summary>
392+
private static string ConvertDevicePathToDosPath(string devicePath)
393+
{
394+
if (string.IsNullOrEmpty(devicePath))
395+
return null;
396+
397+
// Thread-safe lazy initialization of device to drive map
398+
if (_deviceToDriveMap == null)
399+
{
400+
lock (_deviceMapLock)
401+
{
402+
if (_deviceToDriveMap == null)
403+
{
404+
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
405+
foreach (string drive in System.IO.Directory.GetLogicalDrives())
406+
{
407+
string driveLetter = drive.TrimEnd('\\');
408+
string deviceName = QueryDosDevice(driveLetter);
409+
if (deviceName != null)
410+
{
411+
map[deviceName] = driveLetter;
412+
}
413+
}
414+
_deviceToDriveMap = map;
415+
}
416+
}
417+
}
418+
419+
// Try to find a matching device prefix
420+
foreach (var kvp in _deviceToDriveMap)
421+
{
422+
if (devicePath.StartsWith(kvp.Key, StringComparison.OrdinalIgnoreCase))
423+
{
424+
return kvp.Value + devicePath.Substring(kvp.Key.Length);
425+
}
426+
}
427+
428+
return null;
429+
}
430+
431+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
432+
private static extern uint QueryDosDevice(string lpDeviceName, System.Text.StringBuilder lpTargetPath, uint ucchMax);
433+
434+
private static string QueryDosDevice(string driveLetter)
435+
{
436+
var buffer = new System.Text.StringBuilder(260);
437+
if (QueryDosDevice(driveLetter, buffer, (uint)buffer.Capacity) != 0)
438+
{
439+
return buffer.ToString();
440+
}
441+
return null;
442+
}
443+
444+
/// <summary>
445+
/// Gets processes that have a handle to the specified directory or files within it.
446+
/// </summary>
447+
/// <param name="directoryPath">The full path to the directory.</param>
448+
/// <returns>A list of unique Process objects.</returns>
449+
public static List<System.Diagnostics.Process> GetProcessesLockingDirectory(string directoryPath)
450+
{
451+
var processIds = new HashSet<int>();
452+
var processes = new List<System.Diagnostics.Process>();
453+
454+
foreach (HandleInfo hi in GetDirectoryHandles(directoryPath))
455+
{
456+
if (!processIds.Contains(hi.ProcessId))
457+
{
458+
processIds.Add(hi.ProcessId);
459+
try
460+
{
461+
processes.Add(System.Diagnostics.Process.GetProcessById(hi.ProcessId));
462+
}
463+
catch (ArgumentException)
464+
{
465+
// Process no longer exists - this is expected as processes can terminate
466+
// between the time we enumerate handles and try to get the process
467+
}
468+
}
469+
}
470+
471+
return processes;
472+
}
346473
}
347474
}

0 commit comments

Comments
 (0)