From d09253e532b8a85412f7c55657b534733f915d67 Mon Sep 17 00:00:00 2001 From: Alekhya Date: Fri, 17 Jul 2020 22:32:21 -0700 Subject: [PATCH] Functionality to detect Win32 apps which are installed, deleted or renamed while PowerToys is running (#4960) * Added file system wrapper and interface * added win32program repository which would load store app and also handle new apps being added/deleted * Added event handlers to win32 program repo * added paths to monitor and setting FSWs for each location * Events firing as expected * filter extensions added, events fire as expected * override gethashcode so that duplicates don't show up. OnCreated and OnDeleted events trigger as expected * implemented setter for filters in FileSystemWatcher * Rename adds item but does not seem to delete the previous app * catching an exception when a duplicate item is inserted * Removed notify filter for directory because we only need to monitor files * Added exe programs to be indexed in the desktop and startmenu * created a new class to init FileSystemHelpers instead of main * Added fix for shortcut applications to work as expected while renaming and deleting them * Added wrappers for file system operations * Added some tests * Added all tests for appref-ms and added a condition to search in sub directories * Added tests for Exe applications * Added lnk app tests * Added tests for shortcut applications * removed unnecessary wrappers * override Equals for win32 * removed debug statements * Fixed exe issue * Fixed internet shortcut exception * fixed renaming shortcut apps * Added a retry block for ReadAllLines * capitalized method name - RetrieveTargetPath * made naming consistent, helper variables start with underscore * Added the exception condition back * renamed Win32ProgramRepositoryHelper to Win32ProgramFileSystemWatchers * made win32Program repository variable static * changed list to ilist * disposed file system watchers * make retrieveTargetPath upper case in tests --- .../Storage/Win32ProgramRepositoryTest.cs | 363 ++++++++++++++++++ .../Plugins/Microsoft.Plugin.Program/Main.cs | 48 +-- .../Microsoft.Plugin.Program.csproj | 2 + .../Programs/IShellLinkHelper.cs | 15 + .../Programs/ShellLinkHelper.cs | 27 +- .../Programs/Win32.cs | 79 +++- .../Storage/Win32ProgramFileSystemWatchers.cs | 65 ++++ .../Storage/Win32ProgramRepository.cs | 194 ++++++++++ .../Views/Commands/ProgramSettingDisplay.cs | 11 +- .../launcher/Wox.Core/Plugin/PluginManager.cs | 2 +- .../FileVersionInfoWrapper.cs | 23 ++ .../FileSystemHelper/FileWrapper.cs | 41 ++ .../IFileVersionInfoWrapper.cs | 10 + .../FileSystemHelper/IFileWrapper.cs | 11 + .../Storage/FileSystemWatcherWrapper.cs | 26 ++ .../Storage/IFileSystemWatcherWrapper.cs | 20 + .../Storage/ListRepository.cs | 15 +- 17 files changed, 894 insertions(+), 58 deletions(-) create mode 100644 src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/Win32ProgramRepositoryTest.cs create mode 100644 src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/IShellLinkHelper.cs create mode 100644 src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramFileSystemWatchers.cs create mode 100644 src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramRepository.cs create mode 100644 src/modules/launcher/Wox.Infrastructure/FileSystemHelper/FileVersionInfoWrapper.cs create mode 100644 src/modules/launcher/Wox.Infrastructure/FileSystemHelper/FileWrapper.cs create mode 100644 src/modules/launcher/Wox.Infrastructure/FileSystemHelper/IFileVersionInfoWrapper.cs create mode 100644 src/modules/launcher/Wox.Infrastructure/FileSystemHelper/IFileWrapper.cs create mode 100644 src/modules/launcher/Wox.Infrastructure/Storage/FileSystemWatcherWrapper.cs create mode 100644 src/modules/launcher/Wox.Infrastructure/Storage/IFileSystemWatcherWrapper.cs diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/Win32ProgramRepositoryTest.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/Win32ProgramRepositoryTest.cs new file mode 100644 index 000000000..7421bd6f1 --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/Win32ProgramRepositoryTest.cs @@ -0,0 +1,363 @@ +using System; +using System.Collections.Generic; +using Moq; +using NUnit.Framework; +using System.Linq; +using Wox.Infrastructure.Storage; +using Wox.Plugin; +using Microsoft.Plugin.Program.Programs; +using Microsoft.Plugin.Program.Storage; +using System.IO; +using Wox.Infrastructure.FileSystemHelper; +using System.Diagnostics; + +namespace Microsoft.Plugin.Program.UnitTests.Storage +{ + using Win32 = Program.Programs.Win32; + + [TestFixture] + class Win32ProgramRepositoryTest + { + List _fileSystemWatchers; + Settings _settings = new Settings(); + string[] _pathsToWatch = new string[] { "location1", "location2" }; + List> _fileSystemMocks; + + [SetUp] + public void SetFileSystemWatchers() + { + _fileSystemWatchers = new List(); + _fileSystemMocks = new List>(); + for (int index = 0; index < _pathsToWatch.Length; index++) + { + var mockFileWatcher = new Mock(); + _fileSystemMocks.Add(mockFileWatcher); + _fileSystemWatchers.Add(mockFileWatcher.Object); + } + } + + [TestCase("Name", "ExecutableName", "FullPath", "description1", "description2")] + public void Win32Repository_MustNotStoreDuplicates_WhileAddingItemsWithSameHashCode(string name, string exename, string fullPath, string description1, string description2) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + + Win32 item1 = new Win32 + { + Name = name, + ExecutableName = exename, + FullPath = fullPath, + Description = description1 + }; + + Win32 item2 = new Win32 + { + Name = name, + ExecutableName = exename, + FullPath = fullPath, + Description = description2 + }; + + // Act + _win32ProgramRepository.Add(item1); + + Assert.AreEqual(_win32ProgramRepository.Count(), 1); + + // To add an item with the same hashCode, ie, same name, exename and fullPath + _win32ProgramRepository.Add(item2); + + // Assert, count still remains 1 because they are duplicate items + Assert.AreEqual(_win32ProgramRepository.Count(), 1); + } + + [TestCase("path.appref-ms")] + public void Win32ProgramRepository_MustCallOnAppCreatedForApprefApps_WhenCreatedEventIsRaised(string path) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + FileSystemEventArgs e = new FileSystemEventArgs(WatcherChangeTypes.Created, "directory", path); + + // Act + _fileSystemMocks[0].Raise(m => m.Created += null, e); + + // Assert + Assert.AreEqual(_win32ProgramRepository.Count(), 1); + Assert.AreEqual(_win32ProgramRepository.ElementAt(0).AppType, 2); + } + + [TestCase("directory", "path.appref-ms")] + public void Win32ProgramRepository_MustCallOnAppDeletedForApprefApps_WhenDeletedEventIsRaised(string directory, string path) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + FileSystemEventArgs e = new FileSystemEventArgs(WatcherChangeTypes.Deleted, directory, path); + + string fullPath = directory + "\\" + path; + Win32 item = Win32.GetAppFromPath(fullPath); + _win32ProgramRepository.Add(item); + + // Act + _fileSystemMocks[0].Raise(m => m.Deleted += null, e); + + // Assert + Assert.AreEqual(_win32ProgramRepository.Count(), 0); + } + + [TestCase("directory", "oldpath.appref-ms", "newpath.appref-ms")] + public void Win32ProgramRepository_MustCallOnAppRenamedForApprefApps_WhenRenamedEventIsRaised(string directory, string oldpath, string newpath) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + RenamedEventArgs e = new RenamedEventArgs(WatcherChangeTypes.Renamed, directory , newpath, oldpath); + + string oldFullPath = directory + "\\" + oldpath; + string newFullPath = directory + "\\" + newpath; + + Win32 olditem = Win32.GetAppFromPath(oldFullPath); + Win32 newitem = Win32.GetAppFromPath(newFullPath); + _win32ProgramRepository.Add(olditem); + + // Act + _fileSystemMocks[0].Raise(m => m.Renamed += null, e); + + // Assert + Assert.AreEqual(_win32ProgramRepository.Count(), 1); + Assert.IsTrue(_win32ProgramRepository.Contains(newitem)); + Assert.IsFalse(_win32ProgramRepository.Contains(olditem)); + } + + [TestCase("path.exe")] + public void Win32ProgramRepository_MustCallOnAppCreatedForExeApps_WhenCreatedEventIsRaised(string path) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + FileSystemEventArgs e = new FileSystemEventArgs(WatcherChangeTypes.Created, "directory", path); + + // FileVersionInfo must be mocked for exe applications + var mockFileVersionInfo = new Mock(); + mockFileVersionInfo.Setup(m => m.GetVersionInfo(It.IsAny())).Returns((FileVersionInfo)null); + Win32._fileVersionInfoWrapper = mockFileVersionInfo.Object; + + // Act + _fileSystemMocks[0].Raise(m => m.Created += null, e); + + // Assert + Assert.AreEqual(_win32ProgramRepository.Count(), 1); + Assert.AreEqual(_win32ProgramRepository.ElementAt(0).AppType, 2); + } + + [TestCase("directory", "path.exe")] + public void Win32ProgramRepository_MustCallOnAppDeletedForExeApps_WhenDeletedEventIsRaised(string directory, string path) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + FileSystemEventArgs e = new FileSystemEventArgs(WatcherChangeTypes.Deleted, directory, path); + + // FileVersionInfo must be mocked for exe applications + var mockFileVersionInfo = new Mock(); + mockFileVersionInfo.Setup(m => m.GetVersionInfo(It.IsAny())).Returns((FileVersionInfo)null); + Win32._fileVersionInfoWrapper = mockFileVersionInfo.Object; + + string fullPath = directory + "\\" + path; + Win32 item = Win32.GetAppFromPath(fullPath); + _win32ProgramRepository.Add(item); + + // Act + _fileSystemMocks[0].Raise(m => m.Deleted += null, e); + + // Assert + Assert.AreEqual(_win32ProgramRepository.Count(), 0); + } + + [TestCase("directory", "oldpath.appref-ms", "newpath.appref-ms")] + public void Win32ProgramRepository_MustCallOnAppRenamedForExeApps_WhenRenamedEventIsRaised(string directory, string oldpath, string newpath) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + RenamedEventArgs e = new RenamedEventArgs(WatcherChangeTypes.Renamed, directory, newpath, oldpath); + + string oldFullPath = directory + "\\" + oldpath; + string newFullPath = directory + "\\" + newpath; + + // FileVersionInfo must be mocked for exe applications + var mockFileVersionInfo = new Mock(); + mockFileVersionInfo.Setup(m => m.GetVersionInfo(It.IsAny())).Returns((FileVersionInfo)null); + Win32._fileVersionInfoWrapper = mockFileVersionInfo.Object; + + Win32 olditem = Win32.GetAppFromPath(oldFullPath); + Win32 newitem = Win32.GetAppFromPath(newFullPath); + _win32ProgramRepository.Add(olditem); + + // Act + _fileSystemMocks[0].Raise(m => m.Renamed += null, e); + + // Assert + Assert.AreEqual(_win32ProgramRepository.Count(), 1); + Assert.IsTrue(_win32ProgramRepository.Contains(newitem)); + Assert.IsFalse(_win32ProgramRepository.Contains(olditem)); + } + + [TestCase("path.url")] + public void Win32ProgramRepository_MustCallOnAppCreatedForUrlApps_WhenCreatedEventIsRaised(string path) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + FileSystemEventArgs e = new FileSystemEventArgs(WatcherChangeTypes.Created, "directory", path); + + // File.ReadAllLines must be mocked for url applications + var mockFile = new Mock(); + mockFile.Setup(m => m.ReadAllLines(It.IsAny())).Returns(new string[] { "URL=steam://rungameid/1258080" , "IconFile=iconFile"}); + Win32._fileWrapper = mockFile.Object; + + // Act + _fileSystemMocks[0].Raise(m => m.Created += null, e); + + // Assert + Assert.AreEqual(_win32ProgramRepository.Count(), 1); + Assert.AreEqual(_win32ProgramRepository.ElementAt(0).AppType, 1); // Internet Shortcut Application + } + + [TestCase("directory", "path.url")] + public void Win32ProgramRepository_MustCallOnAppDeletedForUrlApps_WhenDeletedEventIsRaised(string directory, string path) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + FileSystemEventArgs e = new FileSystemEventArgs(WatcherChangeTypes.Deleted, directory, path); + + // File.ReadAllLines must be mocked for url applications + var mockFile = new Mock(); + mockFile.Setup(m => m.ReadAllLines(It.IsAny())).Returns(new string[] { "URL=steam://rungameid/1258080", "IconFile=iconFile" }); + Win32._fileWrapper = mockFile.Object; + + string fullPath = directory + "\\" + path; + Win32 item = Win32.GetAppFromPath(fullPath); + _win32ProgramRepository.Add(item); + + // Act + _fileSystemMocks[0].Raise(m => m.Deleted += null, e); + + // Assert + Assert.AreEqual(_win32ProgramRepository.Count(), 0); + } + + [TestCase("directory", "oldpath.url", "newpath.url")] + public void Win32ProgramRepository_MustCallOnAppRenamedForUrlApps_WhenRenamedEventIsRaised(string directory, string oldpath, string newpath) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + RenamedEventArgs e = new RenamedEventArgs(WatcherChangeTypes.Renamed, directory, newpath, oldpath); + + // File.ReadAllLines must be mocked for url applications + var mockFile = new Mock(); + mockFile.Setup(m => m.ReadAllLines(It.IsAny())).Returns(new string[] { "URL=steam://rungameid/1258080", "IconFile=iconFile" }); + Win32._fileWrapper = mockFile.Object; + + string oldFullPath = directory + "\\" + oldpath; + string newFullPath = directory + "\\" + newpath; + + Win32 olditem = Win32.GetAppFromPath(oldFullPath); + Win32 newitem = Win32.GetAppFromPath(newFullPath); + _win32ProgramRepository.Add(olditem); + + // Act + _fileSystemMocks[0].Raise(m => m.Renamed += null, e); + + // Assert + Assert.AreEqual(_win32ProgramRepository.Count(), 1); + Assert.IsTrue(_win32ProgramRepository.Contains(newitem)); + Assert.IsFalse(_win32ProgramRepository.Contains(olditem)); + } + + + [TestCase("path.lnk")] + public void Win32ProgramRepository_MustCallOnAppCreatedForLnkApps_WhenCreatedEventIsRaised(string path) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + FileSystemEventArgs e = new FileSystemEventArgs(WatcherChangeTypes.Created, "directory", path); + + // ShellLinkHelper must be mocked for lnk applications + var mockShellLink = new Mock(); + mockShellLink.Setup(m => m.RetrieveTargetPath(It.IsAny())).Returns(String.Empty); + Win32._helper = mockShellLink.Object; + + // Act + _fileSystemMocks[0].Raise(m => m.Created += null, e); + + // Assert + Assert.AreEqual(_win32ProgramRepository.Count(), 1); + Assert.AreEqual(_win32ProgramRepository.ElementAt(0).AppType, 2); + } + + [TestCase("directory", "path.lnk")] + public void Win32ProgramRepository_MustCallOnAppDeletedForLnkApps_WhenDeletedEventIsRaised(string directory, string path) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + FileSystemEventArgs e = new FileSystemEventArgs(WatcherChangeTypes.Deleted, directory, path); + + // ShellLinkHelper must be mocked for lnk applications + var mockShellLink = new Mock(); + mockShellLink.Setup(m => m.RetrieveTargetPath(It.IsAny())).Returns(String.Empty); + Win32._helper = mockShellLink.Object; + + string fullPath = directory + "\\" + path; + Win32 item = new Win32 + { + Name = "path", + ExecutableName = "path.exe", + ParentDirectory = "directory", + FullPath = "directory\\path.exe", + LnkResolvedPath = "directory\\path.lnk" // This must be equal for lnk applications + }; + _win32ProgramRepository.Add(item); + + // Act + _fileSystemMocks[0].Raise(m => m.Deleted += null, e); + + // Assert + Assert.AreEqual(_win32ProgramRepository.Count(), 0); + } + + [TestCase("directory", "oldpath.lnk", "path.lnk")] + public void Win32ProgramRepository_MustCallOnAppRenamedForLnkApps_WhenRenamedEventIsRaised(string directory, string oldpath, string path) + { + // Arrange + Win32ProgramRepository _win32ProgramRepository = new Win32ProgramRepository(_fileSystemWatchers, new BinaryStorage>("Win32"), _settings, _pathsToWatch); + RenamedEventArgs e = new RenamedEventArgs(WatcherChangeTypes.Renamed, directory, path, oldpath); + + string oldFullPath = directory + "\\" + oldpath; + string FullPath = directory + "\\" + path; + + // ShellLinkHelper must be mocked for lnk applications + var mockShellLink = new Mock(); + mockShellLink.Setup(m => m.RetrieveTargetPath(It.IsAny())).Returns(String.Empty); + Win32._helper = mockShellLink.Object; + + // old item and new item are the actual items when they are in existence + Win32 olditem = new Win32 + { + Name = "oldpath", + ExecutableName = path, + FullPath = FullPath, + }; + + Win32 newitem = new Win32 + { + Name = "path", + ExecutableName = path, + FullPath = FullPath, + }; + + _win32ProgramRepository.Add(olditem); + + // Act + _fileSystemMocks[0].Raise(m => m.Renamed += null, e); + + // Assert + Assert.AreEqual(_win32ProgramRepository.Count(), 1); + Assert.IsTrue(_win32ProgramRepository.Contains(newitem)); + Assert.IsFalse(_win32ProgramRepository.Contains(olditem)); + } + } +} diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs index ddd45bc1a..e852310dd 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs @@ -11,7 +11,6 @@ using Wox.Infrastructure.Logger; using Wox.Infrastructure.Storage; using Wox.Plugin; using Microsoft.Plugin.Program.Views; - using Stopwatch = Wox.Infrastructure.Stopwatch; using Windows.ApplicationModel; using Microsoft.Plugin.Program.Storage; @@ -22,36 +21,39 @@ namespace Microsoft.Plugin.Program public class Main : IPlugin, IPluginI18n, IContextMenu, ISavable, IReloadable, IDisposable { private static readonly object IndexLock = new object(); - internal static Programs.Win32[] _win32s { get; set; } internal static Settings _settings { get; set; } private static bool IsStartupIndexProgramsRequired => _settings.LastIndexTime.AddDays(3) < DateTime.Today; private static PluginInitContext _context; - private static BinaryStorage _win32Storage; private readonly PluginJsonStorage _settingsStorage; private bool _disposed = false; private PackageRepository _packageRepository = new PackageRepository(new PackageCatalogWrapper(), new BinaryStorage>("UWP")); + private static Win32ProgramFileSystemWatchers _win32ProgramRepositoryHelper; + private static Win32ProgramRepository _win32ProgramRepository; public Main() { _settingsStorage = new PluginJsonStorage(); _settings = _settingsStorage.Load(); + // This helper class initializes the file system watchers based on the locations to watch + _win32ProgramRepositoryHelper = new Win32ProgramFileSystemWatchers(); + + // Initialize the Win32ProgramRepository with the settings object + _win32ProgramRepository = new Win32ProgramRepository(_win32ProgramRepositoryHelper._fileSystemWatchers.Cast().ToList(), new BinaryStorage>("Win32"), _settings, _win32ProgramRepositoryHelper._pathsToWatch); Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Preload programs cost", () => { - _win32Storage = new BinaryStorage("Win32"); - _win32s = _win32Storage.TryLoad(new Programs.Win32[] { }); - + _win32ProgramRepository.Load(); _packageRepository.Load(); }); - Log.Info($"|Microsoft.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); + Log.Info($"|Microsoft.Plugin.Program.Main|Number of preload win32 programs <{_win32ProgramRepository.Count()}>"); var a = Task.Run(() => { - if (IsStartupIndexProgramsRequired || !_win32s.Any()) - Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs); + if (IsStartupIndexProgramsRequired || !_win32ProgramRepository.Any()) + Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Win32Program index cost", _win32ProgramRepository.IndexPrograms); }); var b = Task.Run(() => @@ -64,26 +66,19 @@ namespace Microsoft.Plugin.Program Task.WaitAll(a, b); _settings.LastIndexTime = DateTime.Today; + } public void Save() { _settingsStorage.Save(); - _win32Storage.Save(_win32s); + _win32ProgramRepository.Save(); _packageRepository.Save(); } public List Query(Query query) { - Programs.Win32[] win32; - - lock (IndexLock) - { - // just take the reference inside the lock to eliminate query time issues. - win32 = _win32s; - } - - var results1 = win32.AsParallel() + var results1 = _win32ProgramRepository.AsParallel() .Where(p => p.Enabled) .Select(p => p.Result(query.Search, _context.API)); @@ -115,20 +110,9 @@ namespace Microsoft.Plugin.Program } } - public static void IndexWin32Programs() - { - var win32S = Programs.Win32.All(_settings); - lock (IndexLock) - { - _win32s = win32S; - } - } - - - public void IndexPrograms() { - var t1 = Task.Run(() => IndexWin32Programs()); + var t1 = Task.Run(() => _win32ProgramRepository.IndexPrograms()); var t2 = Task.Run(() => _packageRepository.IndexPrograms()); Task.WaitAll(t1, t2); @@ -194,10 +178,10 @@ namespace Microsoft.Plugin.Program if (disposing) { _context.API.ThemeChanged -= OnThemeChanged; + _win32ProgramRepositoryHelper.Dispose(); _disposed = true; } } } - } } \ No newline at end of file diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Microsoft.Plugin.Program.csproj b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Microsoft.Plugin.Program.csproj index 56f28e123..44df2cbd2 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Microsoft.Plugin.Program.csproj +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Microsoft.Plugin.Program.csproj @@ -125,9 +125,11 @@ PreserveNewest + <_Parameter1>Microsoft.Plugin.Program.UnitTests + \ No newline at end of file diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/IShellLinkHelper.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/IShellLinkHelper.cs new file mode 100644 index 000000000..5d4c18dc1 --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/IShellLinkHelper.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Plugin.Program.Programs +{ + public interface IShellLinkHelper + { + string RetrieveTargetPath(string path); + string description { get; set; } + string Arguments { get; set; } + bool hasArguments { get; set; } + + } +} diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/ShellLinkHelper.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/ShellLinkHelper.cs index 5842a386d..c5a74f737 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/ShellLinkHelper.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/ShellLinkHelper.cs @@ -6,10 +6,11 @@ using System.IO; using Accessibility; using System.Runtime.InteropServices.ComTypes; using System.Security.Policy; +using Microsoft.Plugin.Program.Logger; namespace Microsoft.Plugin.Program.Programs { - class ShellLinkHelper + public class ShellLinkHelper : IShellLinkHelper { [Flags()] public enum SLGP_FLAGS @@ -99,19 +100,29 @@ namespace Microsoft.Plugin.Program.Programs { } - // To initialize the app description - public String description = String.Empty; + // Contains the description of the app + public string description { get; set; } = String.Empty; - // Sets to true if the program takes in arguments - public String Arguments = String.Empty; - public bool hasArguments = false; + // Contains the arguments to the app + public string Arguments { get; set; } = String.Empty; + public bool hasArguments { get; set; } = false; // Retrieve the target path using Shell Link - public string retrieveTargetPath(string path) + public string RetrieveTargetPath(string path) { var link = new ShellLink(); const int STGM_READ = 0; - ((IPersistFile)link).Load(path, STGM_READ); + + try + { + ((IPersistFile)link).Load(path, STGM_READ); + } + catch(System.IO.FileNotFoundException ex) + { + ProgramLogger.LogException($"|Win32| ShellLinkHelper.retrieveTargetPath | {path} | Path could not be retrieved", ex); + return String.Empty; + } + var hwnd = new _RemotableHandle(); ((IShellLinkW)link).Resolve(ref hwnd, 0); diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32.cs index 87354bc17..472361e88 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32.cs @@ -15,6 +15,7 @@ using System.Windows.Input; using System.Reflection; using System.Text.RegularExpressions; using Wox.Infrastructure.Logger; +using Wox.Infrastructure.FileSystemHelper; namespace Microsoft.Plugin.Program.Programs { @@ -36,6 +37,10 @@ namespace Microsoft.Plugin.Program.Programs public string Arguments { get; set; } = String.Empty; public string Location => ParentDirectory; public uint AppType { get; set; } + // Wrappers for File Operations + public static IFileVersionInfoWrapper _fileVersionInfoWrapper = new FileVersionInfoWrapper(); + public static IFileWrapper _fileWrapper = new FileWrapper(); + public static IShellLinkHelper _helper = new ShellLinkHelper(); private const string ShortcutExtension = "lnk"; private const string ApplicationReferenceExtension = "appref-ms"; @@ -313,7 +318,7 @@ namespace Microsoft.Plugin.Program.Programs // This function filters Internet Shortcut programs private static Win32 InternetShortcutProgram(string path) { - string[] lines = System.IO.File.ReadAllLines(path); + string[] lines = _fileWrapper.ReadAllLines(path); string appName = string.Empty; string iconPath = string.Empty; string urlPath = string.Empty; @@ -382,8 +387,8 @@ namespace Microsoft.Plugin.Program.Programs { const int MAX_PATH = 260; StringBuilder buffer = new StringBuilder(MAX_PATH); - ShellLinkHelper _helper = new ShellLinkHelper(); - string target = _helper.retrieveTargetPath(path); + + string target = _helper.RetrieveTargetPath(path); if (!string.IsNullOrEmpty(target)) { @@ -403,8 +408,8 @@ namespace Microsoft.Plugin.Program.Programs } else { - var info = FileVersionInfo.GetVersionInfo(target); - if (!string.IsNullOrEmpty(info.FileDescription)) + var info = _fileVersionInfoWrapper.GetVersionInfo(target); + if (!string.IsNullOrEmpty(info?.FileDescription)) { program.Description = info.FileDescription; } @@ -439,9 +444,9 @@ namespace Microsoft.Plugin.Program.Programs try { var program = Win32Program(path); - var info = FileVersionInfo.GetVersionInfo(path); + var info = _fileVersionInfoWrapper.GetVersionInfo(path); - if (!string.IsNullOrEmpty(info.FileDescription)) + if (!string.IsNullOrEmpty(info?.FileDescription)) { program.Description = info.FileDescription; } @@ -457,6 +462,46 @@ namespace Microsoft.Plugin.Program.Programs } } + // Function to get the Win32 application, given the path to the application + public static Win32 GetAppFromPath(string path) + { + Win32 app = null; + const string exeExtension = ".exe"; + const string lnkExtension = ".lnk"; + const string urlExtenion = ".url"; + const string apprefExtension = ".appref-ms"; + + string extension = Path.GetExtension(path); + + if(extension.Equals(exeExtension, StringComparison.OrdinalIgnoreCase)) + { + app = ExeProgram(path); + } + else if(extension.Equals(lnkExtension, StringComparison.OrdinalIgnoreCase)) + { + app = LnkProgram(path); + } + else if(extension.Equals(apprefExtension, StringComparison.OrdinalIgnoreCase)) + { + app = Win32Program(path); + } + else if(extension.Equals(urlExtenion, StringComparison.OrdinalIgnoreCase)) + { + app = InternetShortcutProgram(path); + } + + // if the app is valid, only then return the application, else return null + if(app?.Valid ?? false) + { + return app; + } + else + { + return null; + } + + } + private static IEnumerable ProgramPaths(string directory, string[] suffixes, bool recursiveSearch = true) { if (!Directory.Exists(directory)) @@ -609,8 +654,11 @@ namespace Microsoft.Plugin.Program.Programs var programs1 = paths.AsParallel().Where(p => Extension(p).Equals(ShortcutExtension, StringComparison.OrdinalIgnoreCase)).Select(LnkProgram); var programs2 = paths.AsParallel().Where(p => Extension(p).Equals(ApplicationReferenceExtension, StringComparison.OrdinalIgnoreCase)).Select(Win32Program); var programs3 = paths.AsParallel().Where(p => Extension(p).Equals(InternetShortcutExtension, StringComparison.OrdinalIgnoreCase)).Select(InternetShortcutProgram); + var programs4 = paths.AsParallel().Where(p => Extension(p).Equals(ExeExtension, StringComparison.OrdinalIgnoreCase)).Select(ExeProgram); - return programs1.Concat(programs2).Where(p => p.Valid).Concat(programs3).Where(p => p.Valid); + return programs1.Concat(programs2).Where(p => p.Valid) + .Concat(programs3).Where(p => p.Valid) + .Concat(programs4).Where(p => p.Valid); } private static ParallelQuery StartMenuPrograms(string[] suffixes) @@ -712,6 +760,19 @@ namespace Microsoft.Plugin.Program.Programs return entry; } + // Overriding the object.GetHashCode() function to aid in removing duplicates while adding and removing apps from the concurrent dictionary storage + public override int GetHashCode() + { + removeDuplicatesComparer _removeDuplicatesHelper = new removeDuplicatesComparer(); + return _removeDuplicatesHelper.GetHashCode(this); + } + + public override bool Equals(object obj) + { + removeDuplicatesComparer _removeDuplicatesHelper = new removeDuplicatesComparer(); + return obj is Win32 win && _removeDuplicatesHelper.Equals(this, win); + } + public class removeDuplicatesComparer : IEqualityComparer { public bool Equals(Win32 app1, Win32 app2) @@ -802,6 +863,6 @@ namespace Microsoft.Plugin.Program.Programs return new Win32[0]; } #endif - } + } } } diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramFileSystemWatchers.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramFileSystemWatchers.cs new file mode 100644 index 000000000..96efa82bd --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramFileSystemWatchers.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using Wox.Infrastructure.Storage; + +namespace Microsoft.Plugin.Program.Storage +{ + internal class Win32ProgramFileSystemWatchers : IDisposable + { + + public readonly string[] _pathsToWatch; + public List _fileSystemWatchers; + private bool _disposed = false; + + // This class contains the list of directories to watch and initializes the File System Watchers + public Win32ProgramFileSystemWatchers() + { + _pathsToWatch = GetPathsToWatch(); + SetFileSystemWatchers(); + } + + // Returns an array of paths to be watched + private string[] GetPathsToWatch() + { + string[] paths = new string[] + { + Environment.GetFolderPath(Environment.SpecialFolder.Programs), + Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), + Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + }; + return paths; + } + + // Initializes the FileSystemWatchers + private void SetFileSystemWatchers() + { + _fileSystemWatchers = new List(); + for (int index = 0; index < _pathsToWatch.Length; index++) + { + _fileSystemWatchers.Add(new FileSystemWatcherWrapper()); + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + for(int index = 0; index < _pathsToWatch.Length; index++) + { + _fileSystemWatchers[index].Dispose(); + } + _disposed = true; + } + } + } + + } +} diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramRepository.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramRepository.cs new file mode 100644 index 000000000..f1147d6ee --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramRepository.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.Plugin.Program.Programs; +using Wox.Infrastructure.Logger; +using Wox.Infrastructure.Storage; +using System.IO; +using System.Linq; + +namespace Microsoft.Plugin.Program.Storage +{ + using Win32 = Programs.Win32; + internal class Win32ProgramRepository : ListRepository, IProgramRepository + { + private IStorage> _storage; + private Settings _settings; + private IList _fileSystemWatcherHelpers; + private string[] _pathsToWatch; + private int _numberOfPathsToWatch; + private Collection extensionsToWatch = new Collection{ "*.exe", "*.lnk", "*.appref-ms", "*.url" }; + private readonly string lnkExtension = ".lnk"; + private readonly string urlExtension = ".url"; + + public Win32ProgramRepository(IList fileSystemWatcherHelpers, IStorage> storage, Settings settings, string[] pathsToWatch) + { + this._fileSystemWatcherHelpers = fileSystemWatcherHelpers; + this._storage = storage ?? throw new ArgumentNullException("storage", "Win32ProgramRepository requires an initialized storage interface"); + this._settings = settings ?? throw new ArgumentNullException("settings", "Win32ProgramRepository requires an initialized settings object"); + this._pathsToWatch = pathsToWatch; + this._numberOfPathsToWatch = pathsToWatch.Count(); + InitializeFileSystemWatchers(); + } + + private void InitializeFileSystemWatchers() + { + for(int index = 0; index < _numberOfPathsToWatch; index++) + { + // To set the paths to monitor + _fileSystemWatcherHelpers[index].Path = _pathsToWatch[index]; + + // to be notified when there is a change to a file + _fileSystemWatcherHelpers[index].NotifyFilter = NotifyFilters.FileName; + + // filtering the app types that we want to monitor + _fileSystemWatcherHelpers[index].Filters = extensionsToWatch; + + // Registering the event handlers + _fileSystemWatcherHelpers[index].Created += OnAppCreated; + _fileSystemWatcherHelpers[index].Deleted += OnAppDeleted; + _fileSystemWatcherHelpers[index].Renamed += OnAppRenamed; + + // Enable the file system watcher + _fileSystemWatcherHelpers[index].EnableRaisingEvents = true; + + // Enable it to search in sub folders as well + _fileSystemWatcherHelpers[index].IncludeSubdirectories = true; + } + } + + private void OnAppRenamed(object sender, RenamedEventArgs e) + { + string oldPath = e.OldFullPath; + string newPath = e.FullPath; + + string extension = Path.GetExtension(newPath); + Programs.Win32 newApp = Programs.Win32.GetAppFromPath(newPath); + Programs.Win32 oldApp = null; + + // Once the shortcut application is renamed, the old app does not exist and therefore when we try to get the FullPath we get the lnk path instead of the exe path + // This changes the hashCode() of the old application. + // Therefore, instead of retrieving the old app using the GetAppFromPath(), we construct the application ourself + // This situation is not encountered for other application types because the fullPath is the path itself, instead of being computed by using the path to the app. + try + { + if (extension.Equals(lnkExtension, StringComparison.OrdinalIgnoreCase)) + { + oldApp = new Win32() { Name = Path.GetFileNameWithoutExtension(e.OldName), ExecutableName = newApp.ExecutableName, FullPath = newApp.FullPath }; + } + else if(extension.Equals(urlExtension, StringComparison.OrdinalIgnoreCase)) + { + oldApp = new Win32() { Name = Path.GetFileNameWithoutExtension(e.OldName), ExecutableName = Path.GetFileName(e.OldName), FullPath = newApp.FullPath }; + } + else + { + oldApp = Win32.GetAppFromPath(oldPath); + } + } + catch (Exception ex) + { + Log.Info($"|Win32ProgramRepository|OnAppRenamed-{extension}Program|{oldPath}|Unable to create program from {oldPath}| {ex.Message}"); + } + + + // To remove the old app which has been renamed and to add the new application. + if (oldApp != null) + { + Remove(oldApp); + } + + if (newApp != null) + { + Add(newApp); + } + } + + private void OnAppDeleted(object sender, FileSystemEventArgs e) + { + string path = e.FullPath; + string extension = Path.GetExtension(path); + Programs.Win32 app = null; + + try + { + // To mitigate the issue of not having a FullPath for a shortcut app, we iterate through the items and find the app with the same hashcode. + if (extension.Equals(lnkExtension, StringComparison.OrdinalIgnoreCase)) + { + app = GetAppWithSameLnkResolvedPath(path); + } + else if (extension.Equals(urlExtension, StringComparison.OrdinalIgnoreCase)) + { + app = GetAppWithSameNameAndExecutable(Path.GetFileNameWithoutExtension(path), Path.GetFileName(path)); + } + else + { + app = Programs.Win32.GetAppFromPath(path); + } + } + catch(Exception ex) + { + Log.Info($"|Win32ProgramRepository|OnAppDeleted-{extension}Program|{path}|Unable to create program from {path}| {ex.Message}"); + } + + if (app != null) + { + Remove(app); + } + } + + // When a URL application is deleted, we can no longer get the HashCode directly from the path because the FullPath a Url app is the URL obtained from reading the file + private Win32 GetAppWithSameNameAndExecutable(string name, string executableName) + { + foreach (Win32 app in Items) + { + if (name.Equals(app.Name, StringComparison.OrdinalIgnoreCase) && executableName.Equals(app.ExecutableName, StringComparison.OrdinalIgnoreCase)) + { + return app; + } + } + return null; + } + + // To mitigate the issue faced (as stated above) when a shortcut application is renamed, the Exe FullPath and executable name must be obtained. + // Unlike the rename event args, since we do not have a newPath, we iterate through all the programs and find the one with the same LnkResolved path. + private Programs.Win32 GetAppWithSameLnkResolvedPath(string lnkResolvedPath) + { + foreach(Programs.Win32 app in Items) + { + if (lnkResolvedPath.ToLower().Equals(app.LnkResolvedPath)) + { + return app; + } + } + return null; + } + + private void OnAppCreated(object sender, FileSystemEventArgs e) + { + string path = e.FullPath; + Programs.Win32 app = Programs.Win32.GetAppFromPath(path); + if (app != null) + { + Add(app); + } + } + + public void IndexPrograms() + { + var applications = Programs.Win32.All(_settings); + Set(applications); + } + + public void Save() + { + _storage.Save(Items); + } + + public void Load() + { + var items = _storage.TryLoad(new Programs.Win32[] { }); + Set(items); + } + + } +} diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Views/Commands/ProgramSettingDisplay.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Views/Commands/ProgramSettingDisplay.cs index 59105c019..15d7b175c 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Views/Commands/ProgramSettingDisplay.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Views/Commands/ProgramSettingDisplay.cs @@ -48,7 +48,7 @@ namespace Microsoft.Plugin.Program.Views.Commands internal static void LoadAllApplications(this List list) { - Main._win32s + /*Main._win32s .Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)) .ToList() .ForEach(t1 => ProgramSetting.ProgramSettingDisplayList @@ -60,7 +60,7 @@ namespace Microsoft.Plugin.Program.Views.Commands UniqueIdentifier = t1.UniqueIdentifier, Enabled = t1.Enabled } - )); + ));*/ } internal static void SetProgramSourcesStatus(this List list, List selectedProgramSourcesToDisable, bool status) @@ -70,10 +70,10 @@ namespace Microsoft.Plugin.Program.Views.Commands .ToList() .ForEach(t1 => t1.Enabled = status); - Main._win32s + /*Main._win32s .Where(t1 => selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && t1.Enabled != status)) .ToList() - .ForEach(t1 => t1.Enabled = status); + .ForEach(t1 => t1.Enabled = status);*/ } internal static void StoreDisabledInSettings(this List list) @@ -114,8 +114,7 @@ namespace Microsoft.Plugin.Program.Views.Commands internal static bool IsReindexRequired(this List selectedItems) { - if (selectedItems.Where(t1 => t1.Enabled).Count() > 0 - && selectedItems.Where(t1 => t1.Enabled && !Main._win32s.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)).Count() > 0) + if (selectedItems.Where(t1 => t1.Enabled).Count() > 0) return true; // ProgramSources holds list of user added directories, diff --git a/src/modules/launcher/Wox.Core/Plugin/PluginManager.cs b/src/modules/launcher/Wox.Core/Plugin/PluginManager.cs index 464084d99..bf5516630 100644 --- a/src/modules/launcher/Wox.Core/Plugin/PluginManager.cs +++ b/src/modules/launcher/Wox.Core/Plugin/PluginManager.cs @@ -292,7 +292,7 @@ namespace Wox.Core.Plugin { foreach (var plugin in AllPlugins) { - var disposablePlugin = plugin as IDisposable; + var disposablePlugin = plugin.Plugin as IDisposable; disposablePlugin?.Dispose(); } } diff --git a/src/modules/launcher/Wox.Infrastructure/FileSystemHelper/FileVersionInfoWrapper.cs b/src/modules/launcher/Wox.Infrastructure/FileSystemHelper/FileVersionInfoWrapper.cs new file mode 100644 index 000000000..7a1f02543 --- /dev/null +++ b/src/modules/launcher/Wox.Infrastructure/FileSystemHelper/FileVersionInfoWrapper.cs @@ -0,0 +1,23 @@ +using System.Diagnostics; +using System.IO; + +namespace Wox.Infrastructure.FileSystemHelper +{ + public class FileVersionInfoWrapper : IFileVersionInfoWrapper + { + public FileVersionInfoWrapper() { } + public FileVersionInfo GetVersionInfo(string path) + { + if(File.Exists(path)) + { + return FileVersionInfo.GetVersionInfo(path); + } + else + { + return null; + } + } + + public string FileDescription { get; set; } + } +} diff --git a/src/modules/launcher/Wox.Infrastructure/FileSystemHelper/FileWrapper.cs b/src/modules/launcher/Wox.Infrastructure/FileSystemHelper/FileWrapper.cs new file mode 100644 index 000000000..485928e38 --- /dev/null +++ b/src/modules/launcher/Wox.Infrastructure/FileSystemHelper/FileWrapper.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using Wox.Infrastructure.Logger; +using System.Threading; + +namespace Wox.Infrastructure.FileSystemHelper +{ + public class FileWrapper : IFileWrapper + { + public FileWrapper() { } + + public string[] ReadAllLines(string path) + { + int attempt = 0; + int maxRetries = 5; + + // Sometimes when files are being installed, url applications are written to line by line. + // During this process their contents cannot be read as they are being accessed by an other process. + // This ensures that if we do face this scenario, we retry after some time. + while(attempt < maxRetries) + { + try + { + return File.ReadAllLines(path); + } + catch (IOException ex) + { + attempt++; + Thread.Sleep(500); + Log.Info($"File {path} is being accessed by another process| {ex.Message}"); + + } + } + + return new string[] { String.Empty }; + } + + } +} diff --git a/src/modules/launcher/Wox.Infrastructure/FileSystemHelper/IFileVersionInfoWrapper.cs b/src/modules/launcher/Wox.Infrastructure/FileSystemHelper/IFileVersionInfoWrapper.cs new file mode 100644 index 000000000..70ccbf25a --- /dev/null +++ b/src/modules/launcher/Wox.Infrastructure/FileSystemHelper/IFileVersionInfoWrapper.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; + +namespace Wox.Infrastructure.FileSystemHelper +{ + public interface IFileVersionInfoWrapper + { + FileVersionInfo GetVersionInfo(string path); + string FileDescription { get; set; } + } +} diff --git a/src/modules/launcher/Wox.Infrastructure/FileSystemHelper/IFileWrapper.cs b/src/modules/launcher/Wox.Infrastructure/FileSystemHelper/IFileWrapper.cs new file mode 100644 index 000000000..36cb31d3a --- /dev/null +++ b/src/modules/launcher/Wox.Infrastructure/FileSystemHelper/IFileWrapper.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Wox.Infrastructure.FileSystemHelper +{ + public interface IFileWrapper + { + string[] ReadAllLines(string path); + } +} diff --git a/src/modules/launcher/Wox.Infrastructure/Storage/FileSystemWatcherWrapper.cs b/src/modules/launcher/Wox.Infrastructure/Storage/FileSystemWatcherWrapper.cs new file mode 100644 index 000000000..f64772651 --- /dev/null +++ b/src/modules/launcher/Wox.Infrastructure/Storage/FileSystemWatcherWrapper.cs @@ -0,0 +1,26 @@ +using System.Collections.ObjectModel; +using System.IO; + +namespace Wox.Infrastructure.Storage +{ + // File System Watcher Wrapper class which implements the IFileSystemWatcherWrapper interface + public class FileSystemWatcherWrapper : FileSystemWatcher, IFileSystemWatcherWrapper + { + public FileSystemWatcherWrapper() { } + + Collection IFileSystemWatcherWrapper.Filters + { + get => this.Filters; + set + { + if(value != null) + { + foreach(string filter in value) + { + this.Filters.Add(filter); + } + } + } + } + } +} diff --git a/src/modules/launcher/Wox.Infrastructure/Storage/IFileSystemWatcherWrapper.cs b/src/modules/launcher/Wox.Infrastructure/Storage/IFileSystemWatcherWrapper.cs new file mode 100644 index 000000000..0ddb16fe6 --- /dev/null +++ b/src/modules/launcher/Wox.Infrastructure/Storage/IFileSystemWatcherWrapper.cs @@ -0,0 +1,20 @@ +using System.Collections.ObjectModel; +using System.IO; + +namespace Wox.Infrastructure.Storage +{ + public interface IFileSystemWatcherWrapper + { + // Events to watch out for + event FileSystemEventHandler Created; + event FileSystemEventHandler Deleted; + event RenamedEventHandler Renamed; + + // Properties of File System watcher + Collection Filters { get; set; } + bool EnableRaisingEvents { get; set; } + NotifyFilters NotifyFilter { get; set; } + string Path { get; set; } + bool IncludeSubdirectories { get; set; } + } +} diff --git a/src/modules/launcher/Wox.Infrastructure/Storage/ListRepository.cs b/src/modules/launcher/Wox.Infrastructure/Storage/ListRepository.cs index bcd820b77..2f2d2ea75 100644 --- a/src/modules/launcher/Wox.Infrastructure/Storage/ListRepository.cs +++ b/src/modules/launcher/Wox.Infrastructure/Storage/ListRepository.cs @@ -31,7 +31,14 @@ namespace Wox.Infrastructure.Storage public void Set(IList items) { //enforce that internal representation - _items = new ConcurrentDictionary(items.ToDictionary( i => i.GetHashCode())); + try + { + _items = new ConcurrentDictionary(items.ToDictionary(i => i.GetHashCode())); + } + catch(ArgumentException e) + { + Log.Info($"|LisRepository.Set| Trying to insert a duplicate item", e.Message); + } } public bool Any() @@ -50,7 +57,6 @@ namespace Wox.Infrastructure.Storage public void Remove(T removedItem) { - if (!_items.TryRemove(removedItem.GetHashCode(), out _)) { Log.Error($"|ListRepository.Remove| Item Not Found <{removedItem}>"); @@ -76,5 +82,10 @@ namespace Wox.Infrastructure.Storage { return _items.GetEnumerator(); } + + public int Count() + { + return _items.Count; + } } }