Flow.Launcher/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
2021-01-17 23:45:15 +08:00

478 lines
No EOL
17 KiB
C#

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.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 ApplicationReferenceExtension = "appref-ms";
private const string ExeExtension = "exe";
public Result Result(string query, IPublicAPI api)
{
var title = (Name, Description) switch
{
(var n, null) => n,
(var n, var d) when d.StartsWith(n) => d,
(var n, var d) when n.StartsWith(d) => n,
(var n, var d) when !string.IsNullOrEmpty(d) => $"{n}: {d}",
_ => Name
};
var matchResult = StringMatcher.FuzzySearch(query, title);
if (!matchResult.Success)
return null;
var result = new Result
{
Title = title,
SubTitle = LnkResolvedPath ?? FullPath,
IcoPath = IcoPath,
Score = matchResult.Score,
TitleHighlightData = matchResult.MatchData,
ContextData = this,
Action = e =>
{
var info = new ProcessStartInfo
{
FileName = LnkResolvedPath ?? FullPath,
WorkingDirectory = ParentDirectory,
UseShellExecute = true
};
Main.StartProcess(Process.Start, info);
return true;
}
};
return result;
}
public List<Result> ContextMenus(IPublicAPI api)
{
var contextMenus = new List<Result>
{
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 ExecutableName;
}
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<string> ProgramPaths(string directory, string[] suffixes)
{
if (!Directory.Exists(directory))
return Enumerable.Empty<string>();
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<Win32> UnregisteredPrograms(List<Settings.ProgramSource> sources, string[] suffixes)
{
var paths = sources.Where(s => Directory.Exists(s.Location) && s.Enabled)
.SelectMany(s => ProgramPaths(s.Location, suffixes))
.Where(t1 => !Main._settings.DisabledProgramSources.Any(x => t1 == x.UniqueIdentifier))
.Distinct();
var programs = paths.Select(x => Extension(x) switch
{
ExeExtension => ExeProgram(x),
ShortcutExtension => LnkProgram(x),
_ => Win32Program(x)
});
return programs;
}
private static IEnumerable<Win32> 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 = toFilter
.Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1))
.Distinct()
.Select(x => Extension(x) switch
{
ShortcutExtension => LnkProgram(x),
_ => Win32Program(x)
}).Where(x => x.Valid);
return programs;
}
private static IEnumerable<Win32> AppPathsPrograms(string[] suffixes)
{
// https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121
const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
IEnumerable<Win32> programs = Enumerable.Empty<Win32>();
using var rootMachine = Registry.LocalMachine.OpenSubKey(appPaths);
using var rootUser = Registry.CurrentUser.OpenSubKey(appPaths);
if (rootMachine != null)
{
programs = programs.Concat(GetProgramsFromRegistry(rootMachine));
}
if (rootUser != null)
{
programs = programs.Concat(GetProgramsFromRegistry(rootUser));
}
var disabledProgramsList = Main._settings.DisabledProgramSources;
var toFilter = programs.Where(p => suffixes.Contains(Extension(p.ExecutableName)));
var filtered = toFilter
.Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
.Select(t1 => t1);
return filtered.ToList();
}
private static IEnumerable<Win32> GetProgramsFromRegistry(RegistryKey root)
{
return root
.GetSubKeyNames()
.Select(x => GetProgramPathFromRegistrySubKeys(root, x))
.Distinct()
.Select(GetProgramFromPath);
}
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 new Win32();
path = Environment.ExpandEnvironmentVariables(path);
if (!File.Exists(path))
return new Win32();
var entry = Win32Program(path);
entry.ExecutableName = Path.GetFileName(path);
return entry;
}
private class Win32ComparatorWithDescription : IEqualityComparer<Win32>
{
public static readonly Win32ComparatorWithDescription Default = new Win32ComparatorWithDescription();
public bool Equals(Win32 x, Win32 y)
{
return x?.Description == y?.Description;
}
public int GetHashCode(Win32 obj)
{
return obj.Description.GetHashCode();
}
}
private static Win32[] ProgramsHasher(IEnumerable<Win32> programs)
{
return programs.GroupBy(p => p.FullPath.ToLower())
.SelectMany(g =>
{
if (g.Count() > 1)
return g.Where(p => !string.IsNullOrEmpty(p.Description))
.Distinct(Win32ComparatorWithDescription.Default);
return g;
}).ToArray();
}
public static Win32[] All(Settings settings)
{
try
{
var programs = Enumerable.Empty<Win32>();
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);
}
#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<Win32>();
}
#endif
}
}
}