Files
PowerToys/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32Program.cs
2022-09-16 10:54:58 +02:00

984 lines
40 KiB
C#

// 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.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Reflection;
using System.Security;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.Plugin.Program.Logger;
using Microsoft.Win32;
using Wox.Infrastructure;
using Wox.Infrastructure.FileSystemHelper;
using Wox.Plugin;
using Wox.Plugin.Common;
using Wox.Plugin.Logger;
using DirectoryWrapper = Wox.Infrastructure.FileSystemHelper.DirectoryWrapper;
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;
private static readonly IDirectory Directory = FileSystem.Directory;
public string Name { get; set; }
public string UniqueIdentifier { get; set; }
public string IcoPath { get; set; }
public string FullPath { get; set; }
public string LnkResolvedPath { get; set; }
public string LnkResolvedExecutableName { get; set; }
// Localized name based on windows display language
public string LocalizedName { get; set; } = string.Empty;
public string ParentDirectory { get; set; }
public string ExecutableName { get; set; }
public string Description { get; set; } = string.Empty;
public bool Valid { get; set; }
public bool Enabled { get; set; }
public bool HasArguments => !string.IsNullOrEmpty(Arguments);
public string Arguments { get; set; } = string.Empty;
public string Location => ParentDirectory;
public ApplicationType AppType { get; set; }
// Wrappers for File Operations
public static IFileVersionInfoWrapper FileVersionInfoWrapper { get; set; } = new FileVersionInfoWrapper();
public static IFile FileWrapper { get; set; } = new FileSystem().File;
public static IShellLinkHelper ShellLinkHelper { get; set; } = new ShellLinkHelper();
public static IDirectoryWrapper DirectoryWrapper { get; set; } = new DirectoryWrapper();
private const string ShortcutExtension = "lnk";
private const string ApplicationReferenceExtension = "appref-ms";
private const string InternetShortcutExtension = "url";
private static readonly HashSet<string> ExecutableApplicationExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "exe", "bat", "bin", "com", "msc", "msi", "cmd", "ps1", "job", "msp", "mst", "sct", "ws", "wsh", "wsf" };
private const string ProxyWebApp = "_proxy.exe";
private const string AppIdArgument = "--app-id";
public enum ApplicationType
{
WebApplication = 0,
InternetShortcutApplication = 1,
Win32Application = 2,
ShortcutApplication = 3,
ApprefApplication = 4,
RunCommand = 5,
Folder = 6,
GenericFile = 7,
}
// Function to calculate the score of a result
private int Score(string query)
{
var nameMatch = StringMatcher.FuzzySearch(query, Name);
var locNameMatch = StringMatcher.FuzzySearch(query, LocalizedName);
var descriptionMatch = StringMatcher.FuzzySearch(query, Description);
var executableNameMatch = StringMatcher.FuzzySearch(query, ExecutableName);
var lnkResolvedExecutableNameMatch = StringMatcher.FuzzySearch(query, LnkResolvedExecutableName);
var score = new[] { nameMatch.Score, locNameMatch.Score, descriptionMatch.Score / 2, executableNameMatch.Score, lnkResolvedExecutableNameMatch.Score }.Max();
return score;
}
public bool IsWebApplication()
{
// 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
// Using Ordinal IgnoreCase since this is used internally
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
public bool FilterWebApplication(string query)
{
// If the app is not a web application, then do not filter it
if (!IsWebApplication())
{
return false;
}
string[] subqueries = query?.Split() ?? Array.Empty<string>();
bool nameContainsQuery = false;
bool pathContainsQuery = false;
// check if any space separated query is a part of the app name or path name
foreach (var subquery in subqueries)
{
// Using OrdinalIgnoreCase since these are used internally
if (FullPath.Contains(subquery, StringComparison.OrdinalIgnoreCase))
{
pathContainsQuery = true;
}
if (Name.Contains(subquery, StringComparison.OrdinalIgnoreCase))
{
nameContainsQuery = true;
}
}
return pathContainsQuery && !nameContainsQuery;
}
// Function to set the subtitle based on the Type of application
private string GetSubtitle()
{
switch (AppType)
{
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;
}
}
public bool QueryEqualsNameForRunCommands(string query)
{
if (query != null && AppType == ApplicationType.RunCommand)
{
// Using OrdinalIgnoreCase since this is used internally
if (!query.Equals(Name, StringComparison.OrdinalIgnoreCase) && !query.Equals(ExecutableName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
return true;
}
public Result Result(string query, string queryArguments, IPublicAPI api)
{
if (api == null)
{
throw new ArgumentNullException(nameof(api));
}
var score = Score(query);
if (score <= 0)
{ // no need to create result if this is zero
return null;
}
if (!HasArguments)
{
var noArgumentScoreModifier = 5;
score += noArgumentScoreModifier;
}
else
{
// Filter Web Applications when searching for the main application
if (FilterWebApplication(query))
{
return null;
}
}
// NOTE: This is to display run commands only when there is an exact match, like in start menu
if (!QueryEqualsNameForRunCommands(query))
{
return null;
}
var result = new Result
{
// To set the title for the result to always be the name of the application
Title = !string.IsNullOrEmpty(LocalizedName) ? LocalizedName : Name,
SubTitle = GetSubtitle(),
IcoPath = IcoPath,
Score = score,
ContextData = this,
ProgramArguments = queryArguments,
Action = e =>
{
var info = GetProcessStartInfo(queryArguments);
Main.StartProcess(Process.Start, info);
return true;
},
};
result.TitleHighlightData = StringMatcher.FuzzySearch(query, result.Title).MatchData;
// Using CurrentCulture since this is user facing
var toolTipTitle = string.Format(CultureInfo.CurrentCulture, "{0}: {1}", Properties.Resources.powertoys_run_plugin_program_file_name, result.Title);
var toolTipText = string.Format(CultureInfo.CurrentCulture, "{0}: {1}", Properties.Resources.powertoys_run_plugin_program_file_path, FullPath);
result.ToolTipData = new ToolTipData(toolTipTitle, toolTipText);
return result;
}
public List<ContextMenuResult> ContextMenus(string queryArguments, IPublicAPI api)
{
if (api == null)
{
throw new ArgumentNullException(nameof(api));
}
var contextMenus = new List<ContextMenuResult>();
if (AppType != ApplicationType.InternetShortcutApplication && AppType != ApplicationType.Folder && AppType != ApplicationType.GenericFile)
{
contextMenus.Add(new ContextMenuResult
{
PluginName = Assembly.GetExecutingAssembly().GetName().Name,
Title = Properties.Resources.wox_plugin_program_run_as_administrator,
Glyph = "\xE7EF",
FontFamily = "Segoe MDL2 Assets",
AcceleratorKey = Key.Enter,
AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
Action = _ =>
{
var info = GetProcessStartInfo(queryArguments, RunAsType.Administrator);
Task.Run(() => Main.StartProcess(Process.Start, info));
return true;
},
});
contextMenus.Add(new ContextMenuResult
{
PluginName = Assembly.GetExecutingAssembly().GetName().Name,
Title = Properties.Resources.wox_plugin_program_run_as_user,
Glyph = "\xE7EE",
FontFamily = "Segoe MDL2 Assets",
AcceleratorKey = Key.U,
AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
Action = _ =>
{
var info = GetProcessStartInfo(queryArguments, RunAsType.OtherUser);
Task.Run(() => Main.StartProcess(Process.Start, info));
return true;
},
});
}
contextMenus.Add(
new ContextMenuResult
{
PluginName = Assembly.GetExecutingAssembly().GetName().Name,
Title = Properties.Resources.wox_plugin_program_open_containing_folder,
Glyph = "\xE838",
FontFamily = "Segoe MDL2 Assets",
AcceleratorKey = Key.E,
AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
Action = _ =>
{
Helper.OpenInShell(ParentDirectory);
return true;
},
});
contextMenus.Add(
new ContextMenuResult
{
PluginName = Assembly.GetExecutingAssembly().GetName().Name,
Title = Properties.Resources.wox_plugin_program_open_in_console,
Glyph = "\xE756",
FontFamily = "Segoe MDL2 Assets",
AcceleratorKey = Key.C,
AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
Action = (context) =>
{
try
{
Helper.OpenInConsole(ParentDirectory);
return true;
}
catch (Exception e)
{
Log.Exception($"|Failed to open {Name} in console, {e.Message}", e, GetType());
return false;
}
},
});
return contextMenus;
}
private ProcessStartInfo GetProcessStartInfo(string programArguments, RunAsType runAs = RunAsType.None)
{
return new ProcessStartInfo
{
FileName = LnkResolvedPath ?? FullPath,
WorkingDirectory = ParentDirectory,
UseShellExecute = true,
Arguments = programArguments,
Verb = runAs == RunAsType.Administrator ? "runAs" : runAs == RunAsType.OtherUser ? "runAsUser" : string.Empty,
};
}
private enum RunAsType
{
None,
Administrator,
OtherUser,
}
public override string ToString()
{
return ExecutableName;
}
private static Win32Program CreateWin32Program(string path)
{
try
{
return new Win32Program
{
Name = Path.GetFileNameWithoutExtension(path),
ExecutableName = Path.GetFileName(path),
IcoPath = path,
// Localized name based on windows display language
LocalizedName = ShellLocalization.GetLocalizedName(path),
// Using InvariantCulture since this is user facing
FullPath = path.ToLowerInvariant(),
UniqueIdentifier = path,
ParentDirectory = Directory.GetParent(path).FullName,
Description = string.Empty,
Valid = true,
Enabled = true,
AppType = ApplicationType.Win32Application,
};
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
ProgramLogger.Warn($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
return InvalidProgram;
}
catch (Exception e)
{
ProgramLogger.Exception($"|An unexpected error occurred in the calling method CreateWin32Program at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
return InvalidProgram;
}
}
private static readonly Regex InternetShortcutURLPrefixes = new Regex(@"^steam:\/\/(rungameid|run)\/|^com\.epicgames\.launcher:\/\/apps\/", RegexOptions.Compiled);
// This function filters Internet Shortcut programs
private static Win32Program InternetShortcutProgram(string path)
{
try
{
// 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;
const string urlPrefix = "URL=";
const string iconFilePrefix = "IconFile=";
foreach (string line in lines)
{
// Using OrdinalIgnoreCase since this is used internally
if (line.StartsWith(urlPrefix, StringComparison.OrdinalIgnoreCase))
{
urlPath = line.Substring(urlPrefix.Length);
if (!Uri.TryCreate(urlPath, UriKind.RelativeOrAbsolute, out Uri _))
{
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)
{
validApp = true;
}
}
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 InvalidProgram;
}
try
{
return new Win32Program
{
Name = Path.GetFileNameWithoutExtension(path),
ExecutableName = Path.GetFileName(path),
IcoPath = iconPath,
FullPath = urlPath.ToLowerInvariant(),
UniqueIdentifier = path,
ParentDirectory = Directory.GetParent(path).FullName,
Valid = true,
Enabled = true,
AppType = ApplicationType.InternetShortcutApplication,
};
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
ProgramLogger.Warn($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
return InvalidProgram;
}
}
catch (Exception e)
{
ProgramLogger.Exception($"|An unexpected error occurred in the calling method InternetShortcutProgram at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
return InvalidProgram;
}
}
private static Win32Program LnkProgram(string path)
{
try
{
var program = CreateWin32Program(path);
string target = ShellLinkHelper.RetrieveTargetPath(path);
if (!string.IsNullOrEmpty(target))
{
if (!(File.Exists(target) || Directory.Exists(target)))
{
// If the link points nowhere, consider it invalid.
return InvalidProgram;
}
program.LnkResolvedPath = program.FullPath;
program.LnkResolvedExecutableName = Path.GetFileName(target);
// 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.Description = description;
}
else
{
var info = FileVersionInfoWrapper.GetVersionInfo(target);
if (!string.IsNullOrEmpty(info?.FileDescription))
{
program.Description = info.FileDescription;
}
}
}
return program;
}
catch (System.IO.FileLoadException e)
{
ProgramLogger.Warn($"Couldn't load the link file at {path}. This might be caused by a new link being created and locked by the OS.", e, MethodBase.GetCurrentMethod().DeclaringType, path);
return InvalidProgram;
}
// Only do a catch all in production. This is so make developer aware of any unhandled exception and add the exception handling in.
// Error caused likely due to trying to get the description of the program
catch (Exception e)
{
ProgramLogger.Exception($"|An unexpected error occurred in the calling method LnkProgram at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
return InvalidProgram;
}
}
private static Win32Program ExeProgram(string path)
{
try
{
var program = CreateWin32Program(path);
var info = FileVersionInfoWrapper.GetVersionInfo(path);
if (!string.IsNullOrEmpty(info?.FileDescription))
{
program.Description = info.FileDescription;
}
return program;
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
ProgramLogger.Warn($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
return InvalidProgram;
}
catch (FileNotFoundException e)
{
ProgramLogger.Warn($"|Unable to locate exe file at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
return InvalidProgram;
}
catch (Exception e)
{
ProgramLogger.Exception($"|An unexpected error occurred in the calling method ExeProgram at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
return InvalidProgram;
}
}
// Function to get the application type, given the path to the application
public static ApplicationType GetAppTypeFromPath(string path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
string extension = Extension(path);
// Using OrdinalIgnoreCase since these are used internally with paths
if (ExecutableApplicationExtensions.Contains(extension))
{
return ApplicationType.Win32Application;
}
else if (extension.Equals(ShortcutExtension, StringComparison.OrdinalIgnoreCase))
{
return ApplicationType.ShortcutApplication;
}
else if (extension.Equals(ApplicationReferenceExtension, StringComparison.OrdinalIgnoreCase))
{
return ApplicationType.ApprefApplication;
}
else if (extension.Equals(InternetShortcutExtension, StringComparison.OrdinalIgnoreCase))
{
return ApplicationType.InternetShortcutApplication;
}
else if (string.IsNullOrEmpty(extension) && DirectoryWrapper.Exists(path))
{
return ApplicationType.Folder;
}
return ApplicationType.GenericFile;
}
// Function to get the Win32 application, given the path to the application
public static Win32Program GetAppFromPath(string path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
Win32Program app;
switch (GetAppTypeFromPath(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
return app?.Valid == true
? app
: null;
}
private static IEnumerable<string> ProgramPaths(string directory, IList<string> suffixes, bool recursiveSearch = true)
{
if (!Directory.Exists(directory))
{
return Array.Empty<string>();
}
var files = new List<string>();
var folderQueue = new Queue<string>();
folderQueue.Enqueue(directory);
// Keep track of already visited directories to avoid cycles.
var alreadyVisited = new HashSet<string>();
do
{
var currentDirectory = folderQueue.Dequeue();
if (alreadyVisited.Contains(currentDirectory))
{
continue;
}
alreadyVisited.Add(currentDirectory);
try
{
foreach (var suffix in suffixes)
{
try
{
files.AddRange(Directory.EnumerateFiles(currentDirectory, $"*.{suffix}", SearchOption.TopDirectoryOnly));
}
catch (DirectoryNotFoundException e)
{
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.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);
}
try
{
// If the search is set to be non-recursive, then do not enqueue the child directories.
if (!recursiveSearch)
{
continue;
}
foreach (var childDirectory in Directory.EnumerateDirectories(currentDirectory, "*", new EnumerationOptions()
{
// https://docs.microsoft.com/en-us/dotnet/api/system.io.enumerationoptions?view=net-6.0
// Exclude directories with the Reparse Point file attribute, to avoid loops due to symbolic links / directory junction / mount points.
AttributesToSkip = FileAttributes.Hidden | FileAttributes.System | FileAttributes.ReparsePoint,
RecurseSubdirectories = false,
}))
{
folderQueue.Enqueue(childDirectory);
}
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
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.Count > 0);
return files;
}
private static string Extension(string path)
{
// Using InvariantCulture since this is user facing
var extension = Path.GetExtension(path)?.ToLowerInvariant();
return !string.IsNullOrEmpty(extension)
? extension.Substring(1)
: string.Empty;
}
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 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);
var toFilterAllPaths = new List<string>();
bool isRecursiveSearch = true;
foreach (string path in searchPaths)
{
if (path.Length > 0)
{
// to expand any environment variables present in the path
string directory = Environment.ExpandEnvironmentVariables(path);
var paths = ProgramPaths(directory, suffixes, !isRecursiveSearch);
toFilterAllPaths.AddRange(paths);
}
}
return toFilterAllPaths;
}
private static List<string> IndexPath(IList<string> suffixes, List<string> indexLocations)
=> indexLocations
.SelectMany(indexLocation => ProgramPaths(indexLocation, suffixes))
.ToList();
private static IEnumerable<string> StartMenuProgramPaths(IList<string> suffixes)
{
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu);
var indexLocation = new List<string>() { directory1, directory2 };
return IndexPath(suffixes, indexLocation);
}
private static IEnumerable<string> DesktopProgramPaths(IList<string> suffixes)
{
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory);
var indexLocation = new List<string>() { directory1, directory2 };
return IndexPath(suffixes, indexLocation);
}
private static IEnumerable<string> RegistryAppProgramPaths(IList<string> suffixes)
{
// https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121
const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
var paths = new List<string>();
using (var root = Registry.LocalMachine.OpenSubKey(appPaths))
{
if (root != null)
{
paths.AddRange(GetPathsFromRegistry(root));
}
}
using (var root = Registry.CurrentUser.OpenSubKey(appPaths))
{
if (root != null)
{
paths.AddRange(GetPathsFromRegistry(root));
}
}
return paths
.Where(path => suffixes.Any(suffix => path.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase)))
.Select(ExpandEnvironmentVariables)
.ToList();
}
private static IEnumerable<string> GetPathsFromRegistry(RegistryKey root)
=> root
.GetSubKeyNames()
.Select(x => GetPathFromRegistrySubkey(root, x));
private static string GetPathFromRegistrySubkey(RegistryKey root, string subkey)
{
var path = string.Empty;
try
{
using (var key = root.OpenSubKey(subkey))
{
if (key == null)
{
return string.Empty;
}
var defaultValue = string.Empty;
path = key.GetValue(defaultValue) as string;
}
if (string.IsNullOrEmpty(path))
{
return string.Empty;
}
// fix path like this: ""\"C:\\folder\\executable.exe\""
return path = path.Trim('"', ' ');
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
ProgramLogger.Warn($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
return string.Empty;
}
}
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()
=> Win32ProgramEqualityComparer.Default.GetHashCode(this);
public override bool Equals(object obj)
=> obj is Win32Program win32Program && Win32ProgramEqualityComparer.Default.Equals(this, win32Program);
private class Win32ProgramEqualityComparer : IEqualityComparer<Win32Program>
{
public static readonly Win32ProgramEqualityComparer Default = new Win32ProgramEqualityComparer();
public bool Equals(Win32Program app1, Win32Program app2)
{
if (app1 == null && app2 == null)
{
return true;
}
return app1 != null
&& app2 != null
&& (app1.Name?.ToUpperInvariant(), app1.ExecutableName?.ToUpperInvariant(), app1.FullPath?.ToUpperInvariant())
.Equals((app2.Name?.ToUpperInvariant(), app2.ExecutableName?.ToUpperInvariant(), app2.FullPath?.ToUpperInvariant()));
}
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))
{
return ExeProgram(path);
}
switch (extension)
{
case ShortcutExtension:
return LnkProgram(path);
case ApplicationReferenceExtension:
return CreateWin32Program(path);
case InternetShortcutExtension:
return InternetShortcutProgram(path);
default:
return null;
}
}
private static Win32Program GetRunCommandProgramFromPath(string path)
{
var program = GetProgramFromPath(path);
program.AppType = ApplicationType.RunCommand;
return program;
}
public static IList<Win32Program> All(ProgramPluginSettings settings)
{
if (settings == null)
{
throw new ArgumentNullException(nameof(settings));
}
try
{
// Set an initial size to an expected size to prevent multiple hashSet resizes
const int defaultHashsetSize = 1000;
// 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);
// 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)[]
{
(true, () => CustomProgramPaths(settings.ProgramSources, settings.ProgramSuffixes)),
(settings.EnableStartMenuSource, () => StartMenuProgramPaths(settings.ProgramSuffixes)),
(settings.EnableDesktopSource, () => DesktopProgramPaths(settings.ProgramSuffixes)),
(settings.EnableRegistrySource, () => RegistryAppProgramPaths(settings.ProgramSuffixes)),
};
// Run commands are always set as AppType "RunCommand"
var runCommandSources = new (bool IsEnabled, Func<IEnumerable<string>> GetPaths)[]
{
(settings.EnablePathEnvironmentVariableSource, () => PathEnvironmentProgramPaths(settings.ProgramSuffixes)),
};
var disabledProgramsList = settings.DisabledProgramSources;
// 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)));
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)
{
ProgramLogger.Exception("An unexpected error occurred", e, MethodBase.GetCurrentMethod().DeclaringType, "Not available");
return Array.Empty<Win32Program>();
}
}
}
}