From 5285b5cfee54ce36edc02955f93e51a93c5b5fe2 Mon Sep 17 00:00:00 2001
From: PolarGoose <35307286+PolarGoose@users.noreply.github.com>
Date: Sat, 16 Sep 2023 19:17:26 +0200
Subject: [PATCH] - 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.
---
.github/workflows/main.yaml | 6 +-
README.md | 17 +--
ShowWhatProcessLocksFile.sln | 4 +-
.github/workflows/build.ps1 => build.ps1 | 4 +-
src/App/Gui/Controls/ProcessInfoListView.xaml | 6 +-
src/App/Gui/Controls/ProcessInfoView.xaml | 11 +-
src/App/Gui/Icons.xaml | 30 ++++
src/App/Gui/MainWindow.xaml | 16 ++-
src/App/Gui/MainWindowViewModel.cs | 19 ++-
src/App/LockFinding/HandleExe.cs | 71 +++++++---
src/App/LockFinding/LockFinder.cs | 131 ++++++++++++------
src/App/LockFinding/Utils/CsvParser.cs | 19 +++
src/App/LockFinding/Utils/PathUtils.cs | 18 +++
src/App/ShowWhatProcessLocksFile.csproj | 22 +--
src/App/Utils/CommandLineParser.cs | 2 +-
src/App/Utils/Elevation.cs | 26 ++++
src/App/Utils/ProcessKiller.cs | 6 +-
src/App/app.manifest | 11 --
src/Directory.Build.props | 4 +-
src/Installer/Installer.wixproj | 2 +-
src/Installer/Product.wxs | 31 +++--
src/Test/LockFinderTest.cs | 56 --------
src/Test/LockFinding/LockFinderTest.cs | 80 +++++++++++
src/Test/Test.csproj | 16 ++-
24 files changed, 416 insertions(+), 192 deletions(-)
rename .github/workflows/build.ps1 => build.ps1 (96%)
create mode 100644 src/App/LockFinding/Utils/CsvParser.cs
create mode 100644 src/App/LockFinding/Utils/PathUtils.cs
create mode 100644 src/App/Utils/Elevation.cs
delete mode 100644 src/App/app.manifest
delete mode 100644 src/Test/LockFinderTest.cs
create mode 100644 src/Test/LockFinding/LockFinderTest.cs
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index 20ba469..1d95298 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -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
diff --git a/README.md b/README.md
index f15506d..a259f02 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -9,19 +11,18 @@ A utility to discover what processes lock a specific file or folder.
# 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)
diff --git a/ShowWhatProcessLocksFile.sln b/ShowWhatProcessLocksFile.sln
index 3037700..fa79a9c 100644
--- a/ShowWhatProcessLocksFile.sln
+++ b/ShowWhatProcessLocksFile.sln
@@ -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
diff --git a/.github/workflows/build.ps1 b/build.ps1
similarity index 96%
rename from .github/workflows/build.ps1
rename to build.ps1
index 0f7084b..7c0460d 100644
--- a/.github/workflows/build.ps1
+++ b/build.ps1
@@ -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
diff --git a/src/App/Gui/Controls/ProcessInfoListView.xaml b/src/App/Gui/Controls/ProcessInfoListView.xaml
index 3629090..6a26325 100644
--- a/src/App/Gui/Controls/ProcessInfoListView.xaml
+++ b/src/App/Gui/Controls/ProcessInfoListView.xaml
@@ -20,8 +20,10 @@
Content="{StaticResource CollapseAll_icon}" ToolTip="Collapse All" />
-
+
diff --git a/src/App/Gui/Controls/ProcessInfoView.xaml b/src/App/Gui/Controls/ProcessInfoView.xaml
index 8e2f452..80ca8b8 100644
--- a/src/App/Gui/Controls/ProcessInfoView.xaml
+++ b/src/App/Gui/Controls/ProcessInfoView.xaml
@@ -28,13 +28,16 @@
-
+
Pid: ,
-
+ User: ,
+
-
diff --git a/src/App/Gui/Icons.xaml b/src/App/Gui/Icons.xaml
index bef0c1f..a408a12 100644
--- a/src/App/Gui/Icons.xaml
+++ b/src/App/Gui/Icons.xaml
@@ -174,6 +174,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App/Gui/MainWindow.xaml b/src/App/Gui/MainWindow.xaml
index 18b442c..fd69467 100644
--- a/src/App/Gui/MainWindow.xaml
+++ b/src/App/Gui/MainWindow.xaml
@@ -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}">
+
+
+
@@ -19,12 +23,20 @@
+
-
+
+
diff --git a/src/App/Gui/MainWindowViewModel.cs b/src/App/Gui/MainWindowViewModel.cs
index 0686690..145d1c2 100644
--- a/src/App/Gui/MainWindowViewModel.cs
+++ b/src/App/Gui/MainWindowViewModel.cs
@@ -11,10 +11,12 @@ 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;
@@ -22,7 +24,7 @@ internal class MainWindowViewModel : ViewModelBase
public ViewModelBase MainControl
{
get => mainControl;
- set
+ private set
{
mainControl = value;
OnPropertyChanged();
@@ -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");
@@ -54,7 +63,7 @@ public async void GetLockingInformation()
}
}
- public async void OnProcessesKillRequested(IEnumerable processesToKill)
+ private async void OnProcessesKillRequested(IEnumerable processesToKill)
{
MainControl = new ProgressBarWithTextViewModel("Killing processes");
diff --git a/src/App/LockFinding/HandleExe.cs b/src/App/LockFinding/HandleExe.cs
index 0449e18..bc18b20 100644
--- a/src/App/LockFinding/HandleExe.cs
+++ b/src/App/LockFinding/HandleExe.cs
@@ -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 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 Execute(string path)
+ {
+ var rawOutput = Launch(path);
+ return ParseRawOutput(rawOutput);
+ }
- public static async Task> 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>(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 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]
+ });
}
}
diff --git a/src/App/LockFinding/LockFinder.cs b/src/App/LockFinding/LockFinder.cs
index 2a55536..f977584 100644
--- a/src/App/LockFinding/LockFinder.cs
+++ b/src/App/LockFinding/LockFinder.cs
@@ -1,82 +1,123 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Drawing;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using System.Windows;
using System.Windows.Interop;
+using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
-using ShowWhatProcessLocksFile.Utils;
+using System.Linq;
+using ShowWhatProcessLocksFile.LockFinding.Utils;
namespace ShowWhatProcessLocksFile.LockFinding;
-public class ProcessInfo
+public readonly record struct ProcessInfo(
+ int Pid,
+ string ProcessName,
+ string ProcessExecutableFullName,
+ string UserNameWithDomain,
+ ImageSource Icon,
+ IEnumerable LockedPath);
+
+public static class LockFinder
{
- public int Pid { get; }
- public string Name { get; }
- public string ExecutableFullName { get; }
- public string UserName { get; }
- public ImageSource Icon { get; }
- public IEnumerable LockedFiles { get; }
-
- public ProcessInfo(int pid, string processName, string executableFullName, ImageSource icon, string userName,
- IEnumerable lockedFiles)
+ public static IEnumerable FindWhatProcessesLockPath(string path)
{
- Pid = pid;
- Name = processName;
- ExecutableFullName = executableFullName;
- Icon = icon;
- UserName = userName;
- LockedFiles = lockedFiles;
+ var output = HandleExe.Execute(path);
+
+ var fileHandles = output.Where(el => el.HandleType == "File");
+
+ // File path we supply to Handle.exe is treated as "Show locks for all entities whose names start with this path".
+ // It is not what we want, because if we ask to show what locks "C:\Program Files" folder,
+ // it will also show processes which lock "C:\Program Files (x86)" folder.
+ var handlesFromSpecifiedPath = fileHandles
+ .Where(p =>
+ string.Equals(p.LockedPath, path, StringComparison.OrdinalIgnoreCase) || PathUtils.IsInsideFolder(p.LockedPath, path));
+
+ // There can be several records corresponding to the same process.
+ var handlesBelongingToTheSameProcess = handlesFromSpecifiedPath.GroupBy(el => el.Pid);
+
+ // There can be several records corresponding to the same file locked by the same process but with a different HandleAddress.
+ var handlesBelongingToTheSameProcessWithoutDuplicates = handlesBelongingToTheSameProcess
+ .Select(el => el.GroupBy(e => e.LockedPath));
+
+ var processInfo = handlesBelongingToTheSameProcessWithoutDuplicates
+ .Select(ToProcessInfo);
+
+ var orderedProcessInfo = processInfo
+ .Where(el => el != null)
+ .Select(el => el.Value)
+ .OrderBy(el => el.ProcessName);
+
+ return orderedProcessInfo;
}
-}
-public static class LockFinder
-{
- public static async Task> FindWhatProcessesLockPath(string path)
+ private static ProcessInfo? ToProcessInfo(IEnumerable> el)
+ {
+ var (processName, pid, _, userNameWithDomain, _, _) = el.First().First();
+
+ var process = TryGetProcess(pid);
+ if (process == null)
+ {
+ return null;
+ }
+
+ var processFullName = TryGetProcessFullName(process);
+ var icon = TryGetIcon(processFullName);
+ var lockedPath = el
+ .Select(el => el.Key)
+ .OrderBy(el => el);
+
+ return new ProcessInfo(
+ Pid: pid,
+ ProcessName: processName,
+ ProcessExecutableFullName: processFullName,
+ UserNameWithDomain: userNameWithDomain,
+ Icon: icon,
+ LockedPath: lockedPath);
+ }
+
+ private static Process TryGetProcess(int pid)
{
- var res = await HandleExe.GetProcessesLockingPath(path);
- return res.Select(ToProcessInfo).ToList();
+ try
+ {
+ return Process.GetProcessById(pid);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
}
- private static ProcessInfo ToProcessInfo(LockingProcess p)
+ private static string TryGetProcessFullName(Process process)
{
try
{
- return new ProcessInfo(
- pid: p.pid,
- processName: Path.GetFileName(p.process_full_name),
- executableFullName: p.process_full_name,
- icon: TryGetIcon(p.process_full_name),
- userName: @$"{p.domain}\{p.user}",
- lockedFiles: p.locked_paths.OrderBy(x => x));
+ return process.MainModule.FileName;
}
- catch (Exception ex)
+ catch (Exception)
{
- Log.Warn($"Failed to create a process info from: '{p.pid}' '{p.process_full_name}'. Exception:\n{ex}");
return null;
}
}
- private static ImageSource TryGetIcon(string processExecutable)
+ private static ImageSource TryGetIcon(string executableFullName)
{
- // There are processes for which it is not possible to get an icon.
- // One of such processes is "System"
+ if (executableFullName == null)
+ {
+ return null;
+ }
try
{
- using var ico = Icon.ExtractAssociatedIcon(processExecutable);
- var image = Imaging.CreateBitmapSourceFromHIcon(ico.Handle, Int32Rect.Empty,
- BitmapSizeOptions.FromEmptyOptions());
+ using var ico = Icon.ExtractAssociatedIcon(executableFullName);
+ var image = Imaging.CreateBitmapSourceFromHIcon(ico.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
// We need to freeze the image, otherwise the GUI thread will not be able to use it if this function was called from another process
image.Freeze();
return image;
}
- catch (Exception ex)
+ catch (Exception)
{
- Log.Warn($"Failed to get an icon from executable '{processExecutable}'. Exception:\n{ex}");
return null;
}
}
diff --git a/src/App/LockFinding/Utils/CsvParser.cs b/src/App/LockFinding/Utils/CsvParser.cs
new file mode 100644
index 0000000..0eeae91
--- /dev/null
+++ b/src/App/LockFinding/Utils/CsvParser.cs
@@ -0,0 +1,19 @@
+using Microsoft.VisualBasic.FileIO;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace ShowWhatProcessLocksFile.LockFinding.Utils;
+
+internal static class CsvParser
+{
+ public static IEnumerable Parse(string csv)
+ {
+ using var csvStream = new MemoryStream(Encoding.UTF8.GetBytes(csv));
+ using var parser = new TextFieldParser(csvStream) { TextFieldType = FieldType.Delimited, Delimiters = new[] { "," } };
+ while (!parser.EndOfData)
+ {
+ yield return parser.ReadFields();
+ }
+ }
+}
diff --git a/src/App/LockFinding/Utils/PathUtils.cs b/src/App/LockFinding/Utils/PathUtils.cs
new file mode 100644
index 0000000..0086c64
--- /dev/null
+++ b/src/App/LockFinding/Utils/PathUtils.cs
@@ -0,0 +1,18 @@
+using System;
+using System.IO;
+
+namespace ShowWhatProcessLocksFile.LockFinding.Utils;
+
+public static class PathUtils
+{
+ public static string RemoveTrailingPathSeparator(string path)
+ {
+ path = Path.GetFullPath(path);
+ return path.TrimEnd('\\');
+ }
+
+ public static bool IsInsideFolder(string path, string folder)
+ {
+ return path.StartsWith($@"{RemoveTrailingPathSeparator(folder)}\", StringComparison.InvariantCultureIgnoreCase);
+ }
+}
diff --git a/src/App/ShowWhatProcessLocksFile.csproj b/src/App/ShowWhatProcessLocksFile.csproj
index f1a186b..9044220 100644
--- a/src/App/ShowWhatProcessLocksFile.csproj
+++ b/src/App/ShowWhatProcessLocksFile.csproj
@@ -3,27 +3,31 @@
WinExe
net462
true
- app.manifest
Icon\icon.ico
latest
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
diff --git a/src/App/Utils/CommandLineParser.cs b/src/App/Utils/CommandLineParser.cs
index 5840ae6..45dcd8f 100644
--- a/src/App/Utils/CommandLineParser.cs
+++ b/src/App/Utils/CommandLineParser.cs
@@ -46,7 +46,7 @@ private static bool Exists(string path)
}
}
- public static bool IsUncPath(this string path)
+ private static bool IsUncPath(this string path)
{
return path.StartsWith(@"\\");
}
diff --git a/src/App/Utils/Elevation.cs b/src/App/Utils/Elevation.cs
new file mode 100644
index 0000000..2ab4e0a
--- /dev/null
+++ b/src/App/Utils/Elevation.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+namespace ShowWhatProcessLocksFile.Utils;
+
+internal static class Elevation
+{
+ public static void RestartAsAdmin(string path)
+ {
+ new Process
+ {
+ StartInfo = new ProcessStartInfo(Assembly.GetExecutingAssembly().Location, $"\"{path}\"")
+ {
+ UseShellExecute = true,
+ Verb = "runas"
+ }
+ }.Start();
+ Application.Current.Shutdown();
+ }
+
+ [DllImport("shell32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool IsUserAnAdmin();
+}
diff --git a/src/App/Utils/ProcessKiller.cs b/src/App/Utils/ProcessKiller.cs
index 9f2e121..3a9aa6f 100644
--- a/src/App/Utils/ProcessKiller.cs
+++ b/src/App/Utils/ProcessKiller.cs
@@ -11,19 +11,19 @@ public static void Kill(IEnumerable processes)
{
foreach (var p in processes)
{
- Log.Info($"Request to kill '{p.Name}' Pid:{p.Pid}");
+ Log.Info($"Request to kill '{p.ProcessName}' Pid:{p.Pid}");
var process = Process.GetProcessById(p.Pid);
process.Kill();
}
foreach (var p in processes)
{
- Log.Info($"Waiting for '{p.Name}' Pid:{p.Pid}");
+ Log.Info($"Waiting for '{p.ProcessName}' Pid:{p.Pid}");
var process = Process.GetProcessById(p.Pid);
var res = process.WaitForExit(3000);
if (!res)
{
- throw new Exception($"Timeout waiting for a process to exit: Pid={p.Pid} Name='{p.Name}'");
+ throw new Exception($"Timeout waiting for a process to exit: Pid={p.Pid} Name='{p.ProcessName}'");
}
}
}
diff --git a/src/App/app.manifest b/src/App/app.manifest
deleted file mode 100644
index 5f22ee7..0000000
--- a/src/App/app.manifest
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index c12eb52..aefa590 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -2,8 +2,8 @@
$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\'))
$(ProjectRoot)build\
- $(BuildFolder)$(Configuration)
- $(BuildFolder)obj\$(MSBuildProjectName)\
+ $(BuildFolder)$(Configuration)\$(MSBuildProjectName)
+ $(BuildFolder)obj\$(MSBuildProjectName)\$(Configuration)\
x64
5
true
diff --git a/src/Installer/Installer.wixproj b/src/Installer/Installer.wixproj
index e671775..1329e3f 100644
--- a/src/Installer/Installer.wixproj
+++ b/src/Installer/Installer.wixproj
@@ -1,4 +1,4 @@
-
+
ShowWhatProcessLocksFile
True
diff --git a/src/Installer/Product.wxs b/src/Installer/Product.wxs
index c8ec465..a3b8a2e 100644
--- a/src/Installer/Product.wxs
+++ b/src/Installer/Product.wxs
@@ -1,5 +1,11 @@
-
+
@@ -14,18 +20,17 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Test/LockFinderTest.cs b/src/Test/LockFinderTest.cs
deleted file mode 100644
index d1452fe..0000000
--- a/src/Test/LockFinderTest.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using NUnit.Framework;
-using ShowWhatProcessLocksFile.LockFinding;
-
-namespace Test;
-
-[TestFixture]
-public class LockFinderTest
-{
- [Test]
- public async Task LockedFolder()
- {
- var res = await LockFinder.FindWhatProcessesLockPath(@"C:\");
-
- // Check that has a "System" process
- {
- var proc = res.First(p => p.ExecutableFullName == "System");
- Assert.IsNull(proc.Icon);
- Assert.AreEqual(@"NT AUTHORITY\SYSTEM", proc.UserName);
- Assert.AreEqual(4, proc.Pid);
- }
-
- // Check that has a "svchost" process
- {
- var proc = res.First(p =>
- p.ExecutableFullName == "C:\\Windows\\System32\\svchost.exe"
- && p.UserName == System.Security.Principal.WindowsIdentity.GetCurrent().Name);
- Assert.IsNotNull(proc.Icon);
- Assert.Contains("C:\\Windows\\System32\\en-US\\svchost.exe.mui", proc.LockedFiles.ToList());
- }
-
- // Check that has a "wininit" process
- {
- var proc = res.First(p => p.ExecutableFullName == "C:\\Windows\\System32\\wininit.exe");
- Assert.IsNotNull(proc.Icon);
- Assert.AreEqual(@"NT AUTHORITY\SYSTEM", proc.UserName);
- Assert.Contains("C:\\Windows\\System32\\en-US\\user32.dll.mui", proc.LockedFiles.ToList());
- }
- }
-
- [Test]
- public async Task NonLockedFolder()
- {
- var res = await LockFinder.FindWhatProcessesLockPath(@"C:\Users\Public\Documents");
- Assert.IsEmpty(res);
- }
-
- [Test]
- public void NonExistingFolder()
- {
- Assert.ThrowsAsync(async () =>
- await HandleExe.GetProcessesLockingPath(@"C:\NonExistentFolderBlahBlah"));
- }
-}
diff --git a/src/Test/LockFinding/LockFinderTest.cs b/src/Test/LockFinding/LockFinderTest.cs
new file mode 100644
index 0000000..6d4a617
--- /dev/null
+++ b/src/Test/LockFinding/LockFinderTest.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using ShowWhatProcessLocksFile.LockFinding;
+
+namespace Test.LockFinding;
+
+[TestFixture]
+public class LockFinderTest
+{
+ [TestCase(@"C:\PathThatDoesNotExist")]
+ [TestCase(@"C:\Windows\system.ini")] // existing but not locked path
+ public void Returns_empty_list_If_path_does_mot_exist_or_not_locked(string path)
+ {
+ var processes = LockFinder.FindWhatProcessesLockPath(path);
+ Assert.IsEmpty(processes);
+ }
+
+ [TestCase(
+ @"C:\Windows",
+ "svchost.exe",
+ new[] {
+ @"C:\Windows\System32\en-US\svchost.exe.mui",
+ @"C:\Windows\System32" })]
+ [TestCase(
+ @"C:\Windows\",
+ "svchost.exe",
+ new[] {
+ @"C:\Windows\System32\en-US\svchost.exe.mui",
+ @"C:\Windows\System32" })]
+ [TestCase(
+ @"C:\Windows",
+ "explorer.exe",
+ new[] {
+ @"C:\Windows\en-US\explorer.exe.mui",
+ @"C:\Windows\System32" })]
+ public void If_path_is_locked_Returns_information_about_processes_that_lock_this_path(string path, string processName, IEnumerable pathThatShouldBeLocked)
+ {
+ var processes = LockFinder.FindWhatProcessesLockPath(path).ToList();
+
+ var info = AssertContainsProcessInfo(
+ processes,
+ p => p.ProcessName == processName,
+ $"{processName} process should lock files in the '{path}'");
+ foreach (var p in pathThatShouldBeLocked)
+ {
+ AssertLocksPath(info, p);
+ }
+ }
+
+ [Test]
+ public void Returns_only_information_related_to_the_requested_path()
+ {
+ var processes = LockFinder.FindWhatProcessesLockPath(@"C:\Program Files").ToList();
+ foreach (var lockedPath in processes.SelectMany(proc => proc.LockedPath))
+ {
+ StringAssert.DoesNotStartWith(@"C:\Program Files (x86)", lockedPath);
+ }
+ }
+
+ private static ProcessInfo AssertContainsProcessInfo(IEnumerable processes, Predicate condition, string errorMessage = null)
+ {
+ foreach (var proc in processes)
+ {
+ if (condition(proc))
+ {
+ return proc;
+ }
+ }
+ throw new AssertionException(errorMessage);
+ }
+
+ private static void AssertLocksPath(ProcessInfo process, string lockedPath)
+ {
+ var path = process.LockedPath.FirstOrDefault(p => p == lockedPath);
+ Assert.IsNotNull(path,
+ $"{process}\ndoesn't lock the path '{lockedPath}'.\nIt only locks the following paths:\n* {string.Join("\n* ", process.LockedPath)}");
+ }
+}
diff --git a/src/Test/Test.csproj b/src/Test/Test.csproj
index 9ff41ae..ec3b263 100644
--- a/src/Test/Test.csproj
+++ b/src/Test/Test.csproj
@@ -6,18 +6,22 @@
-
-
-
+
+
+
-
+
-
-
+
+
+
+
+
+