diff --git a/.editorconfig b/.editorconfig
index cab8f8e..821c6e0 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -7,3 +7,6 @@ insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
+
+[*.{xml,wxs}]
+indent_size = 2
diff --git a/.github/workflows/build.ps1 b/.github/workflows/build.ps1
index 3d7c4e4..895ca17 100644
--- a/.github/workflows/build.ps1
+++ b/.github/workflows/build.ps1
@@ -52,12 +52,18 @@ Function FindMsBuild() {
return $msbuild
}
+Function RemoveFileIfExists($fileName) {
+ Info "Remove '$fileName'"
+ Remove-Item $fileName -Force -Recurse -ErrorAction SilentlyContinue
+}
+
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$root = Resolve-Path "$PSScriptRoot/../.."
-$publishDir = "$root/build/Release"
+$buildDir = "$root/build"
+$publishDir = "$buildDir/Release"
$projectName = "ShowWhatProcessLocksFile"
$version = GetVersion
$installerVersion = GetInstallerVersion $version
@@ -65,9 +71,6 @@ $msbuild = FindMsBuild
Info "Version: '$version'. InstallerVersion: '$installerVersion'"
-Info "Remove Publish directory `n $publishDir"
-Remove-Item $publishDir -Force -Recurse -ErrorAction SilentlyContinue
-
Info "Build project"
& $msbuild `
/property:RestorePackagesConfig=true `
@@ -80,10 +83,18 @@ Info "Build project"
$root/$projectName.sln
CheckReturnCodeOfPreviousCommand "build failed"
-# TODO: there are no processes which lock files on Github Actions executors. It makes a lot of test fail.
-# Info "Run tests"
-# & "$root/build/nuget/nunit.consolerunner/*/tools/nunit3-console.exe" `
-# $publishDir/net461/Test.dll `
-# --stoponerror `
-# --noresult
-# CheckReturnCodeOfPreviousCommand "tests failed"
+RemoveFileIfExists "$publishDir/${projectName}.msi.zip"
+Info "Create zip archive from msi installer"
+Compress-Archive -Path "$publishDir/$projectName.msi" -DestinationPath "$publishDir/${projectName}.msi.zip"
+
+# Skip running tests if the build script is run on Github Actions.
+# There are no processes which lock files on Github Actions executors. It makes a lot of test fail.
+if ($null -eq $env:GITHUB_ACTIONS) {
+ & "$buildDir/nuget/nunit.consolerunner/*/tools/nunit3-console.exe" `
+ "$publishDir/net461/Test.dll" `
+ --stoponerror `
+ --labels=Before `
+ --noheader `
+ --noresult
+ CheckReturnCodeOfPreviousCommand "tests failed"
+}
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index daae550..55b683b 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -12,8 +12,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
draft: true
- files: Build/Release/*.msi
+ files: Build/Release/*.msi.zip
- uses: actions/upload-artifact@v2
with:
name: Build artifacts
- path: Build/Release/*.msi
+ path: Build/Release/*.msi.zip
diff --git a/README.md b/README.md
index 506cf10..d563c34 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,10 @@
# ShowWhatProcessLocksFile
An utility to discover what processes lock a specific file or folder.
+
+# Screenshots
+## Context menu
![Screenshot](doc/ContextMenu.png)
-
+## The application
![Screenshot](doc/Screenshot.png)
# System requirements
@@ -12,8 +15,8 @@ An utility to discover what processes lock a specific file or folder.
The application uses [Handle by Mark Russinovich](https://docs.microsoft.com/en-us/sysinternals/downloads/handle) to get information about locking processes. The output of `handle.exe` is parsed and displayed in the GUI.
# How to use
-* Download `ShowWhatProcessLocksFile.msi` from the latest [release](https://github.com/PolarGoose/ShowWhatProcessLocksFile/releases).
-* Run the installer. The installer will install this programm to the `C:\Program Files\ShowWhatProcessLocksFile` folder and add a "Show what locks this file" Windows File Explorer context menu element.
+* 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.
* Use "Show what locks this file" File Explorer's context menu to select a file or folder
* To terminate selected processes, open a context menu by clicking mouse right 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.
diff --git a/doc/ContextMenu.png b/doc/ContextMenu.png
index c0d8fbd..d662344 100644
Binary files a/doc/ContextMenu.png and b/doc/ContextMenu.png differ
diff --git a/doc/Screenshot.png b/doc/Screenshot.png
index 7bb400a..e40e19d 100644
Binary files a/doc/Screenshot.png and b/doc/Screenshot.png differ
diff --git a/src/App/Gui/Controls/ExpandToggleButton.xaml b/src/App/Gui/Controls/ExpandToggleButton.xaml
index 7aa9c43..f13c2d3 100644
--- a/src/App/Gui/Controls/ExpandToggleButton.xaml
+++ b/src/App/Gui/Controls/ExpandToggleButton.xaml
@@ -3,7 +3,25 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
-
+
+
+
diff --git a/src/App/Gui/Controls/IconButton.xaml b/src/App/Gui/Controls/IconButton.xaml
index 3b8a9db..1856d32 100644
--- a/src/App/Gui/Controls/IconButton.xaml
+++ b/src/App/Gui/Controls/IconButton.xaml
@@ -3,8 +3,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
x:Name="self"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
-
+
diff --git a/src/App/Gui/Controls/IconButton.xaml.cs b/src/App/Gui/Controls/IconButton.xaml.cs
index bef3ee6..c21036d 100644
--- a/src/App/Gui/Controls/IconButton.xaml.cs
+++ b/src/App/Gui/Controls/IconButton.xaml.cs
@@ -1,19 +1,19 @@
+using System;
using System.Windows;
using System.Windows.Controls;
-using System.Windows.Media;
namespace ShowWhatProcessLocksFile.Gui.Controls
{
public partial class IconButton : Button
{
- public ImageSource Icon
+ public Uri Icon
{
- get => (ImageSource)GetValue(IconProperty);
+ get => (Uri)GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public static readonly DependencyProperty IconProperty =
- DependencyProperty.Register("Icon", typeof(ImageSource), typeof(IconButton));
+ DependencyProperty.Register("Icon", typeof(Uri), typeof(IconButton));
public IconButton()
{
diff --git a/src/App/Gui/Controls/ProcessInfoListView.xaml b/src/App/Gui/Controls/ProcessInfoListView.xaml
index e2819e3..804521d 100644
--- a/src/App/Gui/Controls/ProcessInfoListView.xaml
+++ b/src/App/Gui/Controls/ProcessInfoListView.xaml
@@ -14,8 +14,8 @@
-
-
+
+
diff --git a/src/App/Gui/Controls/ProcessInfoListViewModel.cs b/src/App/Gui/Controls/ProcessInfoListViewModel.cs
index b8e557f..e99fa66 100644
--- a/src/App/Gui/Controls/ProcessInfoListViewModel.cs
+++ b/src/App/Gui/Controls/ProcessInfoListViewModel.cs
@@ -1,5 +1,5 @@
using ShowWhatProcessLocksFile.Gui.Utils;
-using ShowWhatProcessLocksFile.LockingProcessesInfo;
+using ShowWhatProcessLocksFile.LockFinding;
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/App/Gui/Controls/ProcessInfoView.xaml b/src/App/Gui/Controls/ProcessInfoView.xaml
index fbe5252..7bb3453 100644
--- a/src/App/Gui/Controls/ProcessInfoView.xaml
+++ b/src/App/Gui/Controls/ProcessInfoView.xaml
@@ -1,8 +1,44 @@
-
-
-
Pid: ,
-
\ No newline at end of file
+ xmlns:utils="clr-namespace:ShowWhatProcessLocksFile.Gui.Utils"
+ mc:Ignorable="d"
+ d:DesignHeight="450" d:DesignWidth="800"
+ d:DataContext="{d:DesignInstance Type=local:ProcessInfoViewModel, IsDesignTimeCreatable=False}">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pid: ,
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App/Gui/Controls/ProcessInfoViewModel.cs b/src/App/Gui/Controls/ProcessInfoViewModel.cs
index 9ee1fa8..4f6d97b 100644
--- a/src/App/Gui/Controls/ProcessInfoViewModel.cs
+++ b/src/App/Gui/Controls/ProcessInfoViewModel.cs
@@ -1,4 +1,4 @@
-using ShowWhatProcessLocksFile.LockingProcessesInfo;
+using ShowWhatProcessLocksFile.LockFinding;
namespace ShowWhatProcessLocksFile.Gui.Controls
{
diff --git a/src/App/Gui/Controls/ResultTextView.xaml b/src/App/Gui/Controls/ResultTextView.xaml
index 5f173ba..63ed854 100644
--- a/src/App/Gui/Controls/ResultTextView.xaml
+++ b/src/App/Gui/Controls/ResultTextView.xaml
@@ -2,15 +2,25 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
xmlns:local="clr-namespace:ShowWhatProcessLocksFile.Gui.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
- d:DataContext="{d:DesignInstance Type=local:ResultTextView, IsDesignTimeCreatable=False}">
+ d:DataContext="{d:DesignInstance Type=local:ResultTextViewModel, IsDesignTimeCreatable=False}">
-
-
-
+
+
+
+
+
Checkmark_16x
\ No newline at end of file
diff --git a/src/App/Gui/Icons/CollapseAll_16x.png b/src/App/Gui/Icons/CollapseAll_16x.png
deleted file mode 100644
index 315b176..0000000
Binary files a/src/App/Gui/Icons/CollapseAll_16x.png and /dev/null differ
diff --git a/src/App/Gui/Icons/CollapseAll_16x.svg b/src/App/Gui/Icons/CollapseAll_16x.svg
new file mode 100644
index 0000000..742d5e4
--- /dev/null
+++ b/src/App/Gui/Icons/CollapseAll_16x.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/App/Gui/Icons/ExpandAll_16x.png b/src/App/Gui/Icons/ExpandAll_16x.png
deleted file mode 100644
index 7fe3a9c..0000000
Binary files a/src/App/Gui/Icons/ExpandAll_16x.png and /dev/null differ
diff --git a/src/App/Gui/Icons/ExpandAll_16x.svg b/src/App/Gui/Icons/ExpandAll_16x.svg
new file mode 100644
index 0000000..2cb452c
--- /dev/null
+++ b/src/App/Gui/Icons/ExpandAll_16x.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/App/Gui/Icons/ExpandDown_md_16x.png b/src/App/Gui/Icons/ExpandDown_md_16x.png
deleted file mode 100644
index 1a0d30d..0000000
Binary files a/src/App/Gui/Icons/ExpandDown_md_16x.png and /dev/null differ
diff --git a/src/App/Gui/Icons/ExpandDown_md_16x.svg b/src/App/Gui/Icons/ExpandDown_md_16x.svg
new file mode 100644
index 0000000..481d0d6
--- /dev/null
+++ b/src/App/Gui/Icons/ExpandDown_md_16x.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/App/Gui/Icons/ExpandRight_md_16x.png b/src/App/Gui/Icons/ExpandRight_md_16x.png
deleted file mode 100644
index cbdfe43..0000000
Binary files a/src/App/Gui/Icons/ExpandRight_md_16x.png and /dev/null differ
diff --git a/src/App/Gui/Icons/ExpandRight_md_16x.svg b/src/App/Gui/Icons/ExpandRight_md_16x.svg
new file mode 100644
index 0000000..35e081a
--- /dev/null
+++ b/src/App/Gui/Icons/ExpandRight_md_16x.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/App/Gui/Icons/Refresh_16x.png b/src/App/Gui/Icons/Refresh_16x.png
deleted file mode 100644
index 23ffe55..0000000
Binary files a/src/App/Gui/Icons/Refresh_16x.png and /dev/null differ
diff --git a/src/App/Gui/Icons/Refresh_16x.svg b/src/App/Gui/Icons/Refresh_16x.svg
new file mode 100644
index 0000000..6167e43
--- /dev/null
+++ b/src/App/Gui/Icons/Refresh_16x.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/App/Gui/Icons/StatusCriticalError_16x.png b/src/App/Gui/Icons/StatusCriticalError_16x.png
deleted file mode 100644
index 876b3de..0000000
Binary files a/src/App/Gui/Icons/StatusCriticalError_16x.png and /dev/null differ
diff --git a/src/App/Gui/Icons/StatusCriticalError_16x.svg b/src/App/Gui/Icons/StatusCriticalError_16x.svg
new file mode 100644
index 0000000..1d4645a
--- /dev/null
+++ b/src/App/Gui/Icons/StatusCriticalError_16x.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/App/Gui/MainWindow.xaml b/src/App/Gui/MainWindow.xaml
index d38fd58..5b55f1a 100644
--- a/src/App/Gui/MainWindow.xaml
+++ b/src/App/Gui/MainWindow.xaml
@@ -21,8 +21,8 @@
-
-
+
+
diff --git a/src/App/Gui/MainWindowViewModel.cs b/src/App/Gui/MainWindowViewModel.cs
index 7524689..99e63fb 100644
--- a/src/App/Gui/MainWindowViewModel.cs
+++ b/src/App/Gui/MainWindowViewModel.cs
@@ -1,6 +1,6 @@
using ShowWhatProcessLocksFile.Gui.Controls;
using ShowWhatProcessLocksFile.Gui.Utils;
-using ShowWhatProcessLocksFile.LockingProcessesInfo;
+using ShowWhatProcessLocksFile.LockFinding;
using ShowWhatProcessLocksFile.Utils;
using System;
using System.Collections.Generic;
@@ -43,7 +43,7 @@ public async void GetLockingInformation()
try
{
- var res = await Task.Run(() => ProcessesInfoRetriever.GetProcessesInfo(FilePath));
+ var res = await Task.Run(() => LockFinder.FindWhatProcessesLockPath(FilePath).ToList());
if (res.Any())
{
MainControl = new ProcessInfoListViewModel(res, OnProcessesKillRequested);
diff --git a/src/App/Icon/SvgToIco.ps1 b/src/App/Icon/SvgToIco.ps1
new file mode 100644
index 0000000..3abfb4c
--- /dev/null
+++ b/src/App/Icon/SvgToIco.ps1
@@ -0,0 +1,18 @@
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+# The script requires https://imagemagick.org/script/download.php
+$imageMagickCommand = Get-Command -Name magick
+
+$iconResolutions = 16,20,24,32,40,48,64,256
+$pngImages = @()
+Foreach($r in $iconResolutions) {
+ & $imageMagickCommand convert -size "${r}x${r}" -depth 8 "$PSScriptRoot/icon.svg" "${r}.png"
+ $pngImages += "${r}.png"
+}
+
+& $imageMagickCommand convert $pngImages -compress jpeg "icon.ico"
+
+Foreach($image in $pngImages) {
+ Remove-Item $image
+}
diff --git a/src/App/Icon/icon.ico b/src/App/Icon/icon.ico
new file mode 100644
index 0000000..56e4644
Binary files /dev/null and b/src/App/Icon/icon.ico differ
diff --git a/src/App/Icon/icon.svg b/src/App/Icon/icon.svg
new file mode 100644
index 0000000..b35f706
--- /dev/null
+++ b/src/App/Icon/icon.svg
@@ -0,0 +1,126 @@
+
+
+
+
diff --git a/src/App/LockingProcessesInfo/HandleExe/CommandLine.cs b/src/App/LockFinding/HandleExe/CommandLine.cs
similarity index 92%
rename from src/App/LockingProcessesInfo/HandleExe/CommandLine.cs
rename to src/App/LockFinding/HandleExe/CommandLine.cs
index 88f7479..3ff002a 100644
--- a/src/App/LockingProcessesInfo/HandleExe/CommandLine.cs
+++ b/src/App/LockFinding/HandleExe/CommandLine.cs
@@ -3,7 +3,7 @@
using ShowWhatProcessLocksFile.Utils;
using System;
-namespace ShowWhatProcessLocksFile.LockingProcessesInfo.HandleExe
+namespace ShowWhatProcessLocksFile.LockFinding.HandleExe
{
public static class CommandLine
{
diff --git a/src/App/LockFinding/HandleExe/Handle.cs b/src/App/LockFinding/HandleExe/Handle.cs
new file mode 100644
index 0000000..0e59905
--- /dev/null
+++ b/src/App/LockFinding/HandleExe/Handle.cs
@@ -0,0 +1,19 @@
+using ShowWhatProcessLocksFile.Utils;
+using System;
+using System.IO;
+
+namespace ShowWhatProcessLocksFile.LockFinding.HandleExe
+{
+ public static class Handle
+ {
+ private static readonly string HandleExeFullName = Path.Combine(AppContext.BaseDirectory, "handle.exe");
+
+ public static string Execute(string fileFullName)
+ {
+ // Handle.exe doesn't work if a path contains "\" character at the end
+ fileFullName = PathUtils.RemoveTrailingPathSeparator(fileFullName);
+
+ return CommandLine.Execute(HandleExeFullName, $"-u -nobanner -accepteula \"{fileFullName}\"");
+ }
+ }
+}
diff --git a/src/App/LockingProcessesInfo/HandleExe/HandleOutputParser.cs b/src/App/LockFinding/HandleExe/HandleOutputParser.cs
similarity index 96%
rename from src/App/LockingProcessesInfo/HandleExe/HandleOutputParser.cs
rename to src/App/LockFinding/HandleExe/HandleOutputParser.cs
index ce5ce43..cf3a8e6 100644
--- a/src/App/LockingProcessesInfo/HandleExe/HandleOutputParser.cs
+++ b/src/App/LockFinding/HandleExe/HandleOutputParser.cs
@@ -3,7 +3,7 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
-namespace ShowWhatProcessLocksFile.LockingProcessesInfo.HandleExe
+namespace ShowWhatProcessLocksFile.LockFinding.HandleExe
{
public class HandleOutputParser
{
diff --git a/src/App/LockingProcessesInfo/HandleExe/HandleParsedLine.cs b/src/App/LockFinding/HandleExe/HandleParsedLine.cs
similarity index 93%
rename from src/App/LockingProcessesInfo/HandleExe/HandleParsedLine.cs
rename to src/App/LockFinding/HandleExe/HandleParsedLine.cs
index 7a8cf0f..7dd46b4 100644
--- a/src/App/LockingProcessesInfo/HandleExe/HandleParsedLine.cs
+++ b/src/App/LockFinding/HandleExe/HandleParsedLine.cs
@@ -1,4 +1,4 @@
-namespace ShowWhatProcessLocksFile.LockingProcessesInfo.HandleExe
+namespace ShowWhatProcessLocksFile.LockFinding.HandleExe
{
public enum HandleType
{
diff --git a/src/App/LockFinding/LockFinder.cs b/src/App/LockFinding/LockFinder.cs
new file mode 100644
index 0000000..ebffb55
--- /dev/null
+++ b/src/App/LockFinding/LockFinder.cs
@@ -0,0 +1,112 @@
+using MoreLinq;
+using ShowWhatProcessLocksFile.LockFinding.HandleExe;
+using ShowWhatProcessLocksFile.Utils;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.Linq;
+using System.Windows;
+using System.Windows.Interop;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+
+namespace ShowWhatProcessLocksFile.LockFinding
+{
+ public static class LockFinder
+ {
+ public static IEnumerable FindWhatProcessesLockPath(string path)
+ {
+ var output = Handle.Execute(path);
+ var parsedHandleExeOutput = HandleOutputParser.Parse(output);
+
+ return parsedHandleExeOutput
+ // File path which we supply to Handle.exe is treated as "Show locks for all entities whose names start with this path".
+ // Which 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.
+ // Therefore, we need to manually filter out this extra information.
+ .Where(p => string.Equals(p.FileFullName, path, StringComparison.OrdinalIgnoreCase)
+ || PathUtils.IsInsideFolder(p.FileFullName, path)
+ // If the path contains cyrillic letters, Handle.exe replaces them with '?'.
+ // For example if something locks "C:\файл.txt" the Handle.exe will print "C:\????.txt" as the name of the file.
+ // However, the first condition "string.Equals(p.FileFullName, ...)" will filter such lines out.
+ // Therefore, regardless if this file is locked or not, our app will show that nothing locks this file.
+ || p.FileFullName.Contains('?'))
+ // Handle.exe produces non grouped output. We need to group all locked handles per process.
+ .GroupBy(el => el.Pid)
+ // There can be several records corresponding to the same file locked by the same process but with a different 'HandleCode'.
+ // We don't use 'HandleCode' field and want to leave only one of these records to avoid showing the user that the same file is locked twice.
+ .Select(el => el.DistinctBy(e => e.FileFullName))
+ .Select(el => CreateProcessInfo(el))
+ // Remove all elements for which 'CreateProcessInfo' has failed to extract process information
+ .Where(el => el != null)
+ .OrderBy(el => el.Name);
+ }
+
+ private static ProcessInfo CreateProcessInfo(IEnumerable handleExeParsedLinesForTheSameProcess)
+ {
+ try
+ {
+ var handle = handleExeParsedLinesForTheSameProcess.First();
+ var pid = handle.Pid;
+ var processName = handle.ProcessName;
+ var process = Process.GetProcessById(pid);
+ // There are processes for which it is not possible to get full name and icon.
+ // One of such processes is "System"
+ var processFullName = TryGetProcessFullName(process);
+ var processIcon = TryGetIcon(processFullName);
+
+ return new ProcessInfo(
+ pid: pid,
+ processName: processName,
+ executableFullName: processFullName,
+ process: process,
+ icon: processIcon,
+ userName: handle.UserName,
+ lockedFiles: handleExeParsedLinesForTheSameProcess.Select(l => l.FileFullName).OrderBy(x => x));
+ }
+ catch (Exception ex)
+ {
+ Log.Warn($"Failed to create a process info from: '{handleExeParsedLinesForTheSameProcess.FirstOrDefault()}'. Exception:\n{ex}");
+ return null;
+ }
+ }
+
+ private static string TryGetProcessFullName(Process process)
+ {
+ try
+ {
+ return process.MainModule.FileName;
+ }
+ catch (Exception ex)
+ {
+ Log.Warn($"Failed to get a full name of a process: name='{process.ProcessName}' pid='{process.Id}'. Exception:\n{ex}");
+ return null;
+ }
+ }
+
+ private static ImageSource TryGetIcon(string executableFullName)
+ {
+ if (executableFullName == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ 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)
+ {
+ Log.Warn($"Failed to get an icon from executable '{executableFullName}'. Exception:\n{ex}");
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/App/LockingProcessesInfo/ProcessInfo.cs b/src/App/LockFinding/ProcessInfo.cs
similarity index 93%
rename from src/App/LockingProcessesInfo/ProcessInfo.cs
rename to src/App/LockFinding/ProcessInfo.cs
index 85f3ff1..a388116 100644
--- a/src/App/LockingProcessesInfo/ProcessInfo.cs
+++ b/src/App/LockFinding/ProcessInfo.cs
@@ -3,7 +3,7 @@
using System.Diagnostics;
using System.Windows.Media;
-namespace ShowWhatProcessLocksFile.LockingProcessesInfo
+namespace ShowWhatProcessLocksFile.LockFinding
{
public class ProcessInfo
{
diff --git a/src/App/LockingProcessesInfo/HandleExe/Handle.cs b/src/App/LockingProcessesInfo/HandleExe/Handle.cs
deleted file mode 100644
index 7b2ba02..0000000
--- a/src/App/LockingProcessesInfo/HandleExe/Handle.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-using System.IO;
-
-namespace ShowWhatProcessLocksFile.LockingProcessesInfo.HandleExe
-{
- public static class Handle
- {
- private static readonly string FullName = Path.Combine(AppContext.BaseDirectory, "handle.exe");
-
- public static string Execute(string fileFullName)
- {
- // Handle.exe doesn't work if a path contains "\" character at the end
- if (fileFullName.EndsWith(@"\"))
- {
- fileFullName = fileFullName.Remove(fileFullName.Length - 1);
- }
-
- return CommandLine.Execute(FullName, $"-u -nobanner -accepteula \"{fileFullName}\"");
- }
- }
-}
diff --git a/src/App/LockingProcessesInfo/ProcessesInfoRetriever.cs b/src/App/LockingProcessesInfo/ProcessesInfoRetriever.cs
deleted file mode 100644
index 0b27cec..0000000
--- a/src/App/LockingProcessesInfo/ProcessesInfoRetriever.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using MoreLinq;
-using ShowWhatProcessLocksFile.LockingProcessesInfo.HandleExe;
-using ShowWhatProcessLocksFile.Utils;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Drawing;
-using System.Linq;
-using System.Windows;
-using System.Windows.Interop;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-
-namespace ShowWhatProcessLocksFile.LockingProcessesInfo
-{
- public static class ProcessesInfoRetriever
- {
- public static IEnumerable GetProcessesInfo(string fileFullName)
- {
- var output = Handle.Execute(fileFullName);
- var parsedHandleExeOutput = HandleOutputParser.Parse(output);
-
- // Handle.exe produces non grouped output. We need to group all locked handles per process.
- var groupedByPid = parsedHandleExeOutput.GroupBy(el => el.Pid);
-
- // There can be several records corresponding to the same file locked by the same process but with a different 'HandleCode'.
- // We don't care about the 'HandleCode' and therefore want to leave only one of these records to avoid showing the user that the same file is locked twice.
- var groupedByPidWithoutDiplicateFileNames = groupedByPid.Select(el => el.DistinctBy(e => e.FileFullName));
-
- return groupedByPidWithoutDiplicateFileNames.Select(el => CreateProcessInfo(el)).Where(el => el != null);
- }
-
- private static ProcessInfo CreateProcessInfo(IEnumerable handleExeParsedLinesForTheSameProcess)
- {
- try
- {
- var handle = handleExeParsedLinesForTheSameProcess.First();
- var pid = handle.Pid;
- var processName = handle.ProcessName;
- var process = Process.GetProcessById(pid);
- var processFullName = process.MainModule.FileName;
- var processIcon = GetProcessIcon(processFullName);
-
- return new ProcessInfo(
- pid: pid,
- processName: processName,
- executableFullName: processFullName,
- process: process,
- icon: processIcon,
- userName: handle.UserName,
- lockedFiles: handleExeParsedLinesForTheSameProcess.Select(l => l.FileFullName).OrderBy(x => x));
- }
- catch (Exception ex)
- {
- Log.Warn($"Failed to create a process info from: '{handleExeParsedLinesForTheSameProcess.FirstOrDefault()}'. Exception:\n{ex}");
- return null;
- }
- }
-
- private static ImageSource GetProcessIcon(string executableFullName)
- {
- using (var ico = Icon.ExtractAssociatedIcon(executableFullName))
- {
- return Imaging.CreateBitmapSourceFromHIcon(ico.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
- }
- }
- }
-}
diff --git a/src/App/ShowWhatProcessLocksFile.csproj b/src/App/ShowWhatProcessLocksFile.csproj
index ae38627..c741c32 100644
--- a/src/App/ShowWhatProcessLocksFile.csproj
+++ b/src/App/ShowWhatProcessLocksFile.csproj
@@ -3,42 +3,33 @@
WinExe
net461
true
- x64
app.manifest
-
-
-
- x64
-
-
-
- x64
+ Icon\icon.ico
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
diff --git a/src/App/Utils/CommandLineParser.cs b/src/App/Utils/CommandLineParser.cs
index 07213e9..33c4a41 100644
--- a/src/App/Utils/CommandLineParser.cs
+++ b/src/App/Utils/CommandLineParser.cs
@@ -7,7 +7,7 @@ internal static class CommandLineParser
{
public static string GetFileFullName()
{
- string[] args = Environment.GetCommandLineArgs();
+ var args = Environment.GetCommandLineArgs();
if (args.Length == 1)
{
diff --git a/src/App/Utils/PathUtils.cs b/src/App/Utils/PathUtils.cs
new file mode 100644
index 0000000..eba2cf6
--- /dev/null
+++ b/src/App/Utils/PathUtils.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace ShowWhatProcessLocksFile.Utils
+{
+ public static class PathUtils
+ {
+ public static string RemoveTrailingPathSeparator(string path)
+ {
+ return path.TrimEnd(new char[] { '\\' });
+ }
+
+ public static bool IsInsideFolder(string path, string folder)
+ {
+ return path.StartsWith($@"{RemoveTrailingPathSeparator(folder)}\", StringComparison.InvariantCultureIgnoreCase);
+ }
+ }
+}
diff --git a/src/App/Utils/ProcessKiller.cs b/src/App/Utils/ProcessKiller.cs
index 67f6948..9c0a8cd 100644
--- a/src/App/Utils/ProcessKiller.cs
+++ b/src/App/Utils/ProcessKiller.cs
@@ -1,4 +1,4 @@
-using ShowWhatProcessLocksFile.LockingProcessesInfo;
+using ShowWhatProcessLocksFile.LockFinding;
using System;
using System.Collections.Generic;
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 242ce1b..c12eb52 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -4,6 +4,7 @@
$(ProjectRoot)build\
$(BuildFolder)$(Configuration)
$(BuildFolder)obj\$(MSBuildProjectName)\
+ x64
5
true
0.0-dev
diff --git a/src/Installer/Installer.wixproj b/src/Installer/Installer.wixproj
index f38b0e2..373ab1d 100644
--- a/src/Installer/Installer.wixproj
+++ b/src/Installer/Installer.wixproj
@@ -7,6 +7,7 @@
2.0
ShowWhatProcessLocksFile
Package
+ True
Debug
diff --git a/src/Installer/Product.wxs b/src/Installer/Product.wxs
index e8c0821..61d92f2 100644
--- a/src/Installer/Product.wxs
+++ b/src/Installer/Product.wxs
@@ -1,9 +1,9 @@
-
+
-
+
@@ -13,7 +13,7 @@
-
+
@@ -26,27 +26,38 @@
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
+
+
+
-
+
-
+
+
-
+
diff --git a/src/Test/Test.csproj b/src/Test/Test.csproj
index 96d19a6..1ad19a8 100644
--- a/src/Test/Test.csproj
+++ b/src/Test/Test.csproj
@@ -2,24 +2,12 @@
net461
-
- false
-
- x64
-
-
-
- x64
-
-
-
- x64
-
-
+
+
diff --git a/src/Test/TestData/HandleExeOutput_nothingFound.txt b/src/Test/TestData/HandleExeOutput_nothingFound.txt
index df8e482..6836e3e 100644
--- a/src/Test/TestData/HandleExeOutput_nothingFound.txt
+++ b/src/Test/TestData/HandleExeOutput_nothingFound.txt
@@ -1 +1 @@
-No matching handles found.
+No matching handles found.
diff --git a/src/Test/TestHandleOutputParser.cs b/src/Test/TestHandleOutputParser.cs
index 96719e1..03e2be5 100644
--- a/src/Test/TestHandleOutputParser.cs
+++ b/src/Test/TestHandleOutputParser.cs
@@ -1,5 +1,5 @@
using NUnit.Framework;
-using ShowWhatProcessLocksFile.LockingProcessesInfo.HandleExe;
+using ShowWhatProcessLocksFile.LockFinding.HandleExe;
using System;
using System.IO;
using System.Linq;
diff --git a/src/Test/TestLockFinder.cs b/src/Test/TestLockFinder.cs
new file mode 100644
index 0000000..635a71d
--- /dev/null
+++ b/src/Test/TestLockFinder.cs
@@ -0,0 +1,52 @@
+using NUnit.Framework;
+using ShowWhatProcessLocksFile.LockFinding;
+using ShowWhatProcessLocksFile.Utils;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Test
+{
+ [TestFixture]
+ public class TestLockFinder
+ {
+ [TestCase(@"C:\Program Files")]
+ [TestCase(@"C:\Program Files\")]
+ public void Retrieve_locking_info_for_ProgramFiles_locked_folder(string path)
+ {
+ var processes = LockFinder.FindWhatProcessesLockPath(path);
+ AssertThatContainsExplorerProcess(processes);
+ AssertThatDoesntContainProcessesLockingDifferentPath(processes, path);
+ }
+
+ [TestCase(@"C:")]
+ [TestCase(@"C:\")]
+ public void Retrieve_locking_info_for_disk_C(string path)
+ {
+ var processes = LockFinder.FindWhatProcessesLockPath(path);
+ AssertThatContainsExplorerProcess(processes);
+ }
+
+ [TestCase(@"C:\nonExistingFolder")]
+ [TestCase(@"C:\nonExistingFolder\")]
+ public void Retrieves_nothing_for_folder_which_is_not_locked(string path)
+ {
+ var processes = LockFinder.FindWhatProcessesLockPath(@"C:\nonExistingFolder\");
+ Assert.IsEmpty(processes);
+ }
+
+ private void AssertThatContainsExplorerProcess(IEnumerable lockingProcesses)
+ {
+ var explorerProcesses = lockingProcesses.Where(p => p.Name == "explorer.exe").ToList();
+ Assert.GreaterOrEqual(explorerProcesses.Count, 1);
+ Assert.IsNotEmpty(explorerProcesses[0].ExecutableFullName);
+ Assert.IsNotNull(explorerProcesses[0].Icon);
+ }
+
+ private void AssertThatDoesntContainProcessesLockingDifferentPath(IEnumerable lockingProcesses, string requestedPath)
+ {
+ var otherProcesses = lockingProcesses.Where(p => p.LockedFiles.All(file => !PathUtils.IsInsideFolder(file, requestedPath)));
+ Assert.IsEmpty(otherProcesses,
+ "The locking information should only contain information about processes locking a requested path, not some other pathes");
+ }
+ }
+}
diff --git a/src/Test/TestsIntegration.cs b/src/Test/TestsIntegration.cs
deleted file mode 100644
index 1dc7d36..0000000
--- a/src/Test/TestsIntegration.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using NUnit.Framework;
-using ShowWhatProcessLocksFile.LockingProcessesInfo;
-using System.Linq;
-
-namespace Test
-{
- [TestFixture]
- public class TestsIntegration
- {
- [Test]
- public void Retrieve_locking_info_for_ProgramFiles()
- {
- var processes = ProcessesInfoRetriever.GetProcessesInfo(@"C:\Program Files\");
- var explorerProcesses = processes.Where(p => p.Name == "explorer.exe").ToList();
- Assert.GreaterOrEqual(explorerProcesses.Count, 1);
- Assert.IsNotEmpty(explorerProcesses[0].ExecutableFullName);
- Assert.IsNotNull(explorerProcesses[0].Icon);
- }
-
- [Test]
- public void Retrieve_locking_info_for_C_drive()
- {
- var processes = ProcessesInfoRetriever.GetProcessesInfo(@"C:\");
- var explorerProcesses = processes.Where(p => p.Name == "explorer.exe").ToList();
- Assert.GreaterOrEqual(explorerProcesses.Count, 1);
- Assert.IsNotEmpty(explorerProcesses[0].ExecutableFullName);
- Assert.IsNotNull(explorerProcesses[0].Icon);
- }
-
- [Test]
- public void Retrieves_nothing_for_folder_which_is_not_locked()
- {
- var processes = ProcessesInfoRetriever.GetProcessesInfo(@"C:\nonExistingFolder");
- Assert.IsEmpty(processes);
- }
- }
-}