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

565 lines
21 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.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;
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-09 04:14:07 +00:00
private static readonly string ClassName = nameof(Main);
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; }
private static readonly Lock _lastIndexTimeLock = new();
2025-09-15 07:40:49 +00:00
private static readonly List<Result> emptyResults = [];
2021-05-24 02:21:39 +00:00
private static readonly MemoryCacheOptions cacheOptions = new() { SizeLimit = 1560 };
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
{
var result = await cache.GetOrCreateAsync(query.Search, async entry =>
{
var resultList = await Task.Run(async () =>
{
2025-11-06 12:48:41 +00:00
// Preparing win32 programs
List<Win32> win32s;
bool win32LockAcquired = false;
try
{
await _win32sLock.WaitAsync(token);
win32LockAcquired = true;
win32s = [.. _win32s];
}
catch (OperationCanceledException)
{
return emptyResults;
}
finally
{
// Only release the lock if it was acquired
if (win32LockAcquired) _win32sLock.Release();
}
2025-11-06 12:48:41 +00:00
// Preparing UWP programs
List<UWPApp> uwps;
bool uwpsLockAcquired = false;
try
{
await _uwpsLock.WaitAsync(token);
uwpsLockAcquired = true;
uwps = [.. _uwps];
}
catch (OperationCanceledException)
{
return emptyResults;
}
finally
{
// Only release the lock if it was acquired
if (uwpsLockAcquired) _uwpsLock.Release();
}
2025-11-06 12:48:41 +00:00
// Start querying programs
try
{
// Collect all UWP Windows app directories
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 => string.IsNullOrEmpty(query.Search) || r?.Score > 0)
.ToList();
}
catch (OperationCanceledException)
{
return emptyResults;
}
}, token);
resultList = resultList.Count != 0 ? 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
2025-04-08 11:21:03 +00:00
var _win32sCount = 0;
var _uwpsCount = 0;
2025-04-08 13:46:02 +00:00
await Context.API.StopwatchLogInfoAsync(ClassName, "Preload programs cost", async () =>
{
2025-04-08 11:44:30 +00:00
var pluginCacheDirectory = Context.CurrentPluginMetadata.PluginCacheDirectoryPath;
FilesFolders.ValidateDirectory(pluginCacheDirectory);
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
}
}
// If plugin cache directory is this: D:\\Data\\Cache\\Plugins\\Flow.Launcher.Plugin.Program
// then the parent directory is: D:\\Data\\Cache
// So we can use the parent of the parent directory to get the cache directory path
var directoryInfo = new DirectoryInfo(pluginCacheDirectory);
2025-06-23 05:13:45 +00:00
var cacheDirectory = directoryInfo.Parent?.Parent?.FullName;
// Move old cache files to the new cache directory if cache directory exists
if (!string.IsNullOrEmpty(cacheDirectory))
{
var oldWin32CacheFile = Path.Combine(cacheDirectory, $"{Win32CacheName}.cache");
var newWin32CacheFile = Path.Combine(pluginCacheDirectory, $"{Win32CacheName}.cache");
MoveFile(oldWin32CacheFile, newWin32CacheFile);
var oldUWPCacheFile = Path.Combine(cacheDirectory, $"{UwpCacheName}.cache");
var newUWPCacheFile = Path.Combine(pluginCacheDirectory, $"{UwpCacheName}.cache");
MoveFile(oldUWPCacheFile, newUWPCacheFile);
}
2025-04-08 08:29:03 +00:00
await _win32sLock.WaitAsync();
2025-09-15 07:40:49 +00:00
try
{
_win32s = await context.API.LoadCacheBinaryStorageAsync(Win32CacheName, pluginCacheDirectory, new List<Win32>());
_win32sCount = _win32s.Count;
}
finally
{
_win32sLock.Release();
}
2025-04-08 08:29:03 +00:00
await _uwpsLock.WaitAsync();
2025-09-15 07:40:49 +00:00
try
{
_uwps = await context.API.LoadCacheBinaryStorageAsync(UwpCacheName, pluginCacheDirectory, new List<UWPApp>());
_uwpsCount = _uwps.Count;
}
finally
{
_uwpsLock.Release();
}
2021-01-02 09:58:30 +00:00
});
Context.API.LogInfo(ClassName, $"Number of preload win32 programs <{_win32sCount}>");
Context.API.LogInfo(ClassName, $"Number of preload uwps <{_uwpsCount}>");
2025-04-08 08:29:03 +00:00
2025-04-08 11:21:03 +00:00
var cacheEmpty = _win32sCount == 0 || _uwpsCount == 0;
2021-01-02 09:58:30 +00:00
bool needReindex;
lock (_lastIndexTimeLock)
{
needReindex = _settings.LastIndexTime.AddHours(30) < DateTime.Now;
}
if (cacheEmpty || needReindex)
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);
2025-04-09 04:44:26 +00:00
_ = UWPPackage.WatchPackageChangeAsync();
}
}
public static async Task IndexWin32ProgramsAsync(bool resetCache)
{
2025-04-08 08:29:03 +00:00
await _win32sLock.WaitAsync();
2025-04-08 11:27:53 +00:00
try
2025-04-01 06:21:29 +00:00
{
2025-04-08 11:27:53 +00:00
var win32S = Win32.All(_settings);
_win32s.Clear();
foreach (var win32 in win32S)
{
_win32s.Add(win32);
}
if (resetCache)
{
ResetCache();
}
2025-04-08 11:27:53 +00:00
await Context.API.SaveCacheBinaryStorageAsync<List<Win32>>(Win32CacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
lock (_lastIndexTimeLock)
{
_settings.LastIndexTime = DateTime.Now;
}
}
2025-04-08 11:27:53 +00:00
catch (Exception e)
{
2025-04-09 04:14:07 +00:00
Context.API.LogException(ClassName, "Failed to index Win32 programs", e);
2025-04-08 11:27:53 +00:00
}
finally
{
_win32sLock.Release();
2025-04-01 06:21:29 +00:00
}
}
public static async Task IndexUwpProgramsAsync(bool resetCache)
{
2025-04-08 08:29:03 +00:00
await _uwpsLock.WaitAsync();
2025-04-08 11:27:53 +00:00
try
2025-04-01 06:21:29 +00:00
{
2025-04-08 11:27:53 +00:00
var uwps = UWPPackage.All(_settings);
_uwps.Clear();
foreach (var uwp in uwps)
{
_uwps.Add(uwp);
}
if (resetCache)
{
ResetCache();
}
2025-04-08 11:27:53 +00:00
await Context.API.SaveCacheBinaryStorageAsync<List<UWPApp>>(UwpCacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
lock (_lastIndexTimeLock)
{
_settings.LastIndexTime = DateTime.Now;
}
}
2025-04-08 11:27:53 +00:00
catch (Exception e)
{
2025-04-09 04:14:07 +00:00
Context.API.LogException(ClassName, "Failed to index Uwp programs", e);
2025-04-08 11:27:53 +00:00
}
finally
{
_uwpsLock.Release();
2025-04-01 06:21:29 +00:00
}
}
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
{
await Context.API.StopwatchLogInfoAsync(ClassName, "Win32Program index cost", () => IndexWin32ProgramsAsync(resetCache: true));
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
{
await Context.API.StopwatchLogInfoAsync(ClassName, "UWPProgram index cost", () => IndexUwpProgramsAsync(resetCache: true));
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()
{
var newCache = new MemoryCache(cacheOptions);
// Atomically swap and get the previous cache instance, avoids double-dispose/lost-assignment race
// where each caller receives a distinct prior instance to dispose.
var oldCache = Interlocked.Exchange(ref cache, newCache);
// Dispose the previous instance (if any)- each caller gets a unique prior instance from above
try
{
oldCache?.Dispose();
}
catch (Exception e)
{
Context.API.LogException(ClassName, "Failed to dispose old program cache", e);
}
}
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 =>
{
_ = Task.Run(async () =>
{
try
{
var disabled = await DisableProgramAsync(program);
if (disabled)
{
ResetCache();
Context.API.ShowMsg(
Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"),
Context.API.GetTranslation(
"flowlauncher_plugin_program_disable_dlgtitle_success_message"));
}
Context.API.ReQuery();
}
catch (Exception e)
{
Context.API.LogException(ClassName, "Failed to disable program", e);
}
});
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;
}
private static async Task<bool> DisableProgramAsync(IProgram programToDelete)
{
if (_settings.DisabledProgramSources.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
{
return false;
}
2025-04-08 08:29:03 +00:00
await _uwpsLock.WaitAsync();
2025-09-15 07:40:49 +00:00
try
{
var program = _uwps.FirstOrDefault(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier);
if (program != null)
{
program.Enabled = false;
_settings.DisabledProgramSources.Add(new ProgramSource(program));
// Reindex UWP programs
_ = Task.Run(() => IndexUwpProgramsAsync(resetCache: false));
return true;
}
2025-09-15 07:40:49 +00:00
}
finally
{
2025-04-08 08:29:03 +00:00
_uwpsLock.Release();
2025-09-15 07:40:49 +00:00
}
2025-04-08 08:29:03 +00:00
await _win32sLock.WaitAsync();
2025-09-15 07:40:49 +00:00
try
{
var program = _win32s.FirstOrDefault(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier);
if (program != null)
{
program.Enabled = false;
_settings.DisabledProgramSources.Add(new ProgramSource(program));
// Reindex Win32 programs
_ = Task.Run(() => IndexWin32ProgramsAsync(resetCache: false));
return true;
}
}
2025-09-15 07:40:49 +00:00
finally
2025-04-08 08:59:27 +00:00
{
_win32sLock.Release();
}
2025-09-15 07:40:49 +00:00
return false;
}
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);
2025-07-20 04:57:56 +00:00
Context.API.ShowMsgError(title, message);
}
}
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
}
}