Skip to content

Commit

Permalink
- Enhance locking info retrieval using patched Sysinternals Handle ut…
Browse files Browse the repository at this point in the history
…ility.

- 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.
  • Loading branch information
PolarGoose committed Sep 16, 2023
1 parent d025379 commit 5285b5c
Show file tree
Hide file tree
Showing 24 changed files with 416 additions and 192 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- run: .github/workflows/build.ps1
- run: .\build.ps1
- uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
draft: true
files: Build/Release/*.msi.zip
files: Build/Release/Installer/*.msi.zip
- uses: actions/upload-artifact@v3
with:
name: Build artifacts
path: Build/Release/*.msi.zip
path: Build/Release/Installer/*.msi.zip
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# ShowWhatProcessLocksFile
A utility to discover what processes lock a specific file or folder.
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:
* Supports older versions of Windows
* Lightweight

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

# System requirements
* 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))
* The user should be allowed to run applications as an Administrator.
* 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))

# How it works
The application uses [Handle2](https://github.com/PolarGoose/Handle2) to get information about locking processes.
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.

# How to use
* Download `ShowWhatProcessLocksFile.msi.zip` from the latest [release](https://github.com/PolarGoose/ShowWhatProcessLocksFile/releases).
* 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.
* 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.
* Use `Show what locks this file` File Explorer's context menu to select a file or folder
* To terminate a process, select it and open a context menu by clicking right mouse button
* 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.
* To terminate a process, select it and open a context menu by clicking the right mouse button
* 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.

# How to build
* To work with the codebase, use `Visual Studio 2022` with a plugin [HeatWave for VS2022](https://marketplace.visualstudio.com/items?itemName=FireGiant.FireGiantHeatWaveDev17).
* To build a release, run `.github\workflows\build.ps1` (`git.exe` should be in your PATH)
* To build a release, run `.\build.ps1` (`git.exe` should be in your PATH)
4 changes: 2 additions & 2 deletions ShowWhatProcessLocksFile.sln
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShowWhatProcessLocksFile",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test", "src\Test\Test.csproj", "{19D889E7-E728-4BFF-A68E-5445454F4417}"
EndProject
Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "Installer", "src\Installer\Installer.wixproj", "{8D8AB22B-5677-4A21-B25F-75F2409248EC}"
Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Installer", "src\Installer\Installer.wixproj", "{8D8AB22B-5677-4A21-B25F-75F2409248EC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Files", "Files", "{A8406D94-94FD-4135-875A-1A0FF1E54DC5}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitattributes = .gitattributes
.gitignore = .gitignore
.github\workflows\build.ps1 = .github\workflows\build.ps1
build.ps1 = build.ps1
src\Directory.Build.props = src\Directory.Build.props
.github\workflows\main.yaml = .github\workflows\main.yaml
nuget.config = nuget.config
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/build.ps1 → build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"

$root = Resolve-Path "$PSScriptRoot/../.."
$root = Resolve-Path "$PSScriptRoot"
$buildDir = "$root/build"
$publishDir = "$buildDir/Release"
$publishDir = "$buildDir/Release/Installer"
$projectName = "ShowWhatProcessLocksFile"
$version = GetVersion
$installerVersion = GetInstallerVersion $version
Expand Down
6 changes: 4 additions & 2 deletions src/App/Gui/Controls/ProcessInfoListView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
Content="{StaticResource CollapseAll_icon}" ToolTip="Collapse All" />
</StackPanel>

<ListBox x:Name="processInfoList" Grid.Row="1" SelectionMode="Extended"
ItemsSource="{Binding ProcessInfoViewModels}" ScrollViewer.CanContentScroll="False">
<ListBox Grid.Row="1"
SelectionMode="Extended"
ItemsSource="{Binding ProcessInfoViewModels}"
ScrollViewer.CanContentScroll="False">
<ListBox.ItemTemplate>
<DataTemplate>
<local:ProcessInfoView DataContext="{Binding}" />
Expand Down
11 changes: 7 additions & 4 deletions src/App/Gui/Controls/ProcessInfoView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@
<Image Grid.Column="1" Grid.Row="0" Source="{Binding Process.Icon}" Height="32" Width="32" Margin="5" />

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

<ItemsControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" x:Name="LockedFiles"
ItemsSource="{Binding Process.LockedFiles}"
<ItemsControl Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
ItemsSource="{Binding Process.LockedPath}"
Visibility="{Binding IsExpanded, Converter={StaticResource BooleanToCollapsedVisibilityConverter}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
Expand Down
30 changes: 30 additions & 0 deletions src/App/Gui/Icons.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,36 @@
</Rectangle>
</Viewbox>

<!-- UacShield.xaml -->
<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">
<Rectangle Width="16 " Height="16">
<Rectangle.Resources>
<SolidColorBrush x:Key="canvas" Opacity="0" />
<SolidColorBrush x:Key="light-defaultgrey" Color="#212121" Opacity="1" />
<SolidColorBrush x:Key="white" Color="#ffffff" Opacity="1" />
<SolidColorBrush x:Key="light-blue" Color="#005dba" Opacity="1" />
<SolidColorBrush x:Key="light-yellow" Color="#996f00" Opacity="1" />
</Rectangle.Resources>
<Rectangle.Fill>
<DrawingBrush Stretch="None">
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup x:Name="canvas">
<GeometryDrawing Brush="{DynamicResource canvas}" Geometry="F1M16,16H0V0H16Z" />
</DrawingGroup>
<DrawingGroup x:Name="level_1">
<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" />
<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" />
<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" />
<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" />
</DrawingGroup>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
</Viewbox>

<!-- StatusError.xaml -->
<Viewbox x:Key="StatusError_icon" x:Shared="false" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Rectangle Width="16 " Height="16">
Expand Down
16 changes: 14 additions & 2 deletions src/App/Gui/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ShowWhatProcessLocksFile.Gui"
xmlns:utils="clr-namespace:ShowWhatProcessLocksFile.Gui.Utils"
xmlns:controls="clr-namespace:ShowWhatProcessLocksFile.Gui.Controls"
mc:Ignorable="d"
x:Name="self"
Title="{Binding Title}" Height="450" Width="800"
d:DataContext="{d:DesignInstance Type=local:MainWindowViewModel, IsDesignTimeCreatable=False}">
<Window.Resources>
<utils:BooleanToCollapsedVisibilityConverter x:Key="BooleanToCollapsedVisibilityConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
Expand All @@ -19,12 +23,20 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>

<TextBox Margin="2" Grid.Column="0" IsReadOnly="True" Text="{Binding FilePath, Mode=OneWay}" FontSize="14"
Padding="0,2,0,2" />
<Button Grid.Column="1" Margin="2" Command="{Binding RefreshCommand}"
Content="{StaticResource Refresh_icon}" ToolTip="Refresh" />
<Button Grid.Column="1" Margin="2"
Command="{Binding RefreshCommand}"
Content="{StaticResource Refresh_icon}"
ToolTip="Refresh" />
<Button Grid.Column="2" Margin="2"
Command="{Binding RestartAsAdministratorCommand}"
Visibility="{Binding RelativeSource={RelativeSource Self}, Path=IsEnabled, Converter={StaticResource BooleanToCollapsedVisibilityConverter}}"
Content="{StaticResource UacShield_icon}"
ToolTip="Restart as administrator" />
</Grid>

<ContentControl Grid.Row="1" Content="{Binding MainControl}">
Expand Down
19 changes: 14 additions & 5 deletions src/App/Gui/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ namespace ShowWhatProcessLocksFile.Gui;

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

public RelayCommand RefreshCommand { get; }

public RelayCommand RestartAsAdministratorCommand { get; }

public string FilePath { get; }

private ViewModelBase mainControl;

public ViewModelBase MainControl
{
get => mainControl;
set
private set
{
mainControl = value;
OnPropertyChanged();
Expand All @@ -34,16 +36,23 @@ public MainWindowViewModel(string filePath)
{
FilePath = filePath;
RefreshCommand = new RelayCommand(GetLockingInformation, () => mainControl is not ProgressBarWithTextViewModel);
RestartAsAdministratorCommand = new RelayCommand(RestartAsAdministrator, () => !Elevation.IsUserAnAdmin());

GetLockingInformation();
}

public async void GetLockingInformation()
private void RestartAsAdministrator()
{
Elevation.RestartAsAdmin(FilePath);
}

private async void GetLockingInformation()
{
MainControl = new ProgressBarWithTextViewModel("Getting locking information");

try
{
var res = await LockFinder.FindWhatProcessesLockPath(FilePath);
var res = await Task.Run(() => LockFinder.FindWhatProcessesLockPath(FilePath).ToList());
MainControl = res.Any()
? new ProcessInfoListViewModel(res, OnProcessesKillRequested)
: ResultTextViewModel.Info("Nothing locks this file");
Expand All @@ -54,7 +63,7 @@ public async void GetLockingInformation()
}
}

public async void OnProcessesKillRequested(IEnumerable<ProcessInfo> processesToKill)
private async void OnProcessesKillRequested(IEnumerable<ProcessInfo> processesToKill)
{
MainControl = new ProgressBarWithTextViewModel("Killing processes");

Expand Down
71 changes: 54 additions & 17 deletions src/App/LockFinding/HandleExe.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,77 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
using CliWrap;
using CliWrap.Buffered;
using ShowWhatProcessLocksFile.LockFinding.Utils;

namespace ShowWhatProcessLocksFile.LockFinding;

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

public static class HandleExe
{
private static readonly string HandleExeFullName = Path.Combine(AppContext.BaseDirectory, "Handle2.exe");
private static readonly string HandleExeFullName = Path.Combine(AppContext.BaseDirectory, "handle64_v5.0_Unicode.exe");

public static IEnumerable<HandleInfo> Execute(string path)
{
var rawOutput = Launch(path);
return ParseRawOutput(rawOutput);
}

public static async Task<IEnumerable<LockingProcess>> GetProcessesLockingPath(string path)
private static string Launch(string path)
{
var res = await Cli
// Handle.exe doesn't work if a path contains "\" character at the end
path = PathUtils.RemoveTrailingPathSeparator(path);

var res = Cli
.Wrap(HandleExeFullName)
.WithValidation(CommandResultValidation.None)
.WithArguments(new[] { "--json", path })
.ExecuteBufferedAsync(Encoding.UTF8);
.WithArguments(new[] { "-u", "-nobanner", "-accepteula", "-v", path })
.ExecuteBufferedAsync(Encoding.UTF8)
.GetAwaiter()
.GetResult();
if (res.ExitCode != 0)
{
if (res.StandardOutput == "No matching handles found.\r\n")
{
return "";
}
throw new ApplicationException(
$"{HandleExeFullName} failed for '{path}'. ExitCode={res.ExitCode}. Error message:\n{res.StandardError}");
$"'{HandleExeFullName}' failed for '{path}'.\nExitCode={res.ExitCode}\nStdError:\n{res.StandardError}\nStdOut:\n{res.StandardOutput}");
}

return new JavaScriptSerializer().Deserialize<List<LockingProcess>>(res.StandardOutput);
return res.StandardOutput;
}

// The console output of Handle.exe looks like this:
// Process,PID,User,Handle,Type,Share Flags,Name,Access
// ipf_helper.exe,3456,File,Domain\UserName,0x00000050,C:\Windows\System32\DriverStore\FileRepository\ipf_cpu.inf_amd64_e6050705c26c770f
// sihost.exe,21260,File,Domain\UserName,0x00000044,C:\Windows\System32
// sihost.exe,21260,File,Domain\UserName,0x000005E0,C:\Windows\System32\en-US\windows.storage.dll.mui
// Notes:
// * The header doesn't match the actual data that is being printed. Therefore we have to ignore.
// * There is a trailing whitespace at the end of every line.
private static IEnumerable<HandleInfo> ParseRawOutput(string rawOutput)
{
return CsvParser.Parse(rawOutput).Skip(1).Select(line => new HandleInfo
{
ProcessName = line[0],
Pid = int.Parse(line[1]),
HandleType = line[2],
UserAndDomainName = line[3],
HandleAddress = Convert.ToInt64(line[4], 16),
LockedPath = line[5]
});
}
}
Loading

0 comments on commit 5285b5c

Please sign in to comment.