using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Security; using System.Text; using System.Threading.Tasks; using Microsoft.Win32; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Infrastructure.Logger; using System.Diagnostics; using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher.Plugin.Program.Programs { [Serializable] public class Win32 : IProgram { 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 ParentDirectory { get; set; } public string ExecutableName { get; set; } public string Description { get; set; } public bool Valid { get; set; } public bool Enabled { get; set; } public string Location => ParentDirectory; private const string ShortcutExtension = "lnk"; private const string ExeExtension = "exe"; public Result Result(string query, IPublicAPI api) { string title; var nameMatchResult = StringMatcher.FuzzySearch(query, Name); var descriptionMatchResult = StringMatcher.FuzzySearch(query, Description); var pathMatchResult = new MatchResult(false, 0, new List(), 0); if (ExecutableName != null) // only lnk program will need this one pathMatchResult = StringMatcher.FuzzySearch(query, Path.GetFileNameWithoutExtension(FullPath)); MatchResult matchResult = nameMatchResult; if (nameMatchResult.Score < descriptionMatchResult.Score) matchResult = descriptionMatchResult; if (!matchResult.IsSearchPrecisionScoreMet()) { if (pathMatchResult.IsSearchPrecisionScoreMet()) matchResult = pathMatchResult; else return null; } // We suppose Name won't be null if (Description == null || Name.StartsWith(Description)) { title = Name; } else if (Description.StartsWith(Name)) { title = Description; } else { title = $"{Name}: {Description}"; if (matchResult == descriptionMatchResult) { for (int i = 0; i < descriptionMatchResult.MatchData.Count; i++) { matchResult.MatchData[i] += Name.Length + 2; // 2 is ": " } } } if (matchResult == pathMatchResult) { // path Match won't have valid highlight data matchResult.MatchData = new List(); } var result = new Result { Title = title, SubTitle = LnkResolvedPath ?? FullPath, IcoPath = IcoPath, Score = matchResult.Score, TitleHighlightData = matchResult.MatchData, ContextData = this, Action = _ => { var info = new ProcessStartInfo { FileName = LnkResolvedPath ?? FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true }; Main.StartProcess(Process.Start, info); return true; } }; return result; } public List ContextMenus(IPublicAPI api) { var contextMenus = new List { new Result { Title = api.GetTranslation("flowlauncher_plugin_program_run_as_different_user"), Action = _ => { var info = new ProcessStartInfo { FileName = FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true }; Task.Run(() => Main.StartProcess(ShellCommand.RunAsDifferentUser, info)); return true; }, IcoPath = "Images/user.png" }, new Result { Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), Action = _ => { 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" }, new Result { Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"), Action = _ => { var args = !string.IsNullOrWhiteSpace(Main._settings.CustomizedArgs) ? Main._settings.CustomizedArgs .Replace("%s", $"\"{ParentDirectory}\"") .Replace("%f", $"\"{FullPath}\"") : Main._settings.CustomizedExplorer == Settings.Explorer ? $"/select,\"{FullPath}\"" : Settings.ExplorerArgs; Main.StartProcess(Process.Start, new ProcessStartInfo( !string.IsNullOrWhiteSpace(Main._settings.CustomizedExplorer) ? Main._settings.CustomizedExplorer : Settings.Explorer, args)); return true; }, IcoPath = "Images/folder.png" } }; return contextMenus; } public override string ToString() { return Name; } 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 new Win32() { Valid = false, Enabled = false }; } } 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)) { var extension = Extension(target); if (extension == ExeExtension && File.Exists(target)) { program.LnkResolvedPath = program.FullPath; program.FullPath = Path.GetFullPath(target).ToLower(); program.ExecutableName = Path.GetFileName(target); 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; } } } } return program; } catch (COMException e) { // C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\MiracastView.lnk always cause exception ProgramLogger.LogException($"|Win32|LnkProgram|{path}" + "|Error caused likely due to trying to get the description of the program", e); program.Valid = false; return program; } #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); program.Valid = false; return program; } #endif } 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 (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 new Win32() { Valid = false, Enabled = false }; } } private static IEnumerable ProgramPaths(string directory, string[] suffixes) { if (!Directory.Exists(directory)) return Enumerable.Empty(); return Directory.EnumerateFiles(directory, "*", new EnumerationOptions { IgnoreInaccessible = true, RecurseSubdirectories = true }).Where(x => suffixes.Contains(Extension(x))); } private static string Extension(string path) { var extension = Path.GetExtension(path)?.ToLower(); if (!string.IsNullOrEmpty(extension)) { return extension.Substring(1); } else { return string.Empty; } } private static IEnumerable UnregisteredPrograms(List sources, string[] suffixes) { var paths = ExceptDisabledSource(sources.Where(s => Directory.Exists(s.Location) && s.Enabled) .SelectMany(s => ProgramPaths(s.Location, suffixes)), x => x) .Distinct(); var programs = paths.Select(x => Extension(x) switch { ExeExtension => ExeProgram(x), ShortcutExtension => LnkProgram(x), _ => Win32Program(x) }); return programs; } private static IEnumerable StartMenuPrograms(string[] suffixes) { var disabledProgramsList = Main._settings.DisabledProgramSources; var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Programs); var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms); var paths1 = ProgramPaths(directory1, suffixes); var paths2 = ProgramPaths(directory2, suffixes); var toFilter = paths1.Concat(paths2); var programs = ExceptDisabledSource(toFilter.Distinct()) .Select(x => Extension(x) switch { ShortcutExtension => LnkProgram(x), _ => Win32Program(x) }).Where(x => x.Valid); return programs; } private static IEnumerable AppPathsPrograms(string[] suffixes) { // 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 filtered = ExceptDisabledSource(toFilter); return filtered.Select(GetProgramFromPath).ToList(); // ToList due to disposing issue } 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) { if (string.IsNullOrEmpty(path)) return null; path = Environment.ExpandEnvironmentVariables(path); if (!File.Exists(path)) return null; var entry = Win32Program(path); return entry; } public static IEnumerable ExceptDisabledSource(IEnumerable sources) { return ExceptDisabledSource(sources, x => x); } 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 Win32[] ProgramsHasher(IEnumerable programs) { return programs.GroupBy(p => p.FullPath.ToLower()) .SelectMany(g => { if (g.Count() > 1) return DistinctBy(g.Where(p => !string.IsNullOrEmpty(p.Description)), x => x.Description); return g; }).ToArray(); } public static Win32[] All(Settings settings) { try { var programs = Enumerable.Empty(); var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes); programs = programs.Concat(unregistered); if (settings.EnableRegistrySource) { var appPaths = AppPathsPrograms(settings.ProgramSuffixes); programs = programs.Concat(appPaths); } if (settings.EnableStartMenuSource) { var startMenu = StartMenuPrograms(settings.ProgramSuffixes); programs = programs.Concat(startMenu); } return ProgramsHasher(programs.Where(p => p != null)); } #if DEBUG //This is to make developer aware of any unhandled exception and add in handling. catch (Exception e) { throw e; } #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 } } }