Plugin Async ModelAdd Full Async model, including AsyncPlugin and AsyncReloadable

This commit is contained in:
张弘韬 2021-01-02 15:52:41 +08:00 committed by 弘韬 张
parent d60b873ef5
commit 3cd609377e
11 changed files with 210 additions and 84 deletions

View file

@ -3,6 +3,7 @@ using System.Collections.Concurrent;
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 @@ namespace Flow.Launcher.Core.Plugin
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
/// </summary>
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 @@ namespace Flow.Launcher.Core.Plugin
}
}
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 @@ namespace Flow.Launcher.Core.Plugin
/// Call initialize for all plugins
/// </summary>
/// <returns>return the list of failed to init plugins or null for none</returns>
public static void InitializePlugins(IPublicAPI api)
public static async Task InitializePlugins(IPublicAPI api)
{
API = api;
var failedPlugins = new ConcurrentQueue<PluginPair>();
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 @@ namespace Flow.Launcher.Core.Plugin
pair.Metadata.Disabled = true;
failedPlugins.Enqueue(pair);
}
});
}));
await Task.WhenAll(InitTasks);
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
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 @@ namespace Flow.Launcher.Core.Plugin
if (NonGlobalPlugins.ContainsKey(query.ActionKeyword))
{
var plugin = NonGlobalPlugins[query.ActionKeyword];
return new List<PluginPair> { plugin };
return new List<PluginPair> {plugin};
}
else
{
@ -146,25 +188,42 @@ namespace Flow.Launcher.Core.Plugin
}
}
public static List<Result> QueryForPlugin(PluginPair pair, Query query)
public static async Task<List<Result>> QueryForPlugin(PluginPair pair, Query query, CancellationToken token)
{
var results = new List<Result>();
try
{
var metadata = pair.Metadata;
var milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", () =>
{
results = pair.Plugin.Query(query) ?? new List<Result>();
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<Result>();
UpdatePluginMetadata(results, metadata, query);
break;
case IPlugin plugin:
results = await Task.Run(() => plugin.Query(query), token).ConfigureAwait(false) ??
new List<Result>();
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<Result> results, PluginMetadata metadata, Query query)
@ -182,11 +241,6 @@ namespace Flow.Launcher.Core.Plugin
}
}
private static bool IsGlobalPlugin(PluginMetadata metadata)
{
return metadata.ActionKeywords.Contains(Query.GlobalPluginWildcardSign);
}
/// <summary>
/// get specified plugin, return null if not found
/// </summary>
@ -208,7 +262,7 @@ namespace Flow.Launcher.Core.Plugin
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 @@ namespace Flow.Launcher.Core.Plugin
}
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);
}
/// <summary>
@ -249,6 +306,7 @@ namespace Flow.Launcher.Core.Plugin
{
NonGlobalPlugins[newActionKeyword] = plugin;
}
plugin.Metadata.ActionKeywords.Add(newActionKeyword);
}
@ -262,9 +320,9 @@ namespace Flow.Launcher.Core.Plugin
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 @@ namespace Flow.Launcher.Core.Plugin
}
}
}
}
}

View file

@ -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 @@ namespace Flow.Launcher.Infrastructure
Log.Debug(info);
return milliseconds;
}
/// <summary>
/// This stopwatch will appear only in Debug mode
/// </summary>
public static async Task<long> DebugAsync(string message, Func<Task> 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 @@ namespace Flow.Launcher.Infrastructure
Log.Info(info);
return milliseconds;
}
public static async Task<long> NormalAsync(string message, Func<Task> 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)
{

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
public interface IAsyncPlugin
{
Task<List<Result>> QueryAsync(Query query, CancellationToken token);
Task InitAsync(PluginInitContext context);
}
}

View file

@ -5,6 +5,7 @@ namespace Flow.Launcher.Plugin
public interface IPlugin
{
List<Result> Query(Query query);
void Init(PluginInitContext context);
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
@ -34,7 +35,7 @@ namespace Flow.Launcher.Plugin
/// Plugin's in memory data with new content
/// added by user.
/// </summary>
void ReloadAllPluginData();
Task ReloadAllPluginData();
/// <summary>
/// Check for new Flow Launcher update

View file

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
public interface IAsyncReloadable
{
Task ReloadDataAsync();
}
}

View file

@ -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; }

View file

@ -78,9 +78,9 @@ namespace Flow.Launcher
ImageLoader.Save();
}
public void ReloadAllPluginData()
public async Task ReloadAllPluginData()
{
PluginManager.ReloadData();
await PluginManager.ReloadData();
}
public void ShowMsg(string title, string subTitle = "", string iconPath = "")

View file

@ -405,45 +405,47 @@ namespace Flow.Launcher.ViewModel
}, 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

View file

@ -38,7 +38,7 @@ namespace Flow.Launcher.Plugin.ControlPanel
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);

View file

@ -2,6 +2,7 @@ using System;
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 @@ namespace Flow.Launcher.Plugin.Sys
{
c.TitleHighlightData = titleMatch.MatchData;
}
else
else
{
c.SubTitleHighlightData = subTitleMatch.MatchData;
}
results.Add(c);
}
}
return results;
}
@ -94,13 +97,15 @@ namespace Flow.Launcher.Plugin.Sys
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 @@ namespace Flow.Launcher.Plugin.Sys
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 @@ namespace Flow.Launcher.Plugin.Sys
// 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 @@ namespace Flow.Launcher.Plugin.Sys
{
// 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;
}
},