From 3cd609377e7402669c8dbbf67140bb58551d8ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=BC=98=E9=9F=AC?= Date: Sat, 2 Jan 2021 15:52:41 +0800 Subject: [PATCH 01/34] Plugin Async ModelAdd Full Async model, including AsyncPlugin and AsyncReloadable --- Flow.Launcher.Core/Plugin/PluginManager.cs | 150 ++++++++++++------ Flow.Launcher.Infrastructure/Stopwatch.cs | 32 +++- Flow.Launcher.Plugin/IAsyncPlugin.cs | 12 ++ Flow.Launcher.Plugin/IPlugin.cs | 1 + Flow.Launcher.Plugin/IPublicAPI.cs | 3 +- .../Interfaces/IAsyncReloadable.cs | 9 ++ Flow.Launcher.Plugin/PluginPair.cs | 2 +- Flow.Launcher/PublicAPIInstance.cs | 4 +- Flow.Launcher/ViewModel/MainViewModel.cs | 38 ++--- .../ControlPanelList.cs | 2 +- Plugins/Flow.Launcher.Plugin.Sys/Main.cs | 41 +++-- 11 files changed, 210 insertions(+), 84 deletions(-) create mode 100644 Flow.Launcher.Plugin/IAsyncPlugin.cs create mode 100644 Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 3b697a1ee6c..239f0499d5a 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; @@ -32,7 +33,7 @@ public static class PluginManager /// /// Directories that will hold Flow Launcher plugin directory /// - private static readonly string[] Directories = { Constant.PreinstalledDirectory, DataLocation.PluginsDirectory }; + private static readonly string[] Directories = {Constant.PreinstalledDirectory, DataLocation.PluginsDirectory}; private static void DeletePythonBinding() { @@ -52,12 +53,19 @@ public static void Save() } } - public static void ReloadData() + public static async Task ReloadData() { foreach(var plugin in AllPlugins) { - var reloadablePlugin = plugin.Plugin as IReloadable; - reloadablePlugin?.ReloadData(); + switch (plugin.Plugin) + { + case IReloadable p: + p.ReloadData(); + break; + case IAsyncReloadable p: + await p.ReloadDataAsync(); + break; + } } } @@ -86,24 +94,50 @@ public static void LoadPlugins(PluginsSettings settings) /// Call initialize for all plugins /// /// return the list of failed to init plugins or null for none - public static void InitializePlugins(IPublicAPI api) + public static async Task InitializePlugins(IPublicAPI api) { API = api; var failedPlugins = new ConcurrentQueue(); - Parallel.ForEach(AllPlugins, pair => + + var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate { try { - var milliseconds = Stopwatch.Debug($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", () => + long milliseconds; + + switch (pair.Plugin) { - pair.Plugin.Init(new PluginInitContext - { - CurrentPluginMetadata = pair.Metadata, - API = API - }); - }); + case IAsyncPlugin plugin: + milliseconds = await Stopwatch.DebugAsync( + $"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", + async delegate + { + await plugin.InitAsync(new PluginInitContext + { + CurrentPluginMetadata = pair.Metadata, + API = API + }); + }); + break; + case IPlugin plugin: + milliseconds = Stopwatch.Debug( + $"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", + () => + { + plugin.Init(new PluginInitContext + { + CurrentPluginMetadata = pair.Metadata, + API = API + }); + }); + break; + default: + throw new ArgumentException(); + } + pair.Metadata.InitTime += milliseconds; - Log.Info($"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); + Log.Info( + $"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); } catch (Exception e) { @@ -111,25 +145,33 @@ public static void InitializePlugins(IPublicAPI api) pair.Metadata.Disabled = true; failedPlugins.Enqueue(pair); } - }); + })); + + await Task.WhenAll(InitTasks); _contextMenuPlugins = GetPluginsForInterface(); foreach (var plugin in AllPlugins) { - if (IsGlobalPlugin(plugin.Metadata)) - GlobalPlugins.Add(plugin); - - // Plugins may have multiple ActionKeywords, eg. WebSearch - plugin.Metadata.ActionKeywords - .Where(x => x != Query.GlobalPluginWildcardSign) - .ToList() - .ForEach(x => NonGlobalPlugins[x] = plugin); + foreach (var actionKeyword in plugin.Metadata.ActionKeywords) + { + switch (actionKeyword) + { + case Query.GlobalPluginWildcardSign: + GlobalPlugins.Add(plugin); + break; + default: + NonGlobalPlugins[actionKeyword] = plugin; + break; + } + } } if (failedPlugins.Any()) { var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); - API.ShowMsg($"Fail to Init Plugins", $"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help", "", false); + API.ShowMsg($"Fail to Init Plugins", + $"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help", + "", false); } } @@ -138,7 +180,7 @@ public static List ValidPluginsForQuery(Query query) if (NonGlobalPlugins.ContainsKey(query.ActionKeyword)) { var plugin = NonGlobalPlugins[query.ActionKeyword]; - return new List { plugin }; + return new List {plugin}; } else { @@ -146,25 +188,42 @@ public static List ValidPluginsForQuery(Query query) } } - public static List QueryForPlugin(PluginPair pair, Query query) + public static async Task> QueryForPlugin(PluginPair pair, Query query, CancellationToken token) { var results = new List(); try { var metadata = pair.Metadata; - var milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", () => - { - results = pair.Plugin.Query(query) ?? new List(); - UpdatePluginMetadata(results, metadata, query); - }); + var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", + async () => + { + switch (pair.Plugin) + { + case IAsyncPlugin plugin: + results = await plugin.QueryAsync(query, token).ConfigureAwait(false) ?? + new List(); + UpdatePluginMetadata(results, metadata, query); + break; + case IPlugin plugin: + results = await Task.Run(() => plugin.Query(query), token).ConfigureAwait(false) ?? + new List(); + UpdatePluginMetadata(results, metadata, query); + break; + } + }); metadata.QueryCount += 1; - metadata.AvgQueryTime = metadata.QueryCount == 1 ? milliseconds : (metadata.AvgQueryTime + milliseconds) / 2; + metadata.AvgQueryTime = + metadata.QueryCount == 1 ? milliseconds : (metadata.AvgQueryTime + milliseconds) / 2; } catch (Exception e) { - Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e); + Log.Exception( + $"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", + e); } - return results; + + // null will be fine since the results will only be added into queue if the token hasn't been cancelled + return token.IsCancellationRequested ? results = null : results; } public static void UpdatePluginMetadata(List results, PluginMetadata metadata, Query query) @@ -182,11 +241,6 @@ public static void UpdatePluginMetadata(List results, PluginMetadata met } } - private static bool IsGlobalPlugin(PluginMetadata metadata) - { - return metadata.ActionKeywords.Contains(Query.GlobalPluginWildcardSign); - } - /// /// get specified plugin, return null if not found /// @@ -208,7 +262,7 @@ public static List GetContextMenusForPlugin(Result result) var pluginPair = _contextMenuPlugins.FirstOrDefault(o => o.Metadata.ID == result.PluginID); if (pluginPair != null) { - var plugin = (IContextMenu)pluginPair.Plugin; + var plugin = (IContextMenu) pluginPair.Plugin; try { @@ -222,16 +276,19 @@ public static List GetContextMenusForPlugin(Result result) } catch (Exception e) { - Log.Exception($"|PluginManager.GetContextMenusForPlugin|Can't load context menus for plugin <{pluginPair.Metadata.Name}>", e); + Log.Exception( + $"|PluginManager.GetContextMenusForPlugin|Can't load context menus for plugin <{pluginPair.Metadata.Name}>", + e); } } + return results; } public static bool ActionKeywordRegistered(string actionKeyword) { return actionKeyword != Query.GlobalPluginWildcardSign - && NonGlobalPlugins.ContainsKey(actionKeyword); + && NonGlobalPlugins.ContainsKey(actionKeyword); } /// @@ -249,6 +306,7 @@ public static void AddActionKeyword(string id, string newActionKeyword) { NonGlobalPlugins[newActionKeyword] = plugin; } + plugin.Metadata.ActionKeywords.Add(newActionKeyword); } @@ -262,9 +320,9 @@ public static void RemoveActionKeyword(string id, string oldActionkeyword) if (oldActionkeyword == Query.GlobalPluginWildcardSign && // Plugins may have multiple ActionKeywords that are global, eg. WebSearch plugin.Metadata.ActionKeywords - .Where(x => x == Query.GlobalPluginWildcardSign) - .ToList() - .Count == 1) + .Where(x => x == Query.GlobalPluginWildcardSign) + .ToList() + .Count == 1) { GlobalPlugins.Remove(plugin); } @@ -285,4 +343,4 @@ public static void ReplaceActionKeyword(string id, string oldActionKeyword, stri } } } -} +} \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Stopwatch.cs b/Flow.Launcher.Infrastructure/Stopwatch.cs index d39d90e81b8..dd6edaff93b 100644 --- a/Flow.Launcher.Infrastructure/Stopwatch.cs +++ b/Flow.Launcher.Infrastructure/Stopwatch.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; namespace Flow.Launcher.Infrastructure @@ -22,7 +23,22 @@ public static long Debug(string message, Action action) Log.Debug(info); return milliseconds; } - + + /// + /// This stopwatch will appear only in Debug mode + /// + public static async Task DebugAsync(string message, Func action) + { + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + await action(); + stopWatch.Stop(); + var milliseconds = stopWatch.ElapsedMilliseconds; + string info = $"{message} <{milliseconds}ms>"; + Log.Debug(info); + return milliseconds; + } + public static long Normal(string message, Action action) { var stopWatch = new System.Diagnostics.Stopwatch(); @@ -34,6 +50,20 @@ public static long Normal(string message, Action action) Log.Info(info); return milliseconds; } + + public static async Task NormalAsync(string message, Func action) + { + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + await action(); + stopWatch.Stop(); + var milliseconds = stopWatch.ElapsedMilliseconds; + string info = $"{message} <{milliseconds}ms>"; + Log.Info(info); + return milliseconds; + } + + public static void StartCount(string name, Action action) { diff --git a/Flow.Launcher.Plugin/IAsyncPlugin.cs b/Flow.Launcher.Plugin/IAsyncPlugin.cs new file mode 100644 index 00000000000..36f098e7d64 --- /dev/null +++ b/Flow.Launcher.Plugin/IAsyncPlugin.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + public interface IAsyncPlugin + { + Task> QueryAsync(Query query, CancellationToken token); + Task InitAsync(PluginInitContext context); + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin/IPlugin.cs b/Flow.Launcher.Plugin/IPlugin.cs index 8f7d279fa4a..4cc6d8d40bf 100644 --- a/Flow.Launcher.Plugin/IPlugin.cs +++ b/Flow.Launcher.Plugin/IPlugin.cs @@ -5,6 +5,7 @@ namespace Flow.Launcher.Plugin public interface IPlugin { List Query(Query query); + void Init(PluginInitContext context); } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin/IPublicAPI.cs b/Flow.Launcher.Plugin/IPublicAPI.cs index ccc00d5e938..12e430e07d2 100644 --- a/Flow.Launcher.Plugin/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/IPublicAPI.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin { @@ -34,7 +35,7 @@ public interface IPublicAPI /// Plugin's in memory data with new content /// added by user. /// - void ReloadAllPluginData(); + Task ReloadAllPluginData(); /// /// Check for new Flow Launcher update diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs new file mode 100644 index 00000000000..9c922f6674d --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + public interface IAsyncReloadable + { + Task ReloadDataAsync(); + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin/PluginPair.cs b/Flow.Launcher.Plugin/PluginPair.cs index 910367ec64e..e8954b7a0ef 100644 --- a/Flow.Launcher.Plugin/PluginPair.cs +++ b/Flow.Launcher.Plugin/PluginPair.cs @@ -2,7 +2,7 @@ { public class PluginPair { - public IPlugin Plugin { get; internal set; } + public object Plugin { get; internal set; } public PluginMetadata Metadata { get; internal set; } diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 90d4fff63e8..bcf147be7ea 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -78,9 +78,9 @@ public void SaveAppAllSettings() ImageLoader.Save(); } - public void ReloadAllPluginData() + public async Task ReloadAllPluginData() { - PluginManager.ReloadData(); + await PluginManager.ReloadData(); } public void ShowMsg(string title, string subTitle = "", string iconPath = "") diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index eed30f37732..bc68eb6d634 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -405,45 +405,47 @@ private void QueryResults() }, currentCancellationToken); var plugins = PluginManager.ValidPluginsForQuery(query); - Task.Run(() => + Task.Run(async () => { // so looping will stop once it was cancelled + + Task[] tasks = new Task[plugins.Count]; var parallelOptions = new ParallelOptions { CancellationToken = currentCancellationToken }; try { - Parallel.ForEach(plugins, parallelOptions, plugin => + Parallel.For(0, plugins.Count, parallelOptions, i => { - if (!plugin.Metadata.Disabled) + if (!plugins[i].Metadata.Disabled) { - try - { - var results = PluginManager.QueryForPlugin(plugin, query); - UpdateResultView(results, plugin.Metadata, query); - } - catch(Exception e) - { - Log.Exception("MainViewModel", $"Exception when querying the plugin {plugin.Metadata.Name}", e, "QueryResults"); - } + tasks[i] = QueryTask(i, query, currentCancellationToken); } + else tasks[i] = Task.CompletedTask; // Avoid Null }); + + // 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 } - // this should happen once after all queries are done so progress bar should continue // until the end of all querying _isQueryRunning = false; - if (currentUpdateSource == _updateSource) + if (!currentCancellationToken.IsCancellationRequested) { // update to hidden if this is still the current query ProgressBarVisibility = Visibility.Hidden; } - }, currentCancellationToken).ContinueWith(t => - { - Log.Exception("MainViewModel", "Error when querying plugins", t.Exception?.InnerException, "QueryResults"); - }, TaskContinuationOptions.OnlyOnFaulted); + + async Task QueryTask(int pairIndex, Query query, CancellationToken token) + { + var result = await PluginManager.QueryForPlugin(plugins[pairIndex], query, token); + UpdateResultView(result, plugins[pairIndex].Metadata, query); + } + + }, currentCancellationToken).ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception), + TaskContinuationOptions.OnlyOnFaulted); } } else diff --git a/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs b/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs index fdcffb0b3c1..70afda53673 100644 --- a/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs +++ b/Plugins/Flow.Launcher.Plugin.ControlPanel/ControlPanelList.cs @@ -38,7 +38,7 @@ static extern IntPtr LoadImage(IntPtr hinst, IntPtr lpszName, uint uType, int cxDesired, int cyDesired, uint fuLoad); [DllImport("user32.dll", CharSet = CharSet.Auto)] - extern static bool DestroyIcon(IntPtr handle); + static extern bool DestroyIcon(IntPtr handle); [DllImport("kernel32.dll")] static extern IntPtr FindResource(IntPtr hModule, IntPtr lpName, IntPtr lpType); diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs index 5642b62ed49..7ebab91a173 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Threading.Tasks; using System.Windows; using System.Windows.Forms; using System.Windows.Interop; @@ -67,13 +68,15 @@ public List Query(Query query) { c.TitleHighlightData = titleMatch.MatchData; } - else + else { c.SubTitleHighlightData = subTitleMatch.MatchData; } + results.Add(c); } } + return results; } @@ -94,13 +97,15 @@ private List Commands() IcoPath = "Images\\shutdown.png", Action = c => { - var reuslt = MessageBox.Show(context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_shutdown_computer"), - context.API.GetTranslation("flowlauncher_plugin_sys_shutdown_computer"), - MessageBoxButton.YesNo, MessageBoxImage.Warning); + var reuslt = MessageBox.Show( + context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_shutdown_computer"), + context.API.GetTranslation("flowlauncher_plugin_sys_shutdown_computer"), + MessageBoxButton.YesNo, MessageBoxImage.Warning); if (reuslt == MessageBoxResult.Yes) { Process.Start("shutdown", "/s /t 0"); } + return true; } }, @@ -111,13 +116,15 @@ private List Commands() IcoPath = "Images\\restart.png", Action = c => { - var result = MessageBox.Show(context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_restart_computer"), - context.API.GetTranslation("flowlauncher_plugin_sys_restart_computer"), - MessageBoxButton.YesNo, MessageBoxImage.Warning); + var result = MessageBox.Show( + context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_restart_computer"), + context.API.GetTranslation("flowlauncher_plugin_sys_restart_computer"), + MessageBoxButton.YesNo, MessageBoxImage.Warning); if (result == MessageBoxResult.Yes) { Process.Start("shutdown", "/r /t 0"); } + return true; } }, @@ -163,14 +170,16 @@ private List Commands() // http://www.pinvoke.net/default.aspx/shell32/SHEmptyRecycleBin.html // FYI, couldn't find documentation for this but if the recycle bin is already empty, it will return -2147418113 (0x8000FFFF (E_UNEXPECTED)) // 0 for nothing - var result = SHEmptyRecycleBin(new WindowInteropHelper(Application.Current.MainWindow).Handle, 0); - if (result != (uint) HRESULT.S_OK && result != (uint)0x8000FFFF) + var result = SHEmptyRecycleBin(new WindowInteropHelper(Application.Current.MainWindow).Handle, + 0); + if (result != (uint) HRESULT.S_OK && result != (uint) 0x8000FFFF) { MessageBox.Show($"Error emptying recycle bin, error code: {result}\n" + "please refer to https://msdn.microsoft.com/en-us/library/windows/desktop/aa378137", - "Error", - MessageBoxButton.OK, MessageBoxImage.Error); + "Error", + MessageBoxButton.OK, MessageBoxImage.Error); } + return true; } }, @@ -229,9 +238,13 @@ private List Commands() { // Hide the window first then show msg after done because sometimes the reload could take a while, so not to make user think it's frozen. Application.Current.MainWindow.Hide(); - context.API.ReloadAllPluginData(); - context.API.ShowMsg(context.API.GetTranslation("flowlauncher_plugin_sys_dlgtitle_success"), - context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_all_applicableplugins_reloaded")); + + context.API.ReloadAllPluginData().ContinueWith(_ => + context.API.ShowMsg( + context.API.GetTranslation("flowlauncher_plugin_sys_dlgtitle_success"), + context.API.GetTranslation( + "flowlauncher_plugin_sys_dlgtext_all_applicableplugins_reloaded"))); + return true; } }, From b8f7d899709ee4204d2a0f1cfae8722a2b80cdbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=BC=98=E9=9F=AC?= Date: Sat, 2 Jan 2021 17:25:13 +0800 Subject: [PATCH 02/34] Allows Loading both IPlugin and IAsyncPlugin --- .../Plugin/PluginAssemblyLoader.cs | 10 +- Flow.Launcher.Core/Plugin/PluginsLoader.cs | 104 +++++++++--------- Flow.Launcher/ViewModel/MainViewModel.cs | 9 +- 3 files changed, 63 insertions(+), 60 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs index b9b878a7bda..1a1b17539aa 100644 --- a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs @@ -20,7 +20,7 @@ internal PluginAssemblyLoader(string assemblyFilePath) dependencyResolver = new AssemblyDependencyResolver(assemblyFilePath); assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(assemblyFilePath)); - referencedPluginPackageDependencyResolver = + referencedPluginPackageDependencyResolver = new AssemblyDependencyResolver(Path.Combine(Constant.ProgramDirectory, "Flow.Launcher.Plugin.dll")); } @@ -38,15 +38,15 @@ protected override Assembly Load(AssemblyName assemblyName) // that use Newtonsoft.Json if (assemblyPath == null || ExistsInReferencedPluginPackage(assemblyName)) return null; - + return LoadFromAssemblyPath(assemblyPath); } - internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, Type type) + internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, params Type[] types) { var allTypes = assembly.ExportedTypes; - return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Contains(type)); + return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Intersect(types).Any()); } internal bool ExistsInReferencedPluginPackage(AssemblyName assemblyName) @@ -54,4 +54,4 @@ internal bool ExistsInReferencedPluginPackage(AssemblyName assemblyName) return referencedPluginPackageDependencyResolver.ResolveAssemblyToPath(assemblyName) != null; } } -} +} \ No newline at end of file diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 224dbd85e92..8295761b5ed 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -37,56 +37,59 @@ public static IEnumerable DotNetPlugins(List source) foreach (var metadata in metadatas) { - var milliseconds = Stopwatch.Debug($"|PluginsLoader.DotNetPlugins|Constructor init cost for {metadata.Name}", () => - { - -#if DEBUG - var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); - var assembly = assemblyLoader.LoadAssemblyAndDependencies(); - var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin)); - var plugin = (IPlugin)Activator.CreateInstance(type); -#else - Assembly assembly = null; - IPlugin plugin = null; - - try + var milliseconds = Stopwatch.Debug( + $"|PluginsLoader.DotNetPlugins|Constructor init cost for {metadata.Name}", () => { +#if DEBUG var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); - assembly = assemblyLoader.LoadAssemblyAndDependencies(); - - var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin)); - - plugin = (IPlugin)Activator.CreateInstance(type); - } - catch (Exception e) when (assembly == null) - { - Log.Exception($"|PluginsLoader.DotNetPlugins|Couldn't load assembly for the plugin: {metadata.Name}", e); - } - catch (InvalidOperationException e) - { - Log.Exception($"|PluginsLoader.DotNetPlugins|Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e); - } - catch (ReflectionTypeLoadException e) - { - Log.Exception($"|PluginsLoader.DotNetPlugins|The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e); - } - catch (Exception e) - { - Log.Exception($"|PluginsLoader.DotNetPlugins|The following plugin has errored and can not be loaded: <{metadata.Name}>", e); - } + var assembly = assemblyLoader.LoadAssemblyAndDependencies(); + var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin), + typeof(IAsyncPlugin)); - if (plugin == null) - { - erroredPlugins.Add(metadata.Name); - return; - } + var plugin = Activator.CreateInstance(type); +#else + Assembly assembly = null; + IPlugin plugin = null; + + try + { + var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); + assembly = assemblyLoader.LoadAssemblyAndDependencies(); + + var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin), + typeof(IAsyncPlugin)); + + plugin = Activator.CreateInstance(type); + } + catch (Exception e) when (assembly == null) + { + Log.Exception($"|PluginsLoader.DotNetPlugins|Couldn't load assembly for the plugin: {metadata.Name}", e); + } + catch (InvalidOperationException e) + { + Log.Exception($"|PluginsLoader.DotNetPlugins|Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e); + } + catch (ReflectionTypeLoadException e) + { + Log.Exception($"|PluginsLoader.DotNetPlugins|The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e); + } + catch (Exception e) + { + Log.Exception($"|PluginsLoader.DotNetPlugins|The following plugin has errored and can not be loaded: <{metadata.Name}>", e); + } + + if (plugin == null) + { + erroredPlugins.Add(metadata.Name); + return; + } #endif - plugins.Add(new PluginPair - { - Plugin = plugin, - Metadata = metadata + plugins.Add(new PluginPair + { + Plugin = plugin, + Metadata = metadata + }); }); - }); metadata.InitTime += milliseconds; } @@ -95,15 +98,15 @@ public static IEnumerable DotNetPlugins(List source) var errorPluginString = String.Join(Environment.NewLine, erroredPlugins); var errorMessage = "The following " - + (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ") - + "errored and cannot be loaded:"; + + (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ") + + "errored and cannot be loaded:"; Task.Run(() => { MessageBox.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" + - $"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" + - $"Please refer to the logs for more information","", - MessageBoxButtons.OK, MessageBoxIcon.Warning); + $"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" + + $"Please refer to the logs for more information", "", + MessageBoxButtons.OK, MessageBoxIcon.Warning); }); } @@ -179,6 +182,5 @@ public static IEnumerable ExecutablePlugins(IEnumerable Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception), From f768b0890b8f784983da9cc2ae5a5f2b464a41ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=BC=98=E9=9F=AC?= Date: Sat, 2 Jan 2021 17:58:30 +0800 Subject: [PATCH 03/34] Move Program Plugin to Async model --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 154 ++++++++++--------- 1 file changed, 83 insertions(+), 71 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 8f124f3a40b..0d693d3632f 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; using Flow.Launcher.Infrastructure.Logger; @@ -12,9 +13,8 @@ namespace Flow.Launcher.Plugin.Program { - public class Main : ISettingProvider, IPlugin, IPluginI18n, IContextMenu, ISavable, IReloadable + public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, ISavable, IAsyncReloadable { - private static readonly object IndexLock = new object(); internal static Win32[] _win32s { get; set; } internal static UWP.Application[] _uwps { get; set; } internal static Settings _settings { get; set; } @@ -30,33 +30,6 @@ public class Main : ISettingProvider, IPlugin, IPluginI18n, IContextMenu, ISavab public Main() { _settingsStorage = new PluginJsonStorage(); - _settings = _settingsStorage.Load(); - - Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () => - { - _win32Storage = new BinaryStorage("Win32"); - _win32s = _win32Storage.TryLoad(new Win32[] { }); - _uwpStorage = new BinaryStorage("UWP"); - _uwps = _uwpStorage.TryLoad(new UWP.Application[] { }); - }); - Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); - Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>"); - - var a = Task.Run(() => - { - if (IsStartupIndexProgramsRequired || !_win32s.Any()) - Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs); - }); - - var b = Task.Run(() => - { - if (IsStartupIndexProgramsRequired || !_uwps.Any()) - Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexUWPPrograms); - }); - - Task.WaitAll(a, b); - - _settings.LastIndexTime = DateTime.Today; } public void Save() @@ -66,7 +39,7 @@ public void Save() _uwpStorage.Save(_uwps); } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { Win32[] win32; UWP.Application[] uwps; @@ -74,20 +47,57 @@ public List Query(Query query) win32 = _win32s; uwps = _uwps; - var result = win32.Cast() - .Concat(uwps) - .AsParallel() - .Where(p => p.Enabled) - .Select(p => p.Result(query.Search, _context.API)) - .Where(r => r?.Score > 0) - .ToList(); + + var result = await Task.Run(delegate + { + return win32.Cast() + .Concat(uwps) + .AsParallel() + .WithCancellation(token) + .Where(p => p.Enabled) + .Select(p => p.Result(query.Search, _context.API)) + .Where(r => r?.Score > 0) + .ToList(); + }, token).ConfigureAwait(false); return result; } - public void Init(PluginInitContext context) + public async Task InitAsync(PluginInitContext context) { _context = context; + + await Task.Run(() => + { + _settings = _settingsStorage.Load(); + + Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () => + { + _win32Storage = new BinaryStorage("Win32"); + _win32s = _win32Storage.TryLoad(new Win32[] { }); + _uwpStorage = new BinaryStorage("UWP"); + _uwps = _uwpStorage.TryLoad(new UWP.Application[] { }); + }); + Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); + Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>"); + }); + + + var a = Task.Run(() => + { + if (IsStartupIndexProgramsRequired || !_win32s.Any()) + Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs); + }); + + var b = Task.Run(() => + { + if (IsStartupIndexProgramsRequired || !_uwps.Any()) + Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexUwpPrograms); + }); + + await Task.WhenAll(a, b); + + _settings.LastIndexTime = DateTime.Today; } public static void IndexWin32Programs() @@ -95,10 +105,9 @@ public static void IndexWin32Programs() var win32S = Win32.All(_settings); _win32s = win32S; - } - public static void IndexUWPPrograms() + public static void IndexUwpPrograms() { var windows10 = new Version(10, 0); var support = Environment.OSVersion.Version.Major >= windows10.Major; @@ -106,16 +115,15 @@ public static void IndexUWPPrograms() var applications = support ? UWP.All() : new UWP.Application[] { }; _uwps = applications; - } - public static void IndexPrograms() + public static async Task IndexPrograms() { - var t1 = Task.Run(() => IndexWin32Programs()); + var t1 = Task.Run(IndexWin32Programs); - var t2 = Task.Run(() => IndexUWPPrograms()); + var t2 = Task.Run(IndexUwpPrograms); - Task.WaitAll(t1, t2); + await Task.WhenAll(t1, t2); _settings.LastIndexTime = DateTime.Today; } @@ -145,19 +153,21 @@ public List LoadContextMenus(Result selectedResult) } menuOptions.Add( - new Result - { - Title = _context.API.GetTranslation("flowlauncher_plugin_program_disable_program"), - Action = c => - { - DisableProgram(program); - _context.API.ShowMsg(_context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"), - _context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success_message")); - return false; - }, - IcoPath = "Images/disable.png" - } - ); + new Result + { + Title = _context.API.GetTranslation("flowlauncher_plugin_program_disable_program"), + Action = c => + { + DisableProgram(program); + _context.API.ShowMsg( + _context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"), + _context.API.GetTranslation( + "flowlauncher_plugin_program_disable_dlgtitle_success_message")); + return false; + }, + IcoPath = "Images/disable.png" + } + ); return menuOptions; } @@ -168,21 +178,23 @@ private void DisableProgram(IProgram programToDelete) return; if (_uwps.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) - _uwps.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier).FirstOrDefault().Enabled = false; + _uwps.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier).FirstOrDefault().Enabled = + false; if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) - _win32s.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier).FirstOrDefault().Enabled = false; + _win32s.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier).FirstOrDefault().Enabled = + false; _settings.DisabledProgramSources - .Add( - new Settings.DisabledProgramSource - { - Name = programToDelete.Name, - Location = programToDelete.Location, - UniqueIdentifier = programToDelete.UniqueIdentifier, - Enabled = false - } - ); + .Add( + new Settings.DisabledProgramSource + { + Name = programToDelete.Name, + Location = programToDelete.Location, + UniqueIdentifier = programToDelete.UniqueIdentifier, + Enabled = false + } + ); } public static void StartProcess(Func runProcess, ProcessStartInfo info) @@ -200,9 +212,9 @@ public static void StartProcess(Func runProcess, Proc } } - public void ReloadData() + public async Task ReloadDataAsync() { - IndexPrograms(); + await IndexPrograms(); } } } \ No newline at end of file From 6326d6f3d57e8a5ef6065c210f8d6db7d932ca18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Sat, 2 Jan 2021 20:22:07 +0800 Subject: [PATCH 04/34] Startup async --- Flow.Launcher/App.xaml.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 59bdbc8960f..7417a6f2a93 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -45,9 +45,9 @@ public static void Main() } } - private void OnStartup(object sender, StartupEventArgs e) + private async void OnStartup(object sender, StartupEventArgs e) { - Stopwatch.Normal("|App.OnStartup|Startup cost", () => + await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => { _portable.PreStartCleanUpAfterPortabilityUpdate(); @@ -70,7 +70,7 @@ private void OnStartup(object sender, StartupEventArgs e) _mainVM = new MainViewModel(_settings); var window = new MainWindow(_settings, _mainVM); API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet); - PluginManager.InitializePlugins(API); + await PluginManager.InitializePlugins(API); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); Current.MainWindow = window; From 7be2a956dd31aa6d48a263926ea2e70f0a4e437b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Sat, 2 Jan 2021 21:20:21 +0800 Subject: [PATCH 05/34] Use cancallationToken.IsCancellationRequested instead of comparing current token with global token --- 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 3e1a4b516e2..f9c15204601 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -442,7 +442,8 @@ private void QueryResults() async Task QueryTask(PluginPair plugin, Query query, CancellationToken token) { var results = await PluginManager.QueryForPlugin(plugin, query, token); - UpdateResultView(results, plugin.Metadata, query); + if (!currentCancellationToken.IsCancellationRequested) + UpdateResultView(results, plugin.Metadata, query); } }, currentCancellationToken).ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception), From 280b98b47a1fbdb5cb8a493339123d9586b54ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Sat, 2 Jan 2021 21:23:34 +0800 Subject: [PATCH 06/34] Move the creation of Window later due to async operation --- Flow.Launcher/App.xaml.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 7417a6f2a93..0145dfa3420 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -68,9 +68,10 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => PluginManager.LoadPlugins(_settings.PluginSettings); _mainVM = new MainViewModel(_settings); - var window = new MainWindow(_settings, _mainVM); API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet); await PluginManager.InitializePlugins(API); + var window = new MainWindow(_settings, _mainVM); + Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); Current.MainWindow = window; From 4cb4aa88ef52dcab60c662a312f8b2e953b40ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Sat, 2 Jan 2021 22:00:43 +0800 Subject: [PATCH 07/34] change onstartup name to async --- Flow.Launcher/App.xaml | 2 +- Flow.Launcher/App.xaml.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml index f3347d7fbf6..18addac7398 100644 --- a/Flow.Launcher/App.xaml +++ b/Flow.Launcher/App.xaml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="http://schemas.modernwpf.com/2019" ShutdownMode="OnMainWindowClose" - Startup="OnStartup"> + Startup="OnStartupAsync"> diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 0145dfa3420..06bb16e3be2 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -45,7 +45,7 @@ public static void Main() } } - private async void OnStartup(object sender, StartupEventArgs e) + private async void OnStartupAsync(object sender, StartupEventArgs e) { await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => { From d7805d7a8cbc7eedcee7cc7a62156397f1af5172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Sat, 2 Jan 2021 22:01:15 +0800 Subject: [PATCH 08/34] Make Explorer plugin completely async --- Flow.Launcher.Test/Plugins/ExplorerTest.cs | 4 +- Plugins/Flow.Launcher.Plugin.Explorer/Main.cs | 14 ++- .../DirectoryInfo/DirectoryInfoSearch.cs | 6 +- .../Search/SearchManager.cs | 54 ++++++----- .../Search/WindowsIndex/IndexSearch.cs | 90 +++++++++---------- .../ViewModels/SettingsViewModel.cs | 6 ++ 6 files changed, 96 insertions(+), 78 deletions(-) diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index c9114482599..756ceb2d6c8 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -151,7 +151,7 @@ public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMe var searchManager = new SearchManager(new Settings(), new PluginInitContext()); // When - var results = searchManager.TopLevelDirectorySearchBehaviour( + var results = searchManager.TopLevelDirectorySearchBehaviourAsync( MethodWindowsIndexSearchReturnsZeroResults, MethodDirectoryInfoClassSearchReturnsTwoResults, false, @@ -171,7 +171,7 @@ public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMe var searchManager = new SearchManager(new Settings(), new PluginInitContext()); // When - var results = searchManager.TopLevelDirectorySearchBehaviour( + var results = searchManager.TopLevelDirectorySearchBehaviourAsync( MethodWindowsIndexSearchReturnsZeroResults, MethodDirectoryInfoClassSearchReturnsTwoResults, true, diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index 30a06e882f3..7b56df69146 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -3,11 +3,13 @@ using Flow.Launcher.Plugin.Explorer.ViewModels; using Flow.Launcher.Plugin.Explorer.Views; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using System.Windows.Controls; namespace Flow.Launcher.Plugin.Explorer { - public class Main : ISettingProvider, IPlugin, ISavable, IContextMenu, IPluginI18n + public class Main : ISettingProvider, IAsyncPlugin, ISavable, IContextMenu, IPluginI18n { internal PluginInitContext Context { get; set; } @@ -17,17 +19,21 @@ public class Main : ISettingProvider, IPlugin, ISavable, IContextMenu, IPluginI1 private IContextMenu contextMenu; + private SearchManager searchManager; + public Control CreateSettingPanel() { return new ExplorerSettings(viewModel); } - public void Init(PluginInitContext context) + public async Task InitAsync(PluginInitContext context) { Context = context; viewModel = new SettingsViewModel(context); + await viewModel.LoadStorage(); Settings = viewModel.Settings; contextMenu = new ContextMenu(Context, Settings); + searchManager = new SearchManager(Settings, Context); } public List LoadContextMenus(Result selectedResult) @@ -35,9 +41,9 @@ public List LoadContextMenus(Result selectedResult) return contextMenu.LoadContextMenus(selectedResult); } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { - return new SearchManager(Settings, Context).Search(query); + return await searchManager.SearchAsync(query, token); } public void Save() diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs index 02de0eeaedd..3253b7a7b75 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Windows; namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo { @@ -22,7 +23,7 @@ internal List TopLevelDirectorySearch(Query query, string search) if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > search.LastIndexOf(Constants.DirectorySeperator)) return DirectorySearch(SearchOption.AllDirectories, query, search, criteria); - + return DirectorySearch(SearchOption.TopDirectoryOnly, query, search, criteria); } @@ -57,9 +58,8 @@ private List DirectorySearch(SearchOption searchOption, Query query, str try { var directoryInfo = new System.IO.DirectoryInfo(path); - var fileSystemInfos = directoryInfo.GetFileSystemInfos(searchCriteria, searchOption); - foreach (var fileSystemInfo in fileSystemInfos) + foreach (var fileSystemInfo in directoryInfo.EnumerateFileSystemInfos(searchCriteria, searchOption)) { if ((fileSystemInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index 5b50b7fada6..6b3a969122a 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin.Explorer.Search { @@ -28,20 +30,20 @@ public SearchManager(Settings settings, PluginInitContext context) this.settings = settings; } - internal List Search(Query query) + internal async Task> SearchAsync(Query query, CancellationToken token) { var results = new List(); var querySearch = query.Search; if (IsFileContentSearch(query.ActionKeyword)) - return WindowsIndexFileContentSearch(query, querySearch); + return await WindowsIndexFileContentSearchAsync(query, querySearch, token).ConfigureAwait(false); // This allows the user to type the assigned action keyword and only see the list of quick folder links if (settings.QuickFolderAccessLinks.Count > 0 && query.ActionKeyword == settings.SearchActionKeyword && string.IsNullOrEmpty(query.Search)) - return quickFolderAccess.FolderListAll(query, settings.QuickFolderAccessLinks, context); + return quickFolderAccess.FolderListAll(query, settings.QuickFolderAccessLinks, context); var quickFolderLinks = quickFolderAccess.FolderListMatched(query, settings.QuickFolderAccessLinks, context); @@ -54,11 +56,11 @@ internal List Search(Query query) return EnvironmentVariables.GetEnvironmentStringPathSuggestions(querySearch, query, context); // Query is a location path with a full environment variable, eg. %appdata%\somefolder\ - var isEnvironmentVariablePath = querySearch.Substring(1).Contains("%\\"); + var isEnvironmentVariablePath = querySearch[1..].Contains("%\\"); if (!FilesFolders.IsLocationPathString(querySearch) && !isEnvironmentVariablePath) { - results.AddRange(WindowsIndexFilesAndFoldersSearch(query, querySearch)); + results.AddRange(await WindowsIndexFilesAndFoldersSearchAsync(query, querySearch, token).ConfigureAwait(false)); return results; } @@ -72,29 +74,34 @@ internal List Search(Query query) return results; var useIndexSearch = UseWindowsIndexForDirectorySearch(locationPath); - + results.Add(resultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch)); - results.AddRange(TopLevelDirectorySearchBehaviour(WindowsIndexTopLevelFolderSearch, + if (token.IsCancellationRequested) + return null; + + results.AddRange(await TopLevelDirectorySearchBehaviourAsync(WindowsIndexTopLevelFolderSearchAsync, DirectoryInfoClassSearch, useIndexSearch, query, - locationPath)); + locationPath, + token).ConfigureAwait(false)); return results; } - private List WindowsIndexFileContentSearch(Query query, string querySearchString) + private async Task> WindowsIndexFileContentSearchAsync(Query query, string querySearchString, CancellationToken token) { var queryConstructor = new QueryConstructor(settings); if (string.IsNullOrEmpty(querySearchString)) return new List(); - return indexSearch.WindowsIndexSearch(querySearchString, + return await indexSearch.WindowsIndexSearchAsync(querySearchString, queryConstructor.CreateQueryHelper().ConnectionString, queryConstructor.QueryForFileContentSearch, - query); + query, + token).ConfigureAwait(false); } public bool IsFileContentSearch(string actionKeyword) @@ -109,37 +116,40 @@ private List DirectoryInfoClassSearch(Query query, string querySearch) return directoryInfoSearch.TopLevelDirectorySearch(query, querySearch); } - public List TopLevelDirectorySearchBehaviour( - Func> windowsIndexSearch, + public async Task> TopLevelDirectorySearchBehaviourAsync( + Func>> windowsIndexSearch, Func> directoryInfoClassSearch, bool useIndexSearch, Query query, - string querySearchString) + string querySearchString, + CancellationToken token) { if (!useIndexSearch) return directoryInfoClassSearch(query, querySearchString); - return windowsIndexSearch(query, querySearchString); + return await windowsIndexSearch(query, querySearchString, token); } - private List WindowsIndexFilesAndFoldersSearch(Query query, string querySearchString) + private async Task> WindowsIndexFilesAndFoldersSearchAsync(Query query, string querySearchString, CancellationToken token) { var queryConstructor = new QueryConstructor(settings); - return indexSearch.WindowsIndexSearch(querySearchString, + return await indexSearch.WindowsIndexSearchAsync(querySearchString, queryConstructor.CreateQueryHelper().ConnectionString, queryConstructor.QueryForAllFilesAndFolders, - query); + query, + token).ConfigureAwait(false); } - - private List WindowsIndexTopLevelFolderSearch(Query query, string path) + + private async Task> WindowsIndexTopLevelFolderSearchAsync(Query query, string path, CancellationToken token) { var queryConstructor = new QueryConstructor(settings); - return indexSearch.WindowsIndexSearch(path, + return await indexSearch.WindowsIndexSearchAsync(path, queryConstructor.CreateQueryHelper().ConnectionString, queryConstructor.QueryForTopLevelDirectorySearch, - query); + query, + token).ConfigureAwait(false); } private bool UseWindowsIndexForDirectorySearch(string locationPath) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs index 4f9325c7754..5b1d47ef8cf 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs @@ -5,19 +5,13 @@ using System.Data.OleDb; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex { internal class IndexSearch { - private readonly object _lock = new object(); - - private OleDbConnection conn; - - private OleDbCommand command; - - private OleDbDataReader dataReaderResults; - private readonly ResultManager resultManager; // Reserved keywords in oleDB @@ -28,7 +22,7 @@ internal IndexSearch(PluginInitContext context) resultManager = new ResultManager(context); } - internal List ExecuteWindowsIndexSearch(string indexQueryString, string connectionString, Query query) + internal async Task> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token) { var folderResults = new List(); var fileResults = new List(); @@ -36,47 +30,49 @@ internal List ExecuteWindowsIndexSearch(string indexQueryString, string try { - using (conn = new OleDbConnection(connectionString)) - { - conn.Open(); + using var conn = new OleDbConnection(connectionString); + await conn.OpenAsync(token); + token.ThrowIfCancellationRequested(); - using (command = new OleDbCommand(indexQueryString, conn)) + using var command = new OleDbCommand(indexQueryString, conn); + // Results return as an OleDbDataReader. + using var dataReaderResults = await command.ExecuteReaderAsync(token) as OleDbDataReader; + token.ThrowIfCancellationRequested(); + + if (dataReaderResults.HasRows) + { + while (await dataReaderResults.ReadAsync(token)) { - // Results return as an OleDbDataReader. - using (dataReaderResults = command.ExecuteReader()) + token.ThrowIfCancellationRequested(); + if (dataReaderResults.GetValue(0) != DBNull.Value && dataReaderResults.GetValue(1) != DBNull.Value) { - if (dataReaderResults.HasRows) + // # 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); + + var path = new Uri(encodedFragmentPath).LocalPath; + + if (dataReaderResults.GetString(2) == "Directory") { - while (dataReaderResults.Read()) - { - if (dataReaderResults.GetValue(0) != DBNull.Value && dataReaderResults.GetValue(1) != DBNull.Value) - { - // # 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); - - var path = new Uri(encodedFragmentPath).LocalPath; - - if (dataReaderResults.GetString(2) == "Directory") - { - folderResults.Add(resultManager.CreateFolderResult( - dataReaderResults.GetString(0), - path, - path, - query, true, true)); - } - else - { - fileResults.Add(resultManager.CreateFileResult(path, query, true, true)); - } - } - } + folderResults.Add(resultManager.CreateFolderResult( + dataReaderResults.GetString(0), + path, + path, + query, true, true)); + } + else + { + fileResults.Add(resultManager.CreateFileResult(path, query, true, true)); } } } } } + catch (OperationCanceledException) + { + return new List(); // The source code indicates that without adding members, it won't allocate an array + } catch (InvalidOperationException e) { // Internal error from ExecuteReader(): Connection closed. @@ -91,18 +87,18 @@ internal List ExecuteWindowsIndexSearch(string indexQueryString, string return results.Concat(folderResults.OrderBy(x => x.Title)).Concat(fileResults.OrderBy(x => x.Title)).ToList(); ; } - internal List WindowsIndexSearch(string searchString, string connectionString, Func constructQuery, Query query) + internal async Task> WindowsIndexSearchAsync(string searchString, string connectionString, + Func constructQuery, Query query, + CancellationToken token) { var regexMatch = Regex.Match(searchString, reservedStringPattern); if (regexMatch.Success) return new List(); - lock (_lock) - { - var constructedQuery = constructQuery(searchString); - return ExecuteWindowsIndexSearch(constructedQuery, connectionString, query); - } + var constructedQuery = constructQuery(searchString); + return await ExecuteWindowsIndexSearchAsync(constructedQuery, connectionString, query, token); + } internal bool PathIsIndexed(string path) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index 7fcd77f0775..21bc49741de 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -3,6 +3,7 @@ using Flow.Launcher.Plugin.Explorer.Search; using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; using System.Diagnostics; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin.Explorer.ViewModels { @@ -21,6 +22,11 @@ public SettingsViewModel(PluginInitContext context) Settings = storage.Load(); } + public Task LoadStorage() + { + return Task.Run(() => Settings = storage.Load()); + } + public void Save() { storage.Save(); From 43cee65c4518b01ca49ba9a267446a390d41501e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Sat, 2 Jan 2021 22:21:45 +0800 Subject: [PATCH 09/34] Move PluginManagers to Async Model --- .../Main.cs | 19 +++++++++---------- .../Models/PluginsManifest.cs | 7 +------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs index f10f022d72d..40579e6e58a 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs @@ -7,10 +7,11 @@ using Flow.Launcher.Infrastructure; using System; using System.Threading.Tasks; +using System.Threading; namespace Flow.Launcher.Plugin.PluginsManager { - public class Main : ISettingProvider, IPlugin, ISavable, IContextMenu, IPluginI18n, IReloadable + public class Main : ISettingProvider, IAsyncPlugin, ISavable, IContextMenu, IPluginI18n, IAsyncReloadable { internal PluginInitContext Context { get; set; } @@ -29,13 +30,14 @@ public Control CreateSettingPanel() return new PluginsManagerSettings(viewModel); } - public void Init(PluginInitContext context) + public async Task InitAsync(PluginInitContext context) { Context = context; viewModel = new SettingsViewModel(context); Settings = viewModel.Settings; contextMenu = new ContextMenu(Context); pluginManager = new PluginsManager(Context, Settings); + await pluginManager.UpdateManifest(); lastUpdateTime = DateTime.Now; } @@ -44,7 +46,7 @@ public List LoadContextMenus(Result selectedResult) return contextMenu.LoadContextMenus(selectedResult); } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { var search = query.Search.ToLower(); @@ -53,11 +55,8 @@ public List Query(Query query) if ((DateTime.Now - lastUpdateTime).TotalHours > 12) // 12 hours { - Task.Run(async () => - { - await pluginManager.UpdateManifest(); - lastUpdateTime = DateTime.Now; - }); + await pluginManager.UpdateManifest(); + lastUpdateTime = DateTime.Now; } return search switch @@ -88,9 +87,9 @@ public string GetTranslatedPluginDescription() return Context.API.GetTranslation("plugin_pluginsmanager_plugin_description"); } - public void ReloadData() + public async Task ReloadDataAsync() { - Task.Run(() => pluginManager.UpdateManifest()).Wait(); + await pluginManager.UpdateManifest(); lastUpdateTime = DateTime.Now; } } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs index 814e0764df7..145aadc986a 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs @@ -9,12 +9,7 @@ namespace Flow.Launcher.Plugin.PluginsManager.Models { internal class PluginsManifest { - internal List UserPlugins { get; private set; } - - internal PluginsManifest() - { - Task.Run(async () => await DownloadManifest()).Wait(); - } + internal List UserPlugins { get; private set; } = new List(); internal async Task DownloadManifest() { From 69cb8e61479d477a9f0c05a5d7f40b386af6438d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Sat, 2 Jan 2021 22:30:56 +0800 Subject: [PATCH 10/34] Reload IAsyncReloadable concurrently --- Flow.Launcher.Core/Plugin/PluginManager.cs | 33 ++++++++++++---------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 239f0499d5a..2e938127ce4 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -33,7 +33,7 @@ public static class PluginManager /// /// Directories that will hold Flow Launcher plugin directory /// - private static readonly string[] Directories = {Constant.PreinstalledDirectory, DataLocation.PluginsDirectory}; + private static readonly string[] Directories = { Constant.PreinstalledDirectory, DataLocation.PluginsDirectory }; private static void DeletePythonBinding() { @@ -55,18 +55,21 @@ public static void Save() public static async Task ReloadData() { - foreach(var plugin in AllPlugins) + await Task.WhenAll(AllPlugins.Select(plugin => { - switch (plugin.Plugin) { - case IReloadable p: - p.ReloadData(); - break; - case IAsyncReloadable p: - await p.ReloadDataAsync(); - break; + switch (plugin) + { + case IReloadable p: + p.ReloadData(); // Sync reload means low time consuming + return Task.CompletedTask; + case IAsyncReloadable p: + return p.ReloadDataAsync(); + default: + throw new ArgumentOutOfRangeException(); + } } - } + })); } static PluginManager() @@ -142,7 +145,7 @@ await plugin.InitAsync(new PluginInitContext catch (Exception e) { Log.Exception(nameof(PluginManager), $"Fail to Init plugin: {pair.Metadata.Name}", e); - pair.Metadata.Disabled = true; + pair.Metadata.Disabled = true; failedPlugins.Enqueue(pair); } })); @@ -180,7 +183,7 @@ public static List ValidPluginsForQuery(Query query) if (NonGlobalPlugins.ContainsKey(query.ActionKeyword)) { var plugin = NonGlobalPlugins[query.ActionKeyword]; - return new List {plugin}; + return new List { plugin }; } else { @@ -262,7 +265,7 @@ public static List GetContextMenusForPlugin(Result result) var pluginPair = _contextMenuPlugins.FirstOrDefault(o => o.Metadata.ID == result.PluginID); if (pluginPair != null) { - var plugin = (IContextMenu) pluginPair.Plugin; + var plugin = (IContextMenu)pluginPair.Plugin; try { @@ -326,10 +329,10 @@ public static void RemoveActionKeyword(string id, string oldActionkeyword) { GlobalPlugins.Remove(plugin); } - + if (oldActionkeyword != Query.GlobalPluginWildcardSign) NonGlobalPlugins.Remove(oldActionkeyword); - + plugin.Metadata.ActionKeywords.Remove(oldActionkeyword); } From 6e9e51ec4d674867deb283dd721dd485978cd4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Sun, 3 Jan 2021 10:19:05 +0800 Subject: [PATCH 11/34] Error handling for OperationCancelledException in Program plugin --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 39 ++++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 0d693d3632f..954c238a992 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -47,20 +47,35 @@ public async Task> QueryAsync(Query query, CancellationToken token) win32 = _win32s; uwps = _uwps; + try + { + var result = await Task.Run(delegate + { + try + { + return win32.Cast() + .Concat(uwps) + .AsParallel() + .WithCancellation(token) + .Where(p => p.Enabled) + .Select(p => p.Result(query.Search, _context.API)) + .Where(r => r?.Score > 0) + .ToList(); + } + catch (OperationCanceledException) + { + return null; + } + }, token).ConfigureAwait(false); + + token.ThrowIfCancellationRequested(); - var result = await Task.Run(delegate + return result; + } + catch (OperationCanceledException) { - return win32.Cast() - .Concat(uwps) - .AsParallel() - .WithCancellation(token) - .Where(p => p.Enabled) - .Select(p => p.Result(query.Search, _context.API)) - .Where(r => r?.Score > 0) - .ToList(); - }, token).ConfigureAwait(false); - - return result; + return null; + } } public async Task InitAsync(PluginInitContext context) From 1c200695982e1e0ad170febf0d04498690baea59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Sun, 3 Jan 2021 10:19:33 +0800 Subject: [PATCH 12/34] Rebase to Dev --- Flow.Launcher.Core/Plugin/PluginManager.cs | 47 ++++++++++++---------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 2e938127ce4..b34995ba64f 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -197,36 +197,41 @@ public static async Task> QueryForPlugin(PluginPair pair, Query que try { var metadata = pair.Metadata; - var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", - async () => - { - switch (pair.Plugin) - { - case IAsyncPlugin plugin: - results = await plugin.QueryAsync(query, token).ConfigureAwait(false) ?? - new List(); - UpdatePluginMetadata(results, metadata, query); - break; - case IPlugin plugin: - results = await Task.Run(() => plugin.Query(query), token).ConfigureAwait(false) ?? - new List(); - UpdatePluginMetadata(results, metadata, query); - break; - } - }); + + long milliseconds = -1L; + + switch (pair.Plugin) + { + case IAsyncPlugin plugin: + milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", + async () => results = await plugin.QueryAsync(query, token).ConfigureAwait(false)); + break; + case IPlugin plugin: + await Task.Run(() => milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", () => + results = plugin.Query(query)), token).ConfigureAwait(false); + break; + default: + throw new ArgumentOutOfRangeException(); + } + token.ThrowIfCancellationRequested(); + UpdatePluginMetadata(results, metadata, query); + metadata.QueryCount += 1; metadata.AvgQueryTime = metadata.QueryCount == 1 ? milliseconds : (metadata.AvgQueryTime + milliseconds) / 2; + token.ThrowIfCancellationRequested(); + } + catch (Exception e) when (e is OperationCanceledException || e is TaskCanceledException) + { + return results = null; } catch (Exception e) { - Log.Exception( - $"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", - e); + Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e); } // null will be fine since the results will only be added into queue if the token hasn't been cancelled - return token.IsCancellationRequested ? results = null : results; + return results; } public static void UpdatePluginMetadata(List results, PluginMetadata metadata, Query query) From 731c3cdcbc23e85b38b3b652d970328adc566675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Sun, 3 Jan 2021 10:37:36 +0800 Subject: [PATCH 13/34] Use OperationCancelledException instead of catch Exception and check --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index b34995ba64f..0712908bf04 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -221,7 +221,7 @@ public static async Task> QueryForPlugin(PluginPair pair, Query que metadata.QueryCount == 1 ? milliseconds : (metadata.AvgQueryTime + milliseconds) / 2; token.ThrowIfCancellationRequested(); } - catch (Exception e) when (e is OperationCanceledException || e is TaskCanceledException) + catch (OperationCanceledException) { return results = null; } From ecf2a7a1f7cd73b787a0aa66bb588e9994e98397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Sun, 3 Jan 2021 10:43:05 +0800 Subject: [PATCH 14/34] change plugin type in pluginLoader for Release --- Flow.Launcher.Core/Plugin/PluginsLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 8295761b5ed..b18c07e3c6d 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -49,7 +49,7 @@ public static IEnumerable DotNetPlugins(List source) var plugin = Activator.CreateInstance(type); #else Assembly assembly = null; - IPlugin plugin = null; + object plugin = null; try { From 63e32f1097e6ce061bf431aa6e452f3e47ac19f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Sun, 3 Jan 2021 10:52:59 +0800 Subject: [PATCH 15/34] fix testing --- Flow.Launcher.Test/Plugins/ExplorerTest.cs | 48 ++++++++++++---------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index 756ceb2d6c8..09c7d9a30df 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -7,6 +7,8 @@ using NUnit.Framework; using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Flow.Launcher.Test.Plugins { @@ -17,15 +19,15 @@ namespace Flow.Launcher.Test.Plugins [TestFixture] public class ExplorerTest { - private List MethodWindowsIndexSearchReturnsZeroResults(Query dummyQuery, string dummyString) + private async Task> MethodWindowsIndexSearchReturnsZeroResultsAsync(Query dummyQuery, string dummyString, CancellationToken dummyToken) { return new List(); } private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString) { - return new List - { + return new List + { new Result { Title="Result 1" @@ -64,10 +66,10 @@ public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_Then { // Given var queryConstructor = new QueryConstructor(new Settings()); - + //When var queryString = queryConstructor.QueryForTopLevelDirectorySearch(folderPath); - + // Then Assert.IsTrue(queryString == expectedString, $"Expected string: {expectedString}{Environment.NewLine} " + @@ -112,7 +114,7 @@ public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificIte } [TestCase("scope='file:'")] - public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString) + public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); @@ -130,7 +132,7 @@ public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereR "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " + "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:'")] public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString( - string userSearchString, string expectedString) + string userSearchString, string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); @@ -145,18 +147,19 @@ public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShould } [TestCase] - public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearch() + public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearch() { // Given var searchManager = new SearchManager(new Settings(), new PluginInitContext()); - + // When - var results = searchManager.TopLevelDirectorySearchBehaviourAsync( - MethodWindowsIndexSearchReturnsZeroResults, - MethodDirectoryInfoClassSearchReturnsTwoResults, - false, + var results = await searchManager.TopLevelDirectorySearchBehaviourAsync( + MethodWindowsIndexSearchReturnsZeroResultsAsync, + MethodDirectoryInfoClassSearchReturnsTwoResults, + false, new Query(), - "string not used"); + "string not used", + default); // Then Assert.IsTrue(results.Count == 2, @@ -165,18 +168,19 @@ public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMe } [TestCase] - public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearch() + public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearch() { // Given var searchManager = new SearchManager(new Settings(), new PluginInitContext()); // When - var results = searchManager.TopLevelDirectorySearchBehaviourAsync( - MethodWindowsIndexSearchReturnsZeroResults, + var results = await searchManager.TopLevelDirectorySearchBehaviourAsync( + MethodWindowsIndexSearchReturnsZeroResultsAsync, MethodDirectoryInfoClassSearchReturnsTwoResults, true, new Query(), - "string not used"); + "string not used", + default); // Then Assert.IsTrue(results.Count == 0, @@ -223,7 +227,7 @@ public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileConte var query = new Query { ActionKeyword = "doc:", Search = "search term" }; var searchManager = new SearchManager(new Settings(), new PluginInitContext()); - + // When var result = searchManager.IsFileContentSearch(query.ActionKeyword); @@ -250,7 +254,7 @@ public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString( $"Actual check result is {result} {Environment.NewLine}"); } - + [TestCase(@"C:\SomeFolder\SomeApp", true, @"C:\SomeFolder\")] [TestCase(@"C:\SomeFolder\SomeApp\SomeFile", true, @"C:\SomeFolder\SomeApp\")] [TestCase(@"C:\NonExistentFolder\SomeApp", false, "")] @@ -294,7 +298,7 @@ public void WhenGivenAPath_ThenShouldReturnThePreviousDirectoryPathIfIncompleteO [TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' " + "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND " + "scope='file:c:\\SomeFolder'")] - public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString) + public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); @@ -308,7 +312,7 @@ public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQuery $"Actual string was: {resultString}{Environment.NewLine}"); } - [TestCase("c:\\somefolder\\>somefile","*somefile*")] + [TestCase("c:\\somefolder\\>somefile", "*somefile*")] [TestCase("c:\\somefolder\\somefile", "somefile*")] [TestCase("c:\\somefolder\\", "*")] public void GivenDirectoryInfoSearch_WhenSearchPatternHotKeyIsSearchAll_ThenSearchCriteriaShouldUseCriteriaString(string path, string expectedString) From 29ab9db7ced28bb027c9809431c791f8824e305f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Wed, 6 Jan 2021 19:11:58 +0800 Subject: [PATCH 16/34] fix reloaddata not working and refactor Init code --- Flow.Launcher.Core/Plugin/PluginManager.cs | 59 +++++----------------- Flow.Launcher/PublicAPIInstance.cs | 6 +-- 2 files changed, 17 insertions(+), 48 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 0712908bf04..5ca6e0aed87 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -55,21 +55,12 @@ public static void Save() public static async Task ReloadData() { - await Task.WhenAll(AllPlugins.Select(plugin => + await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch { - { - switch (plugin) - { - case IReloadable p: - p.ReloadData(); // Sync reload means low time consuming - return Task.CompletedTask; - case IAsyncReloadable p: - return p.ReloadDataAsync(); - default: - throw new ArgumentOutOfRangeException(); - } - } - })); + IReloadable p => Task.Run(p.ReloadData), + IAsyncReloadable p => p.ReloadDataAsync(), + _ => Task.CompletedTask, + }).ToArray()); } static PluginManager() @@ -106,38 +97,16 @@ public static async Task InitializePlugins(IPublicAPI api) { try { - long milliseconds; - - switch (pair.Plugin) + var milliseconds = pair.Plugin switch { - case IAsyncPlugin plugin: - milliseconds = await Stopwatch.DebugAsync( - $"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", - async delegate - { - await plugin.InitAsync(new PluginInitContext - { - CurrentPluginMetadata = pair.Metadata, - API = API - }); - }); - break; - case IPlugin plugin: - milliseconds = Stopwatch.Debug( - $"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", - () => - { - plugin.Init(new PluginInitContext - { - CurrentPluginMetadata = pair.Metadata, - API = API - }); - }); - break; - default: - throw new ArgumentException(); - } - + IAsyncPlugin plugin + => await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", + () => plugin.InitAsync(new PluginInitContext(pair.Metadata, API))), + IPlugin plugin + => Stopwatch.Debug($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", + () => plugin.Init(new PluginInitContext(pair.Metadata, API))), + _ => throw new ArgumentException(), + }; pair.Metadata.InitTime += milliseconds; Log.Info( $"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index bcf147be7ea..17673a62afa 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -78,9 +78,9 @@ public void SaveAppAllSettings() ImageLoader.Save(); } - public async Task ReloadAllPluginData() + public Task ReloadAllPluginData() { - await PluginManager.ReloadData(); + return PluginManager.ReloadData(); } public void ShowMsg(string title, string subTitle = "", string iconPath = "") @@ -92,7 +92,7 @@ public void ShowMsg(string title, string subTitle, string iconPath, bool useMain { Application.Current.Dispatcher.Invoke(() => { - var msg = useMainWindowAsOwner ? new Msg {Owner = Application.Current.MainWindow} : new Msg(); + var msg = useMainWindowAsOwner ? new Msg { Owner = Application.Current.MainWindow } : new Msg(); msg.Show(title, subTitle, iconPath); }); } From c129b7b034d59f9886b5dfa6f6e10eda7549fe87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Wed, 6 Jan 2021 19:13:42 +0800 Subject: [PATCH 17/34] Add constrctor for PluginInitContext --- Flow.Launcher.Plugin/PluginInitContext.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Flow.Launcher.Plugin/PluginInitContext.cs b/Flow.Launcher.Plugin/PluginInitContext.cs index 49366a5c618..d42c05d0297 100644 --- a/Flow.Launcher.Plugin/PluginInitContext.cs +++ b/Flow.Launcher.Plugin/PluginInitContext.cs @@ -4,6 +4,12 @@ namespace Flow.Launcher.Plugin { public class PluginInitContext { + public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api) + { + CurrentPluginMetadata = currentPluginMetadata; + API = api; + } + public PluginMetadata CurrentPluginMetadata { get; internal set; } /// From 63a9e03d897c4604679d2f921262a0a34ecfdb17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Wed, 6 Jan 2021 19:33:55 +0800 Subject: [PATCH 18/34] format QueryForPlugin code --- Flow.Launcher.Core/Plugin/PluginManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 5ca6e0aed87..5a5ba197107 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -176,8 +176,8 @@ public static async Task> QueryForPlugin(PluginPair pair, Query que async () => results = await plugin.QueryAsync(query, token).ConfigureAwait(false)); break; case IPlugin plugin: - await Task.Run(() => milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", () => - results = plugin.Query(query)), token).ConfigureAwait(false); + await Task.Run(() => milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", + () => results = plugin.Query(query)), token).ConfigureAwait(false); break; default: throw new ArgumentOutOfRangeException(); @@ -192,6 +192,7 @@ public static async Task> QueryForPlugin(PluginPair pair, Query que } catch (OperationCanceledException) { + // null will be fine since the results will only be added into queue if the token hasn't been cancelled return results = null; } catch (Exception e) @@ -199,7 +200,6 @@ public static async Task> QueryForPlugin(PluginPair pair, Query que Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e); } - // null will be fine since the results will only be added into queue if the token hasn't been cancelled return results; } From 8b602ce0478901f0fa699ec64d1096a1eaef10f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Thu, 7 Jan 2021 09:42:19 +0800 Subject: [PATCH 19/34] add default constructor for plugin init context to solve error in testing --- Flow.Launcher.Plugin/PluginInitContext.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Flow.Launcher.Plugin/PluginInitContext.cs b/Flow.Launcher.Plugin/PluginInitContext.cs index d42c05d0297..04f20e9846c 100644 --- a/Flow.Launcher.Plugin/PluginInitContext.cs +++ b/Flow.Launcher.Plugin/PluginInitContext.cs @@ -4,6 +4,10 @@ namespace Flow.Launcher.Plugin { public class PluginInitContext { + public PluginInitContext() + { + } + public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api) { CurrentPluginMetadata = currentPluginMetadata; From d0d938b92adb32b55487cdb046f5c4fb7a69d38c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Thu, 7 Jan 2021 11:04:07 +0800 Subject: [PATCH 20/34] Add Cancellationtoken for Http.cs --- Flow.Launcher.Infrastructure/Http/Http.cs | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index 8e2832690e4..173be842260 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -8,6 +8,7 @@ using Flow.Launcher.Infrastructure.UserSettings; using System; using System.ComponentModel; +using System.Threading; namespace Flow.Launcher.Infrastructure.Http { @@ -94,17 +95,25 @@ public static async Task Download([NotNull] string url, [NotNull] string filePat /// When supposing the result is long and large, try using GetStreamAsync to avoid reading as string /// /// - /// - public static Task GetAsync([NotNull] string url) + /// The Http result as string. Null if cancellation requested + public static Task GetAsync([NotNull] string url, CancellationToken token = default) { Log.Debug($"|Http.Get|Url <{url}>"); - return GetAsync(new Uri(url.Replace("#", "%23"))); + return GetAsync(new Uri(url.Replace("#", "%23")), token); } - public static async Task GetAsync([NotNull] Uri url) + /// + /// + /// + /// + /// + /// The Http result as string. Null if cancellation requested + public static async Task GetAsync([NotNull] Uri url, CancellationToken token = default) { Log.Debug($"|Http.Get|Url <{url}>"); - using var response = await client.GetAsync(url); + using var response = await client.GetAsync(url, token); + if (token.IsCancellationRequested) + return null; var content = await response.Content.ReadAsStringAsync(); if (response.StatusCode == HttpStatusCode.OK) { @@ -122,10 +131,12 @@ public static async Task GetAsync([NotNull] Uri url) /// /// /// - public static async Task GetStreamAsync([NotNull] string url) + public static async Task GetStreamAsync([NotNull] string url, CancellationToken token = default) { Log.Debug($"|Http.Get|Url <{url}>"); - var response = await client.GetAsync(url); + var response = await client.GetAsync(url, token); + if (token.IsCancellationRequested) + return Stream.Null; return await response.Content.ReadAsStreamAsync(); } } From a4edbc2cb9a97f7a82b51fca4f0b3fe9c5faf642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Thu, 7 Jan 2021 11:09:08 +0800 Subject: [PATCH 21/34] Move WebSearch to Async model --- .../Flow.Launcher.Plugin.WebSearch/Main.cs | 86 +++++++++---------- .../SuggestionSources/Baidu.cs | 5 +- .../SuggestionSources/Bing.cs | 7 +- .../SuggestionSources/Google.cs | 5 +- .../SuggestionSources/SuggestionSource.cs | 3 +- 5 files changed, 53 insertions(+), 53 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs index 3c4d4c67dbf..a573674ec27 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; @@ -13,14 +14,12 @@ namespace Flow.Launcher.Plugin.WebSearch { - public class Main : IPlugin, ISettingProvider, IPluginI18n, ISavable, IResultUpdated + public class Main : IAsyncPlugin, ISettingProvider, IPluginI18n, ISavable, IResultUpdated { private PluginInitContext _context; private readonly Settings _settings; private readonly SettingsViewModel _viewModel; - private CancellationTokenSource _updateSource; - private CancellationToken _updateToken; internal const string Images = "Images"; internal static string DefaultImagesDirectory; @@ -33,7 +32,7 @@ public void Save() _viewModel.Save(); } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { if (FilesFolders.IsLocationPathString(query.Search)) return new List(); @@ -41,11 +40,7 @@ public List Query(Query query) var searchSourceList = new List(); var results = new List(); - _updateSource?.Cancel(); - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; - - _settings.SearchSources.Where(o => (o.ActionKeyword == query.ActionKeyword || o.ActionKeyword == SearchSourceGlobalPluginWildCardSign) + _settings.SearchSources.Where(o => (o.ActionKeyword == query.ActionKeyword || o.ActionKeyword == SearchSourceGlobalPluginWildCardSign) && o.Enabled) .ToList() .ForEach(x => searchSourceList.Add(x)); @@ -94,49 +89,45 @@ public List Query(Query query) }; results.Add(result); - ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs - { - Results = results, - Query = query - }); - - UpdateResultsFromSuggestion(results, keyword, subtitle, searchSource, query); } + + ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs + { + Results = results, + Query = query + }); + + await UpdateResultsFromSuggestionAsync(results, keyword, subtitle, searchSource, query, token).ConfigureAwait(false); } } return results; } - private void UpdateResultsFromSuggestion(List results, string keyword, string subtitle, - SearchSource searchSource, Query query) + private async Task UpdateResultsFromSuggestionAsync(List results, string keyword, string subtitle, + SearchSource searchSource, Query query, CancellationToken token) { if (_settings.EnableSuggestion) { - const int waittime = 300; - var task = Task.Run(async () => - { - var suggestions = await Suggestions(keyword, subtitle, searchSource); - results.AddRange(suggestions); - }, _updateToken); + var suggestions = await SuggestionsAsync(keyword, subtitle, searchSource, token).ConfigureAwait(false); + results.AddRange(suggestions); - if (!task.Wait(waittime)) + token.ThrowIfCancellationRequested(); + + ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs { - task.ContinueWith(_ => ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs - { - Results = results, - Query = query - }), _updateToken); - } + Results = results, + Query = query + }); } } - private async Task> Suggestions(string keyword, string subtitle, SearchSource searchSource) + private async Task> SuggestionsAsync(string keyword, string subtitle, SearchSource searchSource, CancellationToken token) { var source = _settings.SelectedSuggestion; if (source != null) { - var suggestions = await source.Suggestions(keyword); + var suggestions = await source.Suggestions(keyword, token); var resultsFromSuggestion = suggestions.Select(o => new Result { Title = o, @@ -169,19 +160,24 @@ public Main() _settings = _viewModel.Settings; } - public void Init(PluginInitContext context) + public Task InitAsync(PluginInitContext context) { - _context = context; - var pluginDirectory = _context.CurrentPluginMetadata.PluginDirectory; - var bundledImagesDirectory = Path.Combine(pluginDirectory, Images); - - // Default images directory is in the WebSearch's application folder - DefaultImagesDirectory = Path.Combine(pluginDirectory, Images); - Helper.ValidateDataDirectory(bundledImagesDirectory, DefaultImagesDirectory); - - // Custom images directory is in the WebSearch's data location folder - var name = Path.GetFileNameWithoutExtension(_context.CurrentPluginMetadata.ExecuteFileName); - CustomImagesDirectory = Path.Combine(DataLocation.PluginSettingsDirectory, name, "CustomIcons"); + return Task.Run(Init); + + void Init() + { + _context = context; + var pluginDirectory = _context.CurrentPluginMetadata.PluginDirectory; + var bundledImagesDirectory = Path.Combine(pluginDirectory, Images); + + // Default images directory is in the WebSearch's application folder + DefaultImagesDirectory = Path.Combine(pluginDirectory, Images); + Helper.ValidateDataDirectory(bundledImagesDirectory, DefaultImagesDirectory); + + // Custom images directory is in the WebSearch's data location folder + var name = Path.GetFileNameWithoutExtension(_context.CurrentPluginMetadata.ExecuteFileName); + CustomImagesDirectory = Path.Combine(DataLocation.PluginSettingsDirectory, name, "CustomIcons"); + }; } #region ISettingProvider Members diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs index 6772acf8256..0c6d95b4ffa 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs @@ -9,6 +9,7 @@ using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Logger; using System.Net.Http; +using System.Threading; namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources { @@ -16,14 +17,14 @@ public class Baidu : SuggestionSource { private readonly Regex _reg = new Regex("window.baidu.sug\\((.*)\\)"); - public override async Task> Suggestions(string query) + public override async Task> Suggestions(string query, CancellationToken token) { string result; try { const string api = "http://suggestion.baidu.com/su?json=1&wd="; - result = await Http.GetAsync(api + Uri.EscapeUriString(query)).ConfigureAwait(false); + result = await Http.GetAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false); } catch (HttpRequestException e) { diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs index 9c4746711e5..991e4526737 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs @@ -9,19 +9,20 @@ using System.Threading.Tasks; using System.Text.Json; using System.Linq; +using System.Threading; namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources { class Bing : SuggestionSource { - public override async Task> Suggestions(string query) + public override async Task> Suggestions(string query, CancellationToken token) { Stream resultStream; try { const string api = "https://api.bing.com/qsonhs.aspx?q="; - resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query)).ConfigureAwait(false); + resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false); } catch (HttpRequestException e) { @@ -29,7 +30,7 @@ public override async Task> Suggestions(string query) return new List(); } - if (resultStream.Length == 0) return new List(); + if (resultStream.Length == 0) return new List(); // this handles the cancellation JsonElement json; try diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs index 5b9538091b9..d034679afd0 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs @@ -8,18 +8,19 @@ using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Logger; using System.Net.Http; +using System.Threading; namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources { public class Google : SuggestionSource { - public override async Task> Suggestions(string query) + public override async Task> Suggestions(string query, CancellationToken token) { string result; try { const string api = "https://www.google.com/complete/search?output=chrome&q="; - result = await Http.GetAsync(api + Uri.EscapeUriString(query)).ConfigureAwait(false); + result = await Http.GetAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false); } catch (HttpRequestException e) { diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/SuggestionSource.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/SuggestionSource.cs index d6d89415f88..bf444a2f702 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/SuggestionSource.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/SuggestionSource.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources { public abstract class SuggestionSource { - public abstract Task> Suggestions(string query); + public abstract Task> Suggestions(string query, CancellationToken token); } } \ No newline at end of file From f72b716fb4a3b9de8dab6a336825b38bdc5ae338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Thu, 7 Jan 2021 11:20:14 +0800 Subject: [PATCH 22/34] use string.empty instead of null --- Flow.Launcher.Infrastructure/Http/Http.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index 173be842260..4abf0e8325d 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -113,7 +113,7 @@ public static async Task GetAsync([NotNull] Uri url, CancellationToken t Log.Debug($"|Http.Get|Url <{url}>"); using var response = await client.GetAsync(url, token); if (token.IsCancellationRequested) - return null; + return string.Empty; var content = await response.Content.ReadAsStringAsync(); if (response.StatusCode == HttpStatusCode.OK) { From a6609d6b2c86cc167ae15501494434294cb64dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Thu, 7 Jan 2021 21:37:45 +0800 Subject: [PATCH 23/34] Add Cancellationtoken for downloadasync Remove extra action in http for cancellation due to it will throw TaskCancelledExcpetion internally --- Flow.Launcher.Infrastructure/Http/Http.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index 4212a1ab2e6..5af50bc6dca 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -76,11 +76,11 @@ public static void UpdateProxy(ProxyProperty property) }; } - public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath) + public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default) { try { - using var response = await client.GetAsync(url); + using var response = await client.GetAsync(url, token); if (response.StatusCode == HttpStatusCode.OK) { await using var fileStream = new FileStream(filePath, FileMode.CreateNew); @@ -120,8 +120,6 @@ public static async Task GetAsync([NotNull] Uri url, CancellationToken t { Log.Debug($"|Http.Get|Url <{url}>"); using var response = await client.GetAsync(url, token); - if (token.IsCancellationRequested) - return string.Empty; var content = await response.Content.ReadAsStringAsync(); if (response.StatusCode == HttpStatusCode.OK) { @@ -143,8 +141,6 @@ public static async Task GetStreamAsync([NotNull] string url, Cancellati { Log.Debug($"|Http.Get|Url <{url}>"); var response = await client.GetAsync(url, token); - if (token.IsCancellationRequested) - return Stream.Null; return await response.Content.ReadAsStreamAsync(); } } From 8a5f98a6a3b4b73e302c39b2c77534fa32174151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Thu, 7 Jan 2021 21:38:21 +0800 Subject: [PATCH 24/34] Manually handling TaskCancelledException in search suggestion to aviod stunt in debugging --- .../Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs | 4 ++++ .../Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs | 4 ++++ .../SuggestionSources/Google.cs | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs index 0c6d95b4ffa..dcc2b9eef98 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Baidu.cs @@ -26,6 +26,10 @@ public override async Task> Suggestions(string query, CancellationT const string api = "http://suggestion.baidu.com/su?json=1&wd="; result = await Http.GetAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false); } + catch (TaskCanceledException) + { + return null; + } catch (HttpRequestException e) { Log.Exception("|Baidu.Suggestions|Can't get suggestion from baidu", e); diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs index 991e4526737..47bd016c180 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs @@ -24,6 +24,10 @@ public override async Task> Suggestions(string query, CancellationT const string api = "https://api.bing.com/qsonhs.aspx?q="; resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false); } + catch (TaskCanceledException) + { + return null; + } catch (HttpRequestException e) { Log.Exception("|Bing.Suggestions|Can't get suggestion from Bing", e); diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs index d034679afd0..1182013b6cd 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs @@ -22,6 +22,10 @@ public override async Task> Suggestions(string query, CancellationT const string api = "https://www.google.com/complete/search?output=chrome&q="; result = await Http.GetAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false); } + catch (TaskCanceledException) + { + return null; + } catch (HttpRequestException e) { Log.Exception("|Google.Suggestions|Can't get suggestion from google", e); From 86a9cf31c5e67b61d9cb5fb74ba49e02f3b389f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Thu, 7 Jan 2021 21:41:05 +0800 Subject: [PATCH 25/34] Optimize Websearch code --- .../Flow.Launcher.Plugin.WebSearch/Main.cs | 110 +++++++++--------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs index a573674ec27..f76e2811299 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs @@ -40,65 +40,63 @@ public async Task> QueryAsync(Query query, CancellationToken token) var searchSourceList = new List(); var results = new List(); - _settings.SearchSources.Where(o => (o.ActionKeyword == query.ActionKeyword || o.ActionKeyword == SearchSourceGlobalPluginWildCardSign) - && o.Enabled) - .ToList() - .ForEach(x => searchSourceList.Add(x)); - - if (searchSourceList.Any()) + foreach (SearchSource searchSource in _settings.SearchSources.Where(o => (o.ActionKeyword == query.ActionKeyword || + o.ActionKeyword == SearchSourceGlobalPluginWildCardSign) + && o.Enabled)) { - foreach (SearchSource searchSource in searchSourceList) - { - string keyword = string.Empty; - keyword = searchSource.ActionKeyword == SearchSourceGlobalPluginWildCardSign ? query.ToString() : query.Search; - var title = keyword; - string subtitle = _context.API.GetTranslation("flowlauncher_plugin_websearch_search") + " " + searchSource.Title; + string keyword = string.Empty; + keyword = searchSource.ActionKeyword == SearchSourceGlobalPluginWildCardSign ? query.ToString() : query.Search; + var title = keyword; + string subtitle = _context.API.GetTranslation("flowlauncher_plugin_websearch_search") + " " + searchSource.Title; - if (string.IsNullOrEmpty(keyword)) + if (string.IsNullOrEmpty(keyword)) + { + var result = new Result { - var result = new Result - { - Title = subtitle, - SubTitle = string.Empty, - IcoPath = searchSource.IconPath - }; - results.Add(result); - } - else + Title = subtitle, + SubTitle = string.Empty, + IcoPath = searchSource.IconPath + }; + results.Add(result); + } + else + { + var result = new Result { - var result = new Result + Title = title, + SubTitle = subtitle, + Score = 6, + IcoPath = searchSource.IconPath, + ActionKeywordAssigned = searchSource.ActionKeyword == SearchSourceGlobalPluginWildCardSign ? string.Empty : searchSource.ActionKeyword, + Action = c => { - Title = title, - SubTitle = subtitle, - Score = 6, - IcoPath = searchSource.IconPath, - ActionKeywordAssigned = searchSource.ActionKeyword == SearchSourceGlobalPluginWildCardSign ? string.Empty : searchSource.ActionKeyword, - Action = c => + if (_settings.OpenInNewBrowser) { - if (_settings.OpenInNewBrowser) - { - searchSource.Url.Replace("{q}", Uri.EscapeDataString(keyword)).NewBrowserWindow(_settings.BrowserPath); - } - else - { - searchSource.Url.Replace("{q}", Uri.EscapeDataString(keyword)).NewTabInBrowser(_settings.BrowserPath); - } - - return true; + searchSource.Url.Replace("{q}", Uri.EscapeDataString(keyword)).NewBrowserWindow(_settings.BrowserPath); + } + else + { + searchSource.Url.Replace("{q}", Uri.EscapeDataString(keyword)).NewTabInBrowser(_settings.BrowserPath); } - }; - - results.Add(result); - } - ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs - { - Results = results, - Query = query - }); + return true; + } + }; - await UpdateResultsFromSuggestionAsync(results, keyword, subtitle, searchSource, query, token).ConfigureAwait(false); + results.Add(result); } + + ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs + { + Results = results, + Query = query + }); + + await UpdateResultsFromSuggestionAsync(results, keyword, subtitle, searchSource, query, token).ConfigureAwait(false); + + if (token.IsCancellationRequested) + return null; + } return results; @@ -110,15 +108,13 @@ private async Task UpdateResultsFromSuggestionAsync(List results, string if (_settings.EnableSuggestion) { var suggestions = await SuggestionsAsync(keyword, subtitle, searchSource, token).ConfigureAwait(false); + if (token.IsCancellationRequested || !suggestions.Any()) + return; + + results.AddRange(suggestions); token.ThrowIfCancellationRequested(); - - ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs - { - Results = results, - Query = query - }); } } @@ -128,6 +124,10 @@ private async Task> SuggestionsAsync(string keyword, string if (source != null) { var suggestions = await source.Suggestions(keyword, token); + + if (token.IsCancellationRequested) + return null; + var resultsFromSuggestion = suggestions.Select(o => new Result { Title = o, From 919d5d51ab5b8e3ffb15f10f74586e0d26681e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Thu, 7 Jan 2021 21:47:31 +0800 Subject: [PATCH 26/34] add using for Bing search source --- .../Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs index 47bd016c180..e6b0438d1ec 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs @@ -39,7 +39,8 @@ public override async Task> Suggestions(string query, CancellationT JsonElement json; try { - json = (await JsonDocument.ParseAsync(resultStream)).RootElement.GetProperty("AS"); + using (resultStream) + json = (await JsonDocument.ParseAsync(resultStream)).RootElement.GetProperty("AS"); } catch (JsonException e) { From 1003ce416093d2320892c75f50b2082a522cc2e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Thu, 7 Jan 2021 21:48:55 +0800 Subject: [PATCH 27/34] move using to more specific place in Google.cs --- .../SuggestionSources/Google.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs index 17153b9a75a..5c784a7027f 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs @@ -32,24 +32,24 @@ public override async Task> Suggestions(string query, CancellationT return new List(); } - using (resultStream) + + if (resultStream.Length == 0) return new List(); + JsonDocument json; + try { - if (resultStream.Length == 0) return new List(); - JsonDocument json; - try - { + using (resultStream) json = await JsonDocument.ParseAsync(resultStream); - } - catch (JsonException e) - { - Log.Exception("|Google.Suggestions|can't parse suggestions", e); - return new List(); - } + } + catch (JsonException e) + { + Log.Exception("|Google.Suggestions|can't parse suggestions", e); + return new List(); + } - var results = json?.RootElement.EnumerateArray().ElementAt(1); + var results = json?.RootElement.EnumerateArray().ElementAt(1); + + return results?.EnumerateArray().Select(o => o.GetString()).ToList() ?? new List(); - return results?.EnumerateArray().Select(o => o.GetString()).ToList() ?? new List(); - } } public override string ToString() From c939924ec8ec2e28e6eccf7422ac7bf9eb5f3e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Thu, 7 Jan 2021 21:54:09 +0800 Subject: [PATCH 28/34] optimize code in searchsuggestions --- .../SuggestionSources/Bing.cs | 16 +++++----------- .../SuggestionSources/Google.cs | 17 ++++++----------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs index e6b0438d1ec..81725c3f2a5 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs @@ -17,12 +17,15 @@ class Bing : SuggestionSource { public override async Task> Suggestions(string query, CancellationToken token) { - Stream resultStream; + JsonElement json; try { const string api = "https://api.bing.com/qsonhs.aspx?q="; - resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false); + using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false); + if (resultStream.Length == 0) return new List(); // this handles the cancellation + json = (await JsonDocument.ParseAsync(resultStream, cancellationToken: token)).RootElement.GetProperty("AS"); + } catch (TaskCanceledException) { @@ -33,15 +36,6 @@ public override async Task> Suggestions(string query, CancellationT Log.Exception("|Bing.Suggestions|Can't get suggestion from Bing", e); return new List(); } - - if (resultStream.Length == 0) return new List(); // this handles the cancellation - - JsonElement json; - try - { - using (resultStream) - json = (await JsonDocument.ParseAsync(resultStream)).RootElement.GetProperty("AS"); - } catch (JsonException e) { Log.Exception("|Bing.Suggestions|can't parse suggestions", e); diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs index 5c784a7027f..b150d26c123 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs @@ -16,11 +16,15 @@ public class Google : SuggestionSource { public override async Task> Suggestions(string query, CancellationToken token) { - Stream resultStream; + JsonDocument json; + try { const string api = "https://www.google.com/complete/search?output=chrome&q="; - resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query)).ConfigureAwait(false); + using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query)).ConfigureAwait(false); + if (resultStream.Length == 0) return new List(); + json = await JsonDocument.ParseAsync(resultStream); + } catch (TaskCanceledException) { @@ -31,15 +35,6 @@ public override async Task> Suggestions(string query, CancellationT Log.Exception("|Google.Suggestions|Can't get suggestion from google", e); return new List(); } - - - if (resultStream.Length == 0) return new List(); - JsonDocument json; - try - { - using (resultStream) - json = await JsonDocument.ParseAsync(resultStream); - } catch (JsonException e) { Log.Exception("|Google.Suggestions|can't parse suggestions", e); From 92be6fd3ddd1024e320aa6d0b98ab1000fe7e5df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=BC=98=E9=9F=AC?= Date: Sat, 9 Jan 2021 12:01:08 +0800 Subject: [PATCH 29/34] Use Task.Yield to avoid using Parallel.For --- Flow.Launcher/ViewModel/MainViewModel.cs | 59 +++++++++++------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index da977f11f94..c3e32731baa 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -86,7 +86,7 @@ private void RegisterResultsUpdatedEvent() { foreach (var pair in PluginManager.GetPluginsForInterface()) { - var plugin = (IResultUpdated)pair.Plugin; + var plugin = (IResultUpdated) pair.Plugin; plugin.ResultsUpdated += (s, e) => { Task.Run(() => @@ -113,25 +113,13 @@ private void InitializeKeyCommands() } }); - SelectNextItemCommand = new RelayCommand(_ => - { - SelectedResults.SelectNextResult(); - }); + SelectNextItemCommand = new RelayCommand(_ => { SelectedResults.SelectNextResult(); }); - SelectPrevItemCommand = new RelayCommand(_ => - { - SelectedResults.SelectPrevResult(); - }); + SelectPrevItemCommand = new RelayCommand(_ => { SelectedResults.SelectPrevResult(); }); - SelectNextPageCommand = new RelayCommand(_ => - { - SelectedResults.SelectNextPage(); - }); + SelectNextPageCommand = new RelayCommand(_ => { SelectedResults.SelectNextPage(); }); - SelectPrevPageCommand = new RelayCommand(_ => - { - SelectedResults.SelectPrevPage(); - }); + SelectPrevPageCommand = new RelayCommand(_ => { SelectedResults.SelectPrevPage(); }); SelectFirstResultCommand = new RelayCommand(_ => SelectedResults.SelectFirstResult()); @@ -209,6 +197,7 @@ private void InitializeKeyCommands() public ResultsViewModel History { get; private set; } private string _queryText; + public string QueryText { get { return _queryText; } @@ -229,10 +218,12 @@ public void ChangeQueryText(string queryText) QueryTextCursorMovedToEnd = true; QueryText = queryText; } + public bool LastQuerySelected { get; set; } public bool QueryTextCursorMovedToEnd { get; set; } private ResultsViewModel _selectedResults; + private ResultsViewModel SelectedResults { get { return _selectedResults; } @@ -264,6 +255,7 @@ private ResultsViewModel SelectedResults QueryText = string.Empty; } } + _selectedResults.Visbility = Visibility.Visible; } } @@ -324,7 +316,7 @@ private void QueryContextMenu() var filtered = results.Where ( r => StringMatcher.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() - || StringMatcher.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet() + || StringMatcher.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet() ).ToList(); ContextMenu.AddResults(filtered, id); } @@ -351,7 +343,7 @@ private void QueryHistory() 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; @@ -397,7 +389,8 @@ private void QueryResults() _lastQuery = query; 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 + { + // 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 (currentUpdateSource == _updateSource && _isQueryRunning) { ProgressBarVisibility = Visibility.Visible; @@ -410,17 +403,14 @@ private void QueryResults() // so looping will stop once it was cancelled Task[] tasks = new Task[plugins.Count]; - var parallelOptions = new ParallelOptions { CancellationToken = currentCancellationToken }; try { - Parallel.For(0, plugins.Count, parallelOptions, i => + for (var i = 0; i < plugins.Count; i++) { if (!plugins[i].Metadata.Disabled) - { tasks[i] = QueryTask(plugins[i], query, currentCancellationToken); - } else tasks[i] = Task.CompletedTask; // Avoid Null - }); + } // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first await Task.WhenAll(tasks); @@ -434,20 +424,25 @@ private void QueryResults() // until the end of all querying _isQueryRunning = false; if (!currentCancellationToken.IsCancellationRequested) - { // update to hidden if this is still the current query + { + // update to hidden if this is still the current query ProgressBarVisibility = Visibility.Hidden; } // Local Function async Task QueryTask(PluginPair plugin, Query query, CancellationToken token) { + // 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, token); if (!currentCancellationToken.IsCancellationRequested) UpdateResultView(results, plugin.Metadata, query); } - - }, currentCancellationToken).ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception), - TaskContinuationOptions.OnlyOnFaulted); + }, currentCancellationToken).ContinueWith( + t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception), + TaskContinuationOptions.OnlyOnFaulted); } } else @@ -514,6 +509,7 @@ private Result ContextMenuTopMost(Result result) } }; } + return menu; } @@ -559,6 +555,7 @@ private bool HistorySelected() var selected = SelectedResults == History; return selected; } + #region Hotkey private void SetHotkey(string hotkeyStr, EventHandler action) @@ -577,7 +574,8 @@ private void SetHotkey(HotkeyModel hotkey, EventHandler action) catch (Exception) { string errorMsg = - string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr); + string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), + hotkeyStr); MessageBox.Show(errorMsg); } } @@ -627,7 +625,6 @@ private void OnHotkey(object sender, HotkeyEventArgs e) { if (!ShouldIgnoreHotkeys()) { - if (_settings.LastQueryMode == LastQueryMode.Empty) { ChangeQueryText(string.Empty); From 763b51858fc094eb158b98ef023f9ccb24d8d44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Thu, 14 Jan 2021 12:24:41 +0800 Subject: [PATCH 30/34] Add comment in IPublic, IAsyncPlugin, IReloadable, IAsyncReloadable --- Flow.Launcher.Plugin/IAsyncPlugin.cs | 15 +++++++++++++ Flow.Launcher.Plugin/IPlugin.cs | 22 ++++++++++++++++++- .../Interfaces/IAsyncReloadable.cs | 11 ++++++++++ .../Interfaces/IReloadable.cs | 6 ++++- 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Plugin/IAsyncPlugin.cs b/Flow.Launcher.Plugin/IAsyncPlugin.cs index 36f098e7d64..3e1a086bce3 100644 --- a/Flow.Launcher.Plugin/IAsyncPlugin.cs +++ b/Flow.Launcher.Plugin/IAsyncPlugin.cs @@ -4,9 +4,24 @@ namespace Flow.Launcher.Plugin { + /// + /// Asynchronous Plugin Model for Flow Launcher + /// public interface IAsyncPlugin { + /// + /// Asynchronous Querying + /// + /// Query to search + /// Cancel when querying job is obsolete + /// Task> QueryAsync(Query query, CancellationToken token); + + /// + /// Initialize plugin asynchrously (will still wait finish to continue) + /// + /// + /// Task InitAsync(PluginInitContext context); } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin/IPlugin.cs b/Flow.Launcher.Plugin/IPlugin.cs index 4cc6d8d40bf..41c61a6086c 100644 --- a/Flow.Launcher.Plugin/IPlugin.cs +++ b/Flow.Launcher.Plugin/IPlugin.cs @@ -2,10 +2,30 @@ namespace Flow.Launcher.Plugin { + /// + /// Synchronous Plugin Model for Flow Launcher + /// + /// If you assume that Querying or Init method require IO transmission + /// or CPU Intense Job (performing better with cancellation), please try IAsyncPlugin interface + /// + /// public interface IPlugin { + /// + /// Querying when user's search changes + /// + /// This method will be called within a Task.Run, + /// so please avoid synchrously wait for long. + /// + /// + /// Query to search + /// List Query(Query query); - + + /// + /// Initialize plugin + /// + /// void Init(PluginInitContext context); } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs index 9c922f6674d..434d83646d3 100644 --- a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs @@ -2,6 +2,17 @@ namespace Flow.Launcher.Plugin { + /// + /// This interface is to indicate and allow plugins to asyncronously reload their + /// in memory data cache or other mediums when user makes a new change + /// that is not immediately captured. For example, for BrowserBookmark and Program + /// plugin does not automatically detect when a user added a new bookmark or program, + /// so this interface's function is exposed to allow user manually do the reloading after + /// those new additions. + /// + /// The command that allows user to manual reload is exposed via Plugin.Sys, and + /// it will call the plugins that have implemented this interface. + /// public interface IAsyncReloadable { Task ReloadDataAsync(); diff --git a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs index 29b3c15c9b1..d9160b0ea6f 100644 --- a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs +++ b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs @@ -1,7 +1,7 @@ namespace Flow.Launcher.Plugin { /// - /// This interface is to indicate and allow plugins to reload their + /// This interface is to indicate and allow plugins to synchronously reload their /// in memory data cache or other mediums when user makes a new change /// that is not immediately captured. For example, for BrowserBookmark and Program /// plugin does not automatically detect when a user added a new bookmark or program, @@ -10,6 +10,10 @@ /// /// The command that allows user to manual reload is exposed via Plugin.Sys, and /// it will call the plugins that have implemented this interface. + /// + /// + /// If requiring reloading data asynchrouly, please try IAsyncReloadable + /// /// public interface IReloadable { From 114c12bdaf694dd2b5f98fc59b14ff1d1b84c0ad Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Sun, 17 Jan 2021 18:47:19 +1100 Subject: [PATCH 31/34] formatting and description --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- Flow.Launcher.Plugin/IAsyncPlugin.cs | 4 ++++ Flow.Launcher.Plugin/IPlugin.cs | 4 ++-- Flow.Launcher.Plugin/Interfaces/IReloadable.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 9 +++++++-- .../Search/DirectoryInfo/DirectoryInfoSearch.cs | 1 - 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 5a5ba197107..26167e945ac 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -320,4 +320,4 @@ public static void ReplaceActionKeyword(string id, string oldActionKeyword, stri } } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/IAsyncPlugin.cs b/Flow.Launcher.Plugin/IAsyncPlugin.cs index 3e1a086bce3..c6f11aead64 100644 --- a/Flow.Launcher.Plugin/IAsyncPlugin.cs +++ b/Flow.Launcher.Plugin/IAsyncPlugin.cs @@ -12,6 +12,10 @@ public interface IAsyncPlugin /// /// Asynchronous Querying /// + /// + /// If the Querying or Init method requires high IO transmission + /// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncPlugin interface + /// /// Query to search /// Cancel when querying job is obsolete /// diff --git a/Flow.Launcher.Plugin/IPlugin.cs b/Flow.Launcher.Plugin/IPlugin.cs index 41c61a6086c..208f3021b66 100644 --- a/Flow.Launcher.Plugin/IPlugin.cs +++ b/Flow.Launcher.Plugin/IPlugin.cs @@ -5,8 +5,8 @@ namespace Flow.Launcher.Plugin /// /// Synchronous Plugin Model for Flow Launcher /// - /// If you assume that Querying or Init method require IO transmission - /// or CPU Intense Job (performing better with cancellation), please try IAsyncPlugin interface + /// If the Querying or Init method requires high IO transmission + /// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncPlugin interface /// /// public interface IPlugin diff --git a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs index d9160b0ea6f..31611519cd1 100644 --- a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs +++ b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs @@ -12,7 +12,7 @@ /// it will call the plugins that have implemented this interface. /// /// - /// If requiring reloading data asynchrouly, please try IAsyncReloadable + /// If requiring reloading data asynchronously, please use the IAsyncReloadable interface /// /// public interface IReloadable diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index bcb25720448..a062f59dc01 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -408,8 +408,13 @@ private void QueryResults() for (var i = 0; i < plugins.Count; i++) { if (!plugins[i].Metadata.Disabled) + { tasks[i] = QueryTask(plugins[i], query, currentCancellationToken); - else tasks[i] = Task.CompletedTask; // Avoid Null + } + else + { + tasks[i] = Task.CompletedTask; // Avoid Null + } } // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first @@ -429,7 +434,7 @@ private void QueryResults() ProgressBarVisibility = Visibility.Hidden; } - // Local Function + // Local function async Task QueryTask(PluginPair plugin, Query query, CancellationToken token) { // Since it is wrapped within a Task.Run, the synchronous context is null diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs index 3253b7a7b75..88d7d6927c1 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Windows; namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo { From d741a98112a50b81cf4551037b1d6bad6c5b0be7 Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Sun, 17 Jan 2021 18:47:43 +1100 Subject: [PATCH 32/34] fixed last index time during init --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 954c238a992..c283faa8346 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -97,22 +97,31 @@ await Task.Run(() => Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>"); }); + bool indexedWinApps = false; + bool indexedUWPApps = false; var a = Task.Run(() => { if (IsStartupIndexProgramsRequired || !_win32s.Any()) + { Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs); + indexedWinApps = true; + } }); var b = Task.Run(() => { if (IsStartupIndexProgramsRequired || !_uwps.Any()) + { Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexUwpPrograms); + indexedUWPApps = true; + } }); await Task.WhenAll(a, b); - _settings.LastIndexTime = DateTime.Today; + if (indexedWinApps && indexedUWPApps) + _settings.LastIndexTime = DateTime.Today; } public static void IndexWin32Programs() From 969265c24fb617f018ddf38285fb61ec1ac2274b Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Sun, 17 Jan 2021 19:05:28 +1100 Subject: [PATCH 33/34] fix formatting --- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 12 +++++++----- Plugins/Flow.Launcher.Plugin.Sys/Main.cs | 3 +-- .../SuggestionSources/Bing.cs | 6 +++++- .../SuggestionSources/Google.cs | 6 +++++- .../SuggestionSources/SuggestionSource.cs | 2 +- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index c283faa8346..d7413874be1 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -202,12 +202,14 @@ private void DisableProgram(IProgram programToDelete) return; if (_uwps.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) - _uwps.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier).FirstOrDefault().Enabled = - false; + _uwps.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier) + .FirstOrDefault() + .Enabled = false; if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) - _win32s.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier).FirstOrDefault().Enabled = - false; + _win32s.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier) + .FirstOrDefault() + .Enabled = false; _settings.DisabledProgramSources .Add( @@ -241,4 +243,4 @@ public async Task ReloadDataAsync() await IndexPrograms(); } } -} \ No newline at end of file +} diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs index 7ebab91a173..624fe05bc80 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs @@ -170,8 +170,7 @@ private List Commands() // http://www.pinvoke.net/default.aspx/shell32/SHEmptyRecycleBin.html // FYI, couldn't find documentation for this but if the recycle bin is already empty, it will return -2147418113 (0x8000FFFF (E_UNEXPECTED)) // 0 for nothing - var result = SHEmptyRecycleBin(new WindowInteropHelper(Application.Current.MainWindow).Handle, - 0); + var result = SHEmptyRecycleBin(new WindowInteropHelper(Application.Current.MainWindow).Handle, 0); if (result != (uint) HRESULT.S_OK && result != (uint) 0x8000FFFF) { MessageBox.Show($"Error emptying recycle bin, error code: {result}\n" + diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs index 81725c3f2a5..ffde2fda292 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs @@ -22,8 +22,12 @@ public override async Task> Suggestions(string query, CancellationT try { const string api = "https://api.bing.com/qsonhs.aspx?q="; + using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false); - if (resultStream.Length == 0) return new List(); // this handles the cancellation + + if (resultStream.Length == 0) + return new List(); // this handles the cancellation + json = (await JsonDocument.ParseAsync(resultStream, cancellationToken: token)).RootElement.GetProperty("AS"); } diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs index b150d26c123..c33ebd7e126 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs @@ -21,8 +21,12 @@ public override async Task> Suggestions(string query, CancellationT try { const string api = "https://www.google.com/complete/search?output=chrome&q="; + using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query)).ConfigureAwait(false); - if (resultStream.Length == 0) return new List(); + + if (resultStream.Length == 0) + return new List(); + json = await JsonDocument.ParseAsync(resultStream); } diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/SuggestionSource.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/SuggestionSource.cs index bf444a2f702..c58e61141ef 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/SuggestionSource.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/SuggestionSource.cs @@ -8,4 +8,4 @@ public abstract class SuggestionSource { public abstract Task> Suggestions(string query, CancellationToken token); } -} \ No newline at end of file +} From 539f4bf4c46ca574a50dbb3f924b1b5160da7084 Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Sun, 17 Jan 2021 19:10:26 +1100 Subject: [PATCH 34/34] add eol --- Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs | 2 +- Flow.Launcher.Core/Plugin/PluginsLoader.cs | 2 +- Flow.Launcher.Plugin/IAsyncPlugin.cs | 2 +- Flow.Launcher.Plugin/IPlugin.cs | 2 +- Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs index 1a1b17539aa..273698b8676 100644 --- a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs @@ -54,4 +54,4 @@ internal bool ExistsInReferencedPluginPackage(AssemblyName assemblyName) return referencedPluginPackageDependencyResolver.ResolveAssemblyToPath(assemblyName) != null; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index b18c07e3c6d..fcf17844598 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -183,4 +183,4 @@ public static IEnumerable ExecutablePlugins(IEnumerable Task InitAsync(PluginInitContext context); } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/IPlugin.cs b/Flow.Launcher.Plugin/IPlugin.cs index 208f3021b66..203dc9af736 100644 --- a/Flow.Launcher.Plugin/IPlugin.cs +++ b/Flow.Launcher.Plugin/IPlugin.cs @@ -28,4 +28,4 @@ public interface IPlugin /// void Init(PluginInitContext context); } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs index 434d83646d3..fc4ac471550 100644 --- a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs @@ -17,4 +17,4 @@ public interface IAsyncReloadable { Task ReloadDataAsync(); } -} \ No newline at end of file +}