[PT Run] Improve the Win32 Program Indexing speed (#11364)

This commit is contained in:
Roy 2021-06-03 16:11:09 +02:00 committed by GitHub
parent 9a1034e122
commit 266aafb700
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 287 additions and 344 deletions

View file

@ -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

View file

@ -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<Win32Program> 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<Win32Program> 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<Win32Program> 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<Win32Program> 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<Win32Program> apps = Win32Program.DeduplicatePrograms(prgms.AsParallel());
// Assert
Assert.AreEqual(3, apps.Length);
Assert.AreEqual(3, apps.Count);
}
[Test]

View file

@ -258,7 +258,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Storage
// File.ReadAllLines must be mocked for url applications
var mockFile = new Mock<IFile>();
mockFile.Setup(m => m.ReadAllLines(It.IsAny<string>())).Returns(new string[] { "URL=steam://rungameid/1258080", "IconFile=iconFile" });
mockFile.Setup(m => m.ReadLines(It.IsAny<string>())).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<IFile>();
mockFile.Setup(m => m.ReadAllLines(It.IsAny<string>())).Returns(new string[] { "URL=steam://rungameid/1258080", "IconFile=iconFile" });
mockFile.Setup(m => m.ReadLines(It.IsAny<string>())).Returns(new string[] { "URL=steam://rungameid/1258080", "IconFile=iconFile" });
Win32Program.FileWrapper = mockFile.Object;
string oldFullPath = directory + "\\" + oldpath;

View file

@ -17,6 +17,31 @@ namespace Microsoft.Plugin.Program.Logger
/// </summary>
internal static class ProgramLogger
{
/// <summary>
/// Logs an warning
/// </summary>
[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);
}
/// <summary>
/// Logs an exception
/// </summary>
@ -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";
}

View file

@ -53,20 +53,6 @@ namespace Microsoft.Plugin.Program
// Initialize the Win32ProgramRepository with the settings object
_win32ProgramRepository = new Win32ProgramRepository(_win32ProgramRepositoryHelper.FileSystemWatchers.Cast<IFileSystemWatcherWrapper>().ToList(), new BinaryStorage<IList<Programs.Win32Program>>("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)

View file

@ -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<string>();
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<Win32Program> UnregisteredPrograms(List<ProgramSource> sources, IList<string> suffixes)
{
var listToAdd = new List<string>();
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<string> CustomProgramPaths(IEnumerable<ProgramSource> sources, IList<string> suffixes)
=> sources?.Where(programSource => Directory.Exists(programSource.Location) && programSource.Enabled)
.SelectMany(programSource => ProgramPaths(programSource.Location, suffixes))
.ToList() ?? Enumerable.Empty<string>();
// Function to obtain the list of applications, the locations of which have been added to the env variable PATH
private static ParallelQuery<Win32Program> PathEnvironmentPrograms(IList<string> suffixes)
private static IEnumerable<string> PathEnvironmentProgramPaths(IList<string> suffixes)
{
// To get all the locations stored in the PATH env variable
var pathEnvVariable = Environment.GetEnvironmentVariable("PATH");
string[] searchPaths = pathEnvVariable.Split(Path.PathSeparator);
IEnumerable<string> toFilterAllPaths = new List<string>();
var toFilterAllPaths = new List<string>();
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<Win32Program> IndexPath(IList<string> suffixes, List<string> indexLocation)
{
var disabledProgramsList = Main.Settings.DisabledProgramSources;
private static List<string> IndexPath(IList<string> suffixes, List<string> indexLocations)
=> indexLocations
.SelectMany(indexLocation => ProgramPaths(indexLocation, suffixes))
.ToList();
IEnumerable<string> toFilter = new List<string>();
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<Win32Program> StartMenuPrograms(IList<string> suffixes)
private static IEnumerable<string> StartMenuProgramPaths(IList<string> suffixes)
{
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu);
List<string> indexLocation = new List<string>() { directory1, directory2 };
var indexLocation = new List<string>() { directory1, directory2 };
return IndexPath(suffixes, indexLocation);
}
private static ParallelQuery<Win32Program> DesktopPrograms(IList<string> suffixes)
private static IEnumerable<string> DesktopProgramPaths(IList<string> suffixes)
{
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory);
List<string> indexLocation = new List<string>() { directory1, directory2 };
var indexLocation = new List<string>() { directory1, directory2 };
return IndexPath(suffixes, indexLocation);
}
private static ParallelQuery<Win32Program> AppPathsPrograms(IList<string> suffixes)
private static IEnumerable<string> RegisteryAppProgramPaths(IList<string> suffixes)
{
// https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121
const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
var programs = new List<Win32Program>();
var paths = new List<string>();
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<Win32Program> GetProgramsFromRegistry(RegistryKey root)
{
return root
.GetSubKeyNames()
.Select(x => GetProgramPathFromRegistrySubKeys(root, x))
.Distinct()
.Select(x => GetProgramFromPath(x));
}
private static IEnumerable<string> 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<Win32Program>
private class Win32ProgramEqualityComparer : IEqualityComparer<Win32Program>
{
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<Win32Program> DeduplicatePrograms(IEnumerable<Win32Program> programs)
=> new HashSet<Win32Program>(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<Win32Program> 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<Win32Program>(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<Win32Program> All(ProgramPluginSettings settings)
{
if (settings == null)
{
@ -976,36 +879,45 @@ namespace Microsoft.Plugin.Program.Programs
try
{
var programs = new List<Win32Program>().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<string>(defaultHashsetSize);
var runCommandPaths = new HashSet<string>(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<IEnumerable<string>> 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<IEnumerable<string>> 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<string>())
.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<string>())
.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)
{

View file

@ -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);
}