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 | 85 ++++++++----------- .../Views/ProgramSetting.xaml.cs | 4 - 2 files changed, 34 insertions(+), 55 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 0258a10d2..627bf533d 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -78,53 +78,45 @@ namespace Flow.Launcher.Plugin.Program 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; + // 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 _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 result; + return resultList; } private bool HideUninstallersFilter(IProgram program) @@ -306,7 +298,6 @@ namespace Flow.Launcher.Plugin.Program { _win32s.Add(win32); } - ResetCache(); await Context.API.SaveCacheBinaryStorageAsync>(Win32CacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); _settings.LastIndexTime = DateTime.Now; } @@ -331,7 +322,6 @@ namespace Flow.Launcher.Plugin.Program { _uwps.Add(uwp); } - ResetCache(); await Context.API.SaveCacheBinaryStorageAsync>(UwpCacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); _settings.LastIndexTime = DateTime.Now; } @@ -360,13 +350,6 @@ namespace Flow.Launcher.Plugin.Program 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 860f20954..8770dd651 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 @@ namespace Flow.Launcher.Plugin.Program.Views get => _settings.EnableDescription; set { - Main.ResetCache(); _settings.EnableDescription = value; } } @@ -42,7 +41,6 @@ namespace Flow.Launcher.Plugin.Program.Views get => _settings.HideAppsPath; set { - Main.ResetCache(); _settings.HideAppsPath = value; } } @@ -52,7 +50,6 @@ namespace Flow.Launcher.Plugin.Program.Views get => _settings.HideUninstallers; set { - Main.ResetCache(); _settings.HideUninstallers = value; } } @@ -62,7 +59,6 @@ namespace Flow.Launcher.Plugin.Program.Views 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 627bf533d..1a72ea975 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -33,9 +33,6 @@ namespace Flow.Launcher.Plugin.Program 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 1a72ea975..f321e4626 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -31,6 +31,8 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program _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 @@ namespace Flow.Launcher.Plugin.Program _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 f321e4626..e8554198e 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -90,7 +90,7 @@ namespace Flow.Launcher.Plugin.Program .Select(uwp => uwp.Location.TrimEnd('\\')) // Remove trailing slash .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray() : null; - + return _win32s.Cast() .Concat(_uwps) .AsParallel() @@ -293,20 +293,24 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program { _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 @@ namespace Flow.Launcher.Plugin.Program { _uwpsLock.Release(); } + Context.API.LogDebug(ClassName, "End indexing Uwp programs"); } public static async Task IndexProgramsAsync() @@ -357,6 +367,8 @@ namespace Flow.Launcher.Plugin.Program 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 e8554198e..bb850b4de 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -35,6 +35,9 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program 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 + var resultList = await Task.Run(async () => { - // 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); + 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; - resultList = resultList.Count != 0 ? resultList : emptyResults; + 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; + + entry.SetSize(resultList.Count); + entry.SetSlidingExpiration(TimeSpan.FromHours(8)); + + return resultList; + }); + + return result; } private bool HideUninstallersFilter(IProgram program) @@ -305,7 +316,6 @@ namespace Flow.Launcher.Plugin.Program { _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 @@ namespace Flow.Launcher.Plugin.Program { _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 @@ namespace Flow.Launcher.Plugin.Program 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 8770dd651..860f20954 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 @@ namespace Flow.Launcher.Plugin.Program.Views get => _settings.EnableDescription; set { + Main.ResetCache(); _settings.EnableDescription = value; } } @@ -41,6 +42,7 @@ namespace Flow.Launcher.Plugin.Program.Views get => _settings.HideAppsPath; set { + Main.ResetCache(); _settings.HideAppsPath = value; } } @@ -50,6 +52,7 @@ namespace Flow.Launcher.Plugin.Program.Views get => _settings.HideUninstallers; set { + Main.ResetCache(); _settings.HideUninstallers = value; } } @@ -59,6 +62,7 @@ namespace Flow.Launcher.Plugin.Program.Views 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 bb850b4de..088f42817 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -80,12 +80,16 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program .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 @@ namespace Flow.Launcher.Plugin.Program } catch (OperationCanceledException) { + Context.API.LogDebug(ClassName, "Query operation was canceled"); return emptyResults; } finally @@ -120,9 +126,11 @@ namespace Flow.Launcher.Plugin.Program 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 088f42817..79f169642 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -88,6 +88,7 @@ namespace Flow.Launcher.Plugin.Program { 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 fd62566d5..8e295b960 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 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything public static async ValueTask IsEverythingRunningAsync(CancellationToken token = default) { - await _semaphore.WaitAsync(token); + await _semaphore.WaitAsync(); try { @@ -77,8 +77,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything 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 79f169642..201062aa6 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -86,10 +86,10 @@ namespace Flow.Launcher.Plugin.Program 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 8e295b960..35f4c10c6 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 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything 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 201062aa6..f6d48a531 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -80,17 +80,37 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program .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 @@ namespace Flow.Launcher.Plugin.Program } 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 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program { _win32sLock.Release(); } - Context.API.LogDebug(ClassName, "End indexing Win32 programs"); } public static async Task IndexUwpProgramsAsync() @@ -350,7 +359,6 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program { _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 f6d48a531..a2dfecff4 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -142,7 +142,7 @@ namespace Flow.Launcher.Plugin.Program entry.SetSize(resultList.Count); entry.SetSlidingExpiration(TimeSpan.FromHours(8)); - + return resultList; }); @@ -335,6 +335,7 @@ namespace Flow.Launcher.Plugin.Program { _win32s.Add(win32); } + ResetCache(); await Context.API.SaveCacheBinaryStorageAsync>(Win32CacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); lock (_lastIndexTimeLock) { @@ -364,6 +365,7 @@ namespace Flow.Launcher.Plugin.Program { _uwps.Add(uwp); } + ResetCache(); await Context.API.SaveCacheBinaryStorageAsync>(UwpCacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); lock (_lastIndexTimeLock) { @@ -393,7 +395,6 @@ namespace Flow.Launcher.Plugin.Program }); 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 35f4c10c6..28c9b49fc 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 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything 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 28c9b49fc..c8f8de77f 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 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything 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 a2dfecff4..6a8b404b5 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -84,6 +84,8 @@ namespace Flow.Launcher.Plugin.Program { 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 @@ namespace Flow.Launcher.Plugin.Program _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 @@ namespace Flow.Launcher.Plugin.Program _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 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program 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 c8f8de77f..d06fdfa98 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 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything 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 6a8b404b5..aac5de2c2 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -85,7 +85,7 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program } // 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 d06fdfa98..eccbb2bf9 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 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything 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 aac5de2c2..a0f418fe0 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -84,35 +84,31 @@ namespace Flow.Launcher.Plugin.Program { 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 eab9a8c43..568dbb4bd 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -54,6 +54,10 @@ namespace Flow.Launcher.Core.ExternalPlugins 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 eccbb2bf9..d49080280 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 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything 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 a0f418fe0..6b532d901 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -84,7 +84,7 @@ namespace Flow.Launcher.Plugin.Program { var resultList = await Task.Run(async () => { - Context.API.LogDebug(ClassName, "Preparing win32 programs"); + // Preparing win32 programs List win32s; try { @@ -97,7 +97,7 @@ namespace Flow.Launcher.Plugin.Program } _win32sLock.Release(); - Context.API.LogDebug(ClassName, "Preparing UWP programs"); + // Preparing UWP programs List uwps; try { @@ -110,7 +110,7 @@ namespace Flow.Launcher.Plugin.Program } _uwpsLock.Release(); - Context.API.LogDebug(ClassName, "Start querying programs"); + // Start querying programs try { // Collect all UWP Windows app directories @@ -325,7 +325,6 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program 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 568dbb4bd..4fed10d25 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -26,9 +26,11 @@ namespace Flow.Launcher.Core.ExternalPlugins 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 @@ namespace Flow.Launcher.Core.ExternalPlugins } 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 d49080280..a4e959dd9 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 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything 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 6b532d901..456085fca 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -86,29 +86,41 @@ namespace Flow.Launcher.Plugin.Program { // 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