From 107df2bda27a421aa35b4b3c1c48e8a55903c5b9 Mon Sep 17 00:00:00 2001
From: PolarGoose <35307286+PolarGoose@users.noreply.github.com>
Date: Sat, 24 Sep 2022 18:01:52 +0200
Subject: [PATCH] Initial commit
---
.editorconfig | 13 +
.gitattributes | 1 +
.github/workflows/main.yaml | 18 ++
.gitignore | 5 +
Directory.Build.props | 16 +
Handle2.sln | 52 ++++
LICENSE | 21 ++
README.md | 47 +++
build.ps1 | 74 +++++
src/Handle2/FodyWeavers.xml | 5 +
src/Handle2/FodyWeavers.xsd | 141 +++++++++
src/Handle2/Handle2.csproj | 41 +++
src/Handle2/HandleInfo/HandleInfoRetriever.cs | 188 ++++++++++++
src/Handle2/HandleInfo/Interop/NtDll.cs | 284 ++++++++++++++++++
src/Handle2/HandleInfo/Interop/WinApi.cs | 179 +++++++++++
src/Handle2/HandleInfo/Utils/FileUtils.cs | 25 ++
src/Handle2/HandleInfo/Utils/LinqUtils.cs | 10 +
src/Handle2/HandleInfo/Utils/ProcessUtils.cs | 43 +++
src/Handle2/Program.cs | 95 ++++++
src/test/HandleInfo/Utils/FileUtilsTest.cs | 30 ++
src/test/ProgramTest.cs | 136 +++++++++
src/test/Test.csproj | 23 ++
22 files changed, 1447 insertions(+)
create mode 100644 .editorconfig
create mode 100644 .gitattributes
create mode 100644 .github/workflows/main.yaml
create mode 100644 .gitignore
create mode 100644 Directory.Build.props
create mode 100644 Handle2.sln
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 build.ps1
create mode 100644 src/Handle2/FodyWeavers.xml
create mode 100644 src/Handle2/FodyWeavers.xsd
create mode 100644 src/Handle2/Handle2.csproj
create mode 100644 src/Handle2/HandleInfo/HandleInfoRetriever.cs
create mode 100644 src/Handle2/HandleInfo/Interop/NtDll.cs
create mode 100644 src/Handle2/HandleInfo/Interop/WinApi.cs
create mode 100644 src/Handle2/HandleInfo/Utils/FileUtils.cs
create mode 100644 src/Handle2/HandleInfo/Utils/LinqUtils.cs
create mode 100644 src/Handle2/HandleInfo/Utils/ProcessUtils.cs
create mode 100644 src/Handle2/Program.cs
create mode 100644 src/test/HandleInfo/Utils/FileUtilsTest.cs
create mode 100644 src/test/ProgramTest.cs
create mode 100644 src/test/Test.csproj
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..0db9a30
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+root = true
+
+[*]
+end_of_line = lf
+trim_trailing_whitespace = true
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 4
+max_line_length = 150
+
+[*.{xml,wxs,csproj,yaml,props,config,yaml}]
+indent_size = 2
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6313b56
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
new file mode 100644
index 0000000..e6b12a2
--- /dev/null
+++ b/.github/workflows/main.yaml
@@ -0,0 +1,18 @@
+on: push
+
+jobs:
+ build:
+ runs-on: windows-2022
+ steps:
+ - uses: actions/checkout@v4
+ - run: ./build.ps1
+ - uses: softprops/action-gh-release@v2
+ if: startsWith(github.ref, 'refs/tags/')
+ with:
+ draft: true
+ files: build/publish/*.zip
+ fail_on_unmatched_files: true
+ - uses: actions/upload-artifact@v4
+ with:
+ name: Build artifacts
+ path: build/publish/*.zip
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0977fd6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/build/
+.vs/
+.idea/
+CMakeSettings.json
+launchSettings.json
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..4c2199a
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,16 @@
+
+
+ $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))
+ $(ProjectRoot)build\
+ $(BuildFolder)$(Configuration)\$(MSBuildProjectName)
+ $(BuildFolder)obj\$(MSBuildProjectName)\$(Configuration)\
+ net462
+ latest
+ x64
+ enable
+ enable
+ 5
+ true
+ 0.0-dev
+
+
diff --git a/Handle2.sln b/Handle2.sln
new file mode 100644
index 0000000..2680d65
--- /dev/null
+++ b/Handle2.sln
@@ -0,0 +1,52 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.8.34330.188
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test", "src\Test\Test.csproj", "{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Files", "Files", "{ECE2889E-C492-446E-82C2-022D6029A2BB}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ .gitattributes = .gitattributes
+ .gitignore = .gitignore
+ build.ps1 = build.ps1
+ Directory.Build.props = Directory.Build.props
+ .github\workflows\main.yaml = .github\workflows\main.yaml
+ README.md = README.md
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Handle2", "src\Handle2\Handle2.csproj", "{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Debug|x64.ActiveCfg = Debug|x64
+ {D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Debug|x64.Build.0 = Debug|x64
+ {D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Debug|x86.ActiveCfg = Debug|x64
+ {D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Debug|x86.Build.0 = Debug|x64
+ {D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Release|x64.ActiveCfg = Release|x64
+ {D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Release|x64.Build.0 = Release|x64
+ {D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Release|x86.ActiveCfg = Release|x64
+ {D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Release|x86.Build.0 = Release|x64
+ {FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Debug|x64.ActiveCfg = Debug|x64
+ {FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Debug|x64.Build.0 = Debug|x64
+ {FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Debug|x86.ActiveCfg = Debug|x64
+ {FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Debug|x86.Build.0 = Debug|x64
+ {FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Release|x64.ActiveCfg = Release|x64
+ {FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Release|x64.Build.0 = Release|x64
+ {FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Release|x86.ActiveCfg = Release|x64
+ {FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Release|x86.Build.0 = Release|x64
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {729412BC-986E-47AD-B486-9798934F437F}
+ EndGlobalSection
+EndGlobal
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a501e9f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 PolarGoose
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..29ece8a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,47 @@
+# Handle2
+
+An open-source alternative to the [Sysinternals Handle](https://learn.microsoft.com/en-us/sysinternals/downloads/handle)
+* Identifies processes that are locking specific files or folders
+* Shows information about all handles in the system
+* Supports JSON output
+* Full Unicode support
+ * Note: `Sysinternals Handle` doesn't support Unicode file names ([more details](https://superuser.com/questions/1761951/sysinternals-handle-prints-question-marks-instead-of-non-ascii-symbols))
+
+## System requirements
+
+* Windows 7 x64 and higher.
+
+## Usage
+
+```
+> Handle2.exe --help
+
+Handle2 1.0
+A console utility that displays information about system handles and identifies the processes locking a specific file or folder.
+https://github.com/PolarGoose/Handle2
+
+Usage:
+ Handle2.exe [--json] [--path FILE_OR_FOLDER_FULL_NAME|--dump-all-handles]
+Examples:
+ Handle2.exe --path C:\Windows\System32
+ Handle2.exe --json --path C:\Windows\System32\ntdll.dll
+ Handle2.exe --json --path C:\Windows\explorer.exe
+ Handle2.exe --json --dump-all-handles
+
+Command-line options:
+
+ --json (Default: false) Json output
+
+ --path Required. (Default: false) path
+
+ --dump-all-handles Required. (Default: false) Displays information about all system handles.
+
+ --help Display this help screen.
+
+ --version Display version information.
+```
+
+## How to build
+
+* To work with the codebase, `Visual Studio 2022` can be used.
+* To build the project, run `build.ps` script (`git.exe` should be in the PATH)
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 0000000..01d326b
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,74 @@
+Function Info($msg) {
+ Write-Host -ForegroundColor DarkGreen "`nINFO: $msg`n"
+}
+
+Function Error($msg) {
+ Write-Host `n`n
+ Write-Error $msg
+ exit 1
+}
+
+Function CheckReturnCodeOfPreviousCommand($msg) {
+ if(-Not $?) {
+ Error "${msg}. Error code: $LastExitCode"
+ }
+}
+
+Function CreateZipArchive($file, $archiveFile) {
+ Info "Create a zip archive from `n '$file' `n to `n '$archiveFile'"
+ Compress-Archive -Force -Path $file -DestinationPath $archiveFile
+}
+
+Function GetVersion() {
+ $gitCommand = Get-Command -Name git
+
+ $tag = & $gitCommand describe --exact-match --tags HEAD
+ if(-Not $?) {
+ Info "The commit is not tagged. Use 'v0.0-dev' as a tag instead"
+ $tag = "v0.0-dev"
+ }
+
+ $commitHash = & $gitCommand rev-parse --short HEAD
+ CheckReturnCodeOfPreviousCommand "Failed to get git commit hash"
+
+ return "$($tag.Substring(1))-$commitHash"
+}
+
+Function ForceCopy($srcFile, $dstFile) {
+ Info "Copy `n '$srcFile' `n to `n '$dstFile'"
+ New-Item $dstFile -Force -ItemType File > $null
+ Copy-Item $srcFile -Destination $dstFile -Force
+}
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+$root = Resolve-Path $PSScriptRoot
+$buildDir = "$root/build"
+$publishDir = "$buildDir/publish"
+$version = GetVersion
+
+Info "version=$version"
+
+Info "Build the project"
+dotnet build `
+ --nologo `
+ --configuration Release `
+ -verbosity:minimal `
+ /property:DebugType=None `
+ /property:Version=$version `
+ "$root/Handle2.sln"
+CheckReturnCodeOfPreviousCommand "Cmake generation phase failed"
+
+Info "Run tests"
+dotnet test `
+ --nologo `
+ --no-build `
+ --configuration Release `
+ -verbosity:minimal `
+ --logger:"console;verbosity=normal" `
+ "$root/Handle2.sln"
+CheckReturnCodeOfPreviousCommand "Tests failed"
+
+ForceCopy "$buildDir/Release/Handle2/net462/Handle2.exe" "$publishDir/Handle2.exe"
+CreateZipArchive "$publishDir/Handle2.exe" "$publishDir/Handle2.zip"
diff --git a/src/Handle2/FodyWeavers.xml b/src/Handle2/FodyWeavers.xml
new file mode 100644
index 0000000..b5a0e18
--- /dev/null
+++ b/src/Handle2/FodyWeavers.xml
@@ -0,0 +1,5 @@
+
+
+ Interop
+
+
diff --git a/src/Handle2/FodyWeavers.xsd b/src/Handle2/FodyWeavers.xsd
new file mode 100644
index 0000000..05e92c1
--- /dev/null
+++ b/src/Handle2/FodyWeavers.xsd
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+ A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks
+
+
+
+
+ A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.
+
+
+
+
+ A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks
+
+
+
+
+ A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.
+
+
+
+
+ A list of unmanaged 32 bit assembly names to include, delimited with line breaks.
+
+
+
+
+ A list of unmanaged 64 bit assembly names to include, delimited with line breaks.
+
+
+
+
+ The order of preloaded assemblies, delimited with line breaks.
+
+
+
+
+
+ This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.
+
+
+
+
+ Controls if .pdbs for reference assemblies are also embedded.
+
+
+
+
+ Controls if runtime assemblies are also embedded.
+
+
+
+
+ Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.
+
+
+
+
+ Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.
+
+
+
+
+ As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.
+
+
+
+
+ Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.
+
+
+
+
+ Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.
+
+
+
+
+ A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |
+
+
+
+
+ A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.
+
+
+
+
+ A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |
+
+
+
+
+ A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.
+
+
+
+
+ A list of unmanaged 32 bit assembly names to include, delimited with |.
+
+
+
+
+ A list of unmanaged 64 bit assembly names to include, delimited with |.
+
+
+
+
+ The order of preloaded assemblies, delimited with |.
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Handle2/Handle2.csproj b/src/Handle2/Handle2.csproj
new file mode 100644
index 0000000..7a96ee1
--- /dev/null
+++ b/src/Handle2/Handle2.csproj
@@ -0,0 +1,41 @@
+
+
+
+ Exe
+ A console utility that displays information about system handles and identifies the processes locking a specific file or folder.
+https://github.com/PolarGoose/Handle2
+
+Usage:
+ Handle2.exe [--json] [--path FILE_OR_FOLDER_FULL_NAME|--dump-all-handles]
+Examples:
+ Handle2.exe --path C:\Windows\System32
+ Handle2.exe --json --path C:\Windows\System32\ntdll.dll
+ Handle2.exe --json --path C:\Windows\explorer.exe
+ Handle2.exe --json --dump-all-handles
+
+Command-line options:
+
+
+
+
+
+ all
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
diff --git a/src/Handle2/HandleInfo/HandleInfoRetriever.cs b/src/Handle2/HandleInfo/HandleInfoRetriever.cs
new file mode 100644
index 0000000..8963504
--- /dev/null
+++ b/src/Handle2/HandleInfo/HandleInfoRetriever.cs
@@ -0,0 +1,188 @@
+using Handle2.HandleInfo.Interop;
+using Handle2.HandleInfo.Utils;
+using Microsoft.Win32.SafeHandles;
+
+namespace Handle2.HandleInfo;
+
+public record struct HandleInfo(
+ // The result of NtDll.NtQueryObject(OBJECT_INFORMATION_CLASS.ObjectTypeInformation).
+ // Examples:
+ // * File
+ // * Event
+ string? HandleType,
+
+ // the result ot WinApi.GetFileType().
+ WinApi.FileType? FileType,
+
+ // The result of NtDll.NtQueryObject(OBJECT_INFORMATION_CLASS.ObjectNameInformation).
+ // Examples:
+ // * \\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\Control\\Session Manager
+ // * \\Device\\HarddiskVolume3\\Windows\\System32
+ string? Name,
+
+ // If the handle is a file or folder this field contains its full path.
+ // Examples:
+ // * "C:\Users\user\file.txt"
+ // * "C:\Users\user\"
+ string? FullNameIfItIsAFileOrAFolder,
+
+ // The content of the SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX.GrantedAccess field.
+ uint GrantedAccess,
+
+ // The content of the SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX.HandleAttributes field.
+ uint Attributes,
+
+ // The content of the SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX.Object field.
+ ulong AddressInTheKernelMemory);
+
+public record struct ProcessInfo(
+ ulong Pid,
+ string? ProcessExecutablePath,
+ string? UserName,
+ string? DomainName,
+ List Handles);
+
+public static class HandleInfoRetriever
+{
+ private static IEnumerable> QuerySystemHandleInformation()
+ {
+ return NtDll.QuerySystemHandleInformation().GroupBy(handle => handle.UniqueProcessId);
+ }
+
+ private static void AddHandleTypeAndNameInfo(SafeFileHandle handle, ref HandleInfo handleInfo)
+ {
+ handleInfo.FileType = WinApi.GetFileType(handle);
+ handleInfo.Name = NtDll.GetHandleName(handle);
+ if (handleInfo.FileType == WinApi.FileType.FILE_TYPE_DISK)
+ {
+ handleInfo.FullNameIfItIsAFileOrAFolder = WinApi.GetFinalPathNameByHandle(handle);
+ if (handleInfo.FullNameIfItIsAFileOrAFolder is not null)
+ {
+ handleInfo.FullNameIfItIsAFileOrAFolder = FileUtils.AddTrailingSeparatorIfItIsAFolder(handleInfo.FullNameIfItIsAFileOrAFolder);
+ }
+ }
+ }
+
+ public static IEnumerable GetAllProcInfos()
+ {
+ var currentProcess = WinApi.GetCurrentProcess();
+ var result = new List();
+
+ foreach (var (pid, handles) in QuerySystemHandleInformation())
+ {
+ using var openedProcess = ProcessUtils.OpenProcessToDuplicateHandle(pid);
+ if (openedProcess is null)
+ {
+ continue;
+ }
+
+ var processInfo = new ProcessInfo
+ {
+ Pid = pid.ToUInt64(),
+ Handles = []
+ };
+
+ foreach (var h in handles)
+ {
+ using var dupHandle = WinApi.DuplicateHandle(currentProcess, openedProcess, h);
+ if (dupHandle.IsInvalid)
+ {
+ continue;
+ }
+
+ var handle = new HandleInfo
+ {
+ GrantedAccess = h.GrantedAccess,
+ Attributes = h.HandleAttributes,
+ AddressInTheKernelMemory = (ulong)h.Object.ToInt64(),
+ HandleType = NtDll.GetHandleType(dupHandle)
+ };
+
+ if (handle.HandleType is not null)
+ {
+ var task = Task.Run(() =>
+ {
+ AddHandleTypeAndNameInfo(dupHandle, ref handle);
+ });
+ task.Wait(TimeSpan.FromMilliseconds(300));
+ }
+
+ processInfo.Handles.Add(handle);
+ }
+
+ if (processInfo.Handles.Any())
+ {
+ (processInfo.DomainName, processInfo.UserName) = ProcessUtils.GetOwnerDomainAndUserNames(openedProcess);
+ processInfo.ProcessExecutablePath = ProcessUtils.GetExecutablePath(pid);
+ result.Add(processInfo);
+ }
+ }
+
+ return result;
+ }
+
+ public static IEnumerable GetProcInfosLockingPath(string path)
+ {
+ var currentProcess = WinApi.GetCurrentProcess();
+ var result = new List();
+ path = FileUtils.ToCanonicalPath(path);
+
+ foreach (var (pid, handles) in QuerySystemHandleInformation())
+ {
+ using var openedProcess = ProcessUtils.OpenProcessToDuplicateHandle(pid);
+ if (openedProcess is null)
+ {
+ continue;
+ }
+
+ var processInfo = new ProcessInfo
+ {
+ Pid = pid.ToUInt64(),
+ Handles = []
+ };
+
+ foreach (var h in handles)
+ {
+ using var dupHandle = WinApi.DuplicateHandle(currentProcess, openedProcess, h);
+ if (dupHandle.IsInvalid)
+ {
+ continue;
+ }
+
+ using var reopenedHandle = WinApi.ReOpenFile(dupHandle, WinApi.FileDesiredAccess.None, FileShare.None, WinApi.FileFlagsAndAttributes.None);
+ if (reopenedHandle.IsInvalid)
+ {
+ continue;
+ }
+
+ var handle = new HandleInfo
+ {
+ GrantedAccess = h.GrantedAccess,
+ Attributes = h.HandleAttributes,
+ AddressInTheKernelMemory = (ulong)h.Object.ToInt64(),
+ HandleType = NtDll.GetHandleType(reopenedHandle)
+ };
+
+ if (handle.HandleType is not null)
+ {
+ AddHandleTypeAndNameInfo(reopenedHandle, ref handle);
+ }
+
+ if (handle.FullNameIfItIsAFileOrAFolder?.StartsWith(path, StringComparison.InvariantCultureIgnoreCase) == true)
+ {
+ processInfo.Handles.Add(handle);
+ }
+ }
+
+ if (processInfo.Handles.Any())
+ {
+ (processInfo.DomainName, processInfo.UserName) = ProcessUtils.GetOwnerDomainAndUserNames(openedProcess);
+ processInfo.ProcessExecutablePath = ProcessUtils.GetExecutablePath(pid);
+
+ result.Add(processInfo);
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/src/Handle2/HandleInfo/Interop/NtDll.cs b/src/Handle2/HandleInfo/Interop/NtDll.cs
new file mode 100644
index 0000000..5fa8776
--- /dev/null
+++ b/src/Handle2/HandleInfo/Interop/NtDll.cs
@@ -0,0 +1,284 @@
+using Microsoft.Win32.SafeHandles;
+using System.Runtime.InteropServices;
+
+namespace Handle2.HandleInfo.Interop;
+
+public static class NtDll
+{
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX
+ {
+ // Pointer to the handle in the kernel virtual address space.
+ public IntPtr Object;
+
+ // PID that owns the handle
+ public UIntPtr UniqueProcessId;
+
+ // Handle value in the process that owns the handle.
+ public IntPtr HandleValue;
+
+ // Access rights associated with the handle.
+ // Bit mask consisting of the fields defined in the winnt.h
+ // For example: READ_CONTROL|DELETE|SYNCHRONIZE|WRITE_DAC|WRITE_OWNER|EVENT_ALL_ACCESS
+ // The exact information that this field contain depends on the type of the handle.
+ public uint GrantedAccess;
+
+ // This filed is reserved for debugging purposes
+ // For instance, it can store an index to a stack trace that was captured when the handle was created.
+ public ushort CreatorBackTraceIndex;
+
+ // Type of object a handle refers to.
+ // For instance: file, thread, or process
+ public ushort ObjectTypeIndex;
+
+ // Bit mask that provides additional information about the handle.
+ // For example: OBJ_INHERIT, OBJ_EXCLUSIVE
+ // The attributes are defined in the winternl.h
+ public uint HandleAttributes;
+
+ public uint Reserved;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SYSTEM_HANDLE_INFORMATION_EX
+ {
+ public IntPtr NumberOfHandles;
+ public IntPtr Reserved;
+ public SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX Handles; // Single element
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct UNICODE_STRING
+ {
+ public ushort Length;
+ public ushort MaximumLength;
+ public IntPtr Buffer;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct GENERIC_MAPPING
+ {
+ public int GenericRead;
+ public int GenericWrite;
+ public int GenericExecute;
+ public int GenericAll;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct OBJECT_TYPE_INFORMATION
+ {
+ public UNICODE_STRING Name;
+ public uint TotalNumberOfObjects;
+ public uint TotalNumberOfHandles;
+ public uint TotalPagedPoolUsage;
+ public uint TotalNonPagedPoolUsage;
+ public uint TotalNamePoolUsage;
+ public uint TotalHandleTableUsage;
+ public uint HighWaterNumberOfObjects;
+ public uint HighWaterNumberOfHandles;
+ public uint HighWaterPagedPoolUsage;
+ public uint HighWaterNonPagedPoolUsage;
+ public uint HighWaterNamePoolUsage;
+ public uint HighWaterHandleTableUsage;
+ public uint InvalidAttributes;
+ public GENERIC_MAPPING GenericMapping;
+ public uint ValidAccess;
+ public byte SecurityRequired;
+ public byte MaintainHandleCount;
+ public ushort MaintainTypeList;
+ public int PoolType;
+ public int PagedPoolUsage;
+ public int NonPagedPoolUsage;
+ }
+
+ private enum OBJECT_INFORMATION_CLASS
+ {
+ ObjectBasicInformation = 0,
+ ObjectNameInformation = 1,
+ ObjectTypeInformation = 2,
+ ObjectAllTypesInformation = 3,
+ ObjectHandleInformation = 4
+ }
+
+ private enum SYSTEM_INFORMATION_CLASS
+ {
+ SystemExtendedHandleInformation = 64
+ }
+
+ [Flags]
+ public enum DuplicateObjectOptions
+ {
+ None = 0,
+ CloseSource = 1,
+ SameAccess = 2,
+ SameAttributes = 4,
+ NoRightsUpgrade = 8
+ }
+
+ private const uint STATUS_INFO_LENGTH_MISMATCH = 0xC0000004;
+ private const int NT_SUCCESS = 0;
+
+ [DllImport("ntdll.dll")]
+ private static extern uint NtQuerySystemInformation(
+ SYSTEM_INFORMATION_CLASS systemInformationClass,
+ IntPtr systemInformation,
+ int systemInformationLength,
+ out int returnLength);
+
+ [DllImport("ntdll.dll", EntryPoint = "NtQueryObject")]
+ private static extern int NtQueryObject(
+ IntPtr handle,
+ OBJECT_INFORMATION_CLASS objectInformationClass,
+ IntPtr buf,
+ int bufLength,
+ ref int returnLength);
+
+ public static SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX[] QuerySystemHandleInformation()
+ {
+ for (var bufSize = 32 * 1024 * 1024 /* 32Mb */; bufSize <= 1024 * 1024 * 1024 /* 1Gb */; bufSize *= 2)
+ {
+ var buffer = Marshal.AllocHGlobal(bufSize);
+ try
+ {
+ var status = NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS.SystemExtendedHandleInformation, buffer, bufSize, out _);
+
+ if (status == NT_SUCCESS)
+ {
+ var handleInfo = Marshal.PtrToStructure(buffer);
+ return GetHandleEntries(handleInfo, buffer);
+ }
+
+ if (status != STATUS_INFO_LENGTH_MISMATCH)
+ {
+ throw new InvalidOperationException("NtQuerySystemInformation failed with status " + status);
+ }
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(buffer);
+ }
+ }
+
+ throw new InvalidOperationException("NtQuerySystemInformation buffer size is not enough");
+ }
+
+ public static string? GetHandleName(SafeFileHandle handle)
+ {
+ var buffer = QueryObjectInformation(handle, OBJECT_INFORMATION_CLASS.ObjectNameInformation, out _);
+ if (buffer == IntPtr.Zero)
+ {
+ return null;
+ }
+
+ try
+ {
+ var name = (UNICODE_STRING)Marshal.PtrToStructure(buffer, typeof(UNICODE_STRING));
+ return name.Length > 0 ? Marshal.PtrToStringUni(name.Buffer, name.Length / 2) : null;
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(buffer);
+ }
+ }
+
+ // Handle types:
+ // * ALPC Port
+ // * Composition
+ // * CoreMessaging
+ // * DebugObject
+ // * Desktop
+ // * Directory
+ // * DxgkCompositionObject
+ // * DxgkDisplayManagerObject
+ // * DxgkSharedResource
+ // * DxgkSharedSyncObject
+ // * EnergyTracker
+ // * EtwConsumer
+ // * EtwRegistration
+ // * Event
+ // * File
+ // * FilterCommunicationPort
+ // * FilterConnectionPort
+ // * IRTimer
+ // * IoCompletion
+ // * IoCompletionReserve
+ // * Job
+ // * Key
+ // * Mutant
+ // * Partition
+ // * PcwObject
+ // * PowerRequest
+ // * Process
+ // * RawInputManager
+ // * Section
+ // * Semaphore
+ // * Session
+ // * SymbolicLink
+ // * Thread
+ // * Timer
+ // * TmEn
+ // * TmRm
+ // * TmTm
+ // * Token
+ // * TpWorkerFactory
+ // * UserApcReserve
+ // * WaitCompletionPacket
+ // * WindowStation
+ // * WmiGuid
+ public static string? GetHandleType(SafeFileHandle handle)
+ {
+ var buffer = QueryObjectInformation(handle, OBJECT_INFORMATION_CLASS.ObjectTypeInformation, out _);
+ if (buffer == IntPtr.Zero)
+ {
+ return null;
+ }
+
+ try
+ {
+ var info = (OBJECT_TYPE_INFORMATION)Marshal.PtrToStructure(buffer, typeof(OBJECT_TYPE_INFORMATION));
+ return info.Name.Length > 0 ? Marshal.PtrToStringUni(info.Name.Buffer, info.Name.Length / 2) : string.Empty;
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(buffer);
+ }
+ }
+
+ private static IntPtr QueryObjectInformation(SafeFileHandle handle, OBJECT_INFORMATION_CLASS informationClass, out int length)
+ {
+ length = 0;
+ NtQueryObject(handle.DangerousGetHandle(), informationClass, IntPtr.Zero, 0, ref length);
+
+ if (length == 0)
+ {
+ return IntPtr.Zero;
+ }
+
+ var buffer = Marshal.AllocHGlobal(length);
+ if (NtQueryObject(handle.DangerousGetHandle(), informationClass, buffer, length, ref length) != 0)
+ {
+ Marshal.FreeHGlobal(buffer);
+ return IntPtr.Zero;
+ }
+
+ return buffer;
+ }
+
+ private static SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX[] GetHandleEntries(SYSTEM_HANDLE_INFORMATION_EX handleInfo, IntPtr buffer)
+ {
+ var numberOfHandles = handleInfo.NumberOfHandles.ToInt64();
+ var handleEntries = new SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX[numberOfHandles];
+
+ long handleSize = Marshal.SizeOf(typeof(SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX));
+ var current = new IntPtr(buffer.ToInt64() + Marshal.SizeOf(typeof(SYSTEM_HANDLE_INFORMATION_EX)) - handleSize);
+
+ for (long i = 0; i < numberOfHandles; i++)
+ {
+ var handleEntry = Marshal.PtrToStructure(current);
+ handleEntries[i] = handleEntry;
+ current = new IntPtr(current.ToInt64() + handleSize);
+ }
+
+ return handleEntries;
+ }
+}
diff --git a/src/Handle2/HandleInfo/Interop/WinApi.cs b/src/Handle2/HandleInfo/Interop/WinApi.cs
new file mode 100644
index 0000000..b1f29d9
--- /dev/null
+++ b/src/Handle2/HandleInfo/Interop/WinApi.cs
@@ -0,0 +1,179 @@
+using Microsoft.Win32.SafeHandles;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Handle2.HandleInfo.Interop;
+
+public class WinApi
+{
+ [Flags]
+ private enum StandardAccessRights : long
+ {
+ DELETE = 0x00010000L,
+ READ_CONTROL = 0x00020000L,
+ WRITE_DAC = 0x00040000L,
+ WRITE_OWNER = 0x00080000L,
+ SYNCHRONIZE = 0x00100000L,
+ STANDARD_RIGHTS_REQUIRED = 0x000F0000L
+ }
+
+ [Flags]
+ public enum ProcessAccessRights : ulong
+ {
+ PROCESS_ALL_ACCESS = StandardAccessRights.STANDARD_RIGHTS_REQUIRED | StandardAccessRights.SYNCHRONIZE | 0xFFFF,
+ PROCESS_TERMINATE = 0x0001,
+ PROCESS_CREATE_THREAD = 0x0002,
+ PROCESS_SET_SESSIONID = 0x0004,
+ PROCESS_VM_OPERATION = 0x0008,
+ PROCESS_VM_READ = 0x0010,
+ PROCESS_VM_WRITE = 0x0020,
+ PROCESS_DUP_HANDLE = 0x0040,
+ PROCESS_CREATE_PROCESS = 0x0080,
+ PROCESS_SET_QUOTA = 0x0100,
+ PROCESS_SET_INFORMATION = 0x0200,
+ PROCESS_QUERY_INFORMATION = 0x0400,
+ PROCESS_SUSPEND_RESUME = 0x0800,
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000,
+ PROCESS_SET_LIMITED_INFORMATION = 0x2000
+ }
+
+ [Flags]
+ private enum DuplicateObjectOptions : uint
+ {
+ DUPLICATE_CLOSE_SOURCE = 0x00000001,
+ DUPLICATE_SAME_ACCESS = 0x00000002
+ }
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern SafeProcessHandle OpenProcess(
+ ProcessAccessRights dwDesiredAccess,
+ [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
+ UIntPtr dwProcessId);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool DuplicateHandle(
+ SafeProcessHandle hSourceProcessHandle,
+ IntPtr hSourceHandle,
+ SafeProcessHandle hTargetProcessHandle,
+ out SafeFileHandle lpTargetHandle,
+ uint dwDesiredAccess,
+ [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
+ uint dwOptions);
+
+ public static SafeFileHandle DuplicateHandle(
+ SafeProcessHandle currentProcess,
+ SafeProcessHandle handleOwnerProcess,
+ NtDll.SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX handle)
+ {
+ var res = DuplicateHandle(
+ handleOwnerProcess,
+ handle.HandleValue,
+ currentProcess,
+ out var dupHandle,
+ 0,
+ false,
+ 0);
+ return res ? dupHandle : new SafeFileHandle(IntPtr.Zero, true);
+ }
+
+ [DllImport("Kernel32.dll", SetLastError = true, EntryPoint = "GetCurrentProcess")]
+ private static extern IntPtr GetCurrentProcessPrivate();
+
+ public static SafeProcessHandle GetCurrentProcess()
+ {
+ var handle = GetCurrentProcessPrivate();
+ return new SafeProcessHandle(handle, false);
+ }
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ private static extern int GetFinalPathNameByHandleW(SafeFileHandle hFile, [Out] StringBuilder filePathBuffer, int filePathBufferSize, int flags);
+
+ public static string? GetFinalPathNameByHandle(SafeFileHandle hFile)
+ {
+ var buf = new StringBuilder();
+ var result = GetFinalPathNameByHandleW(hFile, buf, buf.Capacity, 0);
+ if (result == 0)
+ {
+ return null;
+ }
+
+ buf.EnsureCapacity(result);
+ result = GetFinalPathNameByHandleW(hFile, buf, buf.Capacity, 0);
+ if (result == 0)
+ {
+ return null;
+ }
+
+ var str = buf.ToString();
+ return str.StartsWith(@"\\?\") ? str[4..] : str;
+ }
+
+ [Flags]
+ public enum TokenAccessRights : ulong
+ {
+ TOKEN_QUERY = 0x0008
+ }
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool OpenProcessToken(SafeProcessHandle processHandle, TokenAccessRights desiredAccess, out SafeFileHandle tokenHandle);
+
+ public enum FileType : uint
+ {
+ FILE_TYPE_UNKNOWN = 0x0000,
+ FILE_TYPE_DISK = 0x0001,
+ FILE_TYPE_CHAR = 0x0002,
+ FILE_TYPE_PIPE = 0x0003,
+ FILE_TYPE_REMOTE = 0x8000
+ }
+
+ [DllImport("Kernel32.dll", SetLastError = true)]
+ public static extern FileType GetFileType(SafeFileHandle hFile);
+
+ [Flags]
+ public enum FileDesiredAccess : uint
+ {
+ None = 0,
+ Synchronize = 0x00100000,
+ Delete = 0x00010000,
+ GenericRead = 0x80000000,
+ GenericWrite = 0x40000000,
+ FileReadAttributes = 0x0080,
+ FileWriteAttributes = 0x00100
+ }
+
+ [Flags]
+ public enum FileFlagsAndAttributes : uint
+ {
+ None = 0,
+ FileAttributeArchive = 0x20,
+ FileAttributeEncrypted = 0x4000,
+ FileAttributeHidden = 0x2,
+ FileAttributeNormal = 0x80,
+ FileAttributeOffline = 0x1000,
+ FileAttributeReadOnly = 0x1,
+ FileAttributeSystem = 0x4,
+ FileAttributeTemporary = 0x100,
+ FileFlagBackupSemantics = 0x02000000,
+ FileFlagDeleteOnClose = 0x04000000,
+ FileFlagNoBuffering = 0x20000000,
+ FileFlagOpenNoRecall = 0x00100000,
+ FileFlagOpenReparsePoint = 0x00200000,
+ FileFlagOverlapped = 0x40000000,
+ FileFlagPosixSemantics = 0x01000000,
+ FileFlagRandomAccess = 0x10000000,
+ FileFlagSessionAware = 0x00800000,
+ FileFlagSequentialScan = 0x08000000,
+ FileFlagWriteThrough = 0x80000000,
+ SecurityAnonymous = 0x00100000
+ }
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern SafeFileHandle ReOpenFile(
+ SafeFileHandle hOriginalFile,
+ FileDesiredAccess dwDesiredAccess,
+ FileShare dwShareMode,
+ FileFlagsAndAttributes dwFlagsAndAttributes);
+}
diff --git a/src/Handle2/HandleInfo/Utils/FileUtils.cs b/src/Handle2/HandleInfo/Utils/FileUtils.cs
new file mode 100644
index 0000000..3089d3a
--- /dev/null
+++ b/src/Handle2/HandleInfo/Utils/FileUtils.cs
@@ -0,0 +1,25 @@
+namespace Handle2.HandleInfo.Utils;
+
+public static class FileUtils
+{
+ public static string AddTrailingSeparatorIfItIsAFolder(string fileOrFolderPath)
+ {
+ return Directory.Exists(fileOrFolderPath) ? fileOrFolderPath + '\\' : fileOrFolderPath;
+ }
+
+ public static string ToCanonicalPath(string fileOrFolderPath)
+ {
+ var p = Path.GetFullPath(fileOrFolderPath);
+ if (!p.EndsWith('\\') && Directory.Exists(p))
+ {
+ p += '\\';
+ }
+
+ return p;
+ }
+
+ public static bool Exists(string path)
+ {
+ return File.Exists(path) || Directory.Exists(path);
+ }
+}
diff --git a/src/Handle2/HandleInfo/Utils/LinqUtils.cs b/src/Handle2/HandleInfo/Utils/LinqUtils.cs
new file mode 100644
index 0000000..8cb8d89
--- /dev/null
+++ b/src/Handle2/HandleInfo/Utils/LinqUtils.cs
@@ -0,0 +1,10 @@
+namespace Handle2.HandleInfo.Utils;
+
+internal static class LinqUtils
+{
+ public static void Deconstruct(this IGrouping grouping, out TKey key, out IEnumerable elements)
+ {
+ key = grouping.Key;
+ elements = grouping;
+ }
+}
diff --git a/src/Handle2/HandleInfo/Utils/ProcessUtils.cs b/src/Handle2/HandleInfo/Utils/ProcessUtils.cs
new file mode 100644
index 0000000..0859b66
--- /dev/null
+++ b/src/Handle2/HandleInfo/Utils/ProcessUtils.cs
@@ -0,0 +1,43 @@
+using Handle2.HandleInfo.Interop;
+using Microsoft.Win32.SafeHandles;
+using System.Diagnostics;
+using System.Security.Principal;
+
+namespace Handle2.HandleInfo.Utils;
+
+public static class ProcessUtils
+{
+ public static (string domain, string user) GetOwnerDomainAndUserNames(SafeProcessHandle openedProcess)
+ {
+ if (!WinApi.OpenProcessToken(openedProcess, WinApi.TokenAccessRights.TOKEN_QUERY, out var tokenHandle))
+ {
+ return ("", "");
+ }
+
+ using (tokenHandle)
+ {
+ using var wi = new WindowsIdentity(tokenHandle.DangerousGetHandle());
+ var domainAndUser = wi.Name.Split('\\');
+ return (domainAndUser[0], domainAndUser[1]);
+ }
+ }
+
+ public static string? GetExecutablePath(UIntPtr procId)
+ {
+ try
+ {
+ using var process = Process.GetProcessById((int)procId);
+ return process.MainModule.FileName;
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ public static SafeProcessHandle? OpenProcessToDuplicateHandle(UIntPtr pid)
+ {
+ var p = WinApi.OpenProcess(WinApi.ProcessAccessRights.PROCESS_DUP_HANDLE | WinApi.ProcessAccessRights.PROCESS_QUERY_INFORMATION, false, pid);
+ return p.IsInvalid ? null : p;
+ }
+}
diff --git a/src/Handle2/Program.cs b/src/Handle2/Program.cs
new file mode 100644
index 0000000..514ed27
--- /dev/null
+++ b/src/Handle2/Program.cs
@@ -0,0 +1,95 @@
+using CommandLine;
+using Handle2.HandleInfo;
+using Handle2.HandleInfo.Utils;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+if (args.Length == 0)
+ args = ["--help"];
+
+string? errorMessage = null;
+
+Parser.Default.ParseArguments(args)
+ .WithParsed(o =>
+ {
+ if (o.DumpAllHandles && o.Json)
+ {
+ Console.Write(
+ JsonSerializer.Serialize(
+ HandleInfoRetriever.GetAllProcInfos(),
+ new JsonSerializerOptions { WriteIndented = true, Converters = { new JsonStringEnumConverter() } }));
+ }
+
+ if (o.DumpAllHandles && !o.Json)
+ {
+ foreach (var procWithHandles in HandleInfoRetriever.GetAllProcInfos())
+ {
+ Console.WriteLine($"{procWithHandles.ProcessExecutablePath} pid: {procWithHandles.Pid} {procWithHandles.DomainName}/{procWithHandles.UserName}");
+
+ foreach (var handle in procWithHandles.Handles)
+ {
+ if (handle.Name == null)
+ {
+ continue;
+ }
+
+ var name = handle.FullNameIfItIsAFileOrAFolder ?? handle.Name;
+ Console.WriteLine($" {handle.HandleType,-25} {name}");
+ }
+
+ Console.WriteLine("------------------------------------------------------------------------------");
+ }
+ }
+
+ if (o.Path != null && !FileUtils.Exists(o.Path))
+ {
+ errorMessage = $"Error: {o.Path} does not exist.";
+ return;
+ }
+
+ if (o.Path != null && !o.Json)
+ {
+ foreach (var procWithHandles in HandleInfoRetriever.GetProcInfosLockingPath(o.Path))
+ {
+ Console.WriteLine($"{procWithHandles.ProcessExecutablePath} pid: {procWithHandles.Pid} {procWithHandles.DomainName}/{procWithHandles.UserName}");
+
+ foreach (var handle in procWithHandles.Handles)
+ {
+ Console.WriteLine($" {handle.FullNameIfItIsAFileOrAFolder}");
+ }
+
+ Console.WriteLine("------------------------------------------------------------------------------");
+ }
+ }
+
+ if (o.Path != null && o.Json)
+ {
+ var processesLockingPath = HandleInfoRetriever.GetProcInfosLockingPath(o.Path);
+
+ Console.Write(
+ JsonSerializer.Serialize(
+ processesLockingPath,
+ new JsonSerializerOptions { WriteIndented = true, Converters = { new JsonStringEnumConverter() } }));
+ }
+ });
+
+if (errorMessage != null)
+{
+ Console.WriteLine(errorMessage);
+ return 1;
+}
+
+return 0;
+
+public sealed class Options
+{
+ [Option("json", Required = false, Default = false,
+ HelpText = "JSON output. For details on the meanings of the fields provided, please consult the HandleInfo and SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX structures in the source code.")]
+ public bool Json { get; set; }
+
+ [Option("path", Required = true, SetName = "path", HelpText = "Displays the processes locking the path")]
+ public string? Path { get; set; }
+
+ [Option("dump-all-handles", Required = true, Default = false, SetName = "dump-all-handles", HelpText = "Displays information about all system handles.")]
+ public bool DumpAllHandles { get; set; }
+}
diff --git a/src/test/HandleInfo/Utils/FileUtilsTest.cs b/src/test/HandleInfo/Utils/FileUtilsTest.cs
new file mode 100644
index 0000000..7d7a7c5
--- /dev/null
+++ b/src/test/HandleInfo/Utils/FileUtilsTest.cs
@@ -0,0 +1,30 @@
+using Handle2.HandleInfo.Utils;
+using NUnit.Framework;
+
+namespace Test.HandleInfo.Utils;
+
+[TestFixture]
+internal class FileUtilsTest
+{
+ [Test]
+ public void AddTrailingSeparatorIfItIsAFolderTest()
+ {
+ Assert.That(FileUtils.AddTrailingSeparatorIfItIsAFolder(@"C:\Windows\System32"), Is.EqualTo(@"C:\Windows\System32\"));
+ Assert.That(FileUtils.AddTrailingSeparatorIfItIsAFolder(@"C:\Windows\System32\"), Is.EqualTo(@"C:\Windows\System32\\"));
+ Assert.That(FileUtils.ToCanonicalPath(@"C:\Windows\System32\ntdll.dll"), Is.EqualTo(@"C:\Windows\System32\ntdll.dll"));
+ }
+
+ [Test]
+ public void CanonicalPathTest()
+ {
+ Assert.That(FileUtils.ToCanonicalPath(@"C:\Windows\System32"), Is.EqualTo(@"C:\Windows\System32\"));
+ Assert.That(FileUtils.ToCanonicalPath(@"C:\Windows\System32\"), Is.EqualTo(@"C:\Windows\System32\"));
+ Assert.That(FileUtils.ToCanonicalPath(@"C:\Windows\System32\\"), Is.EqualTo(@"C:\Windows\System32\"));
+ Assert.That(FileUtils.ToCanonicalPath(@"C:\Windows\\System32/"), Is.EqualTo(@"C:\Windows\System32\"));
+ Assert.That(FileUtils.ToCanonicalPath(@"C:/Windows/System32"), Is.EqualTo(@"C:\Windows\System32\"));
+
+ Assert.That(FileUtils.ToCanonicalPath(@"C:/Windows/System32/ntdll.dll"), Is.EqualTo(@"C:\Windows\System32\ntdll.dll"));
+ Assert.That(FileUtils.ToCanonicalPath(@"C:/Windows/System32//ntdll.dll"), Is.EqualTo(@"C:\Windows\System32\ntdll.dll"));
+ Assert.That(FileUtils.ToCanonicalPath(@"C:\Windows\\System32\ntdll.dll"), Is.EqualTo(@"C:\Windows\System32\ntdll.dll"));
+ }
+}
diff --git a/src/test/ProgramTest.cs b/src/test/ProgramTest.cs
new file mode 100644
index 0000000..6eb98e8
--- /dev/null
+++ b/src/test/ProgramTest.cs
@@ -0,0 +1,136 @@
+using CliWrap;
+using CliWrap.Buffered;
+using NUnit.Framework;
+using System.Text;
+
+namespace Test;
+
+[TestFixture]
+public class ProgramTest
+{
+ [Test]
+ public void Prints_help_when_no_arguments_are_passed()
+ {
+ var res = RunHandle2([]);
+
+ Assert.That(res.ExitCode, Is.EqualTo(0));
+ Assert.That(res.StandardError, Contains.Substring("console utility that displays"));
+ }
+
+ [Test]
+ public void Prints_help_when_help_parameter_is_passed()
+ {
+ var res = RunHandle2(["--help"]);
+
+ Assert.That(res.ExitCode, Is.EqualTo(0));
+ Assert.That(res.StandardError, Contains.Substring("console utility that displays"));
+ }
+
+ [TestCase(["non existing parameter"])]
+ [TestCase(["--path", "123", "--dump-all-handles"])]
+ [TestCase(["--path"])]
+ [TestCase(["--path --json"])]
+ [TestCase(["--dump-all-handles param"])]
+ public void Prints_help_and_an_error_message_when_wrong_parameters_are_passed(object[] args)
+ {
+ var res = RunHandle2(args.Select(a => a.ToString()));
+
+ Assert.That(res.ExitCode, Is.EqualTo(0));
+ Assert.That(res.StandardError, Contains.Substring("console utility that displays"));
+ Assert.That(res.StandardError, Contains.Substring("ERROR(S):"));
+ }
+
+ [Test]
+ public void Dump_all_handle_information()
+ {
+ var res = RunHandle2(["--dump-all-handles"]);
+
+ Assert.That(res.ExitCode, Is.EqualTo(0));
+ Assert.That(res.StandardOutput, Contains.Substring(@"C:\Windows\system32\sihost.exe"));
+ Assert.That(res.StandardOutput, Contains.Substring(@"Desktop"));
+ Assert.That(res.StandardOutput, Contains.Substring(@"\Default"));
+ Assert.That(res.StandardOutput, Contains.Substring(@"C:\Windows\System32\"));
+ Assert.That(res.StandardOutput, Contains.Substring(@"C:\Windows\System32\en-US\KernelBase.dll.mui"));
+ }
+
+ [Test]
+ public void Dump_all_handle_information_json()
+ {
+ var res = RunHandle2(["--dump-all-handles", "--json"]);
+
+ Assert.That(res.ExitCode, Is.EqualTo(0));
+ Assert.That(res.StandardOutput, Contains.Substring(@"GrantedAccess"));
+ Assert.That(res.StandardOutput, Contains.Substring(@"FILE_TYPE_UNKNOWN"));
+ Assert.That(res.StandardOutput, Contains.Substring(@"C:\\Windows\\System32\\en-US\\propsys.dll.mui"));
+ Assert.That(res.StandardOutput, Contains.Substring(@"\\Device\\HarddiskVolume"));
+ Assert.That(res.StandardOutput, Contains.Substring(@" ""HandleType"": ""Section"","));
+ }
+
+ [Test]
+ public void Path_non_locked_folder()
+ {
+ var res = RunHandle2(["--path", @"C:\Windows\system.ini"]);
+
+ Assert.That(res.ExitCode, Is.EqualTo(0));
+ Assert.That(res.StandardOutput, Is.Empty);
+ }
+
+ [Test]
+ public void Path_that_does_not_exist()
+ {
+ var res = RunHandle2(["--path", @"C:\non_existing_path"]);
+
+ Assert.That(res.ExitCode, Is.EqualTo(1));
+ Assert.That(res.StandardOutput, Contains.Substring("Error:"));
+ Assert.That(res.StandardOutput, Contains.Substring("does not exist."));
+ }
+
+ [Test]
+ public void Path_json_that_does_not_exist()
+ {
+ var res = RunHandle2(["--path", @"C:\non_existing_path", "--json"]);
+
+ Assert.That(res.ExitCode, Is.EqualTo(1));
+ Assert.That(res.StandardOutput, Contains.Substring("Error:"));
+ Assert.That(res.StandardOutput, Contains.Substring("does not exist."));
+ }
+
+ [TestCase(@"C:\Windows")]
+ [TestCase(@"C:/Windows")]
+ [TestCase(@"C:/windows")]
+ public void Path_locked_folder(string path)
+ {
+ var res = RunHandle2(["--path", path]);
+
+ Assert.That(res.ExitCode, Is.EqualTo(0));
+ Assert.That(res.StandardOutput, Contains.Substring(@"C:\Windows\system32\svchost.exe"));
+ Assert.That(res.StandardOutput, Contains.Substring(@"C:\Windows\System32\"));
+ Assert.That(res.StandardOutput, Contains.Substring(@"C:\Windows\System32\en-US\KernelBase.dll.mui"));
+ Assert.That(res.StandardOutput, Does.Not.Contain(@"\\Device\\HarddiskVolume"));
+ }
+
+ [TestCase(@"C:\Windows")]
+ [TestCase(@"C:/Windows")]
+ [TestCase(@"C:/windows")]
+ public void Path_json_locked_folder(string path)
+ {
+ var res = RunHandle2(["--path", path, "--json"]);
+
+ Assert.That(res.ExitCode, Is.EqualTo(0));
+ Assert.That(res.StandardOutput, Contains.Substring(@" ""HandleType"": ""File"","));
+ Assert.That(res.StandardOutput, Contains.Substring(@"FILE_TYPE_DISK"));
+ Assert.That(res.StandardOutput, Contains.Substring(@"\\Device\\HarddiskVolume"));
+ Assert.That(res.StandardOutput, Does.Not.Contain(@"FILE_TYPE_UNKNOWN"));
+ }
+
+ private static BufferedCommandResult RunHandle2(IEnumerable args)
+ {
+ return Cli
+ .Wrap(Path.Combine(AppContext.BaseDirectory, "Handle2.exe"))
+ .WithValidation(CommandResultValidation.None)
+ .WithArguments(args)
+ .ExecuteBufferedAsync(Encoding.UTF8)
+ .GetAwaiter()
+ .GetResult();
+ }
+}
diff --git a/src/test/Test.csproj b/src/test/Test.csproj
new file mode 100644
index 0000000..19ca653
--- /dev/null
+++ b/src/test/Test.csproj
@@ -0,0 +1,23 @@
+
+
+
+ false
+ true
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+