diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index dbd8b04619..be5d10eae6 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -554,6 +554,7 @@ EXCLUDEFILES EXCLUDEFOLDERS EXCLUDESUBFOLDERS exe +Executables executionpolicy exename exif @@ -669,6 +670,7 @@ hardcoded Hardlines HARDWAREINPUT hashcode +Hashset hbitmap hbmp hbr @@ -1595,6 +1597,7 @@ REGCLS regedit regex REGISTERCLASSFAILED +Registery registrypath regkey reimplementing @@ -1706,6 +1709,7 @@ serizalization serverside SETCONTEXT setcursor +setenv SETFOCUS SETFOREGROUND SETICON @@ -1742,8 +1746,8 @@ shlwapi shobjidl SHORTCUTATLEAST shortcutcontrol -shortcutguide Shortcutguide +shortcutguide SHORTCUTMAXONEACTIONKEY SHORTCUTNOREPEATEDMODIFIER SHORTCUTONEACTIONKEY @@ -2160,8 +2164,8 @@ windowsx windowwalker winerror WINEVENT -winexe winevt +winexe winforms winfx winget diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/Win32Tests.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/Win32Tests.cs index 81bb79553d..47de8c8091 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/Win32Tests.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/Win32Tests.cs @@ -50,6 +50,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "Microsoft Azure Command Prompt - v2.9", ExecutableName = "cmd.exe", FullPath = "c:\\windows\\system32\\cmd.exe", + Arguments = @"/E:ON /V:ON /K ""C:\Program Files\Microsoft SDKs\Azure\.NET SDK\v2.9\\bin\setenv.cmd""", 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 = Win32Program.ApplicationType.Win32Application, }; @@ -59,6 +60,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "x64 Native Tools Command Prompt for VS 2019", ExecutableName = "cmd.exe", FullPath = "c:\\windows\\system32\\cmd.exe", + Arguments = @"/k ""C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat""", 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 = Win32Program.ApplicationType.Win32Application, }; @@ -262,10 +264,10 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs }; // Act - Win32Program[] apps = Win32Program.DeduplicatePrograms(prgms.AsParallel()); + List apps = Win32Program.DeduplicatePrograms(prgms.AsParallel()); // Assert - Assert.AreEqual(1, apps.Length); + Assert.AreEqual(1, apps.Count); } [Test] @@ -279,10 +281,10 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs }; // Act - Win32Program[] apps = Win32Program.DeduplicatePrograms(prgms.AsParallel()); + List apps = Win32Program.DeduplicatePrograms(prgms.AsParallel()); // Assert - Assert.AreEqual(1, apps.Length); + Assert.AreEqual(1, apps.Count); } [Test] @@ -295,10 +297,10 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs }; // Act - Win32Program[] apps = Win32Program.DeduplicatePrograms(prgms.AsParallel()); + List apps = Win32Program.DeduplicatePrograms(prgms.AsParallel()); // Assert - Assert.AreEqual(1, apps.Length); + Assert.AreEqual(1, apps.Count); } [Test] @@ -312,10 +314,10 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs }; // Act - Win32Program[] apps = Win32Program.DeduplicatePrograms(prgms.AsParallel()); + List apps = Win32Program.DeduplicatePrograms(prgms.AsParallel()); // Assert - Assert.AreEqual(1, apps.Length); + Assert.AreEqual(1, apps.Count); Assert.IsTrue(!string.IsNullOrEmpty(apps[0].LnkResolvedPath)); } @@ -331,10 +333,10 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs }; // Act - Win32Program[] apps = Win32Program.DeduplicatePrograms(prgms.AsParallel()); + List apps = Win32Program.DeduplicatePrograms(prgms.AsParallel()); // Assert - Assert.AreEqual(3, apps.Length); + Assert.AreEqual(3, apps.Count); } [Test] 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 index 920a438b10..c05955fd5a 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/Win32ProgramRepositoryTest.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/Win32ProgramRepositoryTest.cs @@ -258,7 +258,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Storage // 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" }); + mockFile.Setup(m => m.ReadLines(It.IsAny())).Returns(new string[] { "URL=steam://rungameid/1258080", "IconFile=iconFile" }); Win32Program.FileWrapper = mockFile.Object; string fullPath = directory + "\\" + path; @@ -281,7 +281,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Storage // 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" }); + mockFile.Setup(m => m.ReadLines(It.IsAny())).Returns(new string[] { "URL=steam://rungameid/1258080", "IconFile=iconFile" }); Win32Program.FileWrapper = mockFile.Object; string oldFullPath = directory + "\\" + oldpath; diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Logger/ProgramLogger.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Logger/ProgramLogger.cs index 589cf09dcf..e91117f579 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Logger/ProgramLogger.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Logger/ProgramLogger.cs @@ -17,6 +17,31 @@ namespace Microsoft.Plugin.Program.Logger /// internal static class ProgramLogger { + /// + /// Logs an warning + /// + [MethodImpl(MethodImplOptions.Synchronized)] + internal static void Warn(string message, Exception ex, Type fullClassName, string loadingProgramPath, [CallerMemberName] string methodName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + { + string calledMethod = "Not available"; + + if (ex != null) + { + string exceptionCalledMethod = ex.TargetSite != null ? ex.TargetSite.ToString() : ex.StackTrace; + if (!string.IsNullOrEmpty(exceptionCalledMethod)) + { + calledMethod = exceptionCalledMethod; + } + } + + var msg = $"\n\t\tProgram path: {loadingProgramPath}" + + $"\n\t\tException thrown in called method: {calledMethod}" + + $"\n\t\tPossible interpretation of the error: {message}"; + + // removed looping logic since that is inside Log class + Log.Warn(msg, fullClassName, methodName, sourceFilePath, sourceLineNumber); + } + /// /// Logs an exception /// @@ -29,7 +54,7 @@ namespace Microsoft.Plugin.Program.Logger if (IsKnownWinProgramError(ex, methodName) || IsKnownUWPProgramError(ex, methodName)) { - possibleResolution = "Can be ignored and Wox should still continue, however the program may not be loaded"; + possibleResolution = "Can be ignored and PowerToys Run should still continue, however the program may not be loaded"; errorStatus = "KNOWN"; } diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs index f2d5737868..fd98a639e8 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs @@ -53,20 +53,6 @@ namespace Microsoft.Plugin.Program // Initialize the Win32ProgramRepository with the settings object _win32ProgramRepository = new Win32ProgramRepository(_win32ProgramRepositoryHelper.FileSystemWatchers.Cast().ToList(), new BinaryStorage>("Win32"), Settings, _win32ProgramRepositoryHelper.PathsToWatch); - - var a = Task.Run(() => - { - Stopwatch.Normal("Microsoft.Plugin.Program.Main - Win32Program index cost", _win32ProgramRepository.IndexPrograms); - }); - - var b = Task.Run(() => - { - Stopwatch.Normal("Microsoft.Plugin.Program.Main - Package index cost", _packageRepository.IndexPrograms); - }); - - Task.WaitAll(a, b); - - Settings.LastIndexTime = DateTime.Today; } public void Save() @@ -118,6 +104,20 @@ namespace Microsoft.Plugin.Program _context.API.ThemeChanged += OnThemeChanged; UpdateUWPIconPath(_context.API.GetCurrentTheme()); + + var a = Task.Run(() => + { + Stopwatch.Normal("Microsoft.Plugin.Program.Main - Win32Program index cost", _win32ProgramRepository.IndexPrograms); + }); + + var b = Task.Run(() => + { + Stopwatch.Normal("Microsoft.Plugin.Program.Main - Package index cost", _packageRepository.IndexPrograms); + }); + + Task.WaitAll(a, b); + + Settings.LastIndexTime = DateTime.Today; } public void OnThemeChanged(Theme currentTheme, Theme newTheme) diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32Program.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32Program.cs index 40329a6971..d0d9d20f96 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32Program.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -11,7 +11,6 @@ using System.IO.Abstractions; using System.Linq; using System.Reflection; using System.Security; -using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows.Input; @@ -28,6 +27,8 @@ namespace Microsoft.Plugin.Program.Programs [Serializable] public class Win32Program : IProgram { + public static readonly Win32Program InvalidProgram = new Win32Program { Valid = false, Enabled = false }; + private static readonly IFileSystem FileSystem = new FileSystem(); private static readonly IPath Path = FileSystem.Path; private static readonly IFile File = FileSystem.File; @@ -53,7 +54,7 @@ namespace Microsoft.Plugin.Program.Programs public bool Enabled { get; set; } - public bool HasArguments { get; set; } + public bool HasArguments => !string.IsNullOrEmpty(Arguments); public string Arguments { get; set; } = string.Empty; @@ -104,10 +105,12 @@ namespace Microsoft.Plugin.Program.Programs { // To Filter PWAs when the user searches for the main application // All Chromium based applications contain the --app-id argument - // Reference : https://codereview.chromium.org/399045/show + // Reference : https://codereview.chromium.org/399045 // Using Ordinal IgnoreCase since this is used internally - bool isWebApplication = FullPath.Contains(ProxyWebApp, StringComparison.OrdinalIgnoreCase) && Arguments.Contains(AppIdArgument, StringComparison.OrdinalIgnoreCase); - return isWebApplication; + return !string.IsNullOrEmpty(FullPath) && + !string.IsNullOrEmpty(Arguments) && + FullPath.Contains(ProxyWebApp, StringComparison.OrdinalIgnoreCase) && + Arguments.Contains(AppIdArgument, StringComparison.OrdinalIgnoreCase); } // Condition to Filter pinned Web Applications or PWAs when searching for the main application @@ -119,9 +122,6 @@ namespace Microsoft.Plugin.Program.Programs return false; } - // Set the subtitle to 'Web Application' - AppType = ApplicationType.WebApplication; - string[] subqueries = query?.Split() ?? Array.Empty(); bool nameContainsQuery = false; bool pathContainsQuery = false; @@ -145,35 +145,26 @@ namespace Microsoft.Plugin.Program.Programs } // Function to set the subtitle based on the Type of application - private string SetSubtitle() + private string GetSubtitle() { - if (AppType == ApplicationType.Win32Application || AppType == ApplicationType.ShortcutApplication || AppType == ApplicationType.ApprefApplication) + switch (AppType) { - return Properties.Resources.powertoys_run_plugin_program_win32_application; - } - else if (AppType == ApplicationType.InternetShortcutApplication) - { - return Properties.Resources.powertoys_run_plugin_program_internet_shortcut_application; - } - else if (AppType == ApplicationType.WebApplication) - { - return Properties.Resources.powertoys_run_plugin_program_web_application; - } - else if (AppType == ApplicationType.RunCommand) - { - return Properties.Resources.powertoys_run_plugin_program_run_command; - } - else if (AppType == ApplicationType.Folder) - { - return Properties.Resources.powertoys_run_plugin_program_folder_type; - } - else if (AppType == ApplicationType.GenericFile) - { - return Properties.Resources.powertoys_run_plugin_program_generic_file_type; - } - else - { - return string.Empty; + case ApplicationType.Win32Application: + case ApplicationType.ShortcutApplication: + case ApplicationType.ApprefApplication: + return Properties.Resources.powertoys_run_plugin_program_win32_application; + case ApplicationType.InternetShortcutApplication: + return Properties.Resources.powertoys_run_plugin_program_internet_shortcut_application; + case ApplicationType.WebApplication: + return Properties.Resources.powertoys_run_plugin_program_web_application; + case ApplicationType.RunCommand: + return Properties.Resources.powertoys_run_plugin_program_run_command; + case ApplicationType.Folder: + return Properties.Resources.powertoys_run_plugin_program_folder_type; + case ApplicationType.GenericFile: + return Properties.Resources.powertoys_run_plugin_program_generic_file_type; + default: + return string.Empty; } } @@ -226,7 +217,9 @@ namespace Microsoft.Plugin.Program.Programs var result = new Result { - SubTitle = SetSubtitle(), + // To set the title for the result to always be the name of the application + Title = Name, + SubTitle = GetSubtitle(), IcoPath = IcoPath, Score = score, ContextData = this, @@ -241,8 +234,6 @@ namespace Microsoft.Plugin.Program.Programs }, }; - // To set the title for the result to always be the name of the application - result.Title = Name; result.SetTitleHighlightData(StringMatcher.FuzzySearch(query, Name).MatchData); // Using CurrentCulture since this is user facing @@ -312,7 +303,7 @@ namespace Microsoft.Plugin.Program.Programs { try { - Wox.Infrastructure.Helper.OpenInConsole(ParentDirectory); + Helper.OpenInConsole(ParentDirectory); return true; } catch (Exception e) @@ -349,7 +340,7 @@ namespace Microsoft.Plugin.Program.Programs { try { - var p = new Win32Program + return new Win32Program { Name = Path.GetFileNameWithoutExtension(path), ExecutableName = Path.GetFileName(path), @@ -364,35 +355,36 @@ namespace Microsoft.Plugin.Program.Programs Enabled = true, AppType = ApplicationType.Win32Application, }; - return p; } catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { - ProgramLogger.Exception($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); + ProgramLogger.Warn($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); - return new Win32Program() { Valid = false, Enabled = false }; + return InvalidProgram; } catch (Exception e) { ProgramLogger.Exception($"|An unexpected error occurred in the calling method CreateWin32Program at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); - return new Win32Program() { Valid = false, Enabled = false }; + return InvalidProgram; } } + private static readonly Regex InternetShortcutURLPrefixes = new Regex(@"^steam:\/\/(rungameid|run)\/|^com\.epicgames\.launcher:\/\/apps\/", RegexOptions.Compiled); + // This function filters Internet Shortcut programs [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Any error in InternetShortcutProgram should not prevent other programs from loading.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "User facing path needs to be shown in lowercase.")] private static Win32Program InternetShortcutProgram(string path) { try { - string[] lines = FileWrapper.ReadAllLines(path); + // We don't want to read the whole file if we don't need to + var lines = FileWrapper.ReadLines(path); string iconPath = string.Empty; string urlPath = string.Empty; bool validApp = false; - Regex internetShortcutURLPrefixes = new Regex(@"^steam:\/\/(rungameid|run)\/|^com\.epicgames\.launcher:\/\/apps\/"); - const string urlPrefix = "URL="; const string iconFilePrefix = "IconFile="; @@ -403,65 +395,62 @@ namespace Microsoft.Plugin.Program.Programs { urlPath = line.Substring(urlPrefix.Length); - try + if (!Uri.TryCreate(urlPath, UriKind.RelativeOrAbsolute, out Uri _)) { - Uri uri = new Uri(urlPath); - } - catch (UriFormatException e) - { - // To catch the exception if the uri cannot be parsed. - // Link to watson crash: https://watsonportal.microsoft.com/Failure?FailureSearchText=5f871ea7-e886-911f-1b31-131f63f6655b - ProgramLogger.Exception($"url could not be parsed", e, MethodBase.GetCurrentMethod().DeclaringType, urlPath); - return new Win32Program() { Valid = false, Enabled = false }; + ProgramLogger.Warn("url could not be parsed", null, MethodBase.GetCurrentMethod().DeclaringType, urlPath); + return InvalidProgram; } // To filter out only those steam shortcuts which have 'run' or 'rungameid' as the hostname - if (internetShortcutURLPrefixes.Match(urlPath).Success) + if (InternetShortcutURLPrefixes.Match(urlPath).Success) { validApp = true; } } - - // Using OrdinalIgnoreCase since this is used internally - if (line.StartsWith(iconFilePrefix, StringComparison.OrdinalIgnoreCase)) + else if (line.StartsWith(iconFilePrefix, StringComparison.OrdinalIgnoreCase)) { iconPath = line.Substring(iconFilePrefix.Length); } + + // If we resolved an urlPath & and an iconPath quit reading the file + if (!string.IsNullOrEmpty(urlPath) && !string.IsNullOrEmpty(iconPath)) + { + break; + } } if (!validApp) { - return new Win32Program() { Valid = false, Enabled = false }; + return InvalidProgram; } try { - var p = new Win32Program + return new Win32Program { Name = Path.GetFileNameWithoutExtension(path), ExecutableName = Path.GetFileName(path), IcoPath = iconPath, - FullPath = urlPath, + FullPath = urlPath.ToLowerInvariant(), UniqueIdentifier = path, ParentDirectory = Directory.GetParent(path).FullName, Valid = true, Enabled = true, AppType = ApplicationType.InternetShortcutApplication, }; - return p; } catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { - ProgramLogger.Exception($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); + ProgramLogger.Warn($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); - return new Win32Program() { Valid = false, Enabled = false }; + return InvalidProgram; } } catch (Exception e) { ProgramLogger.Exception($"|An unexpected error occurred in the calling method InternetShortcutProgram at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); - return new Win32Program() { Valid = false, Enabled = false }; + return InvalidProgram; } } @@ -472,33 +461,33 @@ namespace Microsoft.Plugin.Program.Programs try { var program = CreateWin32Program(path); - const int MAX_PATH = 260; - StringBuilder buffer = new StringBuilder(MAX_PATH); - string target = ShellLinkHelper.RetrieveTargetPath(path); - if (!string.IsNullOrEmpty(target)) + if (!string.IsNullOrEmpty(target) && (File.Exists(target) || Directory.Exists(target))) { - if (File.Exists(target) || Directory.Exists(target)) + program.LnkResolvedPath = program.FullPath; + + // Using CurrentCulture since this is user facing + program.FullPath = Path.GetFullPath(target).ToLowerInvariant(); + + program.Arguments = ShellLinkHelper.Arguments; + + // A .lnk could be a (Chrome) PWA, set correct AppType + program.AppType = program.IsWebApplication() + ? ApplicationType.WebApplication + : GetAppTypeFromPath(target); + + var description = ShellLinkHelper.Description; + if (!string.IsNullOrEmpty(description)) { - program.LnkResolvedPath = program.FullPath; - - // Using InvariantCulture since this is user facing - program.FullPath = Path.GetFullPath(target).ToLowerInvariant(); - program.AppType = GetAppTypeFromPath(target); - - var description = ShellLinkHelper.Description; - if (!string.IsNullOrEmpty(description)) + program.Description = description; + } + else + { + var info = FileVersionInfoWrapper.GetVersionInfo(target); + if (!string.IsNullOrEmpty(info?.FileDescription)) { - program.Description = description; - } - else - { - var info = FileVersionInfoWrapper.GetVersionInfo(target); - if (!string.IsNullOrEmpty(info?.FileDescription)) - { - program.Description = info.FileDescription; - } + program.Description = info.FileDescription; } } } @@ -512,7 +501,7 @@ namespace Microsoft.Plugin.Program.Programs { ProgramLogger.Exception($"|An unexpected error occurred in the calling method LnkProgram at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); - return new Win32Program() { Valid = false, Enabled = false }; + return InvalidProgram; } } @@ -523,7 +512,6 @@ namespace Microsoft.Plugin.Program.Programs { var program = CreateWin32Program(path); var info = FileVersionInfoWrapper.GetVersionInfo(path); - if (!string.IsNullOrEmpty(info?.FileDescription)) { program.Description = info.FileDescription; @@ -533,21 +521,21 @@ namespace Microsoft.Plugin.Program.Programs } catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { - ProgramLogger.Exception($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); + ProgramLogger.Warn($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); - return new Win32Program() { Valid = false, Enabled = false }; + return InvalidProgram; } catch (FileNotFoundException e) { - ProgramLogger.Exception($"|Unable to locate exe file at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); + ProgramLogger.Warn($"|Unable to locate exe file at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); - return new Win32Program() { Valid = false, Enabled = false }; + return InvalidProgram; } catch (Exception e) { ProgramLogger.Exception($"|An unexpected error occurred in the calling method ExeProgram at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); - return new Win32Program() { Valid = false, Enabled = false }; + return InvalidProgram; } } @@ -560,33 +548,30 @@ namespace Microsoft.Plugin.Program.Programs } string extension = Extension(path); - ApplicationType appType = ApplicationType.GenericFile; // Using OrdinalIgnoreCase since these are used internally with paths if (ExecutableApplicationExtensions.Contains(extension)) { - appType = ApplicationType.Win32Application; + return ApplicationType.Win32Application; } else if (extension.Equals(ShortcutExtension, StringComparison.OrdinalIgnoreCase)) { - appType = ApplicationType.ShortcutApplication; + return ApplicationType.ShortcutApplication; } else if (extension.Equals(ApplicationReferenceExtension, StringComparison.OrdinalIgnoreCase)) { - appType = ApplicationType.ApprefApplication; + return ApplicationType.ApprefApplication; } else if (extension.Equals(InternetShortcutExtension, StringComparison.OrdinalIgnoreCase)) { - appType = ApplicationType.InternetShortcutApplication; + return ApplicationType.InternetShortcutApplication; } - - // If the path exists, check if it is a directory - else if (DirectoryWrapper.Exists(path)) + else if (string.IsNullOrEmpty(extension) && DirectoryWrapper.Exists(path)) { - appType = ApplicationType.Folder; + return ApplicationType.Folder; } - return appType; + return ApplicationType.GenericFile; } // Function to get the Win32 application, given the path to the application @@ -597,37 +582,35 @@ namespace Microsoft.Plugin.Program.Programs throw new ArgumentNullException(nameof(path)); } - Win32Program app = null; - - ApplicationType appType = GetAppTypeFromPath(path); - - if (appType == ApplicationType.Win32Application) + Win32Program app; + switch (GetAppTypeFromPath(path)) { - app = ExeProgram(path); - } - else if (appType == ApplicationType.ShortcutApplication) - { - app = LnkProgram(path); - } - else if (appType == ApplicationType.ApprefApplication) - { - app = CreateWin32Program(path); - app.AppType = ApplicationType.ApprefApplication; - } - else if (appType == ApplicationType.InternetShortcutApplication) - { - app = InternetShortcutProgram(path); + case ApplicationType.Win32Application: + app = ExeProgram(path); + break; + case ApplicationType.ShortcutApplication: + app = LnkProgram(path); + break; + case ApplicationType.ApprefApplication: + app = CreateWin32Program(path); + app.AppType = ApplicationType.ApprefApplication; + break; + case ApplicationType.InternetShortcutApplication: + app = InternetShortcutProgram(path); + break; + case ApplicationType.WebApplication: + case ApplicationType.RunCommand: + case ApplicationType.Folder: + case ApplicationType.GenericFile: + default: + app = null; + break; } // if the app is valid, only then return the application, else return null - if (app?.Valid ?? false) - { - return app; - } - else - { - return null; - } + return app?.Valid == true + ? app + : null; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Minimise the effect of error on other programs")] @@ -655,13 +638,13 @@ namespace Microsoft.Plugin.Program.Programs } catch (DirectoryNotFoundException e) { - ProgramLogger.Exception("|The directory trying to load the program from does not exist", e, MethodBase.GetCurrentMethod().DeclaringType, currentDirectory); + ProgramLogger.Warn("|The directory trying to load the program from does not exist", e, MethodBase.GetCurrentMethod().DeclaringType, currentDirectory); } } } catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { - ProgramLogger.Exception($"|Permission denied when trying to load programs from {currentDirectory}", e, MethodBase.GetCurrentMethod().DeclaringType, currentDirectory); + ProgramLogger.Warn($"|Permission denied when trying to load programs from {currentDirectory}", e, MethodBase.GetCurrentMethod().DeclaringType, currentDirectory); } catch (Exception e) { @@ -683,14 +666,14 @@ namespace Microsoft.Plugin.Program.Programs } catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { - ProgramLogger.Exception($"|Permission denied when trying to load programs from {currentDirectory}", e, MethodBase.GetCurrentMethod().DeclaringType, currentDirectory); + ProgramLogger.Warn($"|Permission denied when trying to load programs from {currentDirectory}", e, MethodBase.GetCurrentMethod().DeclaringType, currentDirectory); } catch (Exception e) { ProgramLogger.Exception($"|An unexpected error occurred in the calling method ProgramPaths at {currentDirectory}", e, MethodBase.GetCurrentMethod().DeclaringType, currentDirectory); } } - while (folderQueue.Any()); + while (folderQueue.Count > 0); return files; } @@ -701,44 +684,23 @@ namespace Microsoft.Plugin.Program.Programs // Using InvariantCulture since this is user facing var extension = Path.GetExtension(path)?.ToLowerInvariant(); - if (!string.IsNullOrEmpty(extension)) - { - return extension.Substring(1); - } - else - { - return string.Empty; - } + return !string.IsNullOrEmpty(extension) + ? extension.Substring(1) + : string.Empty; } - private static ParallelQuery UnregisteredPrograms(List sources, IList suffixes) - { - var listToAdd = new List(); - sources.Where(s => Directory.Exists(s.Location) && s.Enabled) - .SelectMany(s => ProgramPaths(s.Location, suffixes)) - .ToList() - .Where(t1 => !Main.Settings.DisabledProgramSources.Any(x => t1 == x.UniqueIdentifier)) - .ToList() - .ForEach(x => listToAdd.Add(x)); - - var paths = listToAdd.Distinct().ToArray(); - - var programs1 = paths.AsParallel().Where(p => ExecutableApplicationExtensions.Contains(Extension(p))).Select(ExeProgram); - var programs2 = paths.AsParallel().Where(p => Extension(p) == ShortcutExtension).Select(LnkProgram); - var programs3 = from p in paths.AsParallel() - let e = Extension(p) - where e != ShortcutExtension && !ExecutableApplicationExtensions.Contains(e) - select CreateWin32Program(p); - return programs1.Concat(programs2).Where(p => p.Valid).Concat(programs3).Where(p => p.Valid); - } + private static IEnumerable CustomProgramPaths(IEnumerable sources, IList suffixes) + => sources?.Where(programSource => Directory.Exists(programSource.Location) && programSource.Enabled) + .SelectMany(programSource => ProgramPaths(programSource.Location, suffixes)) + .ToList() ?? Enumerable.Empty(); // Function to obtain the list of applications, the locations of which have been added to the env variable PATH - private static ParallelQuery PathEnvironmentPrograms(IList suffixes) + private static IEnumerable PathEnvironmentProgramPaths(IList suffixes) { // To get all the locations stored in the PATH env variable var pathEnvVariable = Environment.GetEnvironmentVariable("PATH"); string[] searchPaths = pathEnvVariable.Split(Path.PathSeparator); - IEnumerable toFilterAllPaths = new List(); + var toFilterAllPaths = new List(); bool isRecursiveSearch = true; foreach (string path in searchPaths) @@ -748,89 +710,47 @@ namespace Microsoft.Plugin.Program.Programs // to expand any environment variables present in the path string directory = Environment.ExpandEnvironmentVariables(path); var paths = ProgramPaths(directory, suffixes, !isRecursiveSearch); - toFilterAllPaths = toFilterAllPaths.Concat(paths); + toFilterAllPaths.AddRange(paths); } } - var allPaths = toFilterAllPaths - .Distinct() - .ToArray(); - - // Using OrdinalIgnoreCase since this is used internally with paths - var programs1 = allPaths.AsParallel().Where(p => Extension(p).Equals(ShortcutExtension, StringComparison.OrdinalIgnoreCase)).Select(LnkProgram); - var programs2 = allPaths.AsParallel().Where(p => Extension(p).Equals(ApplicationReferenceExtension, StringComparison.OrdinalIgnoreCase)).Select(CreateWin32Program); - var programs3 = allPaths.AsParallel().Where(p => Extension(p).Equals(InternetShortcutExtension, StringComparison.OrdinalIgnoreCase)).Select(InternetShortcutProgram); - var programs4 = allPaths.AsParallel().Where(p => ExecutableApplicationExtensions.Contains(Extension(p))).Select(ExeProgram); - - var allPrograms = programs1.Concat(programs2).Where(p => p.Valid) - .Concat(programs3).Where(p => p.Valid) - .Concat(programs4).Where(p => p.Valid) - .Select(p => - { - p.AppType = ApplicationType.RunCommand; - return p; - }); - - return allPrograms; + return toFilterAllPaths; } - private static ParallelQuery IndexPath(IList suffixes, List indexLocation) - { - var disabledProgramsList = Main.Settings.DisabledProgramSources; + private static List IndexPath(IList suffixes, List indexLocations) + => indexLocations + .SelectMany(indexLocation => ProgramPaths(indexLocation, suffixes)) + .ToList(); - IEnumerable toFilter = new List(); - foreach (string location in indexLocation) - { - var programPaths = ProgramPaths(location, suffixes); - toFilter = toFilter.Concat(programPaths); - } - - var paths = toFilter - .Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1)) - .Select(t1 => t1) - .Distinct() - .ToArray(); - - // Using OrdinalIgnoreCase since this is used internally with paths - 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(CreateWin32Program); - var programs3 = paths.AsParallel().Where(p => Extension(p).Equals(InternetShortcutExtension, StringComparison.OrdinalIgnoreCase)).Select(InternetShortcutProgram); - var programs4 = paths.AsParallel().Where(p => ExecutableApplicationExtensions.Contains(Extension(p))).Select(ExeProgram); - - return programs1.Concat(programs2).Where(p => p.Valid) - .Concat(programs3).Where(p => p.Valid) - .Concat(programs4).Where(p => p.Valid); - } - - private static ParallelQuery StartMenuPrograms(IList suffixes) + private static IEnumerable StartMenuProgramPaths(IList suffixes) { var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu); var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu); - List indexLocation = new List() { directory1, directory2 }; + var indexLocation = new List() { directory1, directory2 }; return IndexPath(suffixes, indexLocation); } - private static ParallelQuery DesktopPrograms(IList suffixes) + private static IEnumerable DesktopProgramPaths(IList suffixes) { var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory); - List indexLocation = new List() { directory1, directory2 }; + var indexLocation = new List() { directory1, directory2 }; return IndexPath(suffixes, indexLocation); } - private static ParallelQuery AppPathsPrograms(IList suffixes) + private static IEnumerable RegisteryAppProgramPaths(IList suffixes) { // https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121 const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths"; - var programs = new List(); + var paths = new List(); using (var root = Registry.LocalMachine.OpenSubKey(appPaths)) { if (root != null) { - programs.AddRange(GetProgramsFromRegistry(root)); + paths.AddRange(GetPathsFromRegistry(root)); } } @@ -838,28 +758,22 @@ namespace Microsoft.Plugin.Program.Programs { if (root != null) { - programs.AddRange(GetProgramsFromRegistry(root)); + paths.AddRange(GetPathsFromRegistry(root)); } } - var disabledProgramsList = Main.Settings.DisabledProgramSources; - var toFilter = programs.AsParallel().Where(p => suffixes.Contains(Extension(p.ExecutableName))); - - var filtered = toFilter.Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)).Select(t1 => t1); - - return filtered; + return paths + .Where(path => suffixes.Any(suffix => path.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase))) + .Select(ExpandEnvironmentVariables) + .ToList(); } - private static IEnumerable GetProgramsFromRegistry(RegistryKey root) - { - return root - .GetSubKeyNames() - .Select(x => GetProgramPathFromRegistrySubKeys(root, x)) - .Distinct() - .Select(x => GetProgramFromPath(x)); - } + private static IEnumerable GetPathsFromRegistry(RegistryKey root) + => root + .GetSubKeyNames() + .Select(x => GetPathFromRegisterySubkey(root, x)); - private static string GetProgramPathFromRegistrySubKeys(RegistryKey root, string subkey) + private static string GetPathFromRegisterySubkey(RegistryKey root, string subkey) { var path = string.Empty; try @@ -885,89 +799,78 @@ namespace Microsoft.Plugin.Program.Programs } catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { - ProgramLogger.Exception($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); + ProgramLogger.Warn($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path); return string.Empty; } } - private static Win32Program GetProgramFromPath(string path) - { - if (string.IsNullOrEmpty(path)) - { - return new Win32Program(); - } - - path = Environment.ExpandEnvironmentVariables(path); - - if (!File.Exists(path)) - { - return new Win32Program(); - } - - var entry = CreateWin32Program(path); - entry.ExecutableName = Path.GetFileName(path); - - return entry; - } + private static string ExpandEnvironmentVariables(string path) => + path != null + ? Environment.ExpandEnvironmentVariables(path) + : null; // Overriding the object.GetHashCode() function to aid in removing duplicates while adding and removing apps from the concurrent dictionary storage public override int GetHashCode() - { - return RemoveDuplicatesComparer.Default.GetHashCode(this); - } + => Win32ProgramEqualityComparer.Default.GetHashCode(this); public override bool Equals(object obj) - { - return obj is Win32Program win && RemoveDuplicatesComparer.Default.Equals(this, win); - } + => obj is Win32Program win32Program && Win32ProgramEqualityComparer.Default.Equals(this, win32Program); - private class RemoveDuplicatesComparer : IEqualityComparer + private class Win32ProgramEqualityComparer : IEqualityComparer { - public static readonly RemoveDuplicatesComparer Default = new RemoveDuplicatesComparer(); + public static readonly Win32ProgramEqualityComparer Default = new Win32ProgramEqualityComparer(); public bool Equals(Win32Program app1, Win32Program app2) { - if (!string.IsNullOrEmpty(app1.Name) && !string.IsNullOrEmpty(app2.Name) - && !string.IsNullOrEmpty(app1.ExecutableName) && !string.IsNullOrEmpty(app2.ExecutableName) - && !string.IsNullOrEmpty(app1.FullPath) && !string.IsNullOrEmpty(app2.FullPath)) + if (app1 == null && app2 == null) { - // Using OrdinalIgnoreCase since this is used internally - return app1.Name.Equals(app2.Name, StringComparison.OrdinalIgnoreCase) - && app1.ExecutableName.Equals(app2.ExecutableName, StringComparison.OrdinalIgnoreCase) - && app1.FullPath.Equals(app2.FullPath, StringComparison.OrdinalIgnoreCase); + return true; } - return false; + return app1 != null + && app2 != null + && (app1.Name?.ToUpperInvariant(), app1.ExecutableName?.ToUpperInvariant(), app1.FullPath?.ToUpperInvariant()) + .Equals((app2.Name?.ToUpperInvariant(), app2.ExecutableName?.ToUpperInvariant(), app2.FullPath?.ToUpperInvariant())); } - // Ref : https://stackoverflow.com/questions/2730865/how-do-i-calculate-a-good-hash-code-for-a-list-of-strings public int GetHashCode(Win32Program obj) + => (obj.Name?.ToUpperInvariant(), obj.ExecutableName?.ToUpperInvariant(), obj.FullPath?.ToUpperInvariant()).GetHashCode(); + } + + public static List DeduplicatePrograms(IEnumerable programs) + => new HashSet(programs, Win32ProgramEqualityComparer.Default).ToList(); + + private static Win32Program GetProgramFromPath(string path) + { + var extension = Extension(path); + if (ExecutableApplicationExtensions.Contains(extension)) { - int namePrime = 13; - int executablePrime = 17; - int fullPathPrime = 31; + return ExeProgram(path); + } - int result = 1; - - // Using Ordinal since this is used internally - result = (result * namePrime) + obj.Name.ToUpperInvariant().GetHashCode(StringComparison.Ordinal); - result = (result * executablePrime) + obj.ExecutableName.ToUpperInvariant().GetHashCode(StringComparison.Ordinal); - result = (result * fullPathPrime) + obj.FullPath.ToUpperInvariant().GetHashCode(StringComparison.Ordinal); - - return result; + switch (extension) + { + case ShortcutExtension: + return LnkProgram(path); + case ApplicationReferenceExtension: + return CreateWin32Program(path); + case InternetShortcutExtension: + return InternetShortcutProgram(path); + default: + return null; } } - // Deduplication code - public static Win32Program[] DeduplicatePrograms(ParallelQuery programs) + private static Win32Program GetRunCommandProgramFromPath(string path) { - var uniqueExePrograms = programs.Where(x => !(string.IsNullOrEmpty(x.LnkResolvedPath) && ExecutableApplicationExtensions.Contains(Extension(x.FullPath)) && x.AppType != ApplicationType.RunCommand)); - return new HashSet(uniqueExePrograms, new RemoveDuplicatesComparer()).ToArray(); + var program = GetProgramFromPath(path); + program.AppType = ApplicationType.RunCommand; + return program; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Keeping the process alive but logging the exception")] - public static Win32Program[] All(ProgramPluginSettings settings) + public static IList All(ProgramPluginSettings settings) { if (settings == null) { @@ -976,36 +879,45 @@ namespace Microsoft.Plugin.Program.Programs try { - var programs = new List().AsParallel(); + // Set an initial size to an expected size to prevent multiple hashSet resizes + const int defaultHashsetSize = 1000; - var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes); - programs = programs.Concat(unregistered); + // Multiple paths could have the same programPaths and we don't want to resolve / lookup them multiple times + var paths = new HashSet(defaultHashsetSize); + var runCommandPaths = new HashSet(defaultHashsetSize); - if (settings.EnableRegistrySource) + // Parallelize multiple sources, and priority based on paths which most likely contain .lnks which are formatted + var sources = new (bool IsEnabled, Func> GetPaths)[] { - var appPaths = AppPathsPrograms(settings.ProgramSuffixes); - programs = programs.Concat(appPaths); - } + (true, () => CustomProgramPaths(settings.ProgramSources, settings.ProgramSuffixes)), + (settings.EnableStartMenuSource, () => StartMenuProgramPaths(settings.ProgramSuffixes)), + (settings.EnableDesktopSource, () => DesktopProgramPaths(settings.ProgramSuffixes)), + (settings.EnableRegistrySource, () => RegisteryAppProgramPaths(settings.ProgramSuffixes)), + }; - if (settings.EnableStartMenuSource) + // Run commands are always set as AppType "RunCommand" + var runCommandSources = new (bool IsEnabled, Func> GetPaths)[] { - var startMenu = StartMenuPrograms(settings.ProgramSuffixes); - programs = programs.Concat(startMenu); - } + (settings.EnablePathEnvironmentVariableSource, () => PathEnvironmentProgramPaths(settings.ProgramSuffixes)), + }; - if (settings.EnablePathEnvironmentVariableSource) - { - var appPathEnvironment = PathEnvironmentPrograms(settings.ProgramSuffixes); - programs = programs.Concat(appPathEnvironment); - } + var disabledProgramsList = settings.DisabledProgramSources; - if (settings.EnableDesktopSource) - { - var desktop = DesktopPrograms(settings.ProgramSuffixes); - programs = programs.Concat(desktop); - } + // Get all paths but exclude all normal .Executables + paths.UnionWith(sources + .AsParallel() + .SelectMany(source => source.IsEnabled ? source.GetPaths() : Enumerable.Empty()) + .Where(programPath => disabledProgramsList.All(x => x.UniqueIdentifier != programPath)) + .Where(path => !ExecutableApplicationExtensions.Contains(Extension(path)))); + runCommandPaths.UnionWith(runCommandSources + .AsParallel() + .SelectMany(source => source.IsEnabled ? source.GetPaths() : Enumerable.Empty()) + .Where(programPath => disabledProgramsList.All(x => x.UniqueIdentifier != programPath))); - return DeduplicatePrograms(programs); + var programs = paths.AsParallel().Select(source => GetProgramFromPath(source)); + var runCommandPrograms = runCommandPaths.AsParallel().Select(source => GetRunCommandProgramFromPath(source)); + + return DeduplicatePrograms(programs.Concat(runCommandPrograms).Where(program => program?.Valid == true)); } catch (Exception e) { diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramRepository.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramRepository.cs index 2574ef1207..691a449a71 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramRepository.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramRepository.cs @@ -243,7 +243,7 @@ namespace Microsoft.Plugin.Program.Storage public void IndexPrograms() { var applications = Programs.Win32Program.All(_settings); - Log.Info($"Indexed {applications.Length} win32 applications", GetType()); + Log.Info($"Indexed {applications.Count} win32 applications", GetType()); SetList(applications); }