Flow.Launcher/Plugins/Flow.Launcher.Plugin.Program/Main.cs

452 lines
17 KiB
C#
Raw Normal View History

using System;
2014-01-04 12:26:13 +00:00
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
2014-01-04 12:26:13 +00:00
using System.Linq;
2021-01-02 09:58:30 +00:00
using System.Threading;
2016-11-30 01:07:48 +00:00
using System.Threading.Tasks;
2016-01-06 21:34:42 +00:00
using System.Windows.Controls;
2020-04-21 09:12:17 +00:00
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
2020-04-21 09:12:17 +00:00
using Flow.Launcher.Plugin.Program.Programs;
using Flow.Launcher.Plugin.Program.Views;
2022-10-20 08:36:58 +00:00
using Flow.Launcher.Plugin.Program.Views.Models;
using Flow.Launcher.Plugin.SharedCommands;
2021-05-27 11:28:17 +00:00
using Microsoft.Extensions.Caching.Memory;
using Path = System.IO.Path;
2020-04-21 09:12:17 +00:00
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
2014-01-04 12:26:13 +00:00
2020-04-21 09:12:17 +00:00
namespace Flow.Launcher.Plugin.Program
2014-01-04 12:26:13 +00:00
{
2025-04-01 06:21:29 +00:00
public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, IAsyncReloadable, IDisposable
2014-01-04 12:26:13 +00:00
{
2025-04-01 06:21:29 +00:00
private const string Win32CacheName = "Win32";
private const string UwpCacheName = "UWP";
2025-04-01 06:21:29 +00:00
internal static List<Win32> _win32s { get; private set; }
internal static List<UWPApp> _uwps { get; private set; }
internal static Settings _settings { get; private set; }
2019-10-17 20:53:00 +00:00
2025-04-08 08:29:03 +00:00
internal static SemaphoreSlim _win32sLock = new(1, 1);
internal static SemaphoreSlim _uwpsLock = new(1, 1);
2021-11-06 06:10:29 +00:00
internal static PluginInitContext Context { get; private set; }
2021-05-24 02:21:39 +00:00
private static readonly List<Result> emptyResults = new();
private static readonly MemoryCacheOptions cacheOptions = new() { SizeLimit = 1560 };
2021-05-27 11:28:17 +00:00
private static MemoryCache cache = new(cacheOptions);
private static readonly string[] commonUninstallerNames =
{
"uninst.exe",
"unins000.exe",
"uninst000.exe",
"uninstall.exe"
};
2025-01-24 04:11:48 +00:00
private static readonly string[] commonUninstallerPrefixs =
{
"uninstall",//en
"卸载",//zh-cn
"卸載",//zh-tw
"видалити",//uk-UA
"удалить",//ru
"désinstaller",//fr
"アンインストール",//ja
"deïnstalleren",//nl
"odinstaluj",//pl
"afinstallere",//da
"deinstallieren",//de
"삭제",//ko
"деинсталирај",//sr
"desinstalar",//pt-pt
"desinstalar",//pt-br
"desinstalar",//es
"desinstalar",//es-419
"disinstallare",//it
"avinstallere",//nb-NO
"odinštalovať",//sk
"kaldır",//tr
"odinstalovat",//cs
"إلغاء التثبيت",//ar
"gỡ bỏ",//vi-vn
"הסרה"//he
2025-01-24 04:11:48 +00:00
};
private const string ExeUninstallerSuffix = ".exe";
private const string InkUninstallerSuffix = ".lnk";
private static readonly string WindowsAppPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "WindowsApps");
2021-01-02 09:58:30 +00:00
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
2014-01-04 12:26:13 +00:00
{
2021-05-27 11:28:17 +00:00
var result = await cache.GetOrCreateAsync(query.Search, async entry =>
{
2025-04-08 08:29:03 +00:00
var resultList = await Task.Run(async () =>
{
2025-04-08 08:29:03 +00:00
await _win32sLock.WaitAsync(token);
await _uwpsLock.WaitAsync(token);
try
{
// Collect all UWP Windows app directories
2025-02-21 01:29:43 +00:00
var uwpsDirectories = _settings.HideDuplicatedWindowsApp ? _uwps
.Where(uwp => !string.IsNullOrEmpty(uwp.Location)) // Exclude invalid paths
.Where(uwp => uwp.Location.StartsWith(WindowsAppPath, StringComparison.OrdinalIgnoreCase)) // Keep system apps
.Select(uwp => uwp.Location.TrimEnd('\\')) // Remove trailing slash
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray() : null;
return _win32s.Cast<IProgram>()
.Concat(_uwps)
.AsParallel()
.WithCancellation(token)
.Where(HideUninstallersFilter)
.Where(p => HideDuplicatedWindowsAppFilter(p, uwpsDirectories))
.Where(p => p.Enabled)
.Select(p => p.Result(query.Search, Context.API))
.Where(r => r?.Score > 0)
.ToList();
}
catch (OperationCanceledException)
{
2024-11-24 01:21:27 +00:00
Log.Debug("|Flow.Launcher.Plugin.Program.Main|Query operation cancelled");
return emptyResults;
}
2025-04-08 08:29:03 +00:00
finally
{
_uwpsLock.Release();
_win32sLock.Release();
}
2024-11-23 13:32:42 +00:00
}, token);
resultList = resultList.Any() ? resultList : emptyResults;
entry.SetSize(resultList.Count);
entry.SetSlidingExpiration(TimeSpan.FromHours(8));
return resultList;
});
return result;
2014-01-04 12:26:13 +00:00
}
private bool HideUninstallersFilter(IProgram program)
{
if (!_settings.HideUninstallers) return true;
if (program is not Win32 win32) return true;
2025-01-24 04:11:48 +00:00
// First check the executable path
var fileName = Path.GetFileName(win32.ExecutablePath);
2025-01-24 04:11:48 +00:00
// For cases when the uninstaller is named like "uninst.exe"
if (commonUninstallerNames.Contains(fileName, StringComparer.OrdinalIgnoreCase)) return false;
// For cases when the uninstaller is named like "Uninstall Program Name.exe"
foreach (var prefix in commonUninstallerPrefixs)
{
if (fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
fileName.EndsWith(ExeUninstallerSuffix, StringComparison.OrdinalIgnoreCase))
return false;
}
2025-01-25 22:34:07 +00:00
// Second check the lnk path
2025-01-24 04:11:48 +00:00
if (!string.IsNullOrEmpty(win32.LnkResolvedPath))
{
var inkFileName = Path.GetFileName(win32.FullPath);
// For cases when the uninstaller is named like "Uninstall Program Name.ink"
foreach (var prefix in commonUninstallerPrefixs)
{
if (inkFileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
inkFileName.EndsWith(InkUninstallerSuffix, StringComparison.OrdinalIgnoreCase))
return false;
}
}
return true;
}
2025-02-21 01:29:43 +00:00
private static bool HideDuplicatedWindowsAppFilter(IProgram program, string[] uwpsDirectories)
{
if (uwpsDirectories == null || uwpsDirectories.Length == 0) return true;
if (program is UWPApp) return true;
var location = program.Location.TrimEnd('\\'); // Ensure trailing slash
if (string.IsNullOrEmpty(location))
return true; // Keep if location is invalid
if (!location.StartsWith(WindowsAppPath, StringComparison.OrdinalIgnoreCase))
return true; // Keep if not a Windows app
// Check if the any Win32 executable directory contains UWP Windows app location matches
return !uwpsDirectories.Any(uwpDirectory =>
location.StartsWith(uwpDirectory, StringComparison.OrdinalIgnoreCase));
}
2021-01-02 09:58:30 +00:00
public async Task InitAsync(PluginInitContext context)
2014-01-04 12:26:13 +00:00
{
2021-11-06 18:58:28 +00:00
Context = context;
2021-01-02 09:58:30 +00:00
_settings = context.API.LoadSettingJsonStorage<Settings>();
2021-01-02 09:58:30 +00:00
await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", async () =>
{
2025-04-01 06:21:29 +00:00
var pluginCachePath = Context.CurrentPluginMetadata.PluginCacheDirectoryPath;
FilesFolders.ValidateDirectory(pluginCachePath);
2025-02-24 08:11:49 +00:00
static void MoveFile(string sourcePath, string destinationPath)
{
if (!File.Exists(sourcePath))
{
2025-02-24 08:11:49 +00:00
return;
}
if (File.Exists(destinationPath))
{
2025-02-24 08:11:49 +00:00
try
{
File.Delete(sourcePath);
}
catch (Exception)
{
// Ignore, we will handle next time we start the plugin
}
return;
}
var destinationDirectory = Path.GetDirectoryName(destinationPath);
if (!Directory.Exists(destinationDirectory) && (!string.IsNullOrEmpty(destinationDirectory)))
{
2025-02-24 08:11:49 +00:00
try
{
Directory.CreateDirectory(destinationDirectory);
}
catch (Exception)
{
// Ignore, we will handle next time we start the plugin
}
}
try
{
File.Move(sourcePath, destinationPath);
}
catch (Exception)
{
// Ignore, we will handle next time we start the plugin
}
}
// Move old cache files to the new cache directory
2025-04-01 06:21:29 +00:00
var oldWin32CacheFile = Path.Combine(DataLocation.CacheDirectory, $"{Win32CacheName}.cache");
var newWin32CacheFile = Path.Combine(pluginCachePath, $"{Win32CacheName}.cache");
MoveFile(oldWin32CacheFile, newWin32CacheFile);
2025-04-01 06:21:29 +00:00
var oldUWPCacheFile = Path.Combine(DataLocation.CacheDirectory, $"{UwpCacheName}.cache");
var newUWPCacheFile = Path.Combine(pluginCachePath, $"{UwpCacheName}.cache");
MoveFile(oldUWPCacheFile, newUWPCacheFile);
2025-04-08 08:29:03 +00:00
await _win32sLock.WaitAsync();
2025-04-01 06:21:29 +00:00
_win32s = await context.API.LoadCacheBinaryStorageAsync(Win32CacheName, pluginCachePath, new List<Win32>());
2025-04-08 08:29:03 +00:00
_win32sLock.Release();
await _uwpsLock.WaitAsync();
2025-04-01 06:21:29 +00:00
_uwps = await context.API.LoadCacheBinaryStorageAsync(UwpCacheName, pluginCachePath, new List<UWPApp>());
2025-04-08 08:29:03 +00:00
_uwpsLock.Release();
2021-01-02 09:58:30 +00:00
});
2025-04-08 08:29:03 +00:00
await _win32sLock.WaitAsync();
await _uwpsLock.WaitAsync();
2025-04-01 06:21:29 +00:00
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Count}>");
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Count}>");
bool cacheEmpty = !_win32s.Any() || !_uwps.Any();
2021-01-02 09:58:30 +00:00
2025-04-08 08:29:03 +00:00
_win32sLock.Release();
_uwpsLock.Release();
if (cacheEmpty || _settings.LastIndexTime.AddHours(30) < DateTime.Now)
2021-01-02 09:58:30 +00:00
{
_ = Task.Run(async () =>
{
await IndexProgramsAsync().ConfigureAwait(false);
WatchProgramUpdate();
});
}
else
2021-01-02 09:58:30 +00:00
{
WatchProgramUpdate();
}
2021-05-24 02:21:39 +00:00
static void WatchProgramUpdate()
{
Win32.WatchProgramUpdate(_settings);
_ = UWPPackage.WatchPackageChange();
}
}
2025-04-01 06:21:29 +00:00
public static async Task IndexWin32ProgramsAsync()
{
2019-11-16 00:37:01 +00:00
var win32S = Win32.All(_settings);
2025-04-08 08:29:03 +00:00
await _win32sLock.WaitAsync();
2025-04-01 06:21:29 +00:00
_win32s.Clear();
foreach (var win32 in win32S)
{
_win32s.Add(win32);
}
ResetCache();
2025-04-01 06:21:29 +00:00
await Context.API.SaveCacheBinaryStorageAsync<List<Win32>>(Win32CacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
2023-04-24 11:50:34 +00:00
_settings.LastIndexTime = DateTime.Now;
_win32sLock.Release();
}
2025-04-01 06:21:29 +00:00
public static async Task IndexUwpProgramsAsync()
{
2025-04-01 06:21:29 +00:00
var uwps = UWPPackage.All(_settings);
2025-04-08 08:29:03 +00:00
await _uwpsLock.WaitAsync();
2025-04-01 06:21:29 +00:00
_uwps.Clear();
foreach (var uwp in uwps)
{
_uwps.Add(uwp);
}
ResetCache();
2025-04-01 06:21:29 +00:00
await Context.API.SaveCacheBinaryStorageAsync<List<UWPApp>>(UwpCacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
2023-04-24 11:50:34 +00:00
_settings.LastIndexTime = DateTime.Now;
_uwpsLock.Release();
}
2014-03-22 05:50:20 +00:00
public static async Task IndexProgramsAsync()
{
2025-04-01 06:21:29 +00:00
var win32Task = Task.Run(async () =>
2022-10-29 18:37:22 +00:00
{
2025-04-01 06:21:29 +00:00
await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32ProgramsAsync);
2022-10-29 18:37:22 +00:00
});
2025-04-01 06:21:29 +00:00
var uwpTask = Task.Run(async () =>
2022-10-29 18:37:22 +00:00
{
2025-04-01 06:21:29 +00:00
await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|UWPProgram index cost", IndexUwpProgramsAsync);
2022-10-29 18:37:22 +00:00
});
2025-04-01 06:21:29 +00:00
await Task.WhenAll(win32Task, uwpTask).ConfigureAwait(false);
}
internal static void ResetCache()
{
2021-05-27 11:28:17 +00:00
var oldCache = cache;
cache = new MemoryCache(cacheOptions);
oldCache.Dispose();
2019-11-28 23:38:50 +00:00
}
2019-09-07 05:58:13 +00:00
2016-01-06 21:34:42 +00:00
public Control CreateSettingPanel()
{
2025-04-01 06:21:29 +00:00
return new ProgramSetting(Context, _settings);
}
2015-02-07 12:17:49 +00:00
public string GetTranslatedPluginTitle()
{
2021-11-06 18:58:28 +00:00
return Context.API.GetTranslation("flowlauncher_plugin_program_plugin_name");
2015-02-07 12:17:49 +00:00
}
public string GetTranslatedPluginDescription()
{
2021-11-06 18:58:28 +00:00
return Context.API.GetTranslation("flowlauncher_plugin_program_plugin_description");
2015-02-07 12:17:49 +00:00
}
public List<Result> LoadContextMenus(Result selectedResult)
{
var menuOptions = new List<Result>();
var program = selectedResult.ContextData as IProgram;
if (program != null)
{
2021-11-06 18:58:28 +00:00
menuOptions = program.ContextMenus(Context.API);
}
menuOptions.Add(
2021-01-02 09:58:30 +00:00
new Result
{
2021-11-06 18:58:28 +00:00
Title = Context.API.GetTranslation("flowlauncher_plugin_program_disable_program"),
2021-01-02 09:58:30 +00:00
Action = c =>
{
2025-04-08 08:59:27 +00:00
_ = DisableProgramAsync(program);
2021-11-06 18:58:28 +00:00
Context.API.ShowMsg(
Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"),
Context.API.GetTranslation(
2021-01-02 09:58:30 +00:00
"flowlauncher_plugin_program_disable_dlgtitle_success_message"));
Context.API.ReQuery();
2021-01-02 09:58:30 +00:00
return false;
},
2021-10-07 10:58:54 +00:00
IcoPath = "Images/disable.png",
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xece4"),
2021-01-02 09:58:30 +00:00
}
);
return menuOptions;
}
2025-04-08 08:59:27 +00:00
private static async Task DisableProgramAsync(IProgram programToDelete)
{
if (_settings.DisabledProgramSources.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
return;
2025-04-08 08:29:03 +00:00
await _uwpsLock.WaitAsync();
if (_uwps.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
{
2022-10-28 10:06:44 +00:00
var program = _uwps.First(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier);
program.Enabled = false;
_settings.DisabledProgramSources.Add(new ProgramSource(program));
2025-04-08 08:29:03 +00:00
_uwpsLock.Release();
// Reindex UWP programs
2022-10-28 08:44:44 +00:00
_ = Task.Run(() =>
2022-10-25 06:41:46 +00:00
{
2025-04-01 06:21:29 +00:00
_ = IndexUwpProgramsAsync();
2022-10-25 06:41:46 +00:00
});
2025-04-08 08:29:03 +00:00
return;
}
2025-04-08 08:59:27 +00:00
else
{
_uwpsLock.Release();
}
2025-04-08 08:29:03 +00:00
await _win32sLock.WaitAsync();
if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
{
2022-10-28 10:06:44 +00:00
var program = _win32s.First(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier);
program.Enabled = false;
_settings.DisabledProgramSources.Add(new ProgramSource(program));
2025-04-08 08:29:03 +00:00
_win32sLock.Release();
// Reindex Win32 programs
2022-10-28 08:44:44 +00:00
_ = Task.Run(() =>
2022-10-25 06:41:46 +00:00
{
2025-04-01 06:21:29 +00:00
_ = IndexWin32ProgramsAsync();
2022-10-25 06:41:46 +00:00
});
}
2025-04-08 08:59:27 +00:00
else
{
_win32sLock.Release();
}
}
public static void StartProcess(Func<ProcessStartInfo, Process> runProcess, ProcessStartInfo info)
{
try
{
runProcess(info);
}
catch (Exception)
{
2022-11-29 10:13:55 +00:00
var title = Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error");
var message = string.Format(Context.API.GetTranslation("flowlauncher_plugin_program_run_failed"),
info.FileName);
2022-11-29 10:13:55 +00:00
Context.API.ShowMsg(title, string.Format(message, info.FileName), string.Empty);
}
}
2019-10-06 02:15:06 +00:00
2021-01-02 09:58:30 +00:00
public async Task ReloadDataAsync()
2019-10-06 02:15:06 +00:00
{
await IndexProgramsAsync();
2019-10-06 02:15:06 +00:00
}
2022-10-21 10:39:03 +00:00
public void Dispose()
{
Win32.Dispose();
}
2014-01-04 12:26:13 +00:00
}
}