From b1a48e296a9d3ee22b09b98c894e35dc4a2a3bc4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 13:58:28 +0800 Subject: [PATCH 1/8] Improve code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 6c4236db9d1..8ec29c216ff 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -35,7 +35,7 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private Query _lastQuery; private bool _lastIsHomeQuery; private string _queryTextBeforeLeaveResults; - private string _ignoredQueryText = null; + private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results private readonly FlowLauncherJsonStorage _historyItemsStorage; private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; @@ -67,6 +67,7 @@ public MainViewModel() _queryTextBeforeLeaveResults = ""; _queryText = ""; _lastQuery = new Query(); + _ignoredQueryText = null; // null as invalid value Settings = Ioc.Default.GetRequiredService(); Settings.PropertyChanged += (_, args) => From 639a5aebe5a5a5feaf6f1fa10c4845d56218367d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 14:02:18 +0800 Subject: [PATCH 2/8] Dispose _updateSource when creating new one & Use _updateToken instead of _updateSource.Token --- Flow.Launcher/ViewModel/MainViewModel.cs | 30 ++++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 8ec29c216ff..175f4ff84bb 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -46,6 +46,7 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private readonly TopMostRecord _topMostRecord; private CancellationTokenSource _updateSource; // Used to cancel old query flows + private CancellationToken _updateToken; // Used to avoid ObjectDisposedException of _updateSource.Token private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; @@ -68,6 +69,8 @@ public MainViewModel() _queryText = ""; _lastQuery = new Query(); _ignoredQueryText = null; // null as invalid value + _updateSource = new CancellationTokenSource(); + _updateToken = _updateSource.Token; Settings = Ioc.Default.GetRequiredService(); Settings.PropertyChanged += (_, args) => @@ -249,7 +252,7 @@ public void RegisterResultsUpdatedEvent() return; } - var token = e.Token == default ? _updateSource.Token : e.Token; + var token = e.Token == default ? _updateToken : e.Token; // make a clone to avoid possible issue that plugin will also change the list and items when updating view model var resultsCopy = DeepCloneResults(e.Results, token); @@ -1265,7 +1268,9 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var isHomeQuery = query.RawQuery == string.Empty; + _updateSource?.Dispose(); // Dispose old update source to fix possible cancellation issue _updateSource = new CancellationTokenSource(); + _updateToken = _updateSource.Token; ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; @@ -1273,7 +1278,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b // Switch to ThreadPool thread await TaskScheduler.Default; - if (_updateSource.Token.IsCancellationRequested) return; + if (_updateToken.IsCancellationRequested) return; // Update the query's IsReQuery property to true if this is a re-query query.IsReQuery = isReQuery; @@ -1322,12 +1327,11 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { // Wait 15 millisecond for query change in global query // if query changes, return so that it won't be calculated - await Task.Delay(15, _updateSource.Token); - if (_updateSource.Token.IsCancellationRequested) - return; + await Task.Delay(15, _updateToken); + if (_updateToken.IsCancellationRequested) return; }*/ - _ = Task.Delay(200, _updateSource.Token).ContinueWith(_ => + _ = Task.Delay(200, _updateToken).ContinueWith(_ => { // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet if (_isQueryRunning) @@ -1335,7 +1339,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b ProgressBarVisibility = Visibility.Visible; } }, - _updateSource.Token, + _updateToken, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default); @@ -1346,7 +1350,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { tasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch { - false => QueryTaskAsync(plugin, _updateSource.Token), + false => QueryTaskAsync(plugin, _updateToken), true => Task.CompletedTask }).ToArray(); @@ -1360,7 +1364,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch { - false => QueryTaskAsync(plugin, _updateSource.Token), + false => QueryTaskAsync(plugin, _updateToken), true => Task.CompletedTask }).ToArray(); } @@ -1375,13 +1379,13 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b // nothing to do here } - if (_updateSource.Token.IsCancellationRequested) return; + if (_updateToken.IsCancellationRequested) return; // this should happen once after all queries are done so progress bar should continue // until the end of all querying _isQueryRunning = false; - if (!_updateSource.Token.IsCancellationRequested) + if (!_updateToken.IsCancellationRequested) { // update to hidden if this is still the current query ProgressBarVisibility = Visibility.Hidden; @@ -1448,12 +1452,12 @@ void QueryHistoryTask() var results = GetHistoryItems(historyItems); - if (_updateSource.Token.IsCancellationRequested) return; + if (_updateToken.IsCancellationRequested) return; App.API.LogDebug(ClassName, $"Update results for history"); if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, _historyMetadata, query, - _updateSource.Token))) + _updateToken))) { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } From 265fd9c868e881da4f61060bb40385268c8d6420 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 14:13:14 +0800 Subject: [PATCH 3/8] Add update source lock --- Flow.Launcher/ViewModel/MainViewModel.cs | 27 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 175f4ff84bb..afef8f64eef 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -47,6 +47,7 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private CancellationTokenSource _updateSource; // Used to cancel old query flows private CancellationToken _updateToken; // Used to avoid ObjectDisposedException of _updateSource.Token + private readonly object _updateSourceLock = new(); private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; @@ -69,8 +70,11 @@ public MainViewModel() _queryText = ""; _lastQuery = new Query(); _ignoredQueryText = null; // null as invalid value - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; + lock (_updateSourceLock) + { + _updateSource = new CancellationTokenSource(); + _updateToken = _updateSource.Token; + } Settings = Ioc.Default.GetRequiredService(); Settings.PropertyChanged += (_, args) => @@ -1240,7 +1244,10 @@ private static List GetHistoryItems(IEnumerable historyItem private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) { - _updateSource?.Cancel(); + lock (_updateSourceLock) + { + _updateSource.Cancel(); + } App.API.LogDebug(ClassName, $"Start query with text: <{QueryText}>"); @@ -1268,9 +1275,12 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var isHomeQuery = query.RawQuery == string.Empty; - _updateSource?.Dispose(); // Dispose old update source to fix possible cancellation issue - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; + lock (_updateSourceLock) + { + _updateSource.Dispose(); // Dispose old update source to fix possible cancellation issue + _updateSource = new CancellationTokenSource(); + _updateToken = _updateSource.Token; + } ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; @@ -1888,7 +1898,10 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - _updateSource?.Dispose(); + lock (_updateSourceLock) + { + _updateSource?.Dispose(); + } _resultsUpdateChannelWriter?.Complete(); if (_resultsViewUpdateTask?.IsCompleted == true) { From b156afed0bdac5cbe19749fcad2687d4bf2b9e20 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 14:14:10 +0800 Subject: [PATCH 4/8] Revert "Add update source lock" This reverts commit 265fd9c868e881da4f61060bb40385268c8d6420. --- Flow.Launcher/ViewModel/MainViewModel.cs | 27 ++++++------------------ 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index afef8f64eef..175f4ff84bb 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -47,7 +47,6 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private CancellationTokenSource _updateSource; // Used to cancel old query flows private CancellationToken _updateToken; // Used to avoid ObjectDisposedException of _updateSource.Token - private readonly object _updateSourceLock = new(); private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; @@ -70,11 +69,8 @@ public MainViewModel() _queryText = ""; _lastQuery = new Query(); _ignoredQueryText = null; // null as invalid value - lock (_updateSourceLock) - { - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; - } + _updateSource = new CancellationTokenSource(); + _updateToken = _updateSource.Token; Settings = Ioc.Default.GetRequiredService(); Settings.PropertyChanged += (_, args) => @@ -1244,10 +1240,7 @@ private static List GetHistoryItems(IEnumerable historyItem private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) { - lock (_updateSourceLock) - { - _updateSource.Cancel(); - } + _updateSource?.Cancel(); App.API.LogDebug(ClassName, $"Start query with text: <{QueryText}>"); @@ -1275,12 +1268,9 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var isHomeQuery = query.RawQuery == string.Empty; - lock (_updateSourceLock) - { - _updateSource.Dispose(); // Dispose old update source to fix possible cancellation issue - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; - } + _updateSource?.Dispose(); // Dispose old update source to fix possible cancellation issue + _updateSource = new CancellationTokenSource(); + _updateToken = _updateSource.Token; ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; @@ -1898,10 +1888,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - lock (_updateSourceLock) - { - _updateSource?.Dispose(); - } + _updateSource?.Dispose(); _resultsUpdateChannelWriter?.Complete(); if (_resultsViewUpdateTask?.IsCompleted == true) { From 2672512a62503a76d72777c3716cfe48dc1465b7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 14:14:50 +0800 Subject: [PATCH 5/8] Dispose the old CancellationTokenSource atomically to avoid races --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 175f4ff84bb..383256dcc4a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1268,9 +1268,9 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var isHomeQuery = query.RawQuery == string.Empty; - _updateSource?.Dispose(); // Dispose old update source to fix possible cancellation issue - _updateSource = new CancellationTokenSource(); + var oldSource = Interlocked.Exchange(ref _updateSource, new CancellationTokenSource()); _updateToken = _updateSource.Token; + oldSource?.Dispose(); // Dispose old update source to fix possible cancellation issue ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; From 788cb3cc16cc639ffbe1d55acb3ab20f4f900396 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 19:17:33 +0800 Subject: [PATCH 6/8] Revert "Dispose the old CancellationTokenSource atomically to avoid races" This reverts commit 2672512a62503a76d72777c3716cfe48dc1465b7. --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 383256dcc4a..175f4ff84bb 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1268,9 +1268,9 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var isHomeQuery = query.RawQuery == string.Empty; - var oldSource = Interlocked.Exchange(ref _updateSource, new CancellationTokenSource()); + _updateSource?.Dispose(); // Dispose old update source to fix possible cancellation issue + _updateSource = new CancellationTokenSource(); _updateToken = _updateSource.Token; - oldSource?.Dispose(); // Dispose old update source to fix possible cancellation issue ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; From deb0c2139f6cbf6f6efd99ba640f65d76596edcd Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 19:22:37 +0800 Subject: [PATCH 7/8] Remove unused initialization value --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 175f4ff84bb..aab2e2d0300 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -69,8 +69,6 @@ public MainViewModel() _queryText = ""; _lastQuery = new Query(); _ignoredQueryText = null; // null as invalid value - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; Settings = Ioc.Default.GetRequiredService(); Settings.PropertyChanged += (_, args) => From 297643c3e52d9800154390e40a5518e64cb86d9e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 19:35:40 +0800 Subject: [PATCH 8/8] Revert all changes as master branch --- Flow.Launcher/ViewModel/MainViewModel.cs | 35 +++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index aab2e2d0300..c0b74dc6870 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1266,9 +1266,12 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b var isHomeQuery = query.RawQuery == string.Empty; - _updateSource?.Dispose(); // Dispose old update source to fix possible cancellation issue - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; + _updateSource?.Dispose(); + + var currentUpdateSource = new CancellationTokenSource(); + _updateSource = currentUpdateSource; + var currentCancellationToken = _updateSource.Token; + _updateToken = currentCancellationToken; ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; @@ -1276,7 +1279,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b // Switch to ThreadPool thread await TaskScheduler.Default; - if (_updateToken.IsCancellationRequested) return; + if (currentCancellationToken.IsCancellationRequested) return; // Update the query's IsReQuery property to true if this is a re-query query.IsReQuery = isReQuery; @@ -1325,11 +1328,11 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { // Wait 15 millisecond for query change in global query // if query changes, return so that it won't be calculated - await Task.Delay(15, _updateToken); - if (_updateToken.IsCancellationRequested) return; + await Task.Delay(15, currentCancellationToken); + if (currentCancellationToken.IsCancellationRequested) return; }*/ - _ = Task.Delay(200, _updateToken).ContinueWith(_ => + _ = Task.Delay(200, currentCancellationToken).ContinueWith(_ => { // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet if (_isQueryRunning) @@ -1337,7 +1340,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b ProgressBarVisibility = Visibility.Visible; } }, - _updateToken, + currentCancellationToken, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default); @@ -1348,21 +1351,21 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b { tasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch { - false => QueryTaskAsync(plugin, _updateToken), + false => QueryTaskAsync(plugin, currentCancellationToken), true => Task.CompletedTask }).ToArray(); // Query history results for home page firstly so it will be put on top of the results if (Settings.ShowHistoryResultsForHomePage) { - QueryHistoryTask(); + QueryHistoryTask(currentCancellationToken); } } else { tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch { - false => QueryTaskAsync(plugin, _updateToken), + false => QueryTaskAsync(plugin, currentCancellationToken), true => Task.CompletedTask }).ToArray(); } @@ -1377,13 +1380,13 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b // nothing to do here } - if (_updateToken.IsCancellationRequested) return; + if (currentCancellationToken.IsCancellationRequested) return; // this should happen once after all queries are done so progress bar should continue // until the end of all querying _isQueryRunning = false; - if (!_updateToken.IsCancellationRequested) + if (!currentCancellationToken.IsCancellationRequested) { // update to hidden if this is still the current query ProgressBarVisibility = Visibility.Hidden; @@ -1443,19 +1446,19 @@ await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : } } - void QueryHistoryTask() + void QueryHistoryTask(CancellationToken token) { // Select last history results and revert its order to make sure last history results are on top var historyItems = _history.Items.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); var results = GetHistoryItems(historyItems); - if (_updateToken.IsCancellationRequested) return; + if (token.IsCancellationRequested) return; App.API.LogDebug(ClassName, $"Update results for history"); if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, _historyMetadata, query, - _updateToken))) + token))) { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); }