diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 59f34de74..50ff27f08 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -157,7 +157,7 @@ namespace Flow.Launcher.Core.Plugin } } - public static async Task> QueryForPlugin(PluginPair pair, Query query, CancellationToken token) + public static async Task> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token) { var results = new List(); try @@ -171,7 +171,7 @@ namespace Flow.Launcher.Core.Plugin token.ThrowIfCancellationRequested(); if (results == null) - return results; + return null; UpdatePluginMetadata(results, metadata, query); metadata.QueryCount += 1; @@ -184,10 +184,6 @@ namespace Flow.Launcher.Core.Plugin // null will be fine since the results will only be added into queue if the token hasn't been cancelled return null; } - catch (Exception e) - { - Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e); - } return results; } diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 2f919b5c9..a13c8f1e7 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -49,6 +49,8 @@ + + diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs index 94132b27f..f39430c65 100644 --- a/Flow.Launcher.Infrastructure/Logger/Log.cs +++ b/Flow.Launcher.Infrastructure/Logger/Log.cs @@ -5,6 +5,9 @@ using NLog; using NLog.Config; using NLog.Targets; using Flow.Launcher.Infrastructure.UserSettings; +using JetBrains.Annotations; +using NLog.Targets.Wrappers; +using System.Runtime.ExceptionServices; namespace Flow.Launcher.Infrastructure.Logger { @@ -23,11 +26,13 @@ namespace Flow.Launcher.Infrastructure.Logger } var configuration = new LoggingConfiguration(); - var target = new FileTarget(); - configuration.AddTarget("file", target); - target.FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt"; + var fileTarget = new FileTarget(); + fileTarget.FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt"; + + var fileTargetASyncWrapper = new AsyncTargetWrapper(fileTarget); + configuration.AddTarget("file", fileTargetASyncWrapper); #if DEBUG - var rule = new LoggingRule("*", LogLevel.Debug, target); + var rule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper); #else var rule = new LoggingRule("*", LogLevel.Info, target); #endif @@ -50,13 +55,12 @@ namespace Flow.Launcher.Infrastructure.Logger return valid; } - - - [MethodImpl(MethodImplOptions.Synchronized)] + public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "") - { + { + exception = exception.Demystify(); #if DEBUG - throw exception; + ExceptionDispatchInfo.Capture(exception).Throw(); #else var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName); @@ -131,8 +135,9 @@ namespace Flow.Launcher.Infrastructure.Logger [MethodImpl(MethodImplOptions.Synchronized)] public static void Exception(string message, System.Exception e) { + e = e.Demystify(); #if DEBUG - throw e; + ExceptionDispatchInfo.Capture(e).Throw(); #else if (FormatValid(message)) { diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index ee36e11d8..c7d8fa4a2 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -19,8 +19,10 @@ using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Storage; using Flow.Launcher.Infrastructure.Logger; +using Microsoft.VisualStudio.Threading; using System.Threading.Channels; using ISavable = Flow.Launcher.Plugin.ISavable; +using System.Windows.Threading; namespace Flow.Launcher.ViewModel { @@ -110,7 +112,9 @@ namespace Flow.Launcher.ViewModel } Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends"); - }; + } + + ; void continueAction(Task t) { @@ -118,7 +122,7 @@ namespace Flow.Launcher.ViewModel throw t.Exception; #else Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}"); - _resultsViewUpdateTask = + _resultsViewUpdateTask = Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted); #endif } @@ -137,7 +141,8 @@ namespace Flow.Launcher.ViewModel if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken))) { Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); - }; + } + ; } }; } @@ -237,21 +242,24 @@ namespace Flow.Launcher.ViewModel ReloadPluginDataCommand = new RelayCommand(_ => { - var msg = new Msg { Owner = Application.Current.MainWindow }; + var msg = new Msg + { + Owner = Application.Current.MainWindow + }; MainWindowVisibility = Visibility.Collapsed; PluginManager - .ReloadData() - .ContinueWith(_ => - Application.Current.Dispatcher.Invoke(() => - { - msg.Show( - InternationalizationManager.Instance.GetTranslation("success"), - InternationalizationManager.Instance.GetTranslation("completedSuccessfully"), - ""); - })) - .ConfigureAwait(false); + .ReloadData() + .ContinueWith(_ => + Application.Current.Dispatcher.Invoke(() => + { + msg.Show( + InternationalizationManager.Instance.GetTranslation("success"), + InternationalizationManager.Instance.GetTranslation("completedSuccessfully"), + ""); + })) + .ConfigureAwait(false); }); } @@ -422,7 +430,10 @@ namespace Flow.Launcher.ViewModel Title = string.Format(title, h.Query), SubTitle = string.Format(time, h.ExecutedDateTime), IcoPath = "Images\\history.png", - OriginQuery = new Query { RawQuery = h.Query }, + OriginQuery = new Query + { + RawQuery = h.Query + }, Action = _ => { SelectedResults = Results; @@ -448,7 +459,9 @@ namespace Flow.Launcher.ViewModel } } - private void QueryResults() + private readonly IReadOnlyList _emptyResult = new List(); + + private async void QueryResults() { _updateSource?.Cancel(); @@ -478,74 +491,73 @@ namespace Flow.Launcher.ViewModel var plugins = PluginManager.ValidPluginsForQuery(query); - Task.Run(async () => + if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign) + { + // Wait 45 millisecond for query change in global query + // if query changes, return so that it won't be calculated + await Task.Delay(45, currentCancellationToken); + if (currentCancellationToken.IsCancellationRequested) + return; + } + + _ = 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 (!currentCancellationToken.IsCancellationRequested && _isQueryRunning) { - if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign) - { - // Wait 45 millisecond for query change in global query - // if query changes, return so that it won't be calculated - await Task.Delay(45, currentCancellationToken); - if (currentCancellationToken.IsCancellationRequested) - return; - } + ProgressBarVisibility = Visibility.Visible; + } + }, currentCancellationToken, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default); - _ = 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 (!currentCancellationToken.IsCancellationRequested && _isQueryRunning) - { - ProgressBarVisibility = Visibility.Visible; - } - }, currentCancellationToken); + // plugins is ICollection, meaning LINQ will get the Count and preallocate Array - // plugins is ICollection, meaning LINQ will get the Count and preallocate Array + var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch + { + false => QueryTask(plugin), + true => Task.CompletedTask + }).ToArray(); - Task[] tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch - { - false => QueryTask(plugin), - true => Task.CompletedTask - }).ToArray(); - try - { - // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first - await Task.WhenAll(tasks); - } - catch (OperationCanceledException) - { - // nothing to do here - } + try + { + // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first + await Task.WhenAll(tasks); + } + catch (OperationCanceledException) + { + // nothing to do here + } - if (currentCancellationToken.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 (!currentCancellationToken.IsCancellationRequested) - { - // update to hidden if this is still the current query - ProgressBarVisibility = Visibility.Hidden; - } + // this should happen once after all queries are done so progress bar should continue + // until the end of all querying + _isQueryRunning = false; + if (!currentCancellationToken.IsCancellationRequested) + { + // update to hidden if this is still the current query + ProgressBarVisibility = Visibility.Hidden; + } - // Local function - async Task QueryTask(PluginPair plugin) - { - // Since it is wrapped within a Task.Run, the synchronous context is null - // Task.Yield will force it to run in ThreadPool - await Task.Yield(); + // Local function + async Task QueryTask(PluginPair plugin) + { + // Since it is wrapped within a Task.Run, the synchronous context is null + // Task.Yield will force it to run in ThreadPool + await Task.Yield(); - var results = await PluginManager.QueryForPlugin(plugin, query, currentCancellationToken); - if (currentCancellationToken.IsCancellationRequested || results == null) return; + IReadOnlyList results = await PluginManager.QueryForPluginAsync(plugin, query, currentCancellationToken); + + currentCancellationToken.ThrowIfCancellationRequested(); - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken))) - { - Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); - }; - } - }, currentCancellationToken) - .ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception), - TaskContinuationOptions.OnlyOnFaulted); + results ??= _emptyResult; + + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken))) + { + Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); + } + } } diff --git a/Flow.Launcher/ViewModel/ResultsForUpdate.cs b/Flow.Launcher/ViewModel/ResultsForUpdate.cs index 87d526fd6..94c6a923a 100644 --- a/Flow.Launcher/ViewModel/ResultsForUpdate.cs +++ b/Flow.Launcher/ViewModel/ResultsForUpdate.cs @@ -8,7 +8,7 @@ namespace Flow.Launcher.ViewModel { public struct ResultsForUpdate { - public List Results { get; } + public IReadOnlyList Results { get; } public PluginMetadata Metadata { get; } public string ID { get; } @@ -16,7 +16,7 @@ namespace Flow.Launcher.ViewModel public Query Query { get; } public CancellationToken Token { get; } - public ResultsForUpdate(List results, PluginMetadata metadata, Query query, CancellationToken token) + public ResultsForUpdate(IReadOnlyList results, PluginMetadata metadata, Query query, CancellationToken token) { Results = results; Metadata = metadata; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs index 0748b0cfe..cfb564924 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs @@ -17,20 +17,20 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex // Reserved keywords in oleDB private const string reservedStringPattern = @"^[`\@\#\^,\&\/\\\$\%_]+$"; - internal async static Task> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token) + internal static async Task> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token) { var results = new List(); var fileResults = new List(); try { - using var conn = new OleDbConnection(connectionString); + await using var conn = new OleDbConnection(connectionString); await conn.OpenAsync(token); token.ThrowIfCancellationRequested(); - using var command = new OleDbCommand(indexQueryString, conn); + await using var command = new OleDbCommand(indexQueryString, conn); // Results return as an OleDbDataReader. - using var dataReaderResults = await command.ExecuteReaderAsync(token) as OleDbDataReader; + await using var dataReaderResults = await command.ExecuteReaderAsync(token) as OleDbDataReader; token.ThrowIfCancellationRequested(); if (dataReaderResults.HasRows) @@ -42,18 +42,18 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex { // # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path var encodedFragmentPath = dataReaderResults - .GetString(1) - .Replace("#", "%23", StringComparison.OrdinalIgnoreCase); + .GetString(1) + .Replace("#", "%23", StringComparison.OrdinalIgnoreCase); var path = new Uri(encodedFragmentPath).LocalPath; if (dataReaderResults.GetString(2) == "Directory") { results.Add(ResultManager.CreateFolderResult( - dataReaderResults.GetString(0), - path, - path, - query, 0, true, true)); + dataReaderResults.GetString(0), + path, + path, + query, 0, true, true)); } else { @@ -63,6 +63,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex } } } + catch (OperationCanceledException) + { + // return empty result when cancelled + return results; + } catch (InvalidOperationException e) { // Internal error from ExecuteReader(): Connection closed.