From 030dfc2370f7d6df01a78ff3a00a54dd80bff0b8 Mon Sep 17 00:00:00 2001 From: ryanbodrug-microsoft <56318517+ryanbodrug-microsoft@users.noreply.github.com> Date: Fri, 26 Jun 2020 10:45:40 -0700 Subject: [PATCH] Merging in Theme changes and moving win32Tests to Microsoft.Plugin.Program.UnitTests --- PowerToys.sln | 7 + .../Microsoft.Plugin.Program.UnitTests.csproj | 26 + .../Programs/PackageCatalogWrapperTests.cs | 10 + .../Programs}/Win32Tests.cs | 874 +++++++++--------- .../Storage/ListRepositoryTests.cs | 62 ++ .../Plugins/Microsoft.Plugin.Program/Main.cs | 433 ++++----- .../Programs/IPackageCatalog.cs | 15 + .../Programs/PackageCatalogWrapper.cs | 73 ++ .../Microsoft.Plugin.Program/Programs/UWP.cs | 26 +- .../Storage/PackageRepository.cs | 86 ++ .../Views/Commands/ProgramSettingDisplay.cs | 21 +- .../Views/ProgramSetting.xaml.cs | 1 - .../Storage/BinaryStorage.cs | 2 +- .../Wox.Infrastructure/Storage/IRepository.cs | 16 + .../Wox.Infrastructure/Storage/IStorage.cs | 23 + .../Storage/ListRepository.cs | 68 ++ .../Wox.Infrastructure/StringMatcher.cs | 2 + 17 files changed, 1014 insertions(+), 731 deletions(-) create mode 100644 src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Microsoft.Plugin.Program.UnitTests.csproj create mode 100644 src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/PackageCatalogWrapperTests.cs rename src/modules/launcher/{Wox.Test/Plugins => Plugins/Microsoft.Plugin.Program.UnitTests/Programs}/Win32Tests.cs (95%) create mode 100644 src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/ListRepositoryTests.cs create mode 100644 src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/IPackageCatalog.cs create mode 100644 src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/PackageCatalogWrapper.cs create mode 100644 src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/PackageRepository.cs create mode 100644 src/modules/launcher/Wox.Infrastructure/Storage/IRepository.cs create mode 100644 src/modules/launcher/Wox.Infrastructure/Storage/IStorage.cs create mode 100644 src/modules/launcher/Wox.Infrastructure/Storage/ListRepository.cs diff --git a/PowerToys.sln b/PowerToys.sln index d71848ac4f..52175309c5 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -255,6 +255,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerTest", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedCommon", "src\common\ManagedCommon\ManagedCommon.csproj", "{4AED67B6-55FD-486F-B917-E543DEE2CB3C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Plugin.Program.UnitTests", "src\modules\launcher\Plugins\Microsoft.Plugin.Program.UnitTests\Microsoft.Plugin.Program.UnitTests.csproj", "{42851751-CBC8-45A6-97F5-7A0753F7B4D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -501,6 +503,10 @@ Global {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Debug|x64.Build.0 = Debug|x64 {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Release|x64.ActiveCfg = Release|x64 {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Release|x64.Build.0 = Release|x64 + {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Debug|x64.ActiveCfg = Debug|x64 + {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Debug|x64.Build.0 = Debug|x64 + {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Release|x64.ActiveCfg = Release|x64 + {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -571,6 +577,7 @@ Global {E6410BFC-B341-498C-8C67-312C20CDD8D5} = {1AFB6476-670D-4E80-A464-657E01DFF482} {62173D9A-6724-4C00-A1C8-FB646480A9EC} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} {4AED67B6-55FD-486F-B917-E543DEE2CB3C} = {1AFB6476-670D-4E80-A464-657E01DFF482} + {42851751-CBC8-45A6-97F5-7A0753F7B4D1} = {4AFC9975-2456-4C70-94A4-84073C1CED93} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Microsoft.Plugin.Program.UnitTests.csproj b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Microsoft.Plugin.Program.UnitTests.csproj new file mode 100644 index 0000000000..57e8108df5 --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Microsoft.Plugin.Program.UnitTests.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp3.1 + + false + + x64 + + + + x64 + + + + + + + + + + + + + + diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/PackageCatalogWrapperTests.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/PackageCatalogWrapperTests.cs new file mode 100644 index 0000000000..5bc2428780 --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/PackageCatalogWrapperTests.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Plugin.Program.UnitTests.Programs +{ + class PackageCatalogWrapperTests + { + } +} diff --git a/src/modules/launcher/Wox.Test/Plugins/Win32Tests.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/Win32Tests.cs similarity index 95% rename from src/modules/launcher/Wox.Test/Plugins/Win32Tests.cs rename to src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/Win32Tests.cs index f8054e7f68..b12db73ae1 100644 --- a/src/modules/launcher/Wox.Test/Plugins/Win32Tests.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/Win32Tests.cs @@ -1,439 +1,437 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using NUnit.Framework; -using Wox.Infrastructure; -using Wox.Plugin; -using Microsoft.Plugin.Program.Programs; -using Moq; -using System.IO; -using Microsoft.Plugin.Program; -using System.IO.Packaging; -using Windows.ApplicationModel; - -namespace Wox.Test.Plugins -{ - [TestFixture] +using Moq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; +using Wox.Infrastructure; +using Wox.Plugin; + +using Microsoft.Plugin.Program; +using System.IO.Packaging; +using Windows.ApplicationModel; +namespace Microsoft.Plugin.Program.UnitTests.Programs +{ + using Win32 = Microsoft.Plugin.Program.Programs.Win32; + + [TestFixture] public class Win32Tests - { - static Win32 notepad_appdata = new Win32 - { - Name = "Notepad", - ExecutableName = "notepad.exe", - FullPath = "c:\\windows\\system32\\notepad.exe", - LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk", - AppType = 2 - }; - - static Win32 notepad_users = new Win32 - { - Name = "Notepad", - ExecutableName = "notepad.exe", - FullPath = "c:\\windows\\system32\\notepad.exe", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk", - AppType = 2 - }; - - static Win32 azure_command_prompt = new Win32 - { - Name = "Microsoft Azure Command Prompt - v2.9", - ExecutableName = "cmd.exe", - FullPath = "c:\\windows\\system32\\cmd.exe", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft azure\\microsoft azure sdk for .net\\v2.9\\microsoft azure command prompt - v2.9.lnk", - AppType = 2 - }; - - static Win32 visual_studio_command_prompt = new Win32 - { - Name = "x64 Native Tools Command Prompt for VS 2019", - ExecutableName = "cmd.exe", - FullPath = "c:\\windows\\system32\\cmd.exe", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\visual studio 2019\\visual studio tools\\vc\\x64 native tools command prompt for vs 2019.lnk", - AppType = 2 - }; - - static Win32 command_prompt = new Win32 - { - Name = "Command Prompt", - ExecutableName = "cmd.exe", - FullPath = "c:\\windows\\system32\\cmd.exe", - LnkResolvedPath ="c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\command prompt.lnk", - AppType = 2 - }; - - static Win32 file_explorer = new Win32 - { - Name = "File Explorer", - ExecutableName = "File Explorer.lnk", - FullPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\file explorer.lnk", - LnkResolvedPath = null, - AppType = 2 - }; - - static Win32 wordpad = new Win32 - { - Name = "Wordpad", - ExecutableName = "wordpad.exe", - FullPath = "c:\\program files\\windows nt\\accessories\\wordpad.exe", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\wordpad.lnk", - AppType = 2 - }; - - static Win32 wordpad_duplicate = new Win32 - { - Name = "WORDPAD", - ExecutableName = "WORDPAD.EXE", - FullPath = "c:\\program files\\windows nt\\accessories\\wordpad.exe", - LnkResolvedPath = null, - AppType = 2 - }; - - static Win32 twitter_pwa = new Win32 - { - Name = "Twitter", - FullPath = "c:\\program files (x86)\\google\\chrome\\application\\chrome_proxy.exe", - LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\chrome apps\\twitter.lnk", - Arguments = " --profile-directory=Default --app-id=jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi", - AppType = 0 - }; - - static Win32 pinned_webpage = new Win32 - { - Name = "Web page", - FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge_proxy.exe", - LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\web page.lnk", - Arguments = "--profile-directory=Default --app-id=homljgmgpmcbpjbnjpfijnhipfkiclkd", - AppType = 0 - }; - - static Win32 edge_named_pinned_webpage = new Win32 - { - Name = "edge - Bing", - FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge_proxy.exe", - LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\edge - bing.lnk", - Arguments = " --profile-directory=Default --app-id=aocfnapldcnfbofgmbbllojgocaelgdd", - AppType = 0 - }; - - static Win32 msedge = new Win32 - { - Name = "Microsoft Edge", - ExecutableName = "msedge.exe", - FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge.exe", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft edge.lnk", - AppType = 2 - }; - - static Win32 chrome = new Win32 - { - Name = "Google Chrome", - ExecutableName = "chrome.exe", - FullPath = "c:\\program files (x86)\\google\\chrome\\application\\chrome.exe", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\google chrome.lnk", - AppType = 2 - }; - - static Win32 dummy_proxy_app = new Win32 - { - Name = "Proxy App", - ExecutableName = "test_proxy.exe", - FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\test_proxy.exe", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\test proxy.lnk", - AppType = 2 - }; - - static Win32 cmd_run_command = new Win32 - { - Name = "cmd", - ExecutableName = "cmd.exe", - FullPath = "c:\\windows\\system32\\cmd.exe", - LnkResolvedPath = null, - AppType = 3 // Run command - }; - - static Win32 cmder_run_command = new Win32 - { - Name = "Cmder", - Description = "Cmder: Lovely Console Emulator", - ExecutableName = "Cmder.exe", - FullPath = "c:\\tools\\cmder\\cmder.exe", - LnkResolvedPath = null, - AppType = 3 // Run command - }; - - static Win32 dummy_internetShortcut_app = new Win32 - { - Name = "Shop Titans", - ExecutableName = "Shop Titans.url", - FullPath = "steam://rungameid/1258080", - ParentDirectory = "C:\\Users\\temp\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Steam", - LnkResolvedPath = null, - AppType = 1 - }; - - static Win32 dummy_internetShortcut_app_duplicate = new Win32 - { - Name = "Shop Titans", - ExecutableName = "Shop Titans.url", - FullPath = "steam://rungameid/1258080", - ParentDirectory = "C:\\Users\\temp\\Desktop", - LnkResolvedPath = null, - AppType = 1 - }; - - [Test] - public void DedupFunction_whenCalled_mustRemoveDuplicateNotepads() - { - // Arrange - List prgms = new List(); - prgms.Add(notepad_appdata); - prgms.Add(notepad_users); - - // Act - Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); - - // Assert - Assert.AreEqual(apps.Length, 1); - } - - [Test] - public void DedupFunction_whenCalled_MustRemoveInternetShortcuts() - { - // Arrange - List prgms = new List(); - prgms.Add(dummy_internetShortcut_app); - prgms.Add(dummy_internetShortcut_app_duplicate); - - // Act - Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); - - // Assert - Assert.AreEqual(apps.Length, 1); - } - - [Test] - public void DedupFunction_whenCalled_mustNotRemovelnkWhichdoesNotHaveExe() - { - // Arrange - List prgms = new List(); - prgms.Add(file_explorer); - - // Act - Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); - - // Assert - Assert.AreEqual(apps.Length, 1); - } - - [Test] - public void DedupFunction_mustRemoveDuplicates_forExeExtensionsWithoutLnkResolvedPath() - { - // Arrange - List prgms = new List(); - prgms.Add(wordpad); - prgms.Add(wordpad_duplicate); - - // Act - Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); - - // Assert - Assert.AreEqual(apps.Length, 1); - Assert.IsTrue(!string.IsNullOrEmpty(apps[0].LnkResolvedPath)); - } - - [Test] - public void DedupFunction_mustNotRemovePrograms_withSameExeNameAndFullPath() - { - // Arrange - List prgms = new List(); - prgms.Add(azure_command_prompt); - prgms.Add(visual_studio_command_prompt); - prgms.Add(command_prompt); - - // Act - Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); - - // Assert - Assert.AreEqual(apps.Length, 3); - } - - [Test] - public void FunctionIsWebApplication_ShouldReturnTrue_ForWebApplications() - { - // The IsWebApplication(() function must return true for all PWAs and pinned web pages - Assert.IsTrue(twitter_pwa.IsWebApplication()); - Assert.IsTrue(pinned_webpage.IsWebApplication()); - Assert.IsTrue(edge_named_pinned_webpage.IsWebApplication()); - - // Should not filter apps whose executable name ends with proxy.exe - Assert.IsFalse(dummy_proxy_app.IsWebApplication()); - } - - [TestCase("ignore")] - public void FunctionFilterWebApplication_ShouldReturnFalse_WhenSearchingForTheMainApp(string query) - { - // Irrespective of the query, the FilterWebApplication() Function must not filter main apps such as edge and chrome - Assert.IsFalse(msedge.FilterWebApplication(query)); - Assert.IsFalse(chrome.FilterWebApplication(query)); - } - - [TestCase("edge", ExpectedResult = true)] - [TestCase("EDGE", ExpectedResult = true)] - [TestCase("msedge", ExpectedResult = true)] - [TestCase("Microsoft", ExpectedResult = true)] - [TestCase("edg", ExpectedResult = true)] - [TestCase("Edge page", ExpectedResult = false)] - [TestCase("Edge Web page", ExpectedResult = false)] - public bool EdgeWebSites_ShouldBeFiltered_WhenSearchingForEdge(string query) - { - return pinned_webpage.FilterWebApplication(query); - } - - [TestCase("chrome", ExpectedResult = true)] - [TestCase("CHROME", ExpectedResult = true)] - [TestCase("Google", ExpectedResult = true)] - [TestCase("Google Chrome", ExpectedResult = true)] - [TestCase("Google Chrome twitter", ExpectedResult = false)] - public bool ChromeWebSites_ShouldBeFiltered_WhenSearchingForChrome(string query) - { - return twitter_pwa.FilterWebApplication(query); - } - - [TestCase("twitter", 0, ExpectedResult = false)] - [TestCase("Twit", 0, ExpectedResult = false)] - [TestCase("TWITTER", 0, ExpectedResult = false)] - [TestCase("web", 1, ExpectedResult = false)] - [TestCase("Page", 1, ExpectedResult = false)] - [TestCase("WEB PAGE", 1, ExpectedResult = false)] - [TestCase("edge", 2, ExpectedResult = false)] - [TestCase("EDGE", 2, ExpectedResult = false)] - public bool PinnedWebPages_ShouldNotBeFiltered_WhenSearchingForThem(string query, int Case) - { - const uint CASE_TWITTER = 0; - const uint CASE_WEB_PAGE = 1; - const uint CASE_EDGE_NAMED_WEBPAGE = 2; - - // If the query is a part of the name of the web application, it should not be filtered, - // even if the name is the same as that of the main application, eg: case 2 - edge - if (Case == CASE_TWITTER) - { - return twitter_pwa.FilterWebApplication(query); - } - else if(Case == CASE_WEB_PAGE) - { - return pinned_webpage.FilterWebApplication(query); - } - else if(Case == CASE_EDGE_NAMED_WEBPAGE) - { - return edge_named_pinned_webpage.FilterWebApplication(query); - } - // unreachable code - return true; - } - - [TestCase("Command Prompt")] - [TestCase("cmd")] - [TestCase("cmd.exe")] - [TestCase("ignoreQueryText")] - public void Win32Applications_ShouldNotBeFiltered_WhenFilteringRunCommands(string query) - { - // Even if there is an exact match in the name or exe name, applications should never be filtered - Assert.IsTrue(command_prompt.QueryEqualsNameForRunCommands(query)); - } - - [TestCase("cmd")] - [TestCase("Cmd")] - [TestCase("CMD")] - public void RunCommands_ShouldNotBeFiltered_OnExactMatch(string query) - { - // Partial matches should be filtered as cmd is not equal to cmder - Assert.IsFalse(cmder_run_command.QueryEqualsNameForRunCommands(query)); - - // the query matches the name (cmd) and is therefore not filtered (case-insensitive) - Assert.IsTrue(cmd_run_command.QueryEqualsNameForRunCommands(query)); - } - - [Test] - public void WEB_APPLICATION_ReturnContextMenuWithOpenInConsole_WhenContextMenusIsCalled() - { - // Arrange - var mock = new Mock(); - mock.Setup(x => x.GetTranslation(It.IsAny())).Returns(It.IsAny()); - - // Act - List contextMenuResults = pinned_webpage.ContextMenus(mock.Object); - - // Assert - Assert.AreEqual(contextMenuResults.Count, 3); - mock.Verify(x => x.GetTranslation("wox_plugin_program_run_as_administrator"), Times.Once()); - mock.Verify(x => x.GetTranslation("wox_plugin_program_open_containing_folder"), Times.Once()); - mock.Verify(x => x.GetTranslation("wox_plugin_program_open_in_console"), Times.Once()); - } - - [Test] - public void INTERNET_SHORTCUT_APPLICATION_ReturnContextMenuWithOpenInConsole_WhenContextMenusIsCalled() - { - // Arrange - var mock = new Mock(); - mock.Setup(x => x.GetTranslation(It.IsAny())).Returns(It.IsAny()); - - // Act - List contextMenuResults = dummy_internetShortcut_app.ContextMenus(mock.Object); - - // Assert - Assert.AreEqual(contextMenuResults.Count, 2); - mock.Verify(x => x.GetTranslation("wox_plugin_program_open_containing_folder"), Times.Once()); - mock.Verify(x => x.GetTranslation("wox_plugin_program_open_in_console"), Times.Once()); - } - - [Test] - public void WIN32_APPLICATION_ReturnContextMenuWithOpenInConsole_WhenContextMenusIsCalled() - { - // Arrange - var mock = new Mock(); - mock.Setup(x => x.GetTranslation(It.IsAny())).Returns(It.IsAny()); - - // Act - List contextMenuResults = chrome.ContextMenus(mock.Object); - - // Assert - Assert.AreEqual(contextMenuResults.Count, 3); - mock.Verify(x => x.GetTranslation("wox_plugin_program_run_as_administrator"), Times.Once()); - mock.Verify(x => x.GetTranslation("wox_plugin_program_open_containing_folder"), Times.Once()); - mock.Verify(x => x.GetTranslation("wox_plugin_program_open_in_console"), Times.Once()); - } - - [Test] - public void RUN_COMMAND_ReturnContextMenuWithOpenInConsole_WhenContextMenusIsCalled() - { - // Arrange - var mock = new Mock(); - mock.Setup(x => x.GetTranslation(It.IsAny())).Returns(It.IsAny()); - - // Act - List contextMenuResults = cmd_run_command.ContextMenus(mock.Object); - - // Assert - Assert.AreEqual(contextMenuResults.Count, 3); - mock.Verify(x => x.GetTranslation("wox_plugin_program_run_as_administrator"), Times.Once()); - mock.Verify(x => x.GetTranslation("wox_plugin_program_open_containing_folder"), Times.Once()); - mock.Verify(x => x.GetTranslation("wox_plugin_program_open_in_console"), Times.Once()); - } - - [Test] - public void Win32Apps_ShouldSetNameAsTitle_WhileCreatingResult() - { - var mock = new Mock(); - mock.Setup(x => x.GetTranslation(It.IsAny())).Returns(It.IsAny()); - StringMatcher.Instance = new StringMatcher(); - - // Act - var result = cmder_run_command.Result("cmder", mock.Object); - - // Assert - Assert.IsTrue(result.Title.Equals(cmder_run_command.Name)); - Assert.IsFalse(result.Title.Equals(cmder_run_command.Description)); - } - } -} + { + static Win32 notepad_appdata = new Win32 + { + Name = "Notepad", + ExecutableName = "notepad.exe", + FullPath = "c:\\windows\\system32\\notepad.exe", + LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk", + AppType = 2 + }; + + static Win32 notepad_users = new Win32 + { + Name = "Notepad", + ExecutableName = "notepad.exe", + FullPath = "c:\\windows\\system32\\notepad.exe", + LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk", + AppType = 2 + }; + + static Win32 azure_command_prompt = new Win32 + { + Name = "Microsoft Azure Command Prompt - v2.9", + ExecutableName = "cmd.exe", + FullPath = "c:\\windows\\system32\\cmd.exe", + LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft azure\\microsoft azure sdk for .net\\v2.9\\microsoft azure command prompt - v2.9.lnk", + AppType = 2 + }; + + static Win32 visual_studio_command_prompt = new Win32 + { + Name = "x64 Native Tools Command Prompt for VS 2019", + ExecutableName = "cmd.exe", + FullPath = "c:\\windows\\system32\\cmd.exe", + LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\visual studio 2019\\visual studio tools\\vc\\x64 native tools command prompt for vs 2019.lnk", + AppType = 2 + }; + + static Win32 command_prompt = new Win32 + { + Name = "Command Prompt", + ExecutableName = "cmd.exe", + FullPath = "c:\\windows\\system32\\cmd.exe", + LnkResolvedPath ="c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\command prompt.lnk", + AppType = 2 + }; + + static Win32 file_explorer = new Win32 + { + Name = "File Explorer", + ExecutableName = "File Explorer.lnk", + FullPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\file explorer.lnk", + LnkResolvedPath = null, + AppType = 2 + }; + + static Win32 wordpad = new Win32 + { + Name = "Wordpad", + ExecutableName = "wordpad.exe", + FullPath = "c:\\program files\\windows nt\\accessories\\wordpad.exe", + LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\wordpad.lnk", + AppType = 2 + }; + + static Win32 wordpad_duplicate = new Win32 + { + Name = "WORDPAD", + ExecutableName = "WORDPAD.EXE", + FullPath = "c:\\program files\\windows nt\\accessories\\wordpad.exe", + LnkResolvedPath = null, + AppType = 2 + }; + + static Win32 twitter_pwa = new Win32 + { + Name = "Twitter", + FullPath = "c:\\program files (x86)\\google\\chrome\\application\\chrome_proxy.exe", + LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\chrome apps\\twitter.lnk", + Arguments = " --profile-directory=Default --app-id=jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi", + AppType = 0 + }; + + static Win32 pinned_webpage = new Win32 + { + Name = "Web page", + FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge_proxy.exe", + LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\web page.lnk", + Arguments = "--profile-directory=Default --app-id=homljgmgpmcbpjbnjpfijnhipfkiclkd", + AppType = 0 + }; + + static Win32 edge_named_pinned_webpage = new Win32 + { + Name = "edge - Bing", + FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge_proxy.exe", + LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\edge - bing.lnk", + Arguments = " --profile-directory=Default --app-id=aocfnapldcnfbofgmbbllojgocaelgdd", + AppType = 0 + }; + + static Win32 msedge = new Win32 + { + Name = "Microsoft Edge", + ExecutableName = "msedge.exe", + FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge.exe", + LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft edge.lnk", + AppType = 2 + }; + + static Win32 chrome = new Win32 + { + Name = "Google Chrome", + ExecutableName = "chrome.exe", + FullPath = "c:\\program files (x86)\\google\\chrome\\application\\chrome.exe", + LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\google chrome.lnk", + AppType = 2 + }; + + static Win32 dummy_proxy_app = new Win32 + { + Name = "Proxy App", + ExecutableName = "test_proxy.exe", + FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\test_proxy.exe", + LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\test proxy.lnk", + AppType = 2 + }; + + static Win32 cmd_run_command = new Win32 + { + Name = "cmd", + ExecutableName = "cmd.exe", + FullPath = "c:\\windows\\system32\\cmd.exe", + LnkResolvedPath = null, + AppType = 3 // Run command + }; + + static Win32 cmder_run_command = new Win32 + { + Name = "Cmder", + Description = "Cmder: Lovely Console Emulator", + ExecutableName = "Cmder.exe", + FullPath = "c:\\tools\\cmder\\cmder.exe", + LnkResolvedPath = null, + AppType = 3 // Run command + }; + + static Win32 dummy_internetShortcut_app = new Win32 + { + Name = "Shop Titans", + ExecutableName = "Shop Titans.url", + FullPath = "steam://rungameid/1258080", + ParentDirectory = "C:\\Users\\temp\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Steam", + LnkResolvedPath = null, + AppType = 1 + }; + + static Win32 dummy_internetShortcut_app_duplicate = new Win32 + { + Name = "Shop Titans", + ExecutableName = "Shop Titans.url", + FullPath = "steam://rungameid/1258080", + ParentDirectory = "C:\\Users\\temp\\Desktop", + LnkResolvedPath = null, + AppType = 1 + }; + + [Test] + public void DedupFunction_whenCalled_mustRemoveDuplicateNotepads() + { + // Arrange + List prgms = new List(); + prgms.Add(notepad_appdata); + prgms.Add(notepad_users); + + // Act + Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); + + // Assert + Assert.AreEqual(apps.Length, 1); + } + + [Test] + public void DedupFunction_whenCalled_MustRemoveInternetShortcuts() + { + // Arrange + List prgms = new List(); + prgms.Add(dummy_internetShortcut_app); + prgms.Add(dummy_internetShortcut_app_duplicate); + + // Act + Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); + + // Assert + Assert.AreEqual(apps.Length, 1); + } + + [Test] + public void DedupFunction_whenCalled_mustNotRemovelnkWhichdoesNotHaveExe() + { + // Arrange + List prgms = new List(); + prgms.Add(file_explorer); + + // Act + Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); + + // Assert + Assert.AreEqual(apps.Length, 1); + } + + [Test] + public void DedupFunction_mustRemoveDuplicates_forExeExtensionsWithoutLnkResolvedPath() + { + // Arrange + List prgms = new List(); + prgms.Add(wordpad); + prgms.Add(wordpad_duplicate); + + // Act + Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); + + // Assert + Assert.AreEqual(apps.Length, 1); + Assert.IsTrue(!string.IsNullOrEmpty(apps[0].LnkResolvedPath)); + } + + [Test] + public void DedupFunction_mustNotRemovePrograms_withSameExeNameAndFullPath() + { + // Arrange + List prgms = new List(); + prgms.Add(azure_command_prompt); + prgms.Add(visual_studio_command_prompt); + prgms.Add(command_prompt); + + // Act + Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel()); + + // Assert + Assert.AreEqual(apps.Length, 3); + } + + [Test] + public void FunctionIsWebApplication_ShouldReturnTrue_ForWebApplications() + { + // The IsWebApplication(() function must return true for all PWAs and pinned web pages + Assert.IsTrue(twitter_pwa.IsWebApplication()); + Assert.IsTrue(pinned_webpage.IsWebApplication()); + Assert.IsTrue(edge_named_pinned_webpage.IsWebApplication()); + + // Should not filter apps whose executable name ends with proxy.exe + Assert.IsFalse(dummy_proxy_app.IsWebApplication()); + } + + [TestCase("ignore")] + public void FunctionFilterWebApplication_ShouldReturnFalse_WhenSearchingForTheMainApp(string query) + { + // Irrespective of the query, the FilterWebApplication() Function must not filter main apps such as edge and chrome + Assert.IsFalse(msedge.FilterWebApplication(query)); + Assert.IsFalse(chrome.FilterWebApplication(query)); + } + + [TestCase("edge", ExpectedResult = true)] + [TestCase("EDGE", ExpectedResult = true)] + [TestCase("msedge", ExpectedResult = true)] + [TestCase("Microsoft", ExpectedResult = true)] + [TestCase("edg", ExpectedResult = true)] + [TestCase("Edge page", ExpectedResult = false)] + [TestCase("Edge Web page", ExpectedResult = false)] + public bool EdgeWebSites_ShouldBeFiltered_WhenSearchingForEdge(string query) + { + return pinned_webpage.FilterWebApplication(query); + } + + [TestCase("chrome", ExpectedResult = true)] + [TestCase("CHROME", ExpectedResult = true)] + [TestCase("Google", ExpectedResult = true)] + [TestCase("Google Chrome", ExpectedResult = true)] + [TestCase("Google Chrome twitter", ExpectedResult = false)] + public bool ChromeWebSites_ShouldBeFiltered_WhenSearchingForChrome(string query) + { + return twitter_pwa.FilterWebApplication(query); + } + + [TestCase("twitter", 0, ExpectedResult = false)] + [TestCase("Twit", 0, ExpectedResult = false)] + [TestCase("TWITTER", 0, ExpectedResult = false)] + [TestCase("web", 1, ExpectedResult = false)] + [TestCase("Page", 1, ExpectedResult = false)] + [TestCase("WEB PAGE", 1, ExpectedResult = false)] + [TestCase("edge", 2, ExpectedResult = false)] + [TestCase("EDGE", 2, ExpectedResult = false)] + public bool PinnedWebPages_ShouldNotBeFiltered_WhenSearchingForThem(string query, int Case) + { + const uint CASE_TWITTER = 0; + const uint CASE_WEB_PAGE = 1; + const uint CASE_EDGE_NAMED_WEBPAGE = 2; + + // If the query is a part of the name of the web application, it should not be filtered, + // even if the name is the same as that of the main application, eg: case 2 - edge + if (Case == CASE_TWITTER) + { + return twitter_pwa.FilterWebApplication(query); + } + else if (Case == CASE_WEB_PAGE) + { + return pinned_webpage.FilterWebApplication(query); + } + else if (Case == CASE_EDGE_NAMED_WEBPAGE) + { + return edge_named_pinned_webpage.FilterWebApplication(query); + } + // unreachable code + return true; + } + + [TestCase("Command Prompt")] + [TestCase("cmd")] + [TestCase("cmd.exe")] + [TestCase("ignoreQueryText")] + public void Win32Applications_ShouldNotBeFiltered_WhenFilteringRunCommands(string query) + { + // Even if there is an exact match in the name or exe name, win32 applications should never be filtered + Assert.IsTrue(command_prompt.QueryEqualsNameForRunCommands(query)); + } + + [TestCase("cmd")] + [TestCase("Cmd")] + [TestCase("CMD")] + public void RunCommands_ShouldNotBeFiltered_OnExactMatch(string query) + { + // Partial matches should be filtered as cmd is not equal to cmder + Assert.IsFalse(cmder_run_command.QueryEqualsNameForRunCommands(query)); + + // the query matches the name (cmd) and is therefore not filtered (case-insensitive) + Assert.IsTrue(cmd_run_command.QueryEqualsNameForRunCommands(query)); + } + + [Test] + public void WEB_APPLICATION_ReturnContextMenuWithOpenInConsole_WhenContextMenusIsCalled() + { + // Arrange + var mock = new Mock(); + mock.Setup(x => x.GetTranslation(It.IsAny())).Returns(It.IsAny()); + + // Act + List contextMenuResults = pinned_webpage.ContextMenus(mock.Object); + + // Assert + Assert.AreEqual(contextMenuResults.Count, 3); + mock.Verify(x => x.GetTranslation("wox_plugin_program_run_as_administrator"), Times.Once()); + mock.Verify(x => x.GetTranslation("wox_plugin_program_open_containing_folder"), Times.Once()); + mock.Verify(x => x.GetTranslation("wox_plugin_program_open_in_console"), Times.Once()); + } + + [Test] + public void INTERNET_SHORTCUT_APPLICATION_ReturnContextMenuWithOpenInConsole_WhenContextMenusIsCalled() + { + // Arrange + var mock = new Mock(); + mock.Setup(x => x.GetTranslation(It.IsAny())).Returns(It.IsAny()); + + // Act + List contextMenuResults = dummy_internetShortcut_app.ContextMenus(mock.Object); + + // Assert + Assert.AreEqual(contextMenuResults.Count, 2); + mock.Verify(x => x.GetTranslation("wox_plugin_program_open_containing_folder"), Times.Once()); + mock.Verify(x => x.GetTranslation("wox_plugin_program_open_in_console"), Times.Once()); + } + + [Test] + public void WIN32_APPLICATION_ReturnContextMenuWithOpenInConsole_WhenContextMenusIsCalled() + { + // Arrange + var mock = new Mock(); + mock.Setup(x => x.GetTranslation(It.IsAny())).Returns(It.IsAny()); + + // Act + List contextMenuResults = chrome.ContextMenus(mock.Object); + + // Assert + Assert.AreEqual(contextMenuResults.Count, 3); + mock.Verify(x => x.GetTranslation("wox_plugin_program_run_as_administrator"), Times.Once()); + mock.Verify(x => x.GetTranslation("wox_plugin_program_open_containing_folder"), Times.Once()); + mock.Verify(x => x.GetTranslation("wox_plugin_program_open_in_console"), Times.Once()); + } + + [Test] + public void RUN_COMMAND_ReturnContextMenuWithOpenInConsole_WhenContextMenusIsCalled() + { + // Arrange + var mock = new Mock(); + mock.Setup(x => x.GetTranslation(It.IsAny())).Returns(It.IsAny()); + + // Act + List contextMenuResults = cmd_run_command.ContextMenus(mock.Object); + + // Assert + Assert.AreEqual(contextMenuResults.Count, 3); + mock.Verify(x => x.GetTranslation("wox_plugin_program_run_as_administrator"), Times.Once()); + mock.Verify(x => x.GetTranslation("wox_plugin_program_open_containing_folder"), Times.Once()); + mock.Verify(x => x.GetTranslation("wox_plugin_program_open_in_console"), Times.Once()); + } + + [Test] + public void Win32Apps_ShouldSetNameAsTitle_WhileCreatingResult() + { + var mock = new Mock(); + mock.Setup(x => x.GetTranslation(It.IsAny())).Returns(It.IsAny()); + StringMatcher.Instance = new StringMatcher(); + + // Act + var result = cmder_run_command.Result("cmder", mock.Object); + + // Assert + Assert.IsTrue(result.Title.Equals(cmder_run_command.Name)); + Assert.IsFalse(result.Title.Equals(cmder_run_command.Description)); + } + } +} diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/ListRepositoryTests.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/ListRepositoryTests.cs new file mode 100644 index 0000000000..e4e7e4ab3c --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/ListRepositoryTests.cs @@ -0,0 +1,62 @@ +using Microsoft.Plugin.Program.Storage; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Text; +using Wox.Infrastructure.Storage; + +namespace Microsoft.Plugin.Program.UnitTests.Storage +{ + [TestFixture] + class ListRepositoryTests + { + + [Test] + public void Contains_ShouldReturnTrue_WhenListIsInitializedWithItem() + { + //Arrange + var itemName = "originalItem1"; + var mockStorage = new Mock>>(); + IRepository repository = new ListRepository(mockStorage.Object) { itemName }; + + //Act + var result = repository.Contains(itemName); + + //Assert + Assert.IsTrue(result); + } + + [Test] + public void Contains_ShouldReturnTrue_WhenListIsUpdatedWithAdd() + { + //Arrange + var mockStorage = new Mock>>(); + IRepository repository = new ListRepository(mockStorage.Object); + + //Act + var itemName = "newItem"; + repository.Add(itemName); + var result = repository.Contains(itemName); + + //Assert + Assert.IsTrue(result); + } + + [Test] + public void Contains_ShouldReturnFalse_WhenListIsUpdatedWithRemove() + { + //Arrange + var itemName = "originalItem1"; + var mockStorage = new Mock>>(); + IRepository repository = new ListRepository(mockStorage.Object) { itemName }; + + //Act + repository.Remove(itemName); + var result = repository.Contains(itemName); + + //Assert + Assert.IsFalse(result); + } + } +} diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs index ab4d90f477..ddd45bc1ab 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs @@ -1,207 +1,185 @@ -using Microsoft.PowerToys.Settings.UI.Lib; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Timers; -using System.Windows.Controls; -using Wox.Infrastructure.Logger; -using Wox.Infrastructure.Storage; -using Wox.Plugin; -using Microsoft.Plugin.Program.Views; - -using Stopwatch = Wox.Infrastructure.Stopwatch; -using Microsoft.Plugin.Program.Programs; - -namespace Microsoft.Plugin.Program -{ - public class Main : ISettingProvider, IPlugin, IPluginI18n, IContextMenu, ISavable, IReloadable, IDisposable - { - private static readonly object IndexLock = new object(); - internal static Programs.Win32[] _win32s { get; set; } - internal static Programs.UWP.Application[] _uwps { get; set; } - internal static Settings _settings { get; set; } - - FileSystemWatcher _watcher = null; - System.Timers.Timer _timer = null; - - private static bool IsStartupIndexProgramsRequired => _settings.LastIndexTime.AddDays(3) < DateTime.Today; - - private static PluginInitContext _context; - - private static BinaryStorage _win32Storage; - private static BinaryStorage _uwpStorage; - private readonly PluginJsonStorage _settingsStorage; +using Microsoft.PowerToys.Settings.UI.Lib; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Timers; +using System.Windows.Controls; +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; +using Microsoft.Plugin.Program.Programs; + +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; - - public Main() - { - _settingsStorage = new PluginJsonStorage(); - _settings = _settingsStorage.Load(); - - Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Preload programs cost", () => - { - _win32Storage = new BinaryStorage("Win32"); - _win32s = _win32Storage.TryLoad(new Programs.Win32[] { }); - _uwpStorage = new BinaryStorage("UWP"); - _uwps = _uwpStorage.TryLoad(new Programs.UWP.Application[] { }); - }); - Log.Info($"|Microsoft.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); - Log.Info($"|Microsoft.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>"); - - var a = Task.Run(() => - { - if (IsStartupIndexProgramsRequired || !_win32s.Any()) - Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs); - }); - - var b = Task.Run(() => - { - if (IsStartupIndexProgramsRequired || !_uwps.Any()) - Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Win32Program index cost", IndexUWPPrograms); - }); - - Task.WaitAll(a, b); - - _settings.LastIndexTime = DateTime.Today; - - InitializeFileWatchers(); - InitializeTimer(); - } - - public void Save() - { - _settingsStorage.Save(); - _win32Storage.Save(_win32s); - _uwpStorage.Save(_uwps); - } - - public List Query(Query query) - { - Programs.Win32[] win32; - Programs.UWP.Application[] uwps; - - lock (IndexLock) - { - // just take the reference inside the lock to eliminate query time issues. - win32 = _win32s; - uwps = _uwps; - } - - var results1 = win32.AsParallel() - .Where(p => p.Enabled) - .Select(p => p.Result(query.Search, _context.API)); - - var results2 = uwps.AsParallel() - .Where(p => p.Enabled) - .Select(p => p.Result(query.Search, _context.API)); - - var result = results1.Concat(results2).Where(r => r != null && r.Score > 0).ToList(); - return result; - } - - public void Init(PluginInitContext context) - { - _context = context; - _context.API.ThemeChanged += OnThemeChanged; - UpdateUWPIconPath(_context.API.GetCurrentTheme()); - } - - public void OnThemeChanged(Theme _, Theme currentTheme) - { - UpdateUWPIconPath(currentTheme); - } - - public void UpdateUWPIconPath(Theme theme) - { - foreach (UWP.Application app in _uwps) - { - app.UpdatePath(theme); - } - } - - public static void IndexWin32Programs() - { - var win32S = Programs.Win32.All(_settings); - lock (IndexLock) - { - _win32s = win32S; - } - } - - public static void IndexUWPPrograms() - { - var windows10 = new Version(10, 0); - var support = Environment.OSVersion.Version.Major >= windows10.Major; - - var applications = support ? Programs.UWP.All() : new Programs.UWP.Application[] { }; - lock (IndexLock) - { - _uwps = applications; - } - } - - public static void IndexPrograms() - { - var t1 = Task.Run(() => IndexWin32Programs()); - var t2 = Task.Run(() => IndexUWPPrograms()); - - Task.WaitAll(t1, t2); - - _settings.LastIndexTime = DateTime.Today; - } - - public Control CreateSettingPanel() - { - return new ProgramSetting(_context, _settings, _win32s, _uwps); - } - - public string GetTranslatedPluginTitle() - { - return _context.API.GetTranslation("wox_plugin_program_plugin_name"); - } - - public string GetTranslatedPluginDescription() - { - return _context.API.GetTranslation("wox_plugin_program_plugin_description"); - } - - public List LoadContextMenus(Result selectedResult) - { - var menuOptions = new List(); - var program = selectedResult.ContextData as Programs.IProgram; - if (program != null) - { - menuOptions = program.ContextMenus(_context.API); - } - - return menuOptions; - } - - public static void StartProcess(Func runProcess, ProcessStartInfo info) - { - try - { - runProcess(info); - } - catch (Exception) - { - var name = "Plugin: Program"; - var message = $"Unable to start: {info.FileName}"; - _context.API.ShowMsg(name, message, string.Empty); - } - } - - public void ReloadData() - { - IndexPrograms(); - } - - public void UpdateSettings(PowerLauncherSettings settings) - { - } + private PackageRepository _packageRepository = new PackageRepository(new PackageCatalogWrapper(), new BinaryStorage>("UWP")); + + public Main() + { + _settingsStorage = new PluginJsonStorage(); + _settings = _settingsStorage.Load(); + + Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Preload programs cost", () => + { + _win32Storage = new BinaryStorage("Win32"); + _win32s = _win32Storage.TryLoad(new Programs.Win32[] { }); + + _packageRepository.Load(); + }); + Log.Info($"|Microsoft.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); + + var a = Task.Run(() => + { + if (IsStartupIndexProgramsRequired || !_win32s.Any()) + Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs); + }); + + var b = Task.Run(() => + { + if (IsStartupIndexProgramsRequired || !_packageRepository.Any()) + Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Win32Program index cost", _packageRepository.IndexPrograms); + }); + + + Task.WaitAll(a, b); + + _settings.LastIndexTime = DateTime.Today; + } + + public void Save() + { + _settingsStorage.Save(); + _win32Storage.Save(_win32s); + _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() + .Where(p => p.Enabled) + .Select(p => p.Result(query.Search, _context.API)); + + var results2 = _packageRepository.AsParallel() + .Where(p => p.Enabled) + .Select(p => p.Result(query.Search, _context.API)); + + var result = results1.Concat(results2).Where(r => r != null && r.Score > 0).ToList(); + return result; + } + + public void Init(PluginInitContext context) + { + _context = context; + _context.API.ThemeChanged += OnThemeChanged; + UpdateUWPIconPath(_context.API.GetCurrentTheme()); + } + + public void OnThemeChanged(Theme _, Theme currentTheme) + { + UpdateUWPIconPath(currentTheme); + } + + public void UpdateUWPIconPath(Theme theme) + { + foreach (UWP.Application app in _packageRepository) + { + app.UpdatePath(theme); + } + } + + public static void IndexWin32Programs() + { + var win32S = Programs.Win32.All(_settings); + lock (IndexLock) + { + _win32s = win32S; + } + } + + + + public void IndexPrograms() + { + var t1 = Task.Run(() => IndexWin32Programs()); + var t2 = Task.Run(() => _packageRepository.IndexPrograms()); + + Task.WaitAll(t1, t2); + + _settings.LastIndexTime = DateTime.Today; + } + + public string GetTranslatedPluginTitle() + { + return _context.API.GetTranslation("wox_plugin_program_plugin_name"); + } + + public string GetTranslatedPluginDescription() + { + return _context.API.GetTranslation("wox_plugin_program_plugin_description"); + } + + public List LoadContextMenus(Result selectedResult) + { + var menuOptions = new List(); + var program = selectedResult.ContextData as Programs.IProgram; + if (program != null) + { + menuOptions = program.ContextMenus(_context.API); + } + + return menuOptions; + } + + public static void StartProcess(Func runProcess, ProcessStartInfo info) + { + try + { + runProcess(info); + } + catch (Exception) + { + var name = "Plugin: Program"; + var message = $"Unable to start: {info.FileName}"; + _context.API.ShowMsg(name, message, string.Empty); + } + } + + public void ReloadData() + { + IndexPrograms(); + } + + public void UpdateSettings(PowerLauncherSettings settings) + { + } public void Dispose() { @@ -221,52 +199,5 @@ namespace Microsoft.Plugin.Program } } - void InitializeFileWatchers() - { - // Create a new FileSystemWatcher and set its properties. - _watcher = new FileSystemWatcher(); - var resolvedPath = Environment.ExpandEnvironmentVariables("%ProgramFiles%"); - _watcher.Path = resolvedPath; - - //Filter to create and deletes of 'microsoft.system.package.metadata' directories. - _watcher.NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.FileName; - _watcher.IncludeSubdirectories = true; - - // Add event handlers. - _watcher.Created += OnChanged; - _watcher.Deleted += OnChanged; - - // Begin watching. - _watcher.EnableRaisingEvents = true; - } - - void InitializeTimer() - { - //multiple file writes occur on install / uninstall. Adding a delay before actually indexing. - var delayInterval = 5000; - _timer = new System.Timers.Timer(delayInterval); - _timer.Enabled = true; - _timer.AutoReset = false; - _timer.Elapsed += FileWatchElapsedTimer; - _timer.Stop(); - } - - //When a watched directory changes then reset the timer. - private void OnChanged(object source, FileSystemEventArgs e) - { - Log.Debug($"|Microsoft.Plugin.Program.Main|Directory Changed: {e.FullPath} {e.ChangeType} - Resetting timer."); - _timer.Stop(); - _timer.Start(); - } - - private void FileWatchElapsedTimer(object sender, ElapsedEventArgs e) - { - Task.Run(() => - { - Log.Debug($"|Microsoft.Plugin.Program.Main| ReIndexing UWP Programs"); - IndexUWPPrograms(); - Log.Debug($"|Microsoft.Plugin.Program.Main| Done ReIndexing"); - }); - } - } + } } \ No newline at end of file diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/IPackageCatalog.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/IPackageCatalog.cs new file mode 100644 index 0000000000..14a80898dd --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/IPackageCatalog.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Windows.ApplicationModel; +using Windows.Foundation; + +namespace Microsoft.Plugin.Program.Programs +{ + internal interface IPackageCatalog + { + event TypedEventHandler PackageInstalling; + event TypedEventHandler PackageUninstalling; + event TypedEventHandler PackageUpdating; + } +} diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/PackageCatalogWrapper.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/PackageCatalogWrapper.cs new file mode 100644 index 0000000000..f3aabaf4e0 --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/PackageCatalogWrapper.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Windows.ApplicationModel; +using Windows.Foundation; + +namespace Microsoft.Plugin.Program.Programs +{ + + /// + /// This is a simple wrapper class around the PackageCatalog to facilitate unit testing. + /// + internal class PackageCatalogWrapper : IPackageCatalog + { + PackageCatalog _packageCatalog; + + public PackageCatalogWrapper() + { + //Opens the catalog of packages that is available for the current user. + _packageCatalog = PackageCatalog.OpenForCurrentUser(); + } + + // + // Summary: + // Indicates that an app package is installing. + public event TypedEventHandler PackageInstalling + { + add + { + _packageCatalog.PackageInstalling += value; + } + + remove + { + _packageCatalog.PackageInstalling -= value; + } + } + + // + // Summary: + // Indicates that an app package is installing. + public event TypedEventHandler PackageUninstalling + { + add + { + _packageCatalog.PackageUninstalling += value; + } + + remove + { + _packageCatalog.PackageUninstalling -= value; + } + } + + + // + // Summary: + // Indicates that an app package is installing. + public event TypedEventHandler PackageUpdating + { + add + { + _packageCatalog.PackageUpdating += value; + } + + remove + { + _packageCatalog.PackageUpdating -= value; + } + } + + } +} diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWP.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWP.cs index a266cf8d16..c8954c3701 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWP.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWP.cs @@ -31,7 +31,7 @@ namespace Microsoft.Plugin.Program.Programs { public string Name { get; } public string FullName { get; } - public string FamilyName { get; } + public string FamilyName { get; } public string Location { get; set; } public Application[] Apps { get; set; } @@ -40,24 +40,17 @@ namespace Microsoft.Plugin.Program.Programs public UWP(Package package) { - Location = package.InstalledLocation.Path; + Name = package.Id.Name; FullName = package.Id.FullName; FamilyName = package.Id.FamilyName; - InitializeAppInfo(); - Apps = Apps.Where(a => - { - var valid = - !string.IsNullOrEmpty(a.UserModelId) && - !string.IsNullOrEmpty(a.DisplayName); - return valid; - }).ToArray(); } - private void InitializeAppInfo() + public void InitializeAppInfo(string installedLocation) { + Location = installedLocation; AppxPackageHelper _helper = new AppxPackageHelper(); - var path = Path.Combine(Location, "AppxManifest.xml"); + var path = Path.Combine(installedLocation, "AppxManifest.xml"); var namespaces = XmlNamespaces(path); InitPackageVersion(namespaces); @@ -154,21 +147,14 @@ namespace Microsoft.Plugin.Program.Programs try { u = new UWP(p); + u.InitializeAppInfo(p.InstalledLocation.Path); } -#if !DEBUG catch (Exception e) { ProgramLogger.LogException($"|UWP|All|{p.InstalledLocation}|An unexpected error occurred and " + $"unable to convert Package to UWP for {p.Id.FullName}", e); return new Application[] { }; } -#endif -#if DEBUG //make developer aware and implement handling - catch - { - throw; - } -#endif return u.Apps; }).ToArray(); diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/PackageRepository.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/PackageRepository.cs new file mode 100644 index 0000000000..bc6eb7687e --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/PackageRepository.cs @@ -0,0 +1,86 @@ +using Microsoft.Plugin.Program.Logger; +using Microsoft.Plugin.Program.Programs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.ApplicationModel; +using Wox.Infrastructure.Storage; + +namespace Microsoft.Plugin.Program.Storage +{ + /// + /// A repository for storing packaged applications such as UWP apps or appx packaged desktop apps. + /// This repository will also monitor for changes to the PackageCatelog and update the repository accordingly + /// + internal class PackageRepository : ListRepository, IRepository + { + IPackageCatalog _packageCatalog; + public PackageRepository(IPackageCatalog packageCatalog, IStorage> storage) : base(storage) + { + _packageCatalog = packageCatalog ?? throw new ArgumentNullException("packageCatalog", "PackageRepository expects an interface to be able to subscribe to package events"); + _packageCatalog.PackageInstalling += OnPackageInstalling; + _packageCatalog.PackageUninstalling += OnPackageUninstalling; + } + + public void OnPackageInstalling(PackageCatalog p, PackageInstallingEventArgs args) + { + if (args.IsComplete) + { + + try + { + var uwp = new UWP(args.Package); + uwp.InitializeAppInfo(args.Package.InstalledLocation.Path); + foreach (var app in uwp.Apps) + { + Add(app); + } + } + //InitializeAppInfo will throw if there is no AppxManifest.xml for the package. + //Note there are sometimes multiple packages per application and this doesn't necessarily mean that we haven't found the app. + //eg. "Could not find file 'C:\\Program Files\\WindowsApps\\Microsoft.WindowsTerminalPreview_2020.616.45.0_neutral_~_8wekyb3d8bbwe\\AppxManifest.xml'." + + catch ( System.IO.FileNotFoundException e) + { + ProgramLogger.LogException($"|UWP|OnPackageInstalling|{e.Message}", e); + } + } + } + + public void OnPackageUninstalling(PackageCatalog p, PackageUninstallingEventArgs args) + { + if (args.Progress == 0) + { + //find apps associated with this package. + var uwp = new UWP(args.Package); + var apps = _items.Where(a => a.Package.Equals(uwp)).ToArray(); + foreach(var app in apps) + { + Remove(app); + } + } + } + + public void IndexPrograms() + { + var windows10 = new Version(10, 0); + var support = Environment.OSVersion.Version.Major >= windows10.Major; + + var applications = support ? Programs.UWP.All() : new Programs.UWP.Application[] { }; + Set(applications); + } + + public void Save() + { + _storage.Save(_items); + } + + public void Load() + { + var items = _storage.TryLoad(new Programs.UWP.Application[] { }); + 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 3285722da4..59105c0199 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 @@ -61,20 +61,6 @@ namespace Microsoft.Plugin.Program.Views.Commands Enabled = t1.Enabled } )); - - Main._uwps - .Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)) - .ToList() - .ForEach(t1 => ProgramSetting.ProgramSettingDisplayList - .Add( - new ProgramSource - { - Name = t1.DisplayName, - Location = t1.Package.Location, - UniqueIdentifier = t1.UniqueIdentifier, - Enabled = t1.Enabled - } - )); } internal static void SetProgramSourcesStatus(this List list, List selectedProgramSourcesToDisable, bool status) @@ -88,11 +74,6 @@ namespace Microsoft.Plugin.Program.Views.Commands .Where(t1 => selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && t1.Enabled != status)) .ToList() .ForEach(t1 => t1.Enabled = status); - - Main._uwps - .Where(t1 => selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && t1.Enabled != status)) - .ToList() - .ForEach(t1 => t1.Enabled = status); } internal static void StoreDisabledInSettings(this List list) @@ -133,7 +114,7 @@ namespace Microsoft.Plugin.Program.Views.Commands internal static bool IsReindexRequired(this List selectedItems) { - if (selectedItems.Where(t1 => t1.Enabled && !Main._uwps.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)).Count() > 0 + if (selectedItems.Where(t1 => t1.Enabled).Count() > 0 && selectedItems.Where(t1 => t1.Enabled && !Main._win32s.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)).Count() > 0) return true; diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Views/ProgramSetting.xaml.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Views/ProgramSetting.xaml.cs index 79b5cbbc68..7802eeb59b 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Views/ProgramSetting.xaml.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Views/ProgramSetting.xaml.cs @@ -51,7 +51,6 @@ namespace Microsoft.Plugin.Program.Views Task.Run(() => { Dispatcher.Invoke(() => { indexingPanel.Visibility = Visibility.Visible; }); - Main.IndexPrograms(); Dispatcher.Invoke(() => { indexingPanel.Visibility = Visibility.Hidden; }); }); } diff --git a/src/modules/launcher/Wox.Infrastructure/Storage/BinaryStorage.cs b/src/modules/launcher/Wox.Infrastructure/Storage/BinaryStorage.cs index 83a41cf040..26c62a3a0b 100644 --- a/src/modules/launcher/Wox.Infrastructure/Storage/BinaryStorage.cs +++ b/src/modules/launcher/Wox.Infrastructure/Storage/BinaryStorage.cs @@ -12,7 +12,7 @@ namespace Wox.Infrastructure.Storage /// Storage object using binary data /// Normally, it has better performance, but not readable /// - public class BinaryStorage + public class BinaryStorage : IStorage { // This storage helper returns whether or not to delete the binary storage items private static readonly int BINARY_STORAGE = 0; diff --git a/src/modules/launcher/Wox.Infrastructure/Storage/IRepository.cs b/src/modules/launcher/Wox.Infrastructure/Storage/IRepository.cs new file mode 100644 index 0000000000..ca8b9f1d26 --- /dev/null +++ b/src/modules/launcher/Wox.Infrastructure/Storage/IRepository.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Wox.Infrastructure.Storage +{ + public interface IRepository + { + void Add(T insertedItem); + void Remove(T removedItem); + bool Contains(T item); + void Set(IList list); + bool Any(); + } +} diff --git a/src/modules/launcher/Wox.Infrastructure/Storage/IStorage.cs b/src/modules/launcher/Wox.Infrastructure/Storage/IStorage.cs new file mode 100644 index 0000000000..252cc8a9e9 --- /dev/null +++ b/src/modules/launcher/Wox.Infrastructure/Storage/IStorage.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Wox.Infrastructure.Storage +{ + public interface IStorage + { + /// + /// Saves the data + /// + /// + void Save(T data); + + /// + /// Attempts to load data, otherwise it will return the default provided + /// + /// + /// The loaded data or default + T TryLoad(T defaultData); + } +} diff --git a/src/modules/launcher/Wox.Infrastructure/Storage/ListRepository.cs b/src/modules/launcher/Wox.Infrastructure/Storage/ListRepository.cs new file mode 100644 index 0000000000..93400b5acb --- /dev/null +++ b/src/modules/launcher/Wox.Infrastructure/Storage/ListRepository.cs @@ -0,0 +1,68 @@ +using NLog.Filters; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Wox.Infrastructure; + +namespace Wox.Infrastructure.Storage +{ + /// + /// The intent of this class is to provide a basic subset of 'list' like operations, without exposing callers to the internal representation + /// of the data structure. Currently this is implemented as a list for it's simplicity. + /// + /// + public class ListRepository : IRepository, IEnumerable + { + protected IList _items = new List(); + protected IStorage> _storage; + + public ListRepository(IStorage> storage) + { + _storage = storage ?? throw new ArgumentNullException("storage", "StorageRepository requires an initialized storage interface"); + } + + public void Set(IList items) + { + //enforce that internal representation + _items = items.ToList(); + } + + public bool Any() + { + return _items.Any(); + } + + public void Add(T insertedItem) + { + _items.Add(insertedItem); + } + + public void Remove(T removedItem) + { + _items.Remove(removedItem); + } + + public ParallelQuery AsParallel() + { + return _items.AsParallel(); + } + + public bool Contains(T item) + { + return _items.Contains(item); + } + + public IEnumerator GetEnumerator() + { + return _items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _items.GetEnumerator(); + } + } +} diff --git a/src/modules/launcher/Wox.Infrastructure/StringMatcher.cs b/src/modules/launcher/Wox.Infrastructure/StringMatcher.cs index 6ba0fe006b..43140ed944 100644 --- a/src/modules/launcher/Wox.Infrastructure/StringMatcher.cs +++ b/src/modules/launcher/Wox.Infrastructure/StringMatcher.cs @@ -2,8 +2,10 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Runtime.CompilerServices; using static Wox.Infrastructure.StringMatcher; +[assembly: InternalsVisibleToAttribute("Microsoft.Plugin.Program.UnitTests")] namespace Wox.Infrastructure { public class StringMatcher