using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security; using System.Text; using System.Threading.Tasks; using Microsoft.Win32; using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedModels; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Threading.Channels; using Flow.Launcher.Plugin.Program.Views.Models; using IniParser; using System.Windows.Input; using MemoryPack; namespace Flow.Launcher.Plugin.Program.Programs { [MemoryPackable] public partial class Win32 : IProgram, IEquatable { public string Name { get; set; } public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } // For path comparison public string IcoPath { get; set; } /// /// Path of the file. It's the path of .lnk and .url for .lnk and .url files. /// public string FullPath { get; set; } /// /// Path of the executable for .lnk, or the URL for .url /// public string LnkResolvedPath { get; set; } /// /// Path of the actual executable file /// public string ExecutablePath => LnkResolvedPath ?? FullPath; /// /// Arguments for the executable. /// public string Args { get; set; } public string ParentDirectory { get; set; } /// /// Name of the executable for .lnk files /// public string ExecutableName { get; set; } public string Description { get; set; } public bool Valid { get; set; } public bool Enabled { get; set; } public string Location => ParentDirectory; // Localized name based on windows display language public string LocalizedName { get; set; } = string.Empty; private const string ShortcutExtension = "lnk"; private const string UrlExtension = "url"; private const string ExeExtension = "exe"; private string _uid = string.Empty; private static readonly Win32 Default = new() { Name = string.Empty, Description = string.Empty, IcoPath = string.Empty, FullPath = string.Empty, LnkResolvedPath = null, ParentDirectory = string.Empty, ExecutableName = null, UniqueIdentifier = string.Empty, Valid = false, Enabled = false }; private static MatchResult Match(string query, IReadOnlyCollection candidates) { if (candidates.Count == 0) return null; var match = candidates.Select(candidate => Main.Context.API.FuzzySearch(query, candidate)) .MaxBy(match => match.Score); return match?.IsSearchPrecisionScoreMet() ?? false ? match : null; } public Result Result(string query, IPublicAPI api) { string title; MatchResult matchResult; // Name of the result // Check equality to avoid matching again in candidates bool useLocalizedName = !string.IsNullOrEmpty(LocalizedName) && !Name.Equals(LocalizedName); string resultName = useLocalizedName ? LocalizedName : Name; if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || resultName.Equals(Description)) { title = resultName; matchResult = Main.Context.API.FuzzySearch(query, resultName); } else { // Search in both title = $"{resultName}: {Description}"; var nameMatch = Main.Context.API.FuzzySearch(query, resultName); var descriptionMatch = Main.Context.API.FuzzySearch(query, Description); if (descriptionMatch.Score > nameMatch.Score) { for (int i = 0; i < descriptionMatch.MatchData.Count; i++) { descriptionMatch.MatchData[i] += resultName.Length + 2; // 2 is ": " } matchResult = descriptionMatch; } else { matchResult = nameMatch; } } List candidates = new List(); if (!matchResult.IsSearchPrecisionScoreMet() && !string.IsNullOrEmpty(query)) { if (ExecutableName != null) // only lnk program will need this one { candidates.Add(ExecutableName); } if (useLocalizedName) { candidates.Add(Name); } matchResult = Match(query, candidates); if (matchResult == null) { return null; } else { // Nothing to highlight in title in this case matchResult.MatchData.Clear(); } } string subtitle = string.Empty; if (!Main._settings.HideAppsPath) { if (Extension(FullPath) == UrlExtension) { subtitle = LnkResolvedPath; } else { subtitle = FullPath; } } var result = new Result { Title = title, AutoCompleteText = resultName, SubTitle = subtitle, IcoPath = IcoPath, Score = matchResult.Score, TitleHighlightData = matchResult.MatchData, ContextData = this, TitleToolTip = $"{title}\n{ExecutablePath}", Action = c => { // Ctrl + Enter to open containing folder bool openFolder = c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control; if (openFolder) { Main.Context.API.OpenDirectory(ParentDirectory, FullPath); return true; } // Ctrl + Shift + Enter to run as admin bool runAsAdmin = c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift); var info = new ProcessStartInfo { FileName = FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true, Verb = runAsAdmin ? "runas" : "", }; _ = Task.Run(() => Main.StartProcess(Process.Start, info)); return true; } }; return result; } public List ContextMenus(IPublicAPI api) { var contextMenus = new List { new() { Title = api.GetTranslation("flowlauncher_plugin_program_run_as_different_user"), Action = c => { var info = new ProcessStartInfo { FileName = FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true }; _ = Task.Run(() => Main.StartProcess(ShellCommand.RunAsDifferentUser, info)); return true; }, IcoPath = "Images/user.png", Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe7ee"), }, new() { Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), Action = c => { var info = new ProcessStartInfo { FileName = FullPath, WorkingDirectory = ParentDirectory, Verb = "runas", UseShellExecute = true }; _ = Task.Run(() => Main.StartProcess(Process.Start, info)); return true; }, IcoPath = "Images/cmd.png", Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe7ef"), }, new() { Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"), Action = _ => { Main.Context.API.OpenDirectory(ParentDirectory, FullPath); return true; }, IcoPath = "Images/folder.png", Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe838"), }, }; if (Extension(FullPath) == ShortcutExtension) { contextMenus.Add(OpenTargetFolderContextMenuResult(api)); } return contextMenus; } private Result OpenTargetFolderContextMenuResult(IPublicAPI api) { return new Result { Title = api.GetTranslation("flowlauncher_plugin_program_open_target_folder"), Action = _ => { api.OpenDirectory(Path.GetDirectoryName(ExecutablePath), ExecutablePath); return true; }, IcoPath = "Images/folder.png", Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe8de"), }; } public override string ToString() { return Name; } private static readonly List Watchers = new(); private static Win32 Win32Program(string path) { try { var p = new Win32 { Name = Path.GetFileNameWithoutExtension(path), IcoPath = path, FullPath = path, UniqueIdentifier = path, ParentDirectory = Directory.GetParent(path).FullName, Description = string.Empty, Valid = true, Enabled = true }; return p; } catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { ProgramLogger.LogException($"|Win32|Win32Program|{path}" + $"|Permission denied when trying to load the program from {path}", e); return Default; } #if !DEBUG catch (Exception e) { ProgramLogger.LogException($"|Win32|Win32Program|{path}" + "|An unexpected error occurred in the calling method Win32Program", e); return Default; } #endif } private static Win32 LnkProgram(string path) { var program = Win32Program(path); try { const int MAX_PATH = 260; StringBuilder buffer = new StringBuilder(MAX_PATH); ShellLinkHelper _helper = new ShellLinkHelper(); string target = _helper.retrieveTargetPath(path); if (!string.IsNullOrEmpty(target) && File.Exists(target)) { program.LnkResolvedPath = Path.GetFullPath(target); program.ExecutableName = Path.GetFileNameWithoutExtension(target); var args = _helper.arguments; if (!string.IsNullOrEmpty(args)) { program.Args = args; } var description = _helper.description; if (!string.IsNullOrEmpty(description)) { program.Description = description; } else { var info = FileVersionInfo.GetVersionInfo(target); if (!string.IsNullOrEmpty(info.FileDescription)) { program.Description = info.FileDescription; } } } program.LocalizedName = ShellLocalization.GetLocalizedName(path); return program; } catch (FileNotFoundException e) { ProgramLogger.LogException($"|Win32|LnkProgram|{path}" + "|An unexpected error occurred in the calling method LnkProgram", e); return Default; } #if !DEBUG //Only do a catch all in production. This is so make developer aware of any unhandled exception and add the exception handling in. catch (Exception e) { ProgramLogger.LogException($"|Win32|LnkProgram|{path}" + "|An unexpected error occurred in the calling method LnkProgram", e); return Default; } #endif } private static Win32 UrlProgram(string path, string[] protocols) { var program = Win32Program(path); program.Valid = false; try { var parser = new FileIniDataParser(); var data = parser.ReadFile(path); var urlSection = data["InternetShortcut"]; var url = urlSection?["URL"]; if (string.IsNullOrEmpty(url)) { return program; } foreach (var protocol in protocols) { if (url.StartsWith(protocol)) { program.LnkResolvedPath = url; program.Valid = true; break; } } var iconPath = urlSection?["IconFile"]; if (!string.IsNullOrEmpty(iconPath)) { program.IcoPath = iconPath; } } catch (Exception) { // Many files do not have the required fields, so no logging is done. } return program; } private static Win32 ExeProgram(string path) { try { var program = Win32Program(path); var info = FileVersionInfo.GetVersionInfo(path); if (!string.IsNullOrEmpty(info.FileDescription)) program.Description = info.FileDescription; return program; } catch (FileNotFoundException e) { ProgramLogger.LogException($"|Win32|ExeProgram|{path}" + $"|File not found when trying to load the program from {path}", e); return Default; } catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { ProgramLogger.LogException($"|Win32|ExeProgram|{path}" + $"|Permission denied when trying to load the program from {path}", e); return Default; } } private static IEnumerable EnumerateProgramsInDir(string directory, string[] suffixes, bool recursive = true) { if (!Directory.Exists(directory)) return Enumerable.Empty(); return Directory.EnumerateFiles( directory, "*", new EnumerationOptions { IgnoreInaccessible = true, RecurseSubdirectories = recursive }) .Where(x => suffixes.Contains(Extension(x))); } private static string Extension(string path) { var extension = Path.GetExtension(path)?.ToLowerInvariant(); if (!string.IsNullOrEmpty(extension)) { return extension[1..]; // remove dot } else { return string.Empty; } } private static IEnumerable UnregisteredPrograms(List directories, string[] suffixes, string[] protocols) { // Disabled custom sources are not in DisabledProgramSources var paths = directories.AsParallel() .SelectMany(s => EnumerateProgramsInDir(s, suffixes)); // Remove disabled programs in DisabledProgramSources var programs = ExceptDisabledSource(paths).Select(x => GetProgramFromPath(x, protocols)); return programs; } private static IEnumerable StartMenuPrograms(string[] suffixes, string[] protocols) { var allPrograms = GetStartMenuPaths() .SelectMany(p => EnumerateProgramsInDir(p, suffixes)) .Distinct(); var startupPaths = GetStartupPaths(); var programs = ExceptDisabledSource(allPrograms) .Where(x => !startupPaths.Any(startup => FilesFolders.PathContains(startup, x))) .Select(x => GetProgramFromPath(x, protocols)); return programs; } private static IEnumerable PATHPrograms(string[] suffixes, string[] protocols, List commonParents) { var pathEnv = Environment.GetEnvironmentVariable("Path"); if (String.IsNullOrEmpty(pathEnv)) { return Array.Empty(); } var paths = pathEnv.Split(";", StringSplitOptions.RemoveEmptyEntries).DistinctBy(p => p.ToLowerInvariant()); var toFilter = paths.Where(x => commonParents.All(parent => !FilesFolders.PathContains(parent, x))) .AsParallel() .SelectMany(p => EnumerateProgramsInDir(p, suffixes, recursive: false)); var programs = ExceptDisabledSource(toFilter.Distinct()) .Select(x => GetProgramFromPath(x, protocols)); return programs; } private static IEnumerable AppPathsPrograms(string[] suffixes, string[] protocols) { // https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121 const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths"; IEnumerable toFilter = Enumerable.Empty(); using var rootMachine = Registry.LocalMachine.OpenSubKey(appPaths); using var rootUser = Registry.CurrentUser.OpenSubKey(appPaths); if (rootMachine != null) { toFilter = toFilter.Concat(GetPathFromRegistry(rootMachine)); } if (rootUser != null) { toFilter = toFilter.Concat(GetPathFromRegistry(rootUser)); } toFilter = toFilter.Distinct().Where(p => suffixes.Contains(Extension(p))); var programs = ExceptDisabledSource(toFilter) .Select(x => GetProgramFromPath(x, protocols)).Where(x => x.Valid) .ToList(); // ToList due to disposing issue return programs; } private static IEnumerable GetPathFromRegistry(RegistryKey root) { return root .GetSubKeyNames() .Select(x => GetProgramPathFromRegistrySubKeys(root, x)) .Distinct(); } private static string GetProgramPathFromRegistrySubKeys(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.LogException($"|Win32|GetProgramPathFromRegistrySubKeys|{path}" + $"|Permission denied when trying to load the program from {path}", e); return string.Empty; } } private static Win32 GetProgramFromPath(string path, string[] protocols) { if (string.IsNullOrEmpty(path)) return Default; path = Environment.ExpandEnvironmentVariables(path); return Extension(path) switch { ShortcutExtension => LnkProgram(path), ExeExtension => ExeProgram(path), UrlExtension => UrlProgram(path, protocols), _ => Win32Program(path) }; ; } public static IEnumerable ExceptDisabledSource(IEnumerable paths) { return ExceptDisabledSource(paths, x => x.ToLowerInvariant()); } public static IEnumerable ExceptDisabledSource(IEnumerable sources, Func keySelector) { return Main._settings.DisabledProgramSources.Count == 0 ? sources : ExceptDisabledSourceEnumerable(sources, keySelector); static IEnumerable ExceptDisabledSourceEnumerable(IEnumerable elements, Func selector) { var set = Main._settings.DisabledProgramSources.Select(x => x.UniqueIdentifier).ToHashSet(); foreach (var element in elements) { if (!set.Contains(selector(element))) yield return element; } } } public static IEnumerable DistinctBy(IEnumerable source, Func selector) { var set = new HashSet(); foreach (var item in source) { if (set.Add(selector(item))) yield return item; } } private static IEnumerable ProgramsHasher(IEnumerable programs) { var startMenuPaths = GetStartMenuPaths(); return programs.GroupBy(p => (p.ExecutablePath + p.Args).ToLowerInvariant()) .AsParallel() .SelectMany(g => { // is shortcut and in start menu var startMenu = g.Where(g => g.LnkResolvedPath != null && startMenuPaths.Any(x => FilesFolders.PathContains(x, g.FullPath))) .ToList(); if (startMenu.Any()) return startMenu.Take(1); // distinct by description var temp = g.Where(g => !string.IsNullOrEmpty(g.Description)).ToList(); if (temp.Any()) return temp.Take(1); return g.Take(1); }); } public static Win32[] All(Settings settings) { try { var programs = Enumerable.Empty(); var suffixes = settings.GetSuffixes(); var protocols = settings.GetProtocols(); // Disabled custom sources are not in DisabledProgramSources var sources = settings.ProgramSources.Where(s => Directory.Exists(s.Location) && s.Enabled).Distinct(); var commonParents = GetCommonParents(sources); var unregistered = UnregisteredPrograms(commonParents, suffixes, protocols); programs = programs.Concat(unregistered); var autoIndexPrograms = Enumerable.Empty(); // for single programs, not folders if (settings.EnableRegistrySource) { var appPaths = AppPathsPrograms(suffixes, protocols); autoIndexPrograms = autoIndexPrograms.Concat(appPaths); } if (settings.EnableStartMenuSource) { var startMenu = StartMenuPrograms(suffixes, protocols); autoIndexPrograms = autoIndexPrograms.Concat(startMenu); } if (settings.EnablePathSource) { var path = PATHPrograms(settings.GetSuffixes(), protocols, commonParents); programs = programs.Concat(path); } autoIndexPrograms = ProgramsHasher(autoIndexPrograms).ToArray(); return programs.Concat(autoIndexPrograms).Where(x => x.Valid).Distinct().ToArray(); } #if DEBUG //This is to make developer aware of any unhandled exception and add in handling. catch (Exception) { throw; } #endif #if !DEBUG //Only do a catch all in production. catch (Exception e) { ProgramLogger.LogException("|Win32|All|Not available|An unexpected error occurred", e); return Array.Empty(); } #endif } public override int GetHashCode() { return UniqueIdentifier.GetHashCode(); } public bool Equals([AllowNull] Win32 other) { if (other == null) return false; return UniqueIdentifier == other.UniqueIdentifier; } public override bool Equals(object obj) { if (obj is Win32 other) { return UniqueIdentifier == other.UniqueIdentifier; } else { return false; } } private static IEnumerable GetStartMenuPaths() { var userStartMenu = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu); var commonStartMenu = Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu); return new[] { userStartMenu, commonStartMenu }; } private static IEnumerable GetStartupPaths() { var userStartup = Environment.GetFolderPath(Environment.SpecialFolder.Startup); var commonStartup = Environment.GetFolderPath(Environment.SpecialFolder.CommonStartup); return new[] { userStartup, commonStartup }; } public static void WatchProgramUpdate(Settings settings) { var paths = new List(); if (settings.EnableStartMenuSource) paths.AddRange(GetStartMenuPaths()); var customSources = GetCommonParents(settings.ProgramSources); paths.AddRange(customSources); var fileExtensionToWatch = settings.GetSuffixes(); foreach (var directory in from path in paths where Directory.Exists(path) select path) { WatchDirectory(directory, fileExtensionToWatch); } _ = Task.Run(MonitorDirectoryChangeAsync); } private static readonly Channel indexQueue = Channel.CreateBounded(1); public static async Task MonitorDirectoryChangeAsync() { var reader = indexQueue.Reader; while (await reader.WaitToReadAsync()) { await Task.Delay(500); while (reader.TryRead(out _)) { } await Task.Run(Main.IndexWin32ProgramsAsync); } } public static void WatchDirectory(string directory, string[] extensions) { if (!Directory.Exists(directory)) { throw new ArgumentException("Path Not Exist"); } var watcher = new FileSystemWatcher(directory); watcher.Created += static (_, _) => indexQueue.Writer.TryWrite(default); watcher.Deleted += static (_, _) => indexQueue.Writer.TryWrite(default); watcher.EnableRaisingEvents = true; watcher.IncludeSubdirectories = true; foreach (var extension in extensions) { watcher.Filters.Add($"*.{extension}"); } Watchers.Add(watcher); } public static void Dispose() { foreach (var fileSystemWatcher in Watchers) { fileSystemWatcher.Dispose(); } } private static List GetCommonParents(IEnumerable programSources) { // To avoid unnecessary io // like c:\windows and c:\windows\system32 var grouped = programSources.GroupBy(p => p.Location.ToLowerInvariant()[0]); // group by disk List result = new(); foreach (var group in grouped) { HashSet parents = group.ToHashSet(); foreach (var source in group) { if (parents.Any(p => FilesFolders.PathContains(p.Location, source.Location))) { parents.Remove(source); } } result.AddRange(parents.Select(x => x.Location)); } return result.DistinctBy(x => x.ToLowerInvariant()).ToList(); } } }