Files
PowerToys/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs
Jiří Polášek 52f2561937 CmdPal: Find app for WinGet package (#43943)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR introduces a bit of dark magic to resolve the correct installed
app for a given WinGet package:

- Packaged apps: matched using their package family name.
- Everything else: matched using the product code (GUID) and heuristic
registry lookup.
- The registry rarely stores the executable path directly, so the logic
compares install locations with known apps.
  - It attempts to pick the best candidate while avoiding uninstallers.
  - It’s not science — let’s call it `#666666` magic.
- MSI API support was removed because it's too slow for this scenario.
- If no reliable match is found, the command is skipped for now. The
future plan is to redirect the user to the list of installed apps and
search by display name, but that needs some supporting infrastructure
first.
- The command order for WinGet list entries was updated: **Install /
Uninstall** is now the primary action, ensuring a stable UI since this
command is always available.


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #43671
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-12-03 10:16:25 -06:00

1072 lines
36 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.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Security;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Win32;
using Windows.System;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Serializable]
public class Win32Program : IProgram
{
public static readonly Win32Program InvalidProgram = new() { 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; } = string.Empty;
// Localized name based on windows display language
public string NameLocalized { get; set; } = string.Empty;
public string UniqueIdentifier { get; set; } = string.Empty;
public string IcoPath { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
// Path of app executable or lnk target executable
public string FullPath { get; set; } = string.Empty;
// Localized path based on windows display language
public string FullPathLocalized { get; set; } = string.Empty;
public string ParentDirectory { get; set; } = string.Empty;
public string ExecutableName { get; set; } = string.Empty;
// Localized executable name based on windows display language
public string ExecutableNameLocalized { get; set; } = string.Empty;
// Path to the lnk file on LnkProgram
public string LnkFilePath { get; set; } = string.Empty;
public string LnkResolvedExecutableName { get; set; } = string.Empty;
// Localized path based on windows display language
public string LnkResolvedExecutableNameLocalized { 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;
private const string ShortcutExtension = "lnk";
private const string ApplicationReferenceExtension = "appref-ms";
private const string InternetShortcutExtension = "url";
private static readonly HashSet<string> ExecutableApplicationExtensions = new(StringComparer.OrdinalIgnoreCase) { "exe", "bat", "bin", "com", "cpl", "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,
}
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;
}
var subqueries = query?.Split() ?? Array.Empty<string>();
var nameContainsQuery = false;
var 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
public string Type()
{
switch (AppType)
{
case ApplicationType.Win32Application:
case ApplicationType.ShortcutApplication:
case ApplicationType.ApprefApplication:
return Resources.application;
case ApplicationType.InternetShortcutApplication:
return Resources.internet_shortcut_application;
case ApplicationType.WebApplication:
return Resources.web_application;
case ApplicationType.RunCommand:
return Resources.run_command;
case ApplicationType.Folder:
return Resources.folder;
case ApplicationType.GenericFile:
return Resources.file;
default:
return string.Empty;
}
}
public bool QueryEqualsNameForRunCommands(string query)
{
if (query is not 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 List<IContextItem> GetCommands()
{
List<IContextItem> commands = [];
if (AppType != ApplicationType.InternetShortcutApplication && AppType != ApplicationType.Folder && AppType != ApplicationType.GenericFile)
{
commands.Add(new CommandContextItem(
new RunAsAdminCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory, false))
{
RequestedShortcut = KeyChords.RunAsAdministrator,
});
commands.Add(new CommandContextItem(
new RunAsUserCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory))
{
RequestedShortcut = KeyChords.RunAsDifferentUser,
});
}
commands.Add(new CommandContextItem(
new CopyPathCommand(FullPath))
{
RequestedShortcut = KeyChords.CopyFilePath,
});
commands.Add(new CommandContextItem(
new ShowFileInFolderCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath)
{
Name = Resources.open_location,
})
{
RequestedShortcut = KeyChords.OpenFileLocation,
});
commands.Add(new CommandContextItem(
new OpenInConsoleCommand(ParentDirectory))
{
RequestedShortcut = KeyChords.OpenInConsole,
});
if (AppType == ApplicationType.ShortcutApplication || AppType == ApplicationType.ApprefApplication || AppType == ApplicationType.Win32Application)
{
commands.Add(new CommandContextItem(
new UninstallApplicationConfirmation(this))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete),
IsCritical = true,
});
}
return commands;
}
public override string ToString()
{
return ExecutableName;
}
public string GetAppIdentifier()
{
// Use a combination of name and path to create a unique identifier
return $"{Name}|{FullPath}";
}
private static Win32Program CreateWin32Program(string path)
{
try
{
var parentDir = Directory.GetParent(path);
return new Win32Program
{
Name = Path.GetFileNameWithoutExtension(path),
ExecutableName = Path.GetFileName(path),
IcoPath = path,
// Using InvariantCulture since this is user facing
FullPath = path,
UniqueIdentifier = path,
ParentDirectory = parentDir is null ? string.Empty : parentDir.FullName,
Description = string.Empty,
Valid = true,
Enabled = true,
AppType = ApplicationType.Win32Application,
// Localized name, path and executable based on windows display language
NameLocalized = ShellLocalization.Instance.GetLocalizedName(path),
FullPathLocalized = ShellLocalization.Instance.GetLocalizedPath(path),
ExecutableNameLocalized = Path.GetFileName(ShellLocalization.Instance.GetLocalizedPath(path)),
};
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
Logger.LogError(e.Message);
return InvalidProgram;
}
catch (Exception e)
{
Logger.LogError(e.Message);
return InvalidProgram;
}
}
private static readonly Regex InternetShortcutURLPrefixes = new(@"^steam:\/\/(rungameid|run|open)\/|^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);
var iconPath = string.Empty;
var urlPath = string.Empty;
var validApp = false;
const string urlPrefix = "URL=";
const string iconFilePrefix = "IconFile=";
foreach (var 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 var _))
{
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
{
var parentDir = Directory.GetParent(path);
return new Win32Program
{
Name = Path.GetFileNameWithoutExtension(path),
ExecutableName = Path.GetFileName(path),
IcoPath = iconPath,
FullPath = urlPath,
UniqueIdentifier = path,
ParentDirectory = parentDir is null ? string.Empty : parentDir.FullName,
Valid = true,
Enabled = true,
AppType = ApplicationType.InternetShortcutApplication,
};
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
Logger.LogError(e.Message);
return InvalidProgram;
}
}
catch (Exception e)
{
Logger.LogError(e.Message);
return InvalidProgram;
}
}
private static Win32Program LnkProgram(string path)
{
try
{
var program = CreateWin32Program(path);
var shellLinkHelper = new ShellLinkHelper();
var 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.LnkFilePath = program.FullPath;
program.LnkResolvedExecutableName = Path.GetFileName(target);
program.LnkResolvedExecutableNameLocalized = Path.GetFileName(ShellLocalization.Instance.GetLocalizedPath(target));
// Using CurrentCulture since this is user facing
program.FullPath = Path.GetFullPath(target);
program.FullPathLocalized = ShellLocalization.Instance.GetLocalizedPath(target);
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)
{
Logger.LogError(e.Message);
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)
{
Logger.LogError(e.Message);
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)
{
Logger.LogError(e.Message);
return InvalidProgram;
}
catch (FileNotFoundException e)
{
Logger.LogError(e.Message);
return InvalidProgram;
}
catch (Exception e)
{
Logger.LogError(e.Message);
return InvalidProgram;
}
}
// Function to get the application type, given the path to the application
public static ApplicationType GetAppTypeFromPath(string path)
{
ArgumentNullException.ThrowIfNull(path);
var 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) && System.IO.Directory.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)
{
ArgumentNullException.ThrowIfNull(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)
{
Logger.LogError(e.Message);
}
}
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
Logger.LogError(e.Message);
}
catch (Exception e)
{
Logger.LogError(e.Message);
}
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://learn.microsoft.com/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)
{
Logger.LogError(e.Message);
}
catch (Exception e)
{
Logger.LogError(e.Message);
}
}
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)
{
if (sources is not null)
{
var paths = new List<string>();
foreach (var programSource in sources)
{
if (Directory.Exists(programSource.Location) && programSource.Enabled)
{
paths.AddRange(ProgramPaths(programSource.Location, suffixes));
}
}
return paths;
}
return [];
}
// Function to obtain the list of applications, the locations of which have been added to the env variable PATH
private static List<string> PathEnvironmentProgramPaths(IList<string> suffixes)
{
// To get all the locations stored in the PATH env variable
var pathEnvVariable = Environment.GetEnvironmentVariable("PATH");
var searchPaths = pathEnvVariable?.Split(Path.PathSeparator);
var toFilterAllPaths = new List<string>();
var isRecursiveSearch = true;
if (searchPaths is not null)
{
foreach (var path in searchPaths)
{
if (path.Length > 0)
{
// to expand any environment variables present in the path
var 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)
{
var paths = new List<string>();
foreach (var indexLocation in indexLocations)
{
paths.AddRange(ProgramPaths(indexLocation, suffixes));
}
return paths;
}
private static List<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 List<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 List<string> RegistryAppProgramPaths(IList<string> suffixes)
{
// https://msdn.microsoft.com/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 is not null)
{
paths.AddRange(GetPathsFromRegistry(root));
}
}
using (var root = Registry.CurrentUser.OpenSubKey(appPaths))
{
if (root is not null)
{
paths.AddRange(GetPathsFromRegistry(root));
}
}
var returnedPaths = new List<string>();
foreach (var path in paths)
{
var matchesSuffix = false;
foreach (var suffix in suffixes)
{
if (path.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase))
{
matchesSuffix = true;
break;
}
}
if (matchesSuffix)
{
var expandedPath = ExpandEnvironmentVariables(path);
if (expandedPath is not null)
{
returnedPaths.Add(expandedPath);
}
}
}
return returnedPaths;
}
private static IEnumerable<string> GetPathsFromRegistry(RegistryKey root)
{
var result = new List<string>();
// Get all subkey names
var subKeyNames = root.GetSubKeyNames();
// Process each subkey to extract the path
foreach (var subkeyName in subKeyNames)
{
var path = GetPathFromRegistrySubkey(root, subkeyName);
if (!string.IsNullOrEmpty(path))
{
result.Add(path);
}
}
return result;
}
private static string GetPathFromRegistrySubkey(RegistryKey root, string subkey)
{
var path = string.Empty;
try
{
using (var key = root.OpenSubKey(subkey))
{
if (key is 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)
{
Logger.LogError(e.Message);
return string.Empty;
}
}
private static string ExpandEnvironmentVariables(string path) =>
!string.IsNullOrEmpty(path)
? Environment.ExpandEnvironmentVariables(path)
: string.Empty;
// 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 sealed class Win32ProgramEqualityComparer : IEqualityComparer<Win32Program>
{
public static readonly Win32ProgramEqualityComparer Default = new();
public bool Equals(Win32Program? app1, Win32Program? app2)
{
return app1 is null && app2 is null
? true
: app1 is not null
&& app2 is not 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)
{
// Create a HashSet with the custom equality comparer to automatically deduplicate programs
var uniquePrograms = new HashSet<Win32Program>(Win32ProgramEqualityComparer.Default);
// Filter out invalid programs and add valid ones to the HashSet
foreach (var program in programs)
{
if (program?.Valid == true)
{
uniquePrograms.Add(program);
}
}
// Convert the HashSet to a List for return
var result = new List<Win32Program>(uniquePrograms.Count);
foreach (var program in uniquePrograms)
{
result.Add(program);
}
return result;
}
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 InvalidProgram;
}
}
private static bool TryGetIcoPathForRunCommandProgram(Win32Program program, out string? icoPath)
{
icoPath = null;
if (program.AppType != ApplicationType.RunCommand)
{
return false;
}
if (string.IsNullOrEmpty(program.FullPath))
{
return false;
}
// https://msdn.microsoft.com/library/windows/desktop/ee872121
try
{
var redirectionPath = ReparsePoint.GetTarget(program.FullPath);
if (string.IsNullOrEmpty(redirectionPath))
{
return false;
}
icoPath = ExpandEnvironmentVariables(redirectionPath);
return true;
}
catch (IOException e)
{
Logger.LogError(e.Message);
}
icoPath = null;
return false;
}
private static Win32Program GetRunCommandProgramFromPath(string path)
{
var program = GetProgramFromPath(path);
if (program.Valid)
{
program.AppType = ApplicationType.RunCommand;
if (TryGetIcoPathForRunCommandProgram(program, out var icoPath))
{
program.IcoPath = icoPath ?? string.Empty;
}
}
return program;
}
public static IList<Win32Program> All(AllAppsSettings settings)
{
ArgumentNullException.ThrowIfNull(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 .lnk files 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.RunCommandSuffixes)),
};
var disabledProgramsList = settings.DisabledProgramSources;
// Get all paths but exclude all normal .Executables
var pathBag = new ConcurrentBag<string>();
Parallel.ForEach(sources, source =>
{
if (!source.IsEnabled)
{
return;
}
foreach (var path in source.GetPaths())
{
if (ExecutableApplicationExtensions.Contains(Extension(path)))
{
continue;
}
var isDisabled = false;
foreach (var disabledProgram in disabledProgramsList)
{
if (disabledProgram.UniqueIdentifier == path)
{
isDisabled = true;
break;
}
}
if (!isDisabled)
{
pathBag.Add(path);
}
}
});
paths.UnionWith(pathBag);
var runCommandPathBag = new ConcurrentBag<string>();
Parallel.ForEach(runCommandSources, source =>
{
if (!source.IsEnabled)
{
return;
}
foreach (var path in source.GetPaths())
{
var isDisabled = false;
foreach (var disabledProgram in disabledProgramsList)
{
if (disabledProgram.UniqueIdentifier == path)
{
isDisabled = true;
break;
}
}
if (!isDisabled)
{
runCommandPathBag.Add(path);
}
}
});
runCommandPaths.UnionWith(runCommandPathBag);
var programsList = new ConcurrentBag<Win32Program>();
Parallel.ForEach(paths, source =>
{
var program = GetProgramFromPath(source);
if (program is not null)
{
programsList.Add(program);
}
});
var runCommandProgramsList = new ConcurrentBag<Win32Program>();
Parallel.ForEach(runCommandPaths, source =>
{
var program = GetRunCommandProgramFromPath(source);
if (program is not null)
{
runCommandProgramsList.Add(program);
}
});
List<Win32Program> allPrograms = [.. programsList, .. runCommandProgramsList];
return DeduplicatePrograms(allPrograms);
}
catch (Exception e)
{
Logger.LogError(e.Message);
return Array.Empty<Win32Program>();
}
}
internal AppItem ToAppItem()
{
var app = this;
var icoPath = string.IsNullOrEmpty(app.IcoPath) ?
(app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ?
app.IcoPath :
app.FullPath) :
app.IcoPath;
icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ?
app.FullPath :
icoPath;
return new AppItem()
{
Name = app.Name,
Subtitle = app.Description,
Type = app.Type(),
IcoPath = icoPath,
ExePath = !string.IsNullOrEmpty(app.LnkFilePath) ? app.LnkFilePath : app.FullPath,
DirPath = app.Location,
Commands = app.GetCommands(),
AppIdentifier = app.GetAppIdentifier(),
FullExecutablePath = app.FullPath,
};
}
}