Skip to content

Commit 5285b5c

Browse files
committed
- Enhance locking info retrieval using patched Sysinternals Handle utility.
- Display the username for each process. - Allow the tool to run without admin privileges. - Introduce a button to restart the app in an elevated mode.
1 parent d025379 commit 5285b5c

24 files changed

Lines changed: 416 additions & 192 deletions

.github/workflows/main.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ jobs:
55
runs-on: windows-latest
66
steps:
77
- uses: actions/checkout@v3
8-
- run: .github/workflows/build.ps1
8+
- run: .\build.ps1
99
- uses: softprops/action-gh-release@v1
1010
if: startsWith(github.ref, 'refs/tags/')
1111
env:
1212
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1313
with:
1414
draft: true
15-
files: Build/Release/*.msi.zip
15+
files: Build/Release/Installer/*.msi.zip
1616
- uses: actions/upload-artifact@v3
1717
with:
1818
name: Build artifacts
19-
path: Build/Release/*.msi.zip
19+
path: Build/Release/Installer/*.msi.zip

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# ShowWhatProcessLocksFile
2-
A utility to discover what processes lock a specific file or folder.
2+
A simple clone of [PowerToys File Locksmith](https://learn.microsoft.com/en-us/windows/powertoys/file-locksmith) utility to discover what processes lock a specific file or folder that has the following advantages:
3+
* Supports older versions of Windows
4+
* Lightweight
35

46
# Screenshots
57
## Context menu
@@ -9,19 +11,18 @@ A utility to discover what processes lock a specific file or folder.
911
<img src="doc/Screenshot.png" width="70%" height="70%"/>
1012

1113
# System requirements
12-
* Windows 10 or higher (it can also work on Windows 8 if you install [.Net Framework 4.6.2](https://dotnet.microsoft.com/en-us/download/dotnet-framework/thank-you/net462-web-installer))
13-
* The user should be allowed to run applications as an Administrator.
14+
* Windows 8 x64 or higher (you might need to install [.Net Framework 4.6.2](https://dotnet.microsoft.com/en-us/download/dotnet-framework/thank-you/net462-web-installer))
1415

1516
# How it works
16-
The application uses [Handle2](https://github.com/PolarGoose/Handle2) to get information about locking processes.
17+
The application uses [Sysinternals Handle](https://learn.microsoft.com/en-us/sysinternals/downloads/handle) from the [Sysinternals-console-utils-with-Unicode-support](https://github.com/PolarGoose/Sysinternals-console-utils-with-Unicode-support) to get information about locking processes.
1718

1819
# How to use
1920
* Download `ShowWhatProcessLocksFile.msi.zip` from the latest [release](https://github.com/PolarGoose/ShowWhatProcessLocksFile/releases).
20-
* Run the installer. The installer will install this programm to the `%AppData%\ShowWhatProcessLocksFile` folder and add a `Show what locks this file` Windows File Explorer context menu element.
21+
* Run the installer. The installer will install this program to the `%AppData%\ShowWhatProcessLocksFile` folder and add a `Show what locks this file` Windows File Explorer context menu element.
2122
* Use `Show what locks this file` File Explorer's context menu to select a file or folder
22-
* To terminate a process, select it and open a context menu by clicking right mouse button
23-
* If you want to uninstall the program, use `Control Panel\Programs\Programs and Features`, uninstaller will remove an integration with the context menu and all files which were installed.
23+
* To terminate a process, select it and open a context menu by clicking the right mouse button
24+
* If you want to uninstall the program, use `Control Panel\Programs\Programs and Features`. Uninstaller will remove an integration with the context menu and all installed files.
2425

2526
# How to build
2627
* To work with the codebase, use `Visual Studio 2022` with a plugin [HeatWave for VS2022](https://marketplace.visualstudio.com/items?itemName=FireGiant.FireGiantHeatWaveDev17).
27-
* To build a release, run `.github\workflows\build.ps1` (`git.exe` should be in your PATH)
28+
* To build a release, run `.\build.ps1` (`git.exe` should be in your PATH)

ShowWhatProcessLocksFile.sln

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShowWhatProcessLocksFile",
66
EndProject
77
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test", "src\Test\Test.csproj", "{19D889E7-E728-4BFF-A68E-5445454F4417}"
88
EndProject
9-
Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "Installer", "src\Installer\Installer.wixproj", "{8D8AB22B-5677-4A21-B25F-75F2409248EC}"
9+
Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Installer", "src\Installer\Installer.wixproj", "{8D8AB22B-5677-4A21-B25F-75F2409248EC}"
1010
EndProject
1111
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Files", "Files", "{A8406D94-94FD-4135-875A-1A0FF1E54DC5}"
1212
ProjectSection(SolutionItems) = preProject
1313
.editorconfig = .editorconfig
1414
.gitattributes = .gitattributes
1515
.gitignore = .gitignore
16-
.github\workflows\build.ps1 = .github\workflows\build.ps1
16+
build.ps1 = build.ps1
1717
src\Directory.Build.props = src\Directory.Build.props
1818
.github\workflows\main.yaml = .github\workflows\main.yaml
1919
nuget.config = nuget.config
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ Set-StrictMode -Version Latest
6161
$ErrorActionPreference = "Stop"
6262
$ProgressPreference = "SilentlyContinue"
6363

64-
$root = Resolve-Path "$PSScriptRoot/../.."
64+
$root = Resolve-Path "$PSScriptRoot"
6565
$buildDir = "$root/build"
66-
$publishDir = "$buildDir/Release"
66+
$publishDir = "$buildDir/Release/Installer"
6767
$projectName = "ShowWhatProcessLocksFile"
6868
$version = GetVersion
6969
$installerVersion = GetInstallerVersion $version

src/App/Gui/Controls/ProcessInfoListView.xaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
Content="{StaticResource CollapseAll_icon}" ToolTip="Collapse All" />
2121
</StackPanel>
2222

23-
<ListBox x:Name="processInfoList" Grid.Row="1" SelectionMode="Extended"
24-
ItemsSource="{Binding ProcessInfoViewModels}" ScrollViewer.CanContentScroll="False">
23+
<ListBox Grid.Row="1"
24+
SelectionMode="Extended"
25+
ItemsSource="{Binding ProcessInfoViewModels}"
26+
ScrollViewer.CanContentScroll="False">
2527
<ListBox.ItemTemplate>
2628
<DataTemplate>
2729
<local:ProcessInfoView DataContext="{Binding}" />

src/App/Gui/Controls/ProcessInfoView.xaml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,16 @@
2828
<Image Grid.Column="1" Grid.Row="0" Source="{Binding Process.Icon}" Height="32" Width="32" Margin="5" />
2929

3030
<TextBlock Grid.Column="2" Grid.Row="0" Margin="5" FontSize="12">
31-
<Run FontWeight="Bold" Text="{Binding Process.Name, Mode=OneWay}" /> <LineBreak />
31+
<Run FontWeight="Bold" Text="{Binding Process.ProcessName, Mode=OneWay}" /> <LineBreak />
3232
Pid: <Run Text="{Binding Process.Pid, Mode=OneWay}" />,
33-
<Run Text="{Binding Process.ExecutableFullName, Mode=OneWay}" />
33+
User: <Run Text="{Binding Process.UserNameWithDomain, Mode=OneWay}" />,
34+
<Run Text="{Binding Process.ProcessExecutableFullName, Mode=OneWay}" />
3435
</TextBlock>
3536

36-
<ItemsControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" x:Name="LockedFiles"
37-
ItemsSource="{Binding Process.LockedFiles}"
37+
<ItemsControl Grid.Row="1"
38+
Grid.Column="0"
39+
Grid.ColumnSpan="3"
40+
ItemsSource="{Binding Process.LockedPath}"
3841
Visibility="{Binding IsExpanded, Converter={StaticResource BooleanToCollapsedVisibilityConverter}}">
3942
<ItemsControl.ItemTemplate>
4043
<DataTemplate>

src/App/Gui/Icons.xaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,36 @@
174174
</Rectangle>
175175
</Viewbox>
176176

177+
<!-- UacShield.xaml -->
178+
<Viewbox x:Key="UacShield_icon" Width="16 " Height="16" x:Shared="false" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:System="clr-namespace:System;assembly=mscorlib">
179+
<Rectangle Width="16 " Height="16">
180+
<Rectangle.Resources>
181+
<SolidColorBrush x:Key="canvas" Opacity="0" />
182+
<SolidColorBrush x:Key="light-defaultgrey" Color="#212121" Opacity="1" />
183+
<SolidColorBrush x:Key="white" Color="#ffffff" Opacity="1" />
184+
<SolidColorBrush x:Key="light-blue" Color="#005dba" Opacity="1" />
185+
<SolidColorBrush x:Key="light-yellow" Color="#996f00" Opacity="1" />
186+
</Rectangle.Resources>
187+
<Rectangle.Fill>
188+
<DrawingBrush Stretch="None">
189+
<DrawingBrush.Drawing>
190+
<DrawingGroup>
191+
<DrawingGroup x:Name="canvas">
192+
<GeometryDrawing Brush="{DynamicResource canvas}" Geometry="F1M16,16H0V0H16Z" />
193+
</DrawingGroup>
194+
<DrawingGroup x:Name="level_1">
195+
<GeometryDrawing Brush="{DynamicResource light-defaultgrey}" Geometry="F1M8.3,15.953l-.055.016H7.8l-.054-.016C1.629,14.2.721,7.37,1.093,4l.06-.534L8.021.031l6.868,3.437L14.948,4C15.321,7.37,14.413,14.2,8.3,15.953Z" />
196+
<GeometryDrawing Brush="{DynamicResource white}" Geometry="F1M13.838,7.583c-.05.326-.112.658-.19,1H8.521v6.308a2.013,2.013,0,0,1-.5.172,1.531,1.531,0,0,1-.5-.172V8.583H2.394c-.078-.342-.141-.674-.19-1H7.521V1.315l.5-.252.5.252V7.583Z" />
197+
<GeometryDrawing Brush="{DynamicResource light-blue}" Geometry="F1M13.648,8.583a8.189,8.189,0,0,1-5.127,6.291V8.583ZM8.521,14.875h0c-.169.061-.321.137-.5.189C8.2,15.011,8.352,14.936,8.521,14.875ZM7.516,1.317,2.07,4.062a15.31,15.31,0,0,0,.137,3.521H7.521V3.094h0ZM2.205,7.589h0V7.583h0Z" />
198+
<GeometryDrawing Brush="{DynamicResource light-yellow}" Geometry="F1M7.521,14.874c.169.061.32.137.5.189-.179-.052-.331-.112-.5-.172ZM2.394,8.583a8.186,8.186,0,0,0,5.127,6.291V8.583ZM13.972,4.062,8.516,1.312h.005V7.583h5.314A15.31,15.31,0,0,0,13.972,4.062Z" />
199+
</DrawingGroup>
200+
</DrawingGroup>
201+
</DrawingBrush.Drawing>
202+
</DrawingBrush>
203+
</Rectangle.Fill>
204+
</Rectangle>
205+
</Viewbox>
206+
177207
<!-- StatusError.xaml -->
178208
<Viewbox x:Key="StatusError_icon" x:Shared="false" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
179209
<Rectangle Width="16 " Height="16">

src/App/Gui/MainWindow.xaml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
55
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
66
xmlns:local="clr-namespace:ShowWhatProcessLocksFile.Gui"
7+
xmlns:utils="clr-namespace:ShowWhatProcessLocksFile.Gui.Utils"
78
xmlns:controls="clr-namespace:ShowWhatProcessLocksFile.Gui.Controls"
89
mc:Ignorable="d"
910
x:Name="self"
1011
Title="{Binding Title}" Height="450" Width="800"
1112
d:DataContext="{d:DesignInstance Type=local:MainWindowViewModel, IsDesignTimeCreatable=False}">
13+
<Window.Resources>
14+
<utils:BooleanToCollapsedVisibilityConverter x:Key="BooleanToCollapsedVisibilityConverter" />
15+
</Window.Resources>
1216
<Grid>
1317
<Grid.RowDefinitions>
1418
<RowDefinition Height="auto" />
@@ -19,12 +23,20 @@
1923
<Grid.ColumnDefinitions>
2024
<ColumnDefinition Width="*" />
2125
<ColumnDefinition Width="auto" />
26+
<ColumnDefinition Width="auto" />
2227
</Grid.ColumnDefinitions>
2328

2429
<TextBox Margin="2" Grid.Column="0" IsReadOnly="True" Text="{Binding FilePath, Mode=OneWay}" FontSize="14"
2530
Padding="0,2,0,2" />
26-
<Button Grid.Column="1" Margin="2" Command="{Binding RefreshCommand}"
27-
Content="{StaticResource Refresh_icon}" ToolTip="Refresh" />
31+
<Button Grid.Column="1" Margin="2"
32+
Command="{Binding RefreshCommand}"
33+
Content="{StaticResource Refresh_icon}"
34+
ToolTip="Refresh" />
35+
<Button Grid.Column="2" Margin="2"
36+
Command="{Binding RestartAsAdministratorCommand}"
37+
Visibility="{Binding RelativeSource={RelativeSource Self}, Path=IsEnabled, Converter={StaticResource BooleanToCollapsedVisibilityConverter}}"
38+
Content="{StaticResource UacShield_icon}"
39+
ToolTip="Restart as administrator" />
2840
</Grid>
2941

3042
<ContentControl Grid.Row="1" Content="{Binding MainControl}">

src/App/Gui/MainWindowViewModel.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@ namespace ShowWhatProcessLocksFile.Gui;
1111

1212
internal class MainWindowViewModel : ViewModelBase
1313
{
14-
public string Title => $"{AssemblyInfo.ProgramName} {AssemblyInfo.InformationalVersion}";
14+
public string Title => $"""{AssemblyInfo.ProgramName} {AssemblyInfo.InformationalVersion}{(Elevation.IsUserAnAdmin() ? " (Admin)" : "")}""";
1515

1616
public RelayCommand RefreshCommand { get; }
1717

18+
public RelayCommand RestartAsAdministratorCommand { get; }
19+
1820
public string FilePath { get; }
1921

2022
private ViewModelBase mainControl;
2123

2224
public ViewModelBase MainControl
2325
{
2426
get => mainControl;
25-
set
27+
private set
2628
{
2729
mainControl = value;
2830
OnPropertyChanged();
@@ -34,16 +36,23 @@ public MainWindowViewModel(string filePath)
3436
{
3537
FilePath = filePath;
3638
RefreshCommand = new RelayCommand(GetLockingInformation, () => mainControl is not ProgressBarWithTextViewModel);
39+
RestartAsAdministratorCommand = new RelayCommand(RestartAsAdministrator, () => !Elevation.IsUserAnAdmin());
40+
3741
GetLockingInformation();
3842
}
3943

40-
public async void GetLockingInformation()
44+
private void RestartAsAdministrator()
45+
{
46+
Elevation.RestartAsAdmin(FilePath);
47+
}
48+
49+
private async void GetLockingInformation()
4150
{
4251
MainControl = new ProgressBarWithTextViewModel("Getting locking information");
4352

4453
try
4554
{
46-
var res = await LockFinder.FindWhatProcessesLockPath(FilePath);
55+
var res = await Task.Run(() => LockFinder.FindWhatProcessesLockPath(FilePath).ToList());
4756
MainControl = res.Any()
4857
? new ProcessInfoListViewModel(res, OnProcessesKillRequested)
4958
: ResultTextViewModel.Info("Nothing locks this file");
@@ -54,7 +63,7 @@ public async void GetLockingInformation()
5463
}
5564
}
5665

57-
public async void OnProcessesKillRequested(IEnumerable<ProcessInfo> processesToKill)
66+
private async void OnProcessesKillRequested(IEnumerable<ProcessInfo> processesToKill)
5867
{
5968
MainControl = new ProgressBarWithTextViewModel("Killing processes");
6069

src/App/LockFinding/HandleExe.cs

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,77 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.Linq;
45
using System.Text;
5-
using System.Threading.Tasks;
6-
using System.Web.Script.Serialization;
76
using CliWrap;
87
using CliWrap.Buffered;
8+
using ShowWhatProcessLocksFile.LockFinding.Utils;
99

1010
namespace ShowWhatProcessLocksFile.LockFinding;
1111

12-
public class LockingProcess
13-
{
14-
public int pid;
15-
public string process_full_name;
16-
public string user;
17-
public string domain;
18-
public List<string> locked_paths;
19-
}
12+
// More information on the meaning of the fields that are printed by Handle.exe:
13+
// https://stackoverflow.com/questions/52701911/output-of-sysinternals-handle-exe
14+
public readonly record struct HandleInfo (
15+
string ProcessName,
16+
int Pid,
17+
string HandleType,
18+
string UserAndDomainName,
19+
long HandleAddress,
20+
string LockedPath);
2021

2122
public static class HandleExe
2223
{
23-
private static readonly string HandleExeFullName = Path.Combine(AppContext.BaseDirectory, "Handle2.exe");
24+
private static readonly string HandleExeFullName = Path.Combine(AppContext.BaseDirectory, "handle64_v5.0_Unicode.exe");
25+
26+
public static IEnumerable<HandleInfo> Execute(string path)
27+
{
28+
var rawOutput = Launch(path);
29+
return ParseRawOutput(rawOutput);
30+
}
2431

25-
public static async Task<IEnumerable<LockingProcess>> GetProcessesLockingPath(string path)
32+
private static string Launch(string path)
2633
{
27-
var res = await Cli
34+
// Handle.exe doesn't work if a path contains "\" character at the end
35+
path = PathUtils.RemoveTrailingPathSeparator(path);
36+
37+
var res = Cli
2838
.Wrap(HandleExeFullName)
2939
.WithValidation(CommandResultValidation.None)
30-
.WithArguments(new[] { "--json", path })
31-
.ExecuteBufferedAsync(Encoding.UTF8);
40+
.WithArguments(new[] { "-u", "-nobanner", "-accepteula", "-v", path })
41+
.ExecuteBufferedAsync(Encoding.UTF8)
42+
.GetAwaiter()
43+
.GetResult();
3244
if (res.ExitCode != 0)
3345
{
46+
if (res.StandardOutput == "No matching handles found.\r\n")
47+
{
48+
return "";
49+
}
3450
throw new ApplicationException(
35-
$"{HandleExeFullName} failed for '{path}'. ExitCode={res.ExitCode}. Error message:\n{res.StandardError}");
51+
$"'{HandleExeFullName}' failed for '{path}'.\nExitCode={res.ExitCode}\nStdError:\n{res.StandardError}\nStdOut:\n{res.StandardOutput}");
3652
}
3753

38-
return new JavaScriptSerializer().Deserialize<List<LockingProcess>>(res.StandardOutput);
54+
return res.StandardOutput;
55+
}
56+
57+
// The console output of Handle.exe looks like this:
58+
// Process,PID,User,Handle,Type,Share Flags,Name,Access
59+
// ipf_helper.exe,3456,File,Domain\UserName,0x00000050,C:\Windows\System32\DriverStore\FileRepository\ipf_cpu.inf_amd64_e6050705c26c770f
60+
// sihost.exe,21260,File,Domain\UserName,0x00000044,C:\Windows\System32
61+
// sihost.exe,21260,File,Domain\UserName,0x000005E0,C:\Windows\System32\en-US\windows.storage.dll.mui
62+
// Notes:
63+
// * The header doesn't match the actual data that is being printed. Therefore we have to ignore.
64+
// * There is a trailing whitespace at the end of every line.
65+
private static IEnumerable<HandleInfo> ParseRawOutput(string rawOutput)
66+
{
67+
return CsvParser.Parse(rawOutput).Skip(1).Select(line => new HandleInfo
68+
{
69+
ProcessName = line[0],
70+
Pid = int.Parse(line[1]),
71+
HandleType = line[2],
72+
UserAndDomainName = line[3],
73+
HandleAddress = Convert.ToInt64(line[4], 16),
74+
LockedPath = line[5]
75+
});
3976
}
4077
}

0 commit comments

Comments
 (0)