From 7f5efc5ca11bd54126d52817c02040a49eeae082 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 26 Oct 2025 20:40:37 +0800 Subject: [PATCH 01/17] Refactor caching and improve QueryAsync method Removed the `ResetCache` method and its associated calls to streamline caching management and improve performance. Refactored the `QueryAsync` method for better thread safety, synchronization, and exception handling. Simplified UWP and Win32 program filtering logic and removed redundant code. Eliminated manual cache disposal and reset logic, favoring a more efficient and automated caching mechanism. These changes enhance maintainability, responsiveness, and overall plugin performance. --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 87 ++++++++----------- .../Views/ProgramSetting.xaml.cs | 4 - 2 files changed, 35 insertions(+), 56 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 0258a10d2c7..627bf533d14 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -78,53 +78,45 @@ public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, I public async Task> QueryAsync(Query query, CancellationToken token) { - var result = await cache.GetOrCreateAsync(query.Search, async entry => + var resultList = await Task.Run(async () => { - var resultList = await Task.Run(async () => + await _win32sLock.WaitAsync(token); + await _uwpsLock.WaitAsync(token); + try { - await _win32sLock.WaitAsync(token); - await _uwpsLock.WaitAsync(token); - 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() - .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; - } - finally - { - _uwpsLock.Release(); - _win32sLock.Release(); - } - }, token); - - resultList = resultList.Count != 0 ? resultList : emptyResults; - - entry.SetSize(resultList.Count); - entry.SetSlidingExpiration(TimeSpan.FromHours(8)); + // 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() + .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; + } + finally + { + _uwpsLock.Release(); + _win32sLock.Release(); + } + }, token); - return resultList; - }); + resultList = resultList.Count != 0 ? resultList : emptyResults; - return result; + return resultList; } private bool HideUninstallersFilter(IProgram program) @@ -306,7 +298,6 @@ public static async Task IndexWin32ProgramsAsync() { _win32s.Add(win32); } - ResetCache(); await Context.API.SaveCacheBinaryStorageAsync>(Win32CacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); _settings.LastIndexTime = DateTime.Now; } @@ -331,7 +322,6 @@ public static async Task IndexUwpProgramsAsync() { _uwps.Add(uwp); } - ResetCache(); await Context.API.SaveCacheBinaryStorageAsync>(UwpCacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); _settings.LastIndexTime = DateTime.Now; } @@ -360,13 +350,6 @@ public static async Task IndexProgramsAsync() await Task.WhenAll(win32Task, uwpTask).ConfigureAwait(false); } - internal static void ResetCache() - { - var oldCache = cache; - cache = new MemoryCache(cacheOptions); - oldCache.Dispose(); - } - public Control CreateSettingPanel() { return new ProgramSetting(Context, _settings); diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs index 860f20954b7..8770dd65107 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs @@ -32,7 +32,6 @@ public bool EnableDescription get => _settings.EnableDescription; set { - Main.ResetCache(); _settings.EnableDescription = value; } } @@ -42,7 +41,6 @@ public bool HideAppsPath get => _settings.HideAppsPath; set { - Main.ResetCache(); _settings.HideAppsPath = value; } } @@ -52,7 +50,6 @@ public bool HideUninstallers get => _settings.HideUninstallers; set { - Main.ResetCache(); _settings.HideUninstallers = value; } } @@ -62,7 +59,6 @@ public bool HideDuplicatedWindowsApp get => _settings.HideDuplicatedWindowsApp; set { - Main.ResetCache(); _settings.HideDuplicatedWindowsApp = value; } } From 637d926f7ada1690857e1f5cbb05797e704bea1a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 26 Oct 2025 20:41:06 +0800 Subject: [PATCH 02/17] Remove caching logic and initialize emptyResults list The caching-related code, including `cacheOptions` and `cache`, has been removed from `Main.cs`, indicating that the caching mechanism is no longer in use or has been refactored elsewhere. Additionally, the `emptyResults` list is now explicitly initialized as an empty list (`[]`). No changes were made to the `Context` property or the `commonUninstallerNames` array. --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 627bf533d14..1a72ea975dd 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -33,9 +33,6 @@ public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, I private static readonly List emptyResults = []; - private static readonly MemoryCacheOptions cacheOptions = new() { SizeLimit = 1560 }; - private static MemoryCache cache = new(cacheOptions); - private static readonly string[] commonUninstallerNames = { "uninst.exe", From 6a65f8090f108c565ef6d4151591f8723ab82d1b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 26 Oct 2025 20:51:08 +0800 Subject: [PATCH 03/17] Enhance thread safety and refactor reindexing logic Introduced `_lastIndexTimeLock` to ensure thread-safe access and updates to `_settings.LastIndexTime`, preventing race conditions. Refactored reindexing logic to use a `lock` block for evaluating and updating the reindexing condition. Added `emptyResults` as a static readonly placeholder list. Improved code clarity and maintainability without altering existing functionality. --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 1a72ea975dd..f321e46267d 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -31,6 +31,8 @@ public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, I internal static PluginInitContext Context { get; private set; } + private static readonly Lock _lastIndexTimeLock = new(); + private static readonly List emptyResults = []; private static readonly string[] commonUninstallerNames = @@ -264,7 +266,12 @@ static void MoveFile(string sourcePath, string destinationPath) var cacheEmpty = _win32sCount == 0 || _uwpsCount == 0; - if (cacheEmpty || _settings.LastIndexTime.AddHours(30) < DateTime.Now) + bool needReindex; + lock (_lastIndexTimeLock) + { + needReindex = _settings.LastIndexTime.AddHours(30) < DateTime.Now; + } + if (cacheEmpty || needReindex) { _ = Task.Run(async () => { @@ -296,8 +303,11 @@ public static async Task IndexWin32ProgramsAsync() _win32s.Add(win32); } await Context.API.SaveCacheBinaryStorageAsync>(Win32CacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); + lock (_lastIndexTimeLock) + { _settings.LastIndexTime = DateTime.Now; } + } catch (Exception e) { Context.API.LogException(ClassName, "Failed to index Win32 programs", e); @@ -320,8 +330,11 @@ public static async Task IndexUwpProgramsAsync() _uwps.Add(uwp); } await Context.API.SaveCacheBinaryStorageAsync>(UwpCacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); + lock (_lastIndexTimeLock) + { _settings.LastIndexTime = DateTime.Now; } + } catch (Exception e) { Context.API.LogException(ClassName, "Failed to index Uwp programs", e); From e2fa122362b319548f09bf289c0de1a843af9d6a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 26 Oct 2025 20:56:08 +0800 Subject: [PATCH 04/17] Improve program indexing with logging and thread safety Added detailed debug logging to `IndexWin32ProgramsAsync` and `IndexUwpProgramsAsync` to track the indexing process, including preparation, start, retrieval, caching, and completion. Replaced direct updates to `_settings.LastIndexTime` with a thread-safe lock to prevent race conditions. Enhanced `IndexProgramsAsync` with a debug log to indicate the start of indexing for better traceability. Updated program retrieval logic to process Win32 and UWP programs in parallel with cancellation support and applied the `HideUninstallersFilter` for cleaner results. --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 22 +++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index f321e46267d..e8554198e5a 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -90,7 +90,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) .Select(uwp => uwp.Location.TrimEnd('\\')) // Remove trailing slash .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray() : null; - + return _win32s.Cast() .Concat(_uwps) .AsParallel() @@ -293,20 +293,24 @@ static void WatchProgramUpdate() public static async Task IndexWin32ProgramsAsync() { + Context.API.LogDebug(ClassName, "Prepare indexing Win32 programs"); await _win32sLock.WaitAsync(); + Context.API.LogDebug(ClassName, "Start indexing Win32 programs"); try { var win32S = Win32.All(_settings); + Context.API.LogDebug(ClassName, "Get all Win32 programs"); _win32s.Clear(); foreach (var win32 in win32S) { _win32s.Add(win32); } + Context.API.LogDebug(ClassName, "Cache all Win32 programs"); await Context.API.SaveCacheBinaryStorageAsync>(Win32CacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); lock (_lastIndexTimeLock) { - _settings.LastIndexTime = DateTime.Now; - } + _settings.LastIndexTime = DateTime.Now; + } } catch (Exception e) { @@ -316,24 +320,29 @@ public static async Task IndexWin32ProgramsAsync() { _win32sLock.Release(); } + Context.API.LogDebug(ClassName, "End indexing Win32 programs"); } public static async Task IndexUwpProgramsAsync() { + Context.API.LogDebug(ClassName, "Prepare indexing Uwp programs"); await _uwpsLock.WaitAsync(); + Context.API.LogDebug(ClassName, "Start indexing Uwp programs"); try { var uwps = UWPPackage.All(_settings); + Context.API.LogDebug(ClassName, "Get all Uwp programs"); _uwps.Clear(); foreach (var uwp in uwps) { _uwps.Add(uwp); } + Context.API.LogDebug(ClassName, "Cache all Uwp programs"); await Context.API.SaveCacheBinaryStorageAsync>(UwpCacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); lock (_lastIndexTimeLock) { - _settings.LastIndexTime = DateTime.Now; - } + _settings.LastIndexTime = DateTime.Now; + } } catch (Exception e) { @@ -343,6 +352,7 @@ public static async Task IndexUwpProgramsAsync() { _uwpsLock.Release(); } + Context.API.LogDebug(ClassName, "End indexing Uwp programs"); } public static async Task IndexProgramsAsync() @@ -357,6 +367,8 @@ public static async Task IndexProgramsAsync() await Context.API.StopwatchLogInfoAsync(ClassName, "UWPProgram index cost", IndexUwpProgramsAsync); }); + Context.API.LogDebug(ClassName, "Start indexing"); + await Task.WhenAll(win32Task, uwpTask).ConfigureAwait(false); } From f632a4b773a2a5f4b229685e6139f7ac843bf08c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 26 Oct 2025 20:59:29 +0800 Subject: [PATCH 05/17] Add caching to QueryAsync and integrate cache reset logic Introduced a MemoryCache to improve QueryAsync performance by caching query results, reducing redundant computations. Added a ResetCache method to reinitialize the cache when settings are updated. Integrated cache reset calls into settings property setters to ensure consistency. Refactored query logic to leverage MemoryCache.GetOrCreateAsync for streamlined caching. Removed redundant code and debug logging for improved readability and maintainability. Ensured thread safety with proper locking mechanisms. Simplified and consolidated caching logic for better maintainability. --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 90 +++++++++++-------- .../Views/ProgramSetting.xaml.cs | 4 + 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index e8554198e5a..bb850b4de67 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -35,6 +35,9 @@ public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, I private static readonly List emptyResults = []; + private static readonly MemoryCacheOptions cacheOptions = new() { SizeLimit = 1560 }; + private static MemoryCache cache = new(cacheOptions); + private static readonly string[] commonUninstallerNames = { "uninst.exe", @@ -77,45 +80,53 @@ public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, I public async Task> QueryAsync(Query query, CancellationToken token) { - var resultList = await Task.Run(async () => + var result = await cache.GetOrCreateAsync(query.Search, async entry => { - await _win32sLock.WaitAsync(token); - await _uwpsLock.WaitAsync(token); - 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() - .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; - } - finally + var resultList = await Task.Run(async () => { - _uwpsLock.Release(); - _win32sLock.Release(); - } - }, token); + await _win32sLock.WaitAsync(token); + await _uwpsLock.WaitAsync(token); + 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() + .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; + } + finally + { + _uwpsLock.Release(); + _win32sLock.Release(); + } + }, token); + + resultList = resultList.Count != 0 ? resultList : emptyResults; - resultList = resultList.Count != 0 ? resultList : emptyResults; + entry.SetSize(resultList.Count); + entry.SetSlidingExpiration(TimeSpan.FromHours(8)); - return resultList; + return resultList; + }); + + return result; } private bool HideUninstallersFilter(IProgram program) @@ -305,7 +316,6 @@ public static async Task IndexWin32ProgramsAsync() { _win32s.Add(win32); } - Context.API.LogDebug(ClassName, "Cache all Win32 programs"); await Context.API.SaveCacheBinaryStorageAsync>(Win32CacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); lock (_lastIndexTimeLock) { @@ -337,7 +347,6 @@ public static async Task IndexUwpProgramsAsync() { _uwps.Add(uwp); } - Context.API.LogDebug(ClassName, "Cache all Uwp programs"); await Context.API.SaveCacheBinaryStorageAsync>(UwpCacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); lock (_lastIndexTimeLock) { @@ -372,6 +381,13 @@ public static async Task IndexProgramsAsync() await Task.WhenAll(win32Task, uwpTask).ConfigureAwait(false); } + internal static void ResetCache() + { + var oldCache = cache; + cache = new MemoryCache(cacheOptions); + oldCache.Dispose(); + } + public Control CreateSettingPanel() { return new ProgramSetting(Context, _settings); diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs index 8770dd65107..860f20954b7 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs @@ -32,6 +32,7 @@ public bool EnableDescription get => _settings.EnableDescription; set { + Main.ResetCache(); _settings.EnableDescription = value; } } @@ -41,6 +42,7 @@ public bool HideAppsPath get => _settings.HideAppsPath; set { + Main.ResetCache(); _settings.HideAppsPath = value; } } @@ -50,6 +52,7 @@ public bool HideUninstallers get => _settings.HideUninstallers; set { + Main.ResetCache(); _settings.HideUninstallers = value; } } @@ -59,6 +62,7 @@ public bool HideDuplicatedWindowsApp get => _settings.HideDuplicatedWindowsApp; set { + Main.ResetCache(); _settings.HideDuplicatedWindowsApp = value; } } From 4cf942ac3aaeffa3bdd0035f689b0311ac9767ba Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 26 Oct 2025 21:01:07 +0800 Subject: [PATCH 06/17] Add detailed debug logging for query execution process Enhanced logging to provide better traceability and insights: - Added debug logs for query reception, cache misses, and lock acquisition. - Logged query cancellation and completion with result counts. - Added logs for caching results, including item counts and query details. - Improved logging for filtering and program selection processes. - Ensured no functional changes to existing query and filtering logic. --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index bb850b4de67..088f42817f2 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -80,12 +80,16 @@ public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, I public async Task> QueryAsync(Query query, CancellationToken token) { + Context.API.LogDebug(ClassName, $"Query received: {query.Search}"); var result = await cache.GetOrCreateAsync(query.Search, async entry => { + Context.API.LogDebug(ClassName, $"Cache miss for query: {query.Search}"); var resultList = await Task.Run(async () => { + Context.API.LogDebug(ClassName, "Acquiring locks for querying programs"); await _win32sLock.WaitAsync(token); await _uwpsLock.WaitAsync(token); + Context.API.LogDebug(ClassName, "Locks acquired for querying programs"); try { // Collect all UWP Windows app directories @@ -95,6 +99,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) .Select(uwp => uwp.Location.TrimEnd('\\')) // Remove trailing slash .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray() : null; + Context.API.LogDebug(ClassName, "Start filtering and selecting programs"); return _win32s.Cast() .Concat(_uwps) @@ -109,6 +114,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) } catch (OperationCanceledException) { + Context.API.LogDebug(ClassName, "Query operation was canceled"); return emptyResults; } finally @@ -120,9 +126,11 @@ public async Task> QueryAsync(Query query, CancellationToken token) resultList = resultList.Count != 0 ? resultList : emptyResults; + Context.API.LogDebug(ClassName, $"Query completed with {resultList.Count} results"); entry.SetSize(resultList.Count); entry.SetSlidingExpiration(TimeSpan.FromHours(8)); - + Context.API.LogDebug(ClassName, $"Caching results for query: {query.Search} with {resultList.Count} items"); + return resultList; }); From 6e17d5d7565e0146d01811ab8b96d0a74816aaa9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 4 Nov 2025 12:27:59 +0800 Subject: [PATCH 07/17] Improve logging for Win32 program lock acquisition Added a debug log statement to indicate when the lock for querying Win32 programs is being acquired. This enhances granularity in logging, making it easier to distinguish between the acquisition of locks for Win32 and UWP programs. Improves traceability and debugging of the program's execution flow. --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 088f42817f2..79f1696429e 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -88,6 +88,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) { Context.API.LogDebug(ClassName, "Acquiring locks for querying programs"); await _win32sLock.WaitAsync(token); + Context.API.LogDebug(ClassName, "Acquiring locks for querying win32 programs"); await _uwpsLock.WaitAsync(token); Context.API.LogDebug(ClassName, "Locks acquired for querying programs"); try From db0c86d50ceeaee66c415d88922ac4c0f6a843c9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 4 Nov 2025 14:25:55 +0800 Subject: [PATCH 08/17] Remove CancellationToken from semaphore WaitAsync calls Removed the `CancellationToken` parameter from `WaitAsync` calls on semaphores in `EverythingAPI.cs` and `Main.cs`. This change eliminates cancellation support for semaphore waits, likely due to a design decision prioritizing simplicity or avoiding issues with cancellation handling. In `EverythingAPI.cs`, `WaitAsync(token)` was replaced with `WaitAsync()` in two methods. Similarly, in `Main.cs`, the `WaitAsync` calls for `_win32sLock` and `_uwpsLock` were updated to remove the `token` parameter. Note: This change may impact the ability to gracefully handle cancellation during semaphore waits. --- .../Search/Everything/EverythingAPI.cs | 5 ++--- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs index fd62566d54e..8e295b9600c 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs @@ -48,7 +48,7 @@ public static bool IsFastSortOption(EverythingSortOption sortOption) public static async ValueTask IsEverythingRunningAsync(CancellationToken token = default) { - await _semaphore.WaitAsync(token); + await _semaphore.WaitAsync(); try { @@ -77,8 +77,7 @@ public static async IAsyncEnumerable SearchAsync(EverythingSearchO if (option.MaxCount < 0) throw new ArgumentOutOfRangeException(nameof(option.MaxCount), option.MaxCount, "MaxCount must be greater than or equal to 0"); - await _semaphore.WaitAsync(token); - + await _semaphore.WaitAsync(); try { diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 79f1696429e..201062aa611 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -86,10 +86,10 @@ public async Task> QueryAsync(Query query, CancellationToken token) Context.API.LogDebug(ClassName, $"Cache miss for query: {query.Search}"); var resultList = await Task.Run(async () => { - Context.API.LogDebug(ClassName, "Acquiring locks for querying programs"); - await _win32sLock.WaitAsync(token); Context.API.LogDebug(ClassName, "Acquiring locks for querying win32 programs"); - await _uwpsLock.WaitAsync(token); + await _win32sLock.WaitAsync(); + Context.API.LogDebug(ClassName, "Acquiring locks for querying uwp programs"); + await _uwpsLock.WaitAsync(); Context.API.LogDebug(ClassName, "Locks acquired for querying programs"); try { From 3d8fd1d35295dc54c8264b1ebb262ffe8eda0bb8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 6 Nov 2025 15:28:13 +0800 Subject: [PATCH 09/17] Improve cancellation, locking, and logging mechanisms Enhanced cancellation handling by adding `token.IsCancellationRequested` checks to improve responsiveness. Refactored locking mechanisms for `_win32sLock` and `_uwpsLock` using `try-finally` blocks to ensure proper acquisition and release, improving thread safety and preventing deadlocks. Reorganized Win32 and UWP program querying logic for better modularity and readability. Replaced shared collection access with local variables to improve clarity and maintain thread safety. Simplified empty result handling by directly returning `emptyResults` when canceled. Removed redundant debug log statements to reduce verbosity and updated remaining logs for clarity. Suppressed unused result warnings by replacing direct calls to `EverythingApiDllImport.Everything_GetMajorVersion()` with null-coalescing assignments. --- .../Search/Everything/EverythingAPI.cs | 5 +- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 47 +++++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs index 8e295b9600c..35f4c10c61c 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs @@ -52,7 +52,10 @@ public static async ValueTask IsEverythingRunningAsync(CancellationToken t try { - EverythingApiDllImport.Everything_GetMajorVersion(); + if (token.IsCancellationRequested) + return false; + + _ = EverythingApiDllImport.Everything_GetMajorVersion(); var result = EverythingApiDllImport.Everything_GetLastError() != StateCode.IPCError; return result; } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 201062aa611..f6d48a531f5 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -80,17 +80,37 @@ public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, I public async Task> QueryAsync(Query query, CancellationToken token) { - Context.API.LogDebug(ClassName, $"Query received: {query.Search}"); var result = await cache.GetOrCreateAsync(query.Search, async entry => { - Context.API.LogDebug(ClassName, $"Cache miss for query: {query.Search}"); var resultList = await Task.Run(async () => { - Context.API.LogDebug(ClassName, "Acquiring locks for querying win32 programs"); + Context.API.LogDebug(ClassName, "Preparing win32 programs"); + List win32s; await _win32sLock.WaitAsync(); - Context.API.LogDebug(ClassName, "Acquiring locks for querying uwp programs"); + try + { + win32s = [.. _win32s]; + if (token.IsCancellationRequested) return emptyResults; + } + finally + { + _win32sLock.Release(); + } + + Context.API.LogDebug(ClassName, "Preparing UWP programs"); + List uwps; await _uwpsLock.WaitAsync(); - Context.API.LogDebug(ClassName, "Locks acquired for querying programs"); + try + { + uwps = [.. _uwps]; + if (token.IsCancellationRequested) return emptyResults; + } + finally + { + _uwpsLock.Release(); + } + + Context.API.LogDebug(ClassName, "Start hanlding programs"); try { // Collect all UWP Windows app directories @@ -100,10 +120,9 @@ public async Task> QueryAsync(Query query, CancellationToken token) .Select(uwp => uwp.Location.TrimEnd('\\')) // Remove trailing slash .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray() : null; - Context.API.LogDebug(ClassName, "Start filtering and selecting programs"); - return _win32s.Cast() - .Concat(_uwps) + return win32s.Cast() + .Concat(uwps) .AsParallel() .WithCancellation(token) .Where(HideUninstallersFilter) @@ -115,22 +134,14 @@ public async Task> QueryAsync(Query query, CancellationToken token) } catch (OperationCanceledException) { - Context.API.LogDebug(ClassName, "Query operation was canceled"); return emptyResults; } - finally - { - _uwpsLock.Release(); - _win32sLock.Release(); - } }, token); resultList = resultList.Count != 0 ? resultList : emptyResults; - Context.API.LogDebug(ClassName, $"Query completed with {resultList.Count} results"); entry.SetSize(resultList.Count); entry.SetSlidingExpiration(TimeSpan.FromHours(8)); - Context.API.LogDebug(ClassName, $"Caching results for query: {query.Search} with {resultList.Count} items"); return resultList; }); @@ -319,7 +330,6 @@ public static async Task IndexWin32ProgramsAsync() try { var win32S = Win32.All(_settings); - Context.API.LogDebug(ClassName, "Get all Win32 programs"); _win32s.Clear(); foreach (var win32 in win32S) { @@ -339,7 +349,6 @@ public static async Task IndexWin32ProgramsAsync() { _win32sLock.Release(); } - Context.API.LogDebug(ClassName, "End indexing Win32 programs"); } public static async Task IndexUwpProgramsAsync() @@ -350,7 +359,6 @@ public static async Task IndexUwpProgramsAsync() try { var uwps = UWPPackage.All(_settings); - Context.API.LogDebug(ClassName, "Get all Uwp programs"); _uwps.Clear(); foreach (var uwp in uwps) { @@ -370,7 +378,6 @@ public static async Task IndexUwpProgramsAsync() { _uwpsLock.Release(); } - Context.API.LogDebug(ClassName, "End indexing Uwp programs"); } public static async Task IndexProgramsAsync() From 7e332fa615a790f5d45abd5f6edfb2ea92e501fa Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 6 Nov 2025 15:29:38 +0800 Subject: [PATCH 10/17] Refactor caching and indexing logic Added `ResetCache` calls after clearing `_win32s` and `_uwps` lists to ensure proper cache reset during indexing. Updated logic to return `resultList` after setting cache size and expiration for improved clarity. Removed `await Task.WhenAll` to adjust asynchronous flow in the indexing process. --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index f6d48a531f5..a2dfecff44e 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -142,7 +142,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) entry.SetSize(resultList.Count); entry.SetSlidingExpiration(TimeSpan.FromHours(8)); - + return resultList; }); @@ -335,6 +335,7 @@ public static async Task IndexWin32ProgramsAsync() { _win32s.Add(win32); } + ResetCache(); await Context.API.SaveCacheBinaryStorageAsync>(Win32CacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); lock (_lastIndexTimeLock) { @@ -364,6 +365,7 @@ public static async Task IndexUwpProgramsAsync() { _uwps.Add(uwp); } + ResetCache(); await Context.API.SaveCacheBinaryStorageAsync>(UwpCacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); lock (_lastIndexTimeLock) { @@ -393,7 +395,6 @@ public static async Task IndexProgramsAsync() }); Context.API.LogDebug(ClassName, "Start indexing"); - await Task.WhenAll(win32Task, uwpTask).ConfigureAwait(false); } From 30d7f67d426e4d5248034efad5b568f7e4d7a804 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 6 Nov 2025 15:30:32 +0800 Subject: [PATCH 11/17] Refactor token cancellation check for readability Simplified the `if` statement that checks for token cancellation by condensing it into a single line. This improves code readability and eliminates unnecessary line breaks. --- .../Search/Everything/EverythingAPI.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs index 35f4c10c61c..28c9b49fc03 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs @@ -52,9 +52,7 @@ public static async ValueTask IsEverythingRunningAsync(CancellationToken t try { - if (token.IsCancellationRequested) - return false; - + if (token.IsCancellationRequested) return false; _ = EverythingApiDllImport.Everything_GetMajorVersion(); var result = EverythingApiDllImport.Everything_GetLastError() != StateCode.IPCError; return result; From bfaff5cca5eeb71254ca51b463a1cd0b876e788c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 6 Nov 2025 15:32:08 +0800 Subject: [PATCH 12/17] Improve semaphore usage and logging clarity Added comments in `EverythingAPI.cs` and `Main.cs` to explain why `CancellationToken` is not directly passed to semaphore locks, preventing unexpected `OperationCanceledException`. Updated debug log messages in `Main.cs` for better clarity, including changing "Start handling programs" to "Start querying programs". Removed redundant log messages to improve logging consistency. --- .../Search/Everything/EverythingAPI.cs | 2 ++ Plugins/Flow.Launcher.Plugin.Program/Main.cs | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs index 28c9b49fc03..c8f8de77f2b 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs @@ -48,6 +48,8 @@ public static bool IsFastSortOption(EverythingSortOption sortOption) public static async ValueTask IsEverythingRunningAsync(CancellationToken token = default) { + // We do not directly pass token here, but we check IsCancellationRequested inside the lock + // So that it will not raise OperationCanceledException, which is not expected by the caller.` await _semaphore.WaitAsync(); try diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index a2dfecff44e..6a8b404b5e2 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -84,6 +84,8 @@ public async Task> QueryAsync(Query query, CancellationToken token) { var resultList = await Task.Run(async () => { + // We do not directly pass token here, but we check IsCancellationRequested inside the lock + // So that it will not raise OperationCanceledException, which is not expected by the caller.` Context.API.LogDebug(ClassName, "Preparing win32 programs"); List win32s; await _win32sLock.WaitAsync(); @@ -97,6 +99,8 @@ public async Task> QueryAsync(Query query, CancellationToken token) _win32sLock.Release(); } + // We do not directly pass token here, but we check IsCancellationRequested inside the lock + // So that it will not raise OperationCanceledException, which is not expected by the caller.` Context.API.LogDebug(ClassName, "Preparing UWP programs"); List uwps; await _uwpsLock.WaitAsync(); @@ -110,7 +114,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) _uwpsLock.Release(); } - Context.API.LogDebug(ClassName, "Start hanlding programs"); + Context.API.LogDebug(ClassName, "Start querying programs"); try { // Collect all UWP Windows app directories @@ -324,7 +328,6 @@ static void WatchProgramUpdate() public static async Task IndexWin32ProgramsAsync() { - Context.API.LogDebug(ClassName, "Prepare indexing Win32 programs"); await _win32sLock.WaitAsync(); Context.API.LogDebug(ClassName, "Start indexing Win32 programs"); try @@ -354,7 +357,6 @@ public static async Task IndexWin32ProgramsAsync() public static async Task IndexUwpProgramsAsync() { - Context.API.LogDebug(ClassName, "Prepare indexing Uwp programs"); await _uwpsLock.WaitAsync(); Context.API.LogDebug(ClassName, "Start indexing Uwp programs"); try From d0a47c84b9b5641330af13808cac387bfa458b2e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 6 Nov 2025 15:39:19 +0800 Subject: [PATCH 13/17] Clarify CancellationToken handling in comments Updated comments to explain the rationale for not directly passing CancellationToken to methods and instead checking IsCancellationRequested within locks. This prevents unexpected OperationCanceledException. Changes made in EverythingAPI.cs (IsEverythingRunningAsync) and Main.cs (Win32 and UWP program preparation). No functional changes to the code. --- .../Search/Everything/EverythingAPI.cs | 2 +- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs index c8f8de77f2b..d06fdfa9847 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs @@ -49,7 +49,7 @@ public static bool IsFastSortOption(EverythingSortOption sortOption) public static async ValueTask IsEverythingRunningAsync(CancellationToken token = default) { // We do not directly pass token here, but we check IsCancellationRequested inside the lock - // So that it will not raise OperationCanceledException, which is not expected by the caller.` + // So that it will not raise OperationCanceledException, which is not expected by the caller. await _semaphore.WaitAsync(); try diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 6a8b404b5e2..aac5de2c214 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -85,7 +85,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) var resultList = await Task.Run(async () => { // We do not directly pass token here, but we check IsCancellationRequested inside the lock - // So that it will not raise OperationCanceledException, which is not expected by the caller.` + // So that it will not raise OperationCanceledException, which is not expected by the caller. Context.API.LogDebug(ClassName, "Preparing win32 programs"); List win32s; await _win32sLock.WaitAsync(); @@ -100,7 +100,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) } // We do not directly pass token here, but we check IsCancellationRequested inside the lock - // So that it will not raise OperationCanceledException, which is not expected by the caller.` + // So that it will not raise OperationCanceledException, which is not expected by the caller. Context.API.LogDebug(ClassName, "Preparing UWP programs"); List uwps; await _uwpsLock.WaitAsync(); From 49f89e33b5127a434bd903265cf7cc977624d8ae Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 6 Nov 2025 20:46:29 +0800 Subject: [PATCH 14/17] Make locking operations cancelable with tokens Updated `_semaphore.WaitAsync` in `EverythingAPI.cs` to accept a `CancellationToken` and handle `OperationCanceledException` gracefully, returning `false` instead of propagating the exception. Refactored locking mechanisms in `Main.cs` to use `CancellationToken` for `_win32sLock` and `_uwpsLock`. Added `try-catch` blocks to handle `OperationCanceledException` and ensure proper lock release. Methods now return `emptyResults` when operations are canceled. --- .../Search/Everything/EverythingAPI.cs | 12 +++++++---- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 20 ++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs index d06fdfa9847..eccbb2bf9ce 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs @@ -48,13 +48,17 @@ public static bool IsFastSortOption(EverythingSortOption sortOption) public static async ValueTask IsEverythingRunningAsync(CancellationToken token = default) { - // We do not directly pass token here, but we check IsCancellationRequested inside the lock - // So that it will not raise OperationCanceledException, which is not expected by the caller. - await _semaphore.WaitAsync(); + try + { + await _semaphore.WaitAsync(token); + } + catch (OperationCanceledException) + { + return false; + } try { - if (token.IsCancellationRequested) return false; _ = EverythingApiDllImport.Everything_GetMajorVersion(); var result = EverythingApiDllImport.Everything_GetLastError() != StateCode.IPCError; return result; diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index aac5de2c214..a0f418fe0d6 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -84,35 +84,31 @@ public async Task> QueryAsync(Query query, CancellationToken token) { var resultList = await Task.Run(async () => { - // We do not directly pass token here, but we check IsCancellationRequested inside the lock - // So that it will not raise OperationCanceledException, which is not expected by the caller. Context.API.LogDebug(ClassName, "Preparing win32 programs"); List win32s; - await _win32sLock.WaitAsync(); try { + await _win32sLock.WaitAsync(token); win32s = [.. _win32s]; - if (token.IsCancellationRequested) return emptyResults; } - finally + catch (OperationCanceledException) { - _win32sLock.Release(); + return emptyResults; } + _win32sLock.Release(); - // We do not directly pass token here, but we check IsCancellationRequested inside the lock - // So that it will not raise OperationCanceledException, which is not expected by the caller. Context.API.LogDebug(ClassName, "Preparing UWP programs"); List uwps; - await _uwpsLock.WaitAsync(); try { + await _uwpsLock.WaitAsync(token); uwps = [.. _uwps]; - if (token.IsCancellationRequested) return emptyResults; } - finally + catch (OperationCanceledException) { - _uwpsLock.Release(); + return emptyResults; } + _uwpsLock.Release(); Context.API.LogDebug(ClassName, "Start querying programs"); try From 88fd1e56d030a3de62eac30be9f805db4885c660 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 6 Nov 2025 20:47:54 +0800 Subject: [PATCH 15/17] Handle OperationCanceledException gracefully Added a `catch` block for `OperationCanceledException` in `PluginsManifest.cs` to ignore canceled operations. Updated `EverythingAPI.cs` to use cancellation tokens with `_semaphore.WaitAsync` and handle cancellations by exiting the method cleanly with `yield break`. --- Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs | 4 ++++ .../Search/Everything/EverythingAPI.cs | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index eab9a8c43ca..568dbb4bd4a 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -54,6 +54,10 @@ public static async Task UpdateManifestAsync(bool usePrimaryUrlOnly = fals return true; } } + catch (OperationCanceledException) + { + // Ignored + } catch (Exception e) { PublicApi.Instance.LogException(ClassName, "Http request failed", e); diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs index eccbb2bf9ce..d4908028085 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs @@ -84,7 +84,14 @@ public static async IAsyncEnumerable SearchAsync(EverythingSearchO if (option.MaxCount < 0) throw new ArgumentOutOfRangeException(nameof(option.MaxCount), option.MaxCount, "MaxCount must be greater than or equal to 0"); - await _semaphore.WaitAsync(); + try + { + await _semaphore.WaitAsync(token); + } + catch (OperationCanceledException) + { + yield break; + } try { From 05c8dd2fe11755b9982ad5305824aa710ad9e441 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 6 Nov 2025 20:48:41 +0800 Subject: [PATCH 16/17] Remove unnecessary debug information --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index a0f418fe0d6..6b532d9019e 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -84,7 +84,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) { var resultList = await Task.Run(async () => { - Context.API.LogDebug(ClassName, "Preparing win32 programs"); + // Preparing win32 programs List win32s; try { @@ -97,7 +97,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) } _win32sLock.Release(); - Context.API.LogDebug(ClassName, "Preparing UWP programs"); + // Preparing UWP programs List uwps; try { @@ -110,7 +110,7 @@ public async Task> QueryAsync(Query query, CancellationToken token) } _uwpsLock.Release(); - Context.API.LogDebug(ClassName, "Start querying programs"); + // Start querying programs try { // Collect all UWP Windows app directories @@ -325,7 +325,6 @@ static void WatchProgramUpdate() public static async Task IndexWin32ProgramsAsync() { await _win32sLock.WaitAsync(); - Context.API.LogDebug(ClassName, "Start indexing Win32 programs"); try { var win32S = Win32.All(_settings); @@ -354,7 +353,6 @@ public static async Task IndexWin32ProgramsAsync() public static async Task IndexUwpProgramsAsync() { await _uwpsLock.WaitAsync(); - Context.API.LogDebug(ClassName, "Start indexing Uwp programs"); try { var uwps = UWPPackage.All(_settings); @@ -392,7 +390,6 @@ public static async Task IndexProgramsAsync() await Context.API.StopwatchLogInfoAsync(ClassName, "UWPProgram index cost", IndexUwpProgramsAsync); }); - Context.API.LogDebug(ClassName, "Start indexing"); await Task.WhenAll(win32Task, uwpTask).ConfigureAwait(false); } From 2adbc334a2d20ff5f68808484725a02a6c2662f6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 7 Nov 2025 15:30:07 +0800 Subject: [PATCH 17/17] Improve semaphore lock handling and code robustness Added `lockAcquired` flags in `PluginsManifest.cs` and `Main.cs` to ensure semaphore locks are only released if successfully acquired, preventing potential runtime errors. Updated `finally` blocks to conditionally release locks based on these flags. Removed redundant cancellation check in `EverythingAPI.cs` to simplify code, assuming cancellation is handled elsewhere. These changes enhance reliability and maintainability of the codebase. --- .../ExternalPlugins/PluginsManifest.cs | 5 ++++- .../Search/Everything/EverythingAPI.cs | 2 -- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 16 ++++++++++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index 568dbb4bd4a..4fed10d25ff 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -26,9 +26,11 @@ public static class PluginsManifest public static async Task UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) { + bool lockAcquired = false; try { await manifestUpdateLock.WaitAsync(token).ConfigureAwait(false); + lockAcquired = true; if (UserPlugins == null || usePrimaryUrlOnly || DateTime.Now.Subtract(lastFetchedAt) >= fetchTimeout) { @@ -64,7 +66,8 @@ public static async Task UpdateManifestAsync(bool usePrimaryUrlOnly = fals } finally { - manifestUpdateLock.Release(); + // Only release the lock if it was acquired + if (lockAcquired) manifestUpdateLock.Release(); } return false; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs index d4908028085..a4e959dd9c4 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs @@ -133,8 +133,6 @@ public static async IAsyncEnumerable SearchAsync(EverythingSearchO EverythingApiDllImport.Everything_SetRequestFlags(EVERYTHING_REQUEST_FULL_PATH_AND_FILE_NAME); } - - if (token.IsCancellationRequested) yield break; if (!EverythingApiDllImport.Everything_QueryW(true)) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 6b532d9019e..456085fcaf2 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -86,29 +86,41 @@ public async Task> QueryAsync(Query query, CancellationToken token) { // Preparing win32 programs List win32s; + bool win32LockAcquired = false; try { await _win32sLock.WaitAsync(token); + win32LockAcquired = true; win32s = [.. _win32s]; } catch (OperationCanceledException) { return emptyResults; } - _win32sLock.Release(); + finally + { + // Only release the lock if it was acquired + if (win32LockAcquired) _win32sLock.Release(); + } // Preparing UWP programs List uwps; + bool uwpsLockAcquired = false; try { await _uwpsLock.WaitAsync(token); + uwpsLockAcquired = true; uwps = [.. _uwps]; } catch (OperationCanceledException) { return emptyResults; } - _uwpsLock.Release(); + finally + { + // Only release the lock if it was acquired + if (uwpsLockAcquired) _uwpsLock.Release(); + } // Start querying programs try