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 @@ +CollapseAll_16x \ 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 @@ +ExpandAll_16x \ 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 @@ +CollapseChevronDown_md_16x \ 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 @@ +ExpandChevronRight_md_16x \ 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 @@ +Refresh_16x \ 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 @@ +StatusCriticalError_16x \ 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); - } - } -}