Merge pull request #267 from taooceros/PluginAsyncModel

Plugin async model
This commit is contained in:
Jeremy Wu 2021-01-17 19:18:53 +11:00 committed by GitHub
commit e9e99ab338
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 731 additions and 506 deletions

View file

@ -20,7 +20,7 @@ namespace Flow.Launcher.Core.Plugin
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 @@ namespace Flow.Launcher.Core.Plugin
// 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)

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;
@ -52,13 +53,14 @@ namespace Flow.Launcher.Core.Plugin
}
}
public static void ReloadData()
public static async Task ReloadData()
{
foreach(var plugin in AllPlugins)
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
{
var reloadablePlugin = plugin.Plugin as IReloadable;
reloadablePlugin?.ReloadData();
}
IReloadable p => Task.Run(p.ReloadData),
IAsyncReloadable p => p.ReloadDataAsync(),
_ => Task.CompletedTask,
}).ToArray());
}
static PluginManager()
@ -86,50 +88,62 @@ 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}>", () =>
var milliseconds = pair.Plugin switch
{
pair.Plugin.Init(new PluginInitContext
{
CurrentPluginMetadata = pair.Metadata,
API = API
});
});
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>");
Log.Info(
$"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>");
}
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);
}
});
}));
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);
}
}
@ -146,24 +160,46 @@ 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}", () =>
long milliseconds = -1L;
switch (pair.Plugin)
{
results = pair.Plugin.Query(query) ?? new List<Result>();
UpdatePluginMetadata(results, metadata, query);
});
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;
metadata.AvgQueryTime =
metadata.QueryCount == 1 ? milliseconds : (metadata.AvgQueryTime + milliseconds) / 2;
token.ThrowIfCancellationRequested();
}
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)
{
Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e);
}
return results;
}
@ -182,11 +218,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>
@ -222,16 +253,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 +283,7 @@ namespace Flow.Launcher.Core.Plugin
{
NonGlobalPlugins[newActionKeyword] = plugin;
}
plugin.Metadata.ActionKeywords.Add(newActionKeyword);
}
@ -262,16 +297,16 @@ 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);
}
if (oldActionkeyword != Query.GlobalPluginWildcardSign)
NonGlobalPlugins.Remove(oldActionkeyword);
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
}

View file

@ -37,56 +37,59 @@ namespace Flow.Launcher.Core.Plugin
foreach (var metadata in metadatas)
{
var milliseconds = Stopwatch.Debug($"|PluginsLoader.DotNetPlugins|Constructor init cost for {metadata.Name}", () =>
{
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 assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
assembly = assemblyLoader.LoadAssemblyAndDependencies();
var assembly = assemblyLoader.LoadAssemblyAndDependencies();
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin),
typeof(IAsyncPlugin));
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin));
var plugin = Activator.CreateInstance(type);
#else
Assembly assembly = null;
object plugin = null;
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);
}
try
{
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
assembly = assemblyLoader.LoadAssemblyAndDependencies();
if (plugin == null)
{
erroredPlugins.Add(metadata.Name);
return;
}
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 @@ namespace Flow.Launcher.Core.Plugin
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 @@ namespace Flow.Launcher.Core.Plugin
Metadata = metadata
});
}
}
}
}

View file

@ -8,6 +8,7 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using System;
using System.ComponentModel;
using System.Threading;
namespace Flow.Launcher.Infrastructure.Http
{
@ -75,11 +76,11 @@ namespace Flow.Launcher.Infrastructure.Http
};
}
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);
@ -102,40 +103,32 @@ namespace Flow.Launcher.Infrastructure.Http
/// When supposing the result larger than 83kb, try using GetStreamAsync to avoid reading as string
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static Task<string> GetAsync([NotNull] string url)
/// <returns>The Http result as string. Null if cancellation requested</returns>
public static Task<string> 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);
}
/// <summary>
/// Asynchrously get the result as string from url.
/// When supposing the result larger than 83kb, try using GetStreamAsync to avoid reading as string
///
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static async Task<string> GetAsync([NotNull] Uri url)
/// <param name="token"></param>
/// <returns>The Http result as string. Null if cancellation requested</returns>
public static async Task<string> GetAsync([NotNull] Uri url, CancellationToken token = default)
{
Log.Debug($"|Http.Get|Url <{url}>");
try
using var response = await client.GetAsync(url, token);
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode == HttpStatusCode.OK)
{
using var response = await client.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode == HttpStatusCode.OK)
{
return content;
}
else
{
throw new HttpRequestException(
$"Error code <{response.StatusCode}> with content <{content}> returned from <{url}>");
}
return content;
}
catch (HttpRequestException e)
else
{
Log.Exception("Infrastructure.Http", "Http Request Error", e, "GetAsync");
throw;
throw new HttpRequestException(
$"Error code <{response.StatusCode}> with content <{content}> returned from <{url}>");
}
}
@ -144,19 +137,11 @@ namespace Flow.Launcher.Infrastructure.Http
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static async Task<Stream> GetStreamAsync([NotNull] string url)
public static async Task<Stream> GetStreamAsync([NotNull] string url, CancellationToken token = default)
{
try
{
Log.Debug($"|Http.Get|Url <{url}>");
var response = await client.GetAsync(url);
return await response.Content.ReadAsStreamAsync();
}
catch (HttpRequestException e)
{
Log.Exception("Infrastructure.Http", "Http Request Error", e, "GetStreamAsync");
throw;
}
Log.Debug($"|Http.Get|Url <{url}>");
var response = await client.GetAsync(url, token);
return await response.Content.ReadAsStreamAsync();
}
}
}

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,31 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Asynchronous Plugin Model for Flow Launcher
/// </summary>
public interface IAsyncPlugin
{
/// <summary>
/// Asynchronous Querying
/// </summary>
/// <para>
/// If the Querying or Init method requires high IO transmission
/// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncPlugin interface
/// </para>
/// <param name="query">Query to search</param>
/// <param name="token">Cancel when querying job is obsolete</param>
/// <returns></returns>
Task<List<Result>> QueryAsync(Query query, CancellationToken token);
/// <summary>
/// Initialize plugin asynchrously (will still wait finish to continue)
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
Task InitAsync(PluginInitContext context);
}
}

View file

@ -2,9 +2,30 @@
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Synchronous Plugin Model for Flow Launcher
/// <para>
/// If the Querying or Init method requires high IO transmission
/// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncPlugin interface
/// </para>
/// </summary>
public interface IPlugin
{
/// <summary>
/// Querying when user's search changes
/// <para>
/// This method will be called within a Task.Run,
/// so please avoid synchrously wait for long.
/// </para>
/// </summary>
/// <param name="query">Query to search</param>
/// <returns></returns>
List<Result> Query(Query query);
/// <summary>
/// Initialize plugin
/// </summary>
/// <param name="context"></param>
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,20 @@
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// 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.
/// </summary>
public interface IAsyncReloadable
{
Task ReloadDataAsync();
}
}

View file

@ -1,7 +1,7 @@
namespace Flow.Launcher.Plugin
{
/// <summary>
/// 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.
///
/// <para>
/// If requiring reloading data asynchronously, please use the IAsyncReloadable interface
/// </para>
/// </summary>
public interface IReloadable
{

View file

@ -4,6 +4,16 @@ namespace Flow.Launcher.Plugin
{
public class PluginInitContext
{
public PluginInitContext()
{
}
public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api)
{
CurrentPluginMetadata = currentPluginMetadata;
API = api;
}
public PluginMetadata CurrentPluginMetadata { get; internal set; }
/// <summary>

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

@ -7,6 +7,8 @@ using Flow.Launcher.Plugin.SharedCommands;
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<Result> MethodWindowsIndexSearchReturnsZeroResults(Query dummyQuery, string dummyString)
private async Task<List<Result>> MethodWindowsIndexSearchReturnsZeroResultsAsync(Query dummyQuery, string dummyString, CancellationToken dummyToken)
{
return new List<Result>();
}
private List<Result> MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString)
{
return new List<Result>
{
return new List<Result>
{
new Result
{
Title="Result 1"
@ -64,10 +66,10 @@ namespace Flow.Launcher.Test.Plugins
{
// 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 @@ namespace Flow.Launcher.Test.Plugins
}
[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 @@ namespace Flow.Launcher.Test.Plugins
"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 @@ namespace Flow.Launcher.Test.Plugins
}
[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.TopLevelDirectorySearchBehaviour(
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 @@ namespace Flow.Launcher.Test.Plugins
}
[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.TopLevelDirectorySearchBehaviour(
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 @@ namespace Flow.Launcher.Test.Plugins
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 @@ namespace Flow.Launcher.Test.Plugins
$"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 @@ namespace Flow.Launcher.Test.Plugins
[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 @@ namespace Flow.Launcher.Test.Plugins
$"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)

View file

@ -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">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>

View file

@ -45,9 +45,9 @@ namespace Flow.Launcher
}
}
private void OnStartup(object sender, StartupEventArgs e)
private async void OnStartupAsync(object sender, StartupEventArgs e)
{
Stopwatch.Normal("|App.OnStartup|Startup cost", () =>
await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () =>
{
_portable.PreStartCleanUpAfterPortabilityUpdate();
@ -68,9 +68,10 @@ namespace Flow.Launcher
PluginManager.LoadPlugins(_settings.PluginSettings);
_mainVM = new MainViewModel(_settings);
var window = new MainWindow(_settings, _mainVM);
API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet);
PluginManager.InitializePlugins(API);
await PluginManager.InitializePlugins(API);
var window = new MainWindow(_settings, _mainVM);
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
Current.MainWindow = window;

View file

@ -78,9 +78,9 @@ namespace Flow.Launcher
ImageLoader.Save();
}
public void ReloadAllPluginData()
public Task ReloadAllPluginData()
{
PluginManager.ReloadData();
return PluginManager.ReloadData();
}
public void ShowMsg(string title, string subTitle = "", string iconPath = "")
@ -92,7 +92,7 @@ namespace Flow.Launcher
{
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);
});
}

View file

@ -86,7 +86,7 @@ namespace Flow.Launcher.ViewModel
{
foreach (var pair in PluginManager.GetPluginsForInterface<IResultUpdated>())
{
var plugin = (IResultUpdated)pair.Plugin;
var plugin = (IResultUpdated) pair.Plugin;
plugin.ResultsUpdated += (s, e) =>
{
Task.Run(() =>
@ -113,25 +113,13 @@ namespace Flow.Launcher.ViewModel
}
});
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 @@ namespace Flow.Launcher.ViewModel
public ResultsViewModel History { get; private set; }
private string _queryText;
public string QueryText
{
get { return _queryText; }
@ -229,10 +218,12 @@ namespace Flow.Launcher.ViewModel
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 @@ namespace Flow.Launcher.ViewModel
QueryText = string.Empty;
}
}
_selectedResults.Visbility = Visibility.Visible;
}
}
@ -324,7 +316,7 @@ namespace Flow.Launcher.ViewModel
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 @@ namespace Flow.Launcher.ViewModel
Title = string.Format(title, h.Query),
SubTitle = string.Format(time, h.ExecutedDateTime),
IcoPath = "Images\\history.png",
OriginQuery = new Query { RawQuery = h.Query },
OriginQuery = new Query {RawQuery = h.Query},
Action = _ =>
{
SelectedResults = Results;
@ -397,7 +389,8 @@ namespace Flow.Launcher.ViewModel
_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;
@ -405,45 +398,56 @@ namespace Flow.Launcher.ViewModel
}, currentCancellationToken);
var plugins = PluginManager.ValidPluginsForQuery(query);
Task.Run(() =>
Task.Run(async () =>
{
// so looping will stop once it was cancelled
var parallelOptions = new ParallelOptions { CancellationToken = currentCancellationToken };
Task[] tasks = new Task[plugins.Count];
try
{
Parallel.ForEach(plugins, parallelOptions, plugin =>
for (var i = 0; i < plugins.Count; 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(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);
}
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)
{ // update to hidden if this is still the current query
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);
// 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);
}
}
else
@ -510,6 +514,7 @@ namespace Flow.Launcher.ViewModel
}
};
}
return menu;
}
@ -555,6 +560,7 @@ namespace Flow.Launcher.ViewModel
var selected = SelectedResults == History;
return selected;
}
#region Hotkey
private void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
@ -573,7 +579,8 @@ namespace Flow.Launcher.ViewModel
catch (Exception)
{
string errorMsg =
string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"),
hotkeyStr);
MessageBox.Show(errorMsg);
}
}
@ -623,7 +630,6 @@ namespace Flow.Launcher.ViewModel
{
if (!ShouldIgnoreHotkeys())
{
if (_settings.LastQueryMode == LastQueryMode.Empty)
{
ChangeQueryText(string.Empty);

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

@ -3,11 +3,13 @@ using Flow.Launcher.Plugin.Explorer.Search;
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 @@ namespace Flow.Launcher.Plugin.Explorer
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<Result> LoadContextMenus(Result selectedResult)
@ -35,9 +41,9 @@ namespace Flow.Launcher.Plugin.Explorer
return contextMenu.LoadContextMenus(selectedResult);
}
public List<Result> Query(Query query)
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
{
return new SearchManager(Settings, Context).Search(query);
return await searchManager.SearchAsync(query, token);
}
public void Save()

View file

@ -22,7 +22,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
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 +57,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
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;

View file

@ -5,6 +5,8 @@ using Flow.Launcher.Plugin.SharedCommands;
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 @@ namespace Flow.Launcher.Plugin.Explorer.Search
this.settings = settings;
}
internal List<Result> Search(Query query)
internal async Task<List<Result>> SearchAsync(Query query, CancellationToken token)
{
var results = new List<Result>();
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 @@ namespace Flow.Launcher.Plugin.Explorer.Search
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 @@ namespace Flow.Launcher.Plugin.Explorer.Search
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<Result> WindowsIndexFileContentSearch(Query query, string querySearchString)
private async Task<List<Result>> WindowsIndexFileContentSearchAsync(Query query, string querySearchString, CancellationToken token)
{
var queryConstructor = new QueryConstructor(settings);
if (string.IsNullOrEmpty(querySearchString))
return new List<Result>();
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 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return directoryInfoSearch.TopLevelDirectorySearch(query, querySearch);
}
public List<Result> TopLevelDirectorySearchBehaviour(
Func<Query, string, List<Result>> windowsIndexSearch,
public async Task<List<Result>> TopLevelDirectorySearchBehaviourAsync(
Func<Query, string, CancellationToken, Task<List<Result>>> windowsIndexSearch,
Func<Query, string, List<Result>> 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<Result> WindowsIndexFilesAndFoldersSearch(Query query, string querySearchString)
private async Task<List<Result>> 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<Result> WindowsIndexTopLevelFolderSearch(Query query, string path)
private async Task<List<Result>> 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)

View file

@ -5,19 +5,13 @@ using System.Collections.Generic;
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 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
resultManager = new ResultManager(context);
}
internal List<Result> ExecuteWindowsIndexSearch(string indexQueryString, string connectionString, Query query)
internal async Task<List<Result>> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token)
{
var folderResults = new List<Result>();
var fileResults = new List<Result>();
@ -36,47 +30,49 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
try
{
using (conn = new OleDbConnection(connectionString))
using var conn = new OleDbConnection(connectionString);
await conn.OpenAsync(token);
token.ThrowIfCancellationRequested();
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)
{
conn.Open();
using (command = new OleDbCommand(indexQueryString, conn))
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)
{
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;
// # 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);
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));
}
}
}
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));
}
}
}
}
}
catch (OperationCanceledException)
{
return new List<Result>(); // 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 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
return results.Concat(folderResults.OrderBy(x => x.Title)).Concat(fileResults.OrderBy(x => x.Title)).ToList(); ;
}
internal List<Result> WindowsIndexSearch(string searchString, string connectionString, Func<string, string> constructQuery, Query query)
internal async Task<List<Result>> WindowsIndexSearchAsync(string searchString, string connectionString,
Func<string, string> constructQuery, Query query,
CancellationToken token)
{
var regexMatch = Regex.Match(searchString, reservedStringPattern);
if (regexMatch.Success)
return new List<Result>();
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)

View file

@ -3,6 +3,7 @@ using Flow.Launcher.Infrastructure.Storage;
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 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
Settings = storage.Load();
}
public Task LoadStorage()
{
return Task.Run(() => Settings = storage.Load());
}
public void Save()
{
storage.Save();

View file

@ -7,10 +7,11 @@ using System.Windows.Controls;
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 @@ namespace Flow.Launcher.Plugin.PluginsManager
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 @@ namespace Flow.Launcher.Plugin.PluginsManager
return contextMenu.LoadContextMenus(selectedResult);
}
public List<Result> Query(Query query)
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
{
var search = query.Search.ToLower();
@ -53,11 +55,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
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 @@ namespace Flow.Launcher.Plugin.PluginsManager
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;
}
}

View file

@ -9,12 +9,7 @@ namespace Flow.Launcher.Plugin.PluginsManager.Models
{
internal class PluginsManifest
{
internal List<UserPlugin> UserPlugins { get; private set; }
internal PluginsManifest()
{
Task.Run(async () => await DownloadManifest()).Wait();
}
internal List<UserPlugin> UserPlugins { get; private set; } = new List<UserPlugin>();
internal async Task DownloadManifest()
{

View file

@ -2,6 +2,7 @@ using System;
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 @@ using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
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 @@ namespace Flow.Launcher.Plugin.Program
public Main()
{
_settingsStorage = new PluginJsonStorage<Settings>();
_settings = _settingsStorage.Load();
Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () =>
{
_win32Storage = new BinaryStorage<Win32[]>("Win32");
_win32s = _win32Storage.TryLoad(new Win32[] { });
_uwpStorage = new BinaryStorage<UWP.Application[]>("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 @@ namespace Flow.Launcher.Plugin.Program
_uwpStorage.Save(_uwps);
}
public List<Result> Query(Query query)
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
{
Win32[] win32;
UWP.Application[] uwps;
@ -74,20 +47,81 @@ namespace Flow.Launcher.Plugin.Program
win32 = _win32s;
uwps = _uwps;
var result = win32.Cast<IProgram>()
.Concat(uwps)
.AsParallel()
.Where(p => p.Enabled)
.Select(p => p.Result(query.Search, _context.API))
.Where(r => r?.Score > 0)
.ToList();
try
{
var result = await Task.Run(delegate
{
try
{
return win32.Cast<IProgram>()
.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);
return result;
token.ThrowIfCancellationRequested();
return result;
}
catch (OperationCanceledException)
{
return null;
}
}
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[]>("Win32");
_win32s = _win32Storage.TryLoad(new Win32[] { });
_uwpStorage = new BinaryStorage<UWP.Application[]>("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}>");
});
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);
if (indexedWinApps && indexedUWPApps)
_settings.LastIndexTime = DateTime.Today;
}
public static void IndexWin32Programs()
@ -95,10 +129,9 @@ namespace Flow.Launcher.Plugin.Program
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 +139,15 @@ namespace Flow.Launcher.Plugin.Program
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 +177,21 @@ namespace Flow.Launcher.Plugin.Program
}
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 +202,25 @@ namespace Flow.Launcher.Plugin.Program
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<ProcessStartInfo, Process> runProcess, ProcessStartInfo info)
@ -200,9 +238,9 @@ namespace Flow.Launcher.Plugin.Program
}
}
public void ReloadData()
public async Task ReloadDataAsync()
{
IndexPrograms();
await IndexPrograms();
}
}
}
}

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;
}
},
@ -164,13 +171,14 @@ namespace Flow.Launcher.Plugin.Sys
// 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)
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 +237,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;
}
},

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
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 @@ using Flow.Launcher.Plugin.SharedCommands;
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 @@ namespace Flow.Launcher.Plugin.WebSearch
_viewModel.Save();
}
public List<Result> Query(Query query)
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
{
if (FilesFolders.IsLocationPathString(query.Search))
return new List<Result>();
@ -41,102 +40,94 @@ namespace Flow.Launcher.Plugin.WebSearch
var searchSourceList = new List<SearchSource>();
var results = new List<Result>();
_updateSource?.Cancel();
_updateSource = new CancellationTokenSource();
_updateToken = _updateSource.Token;
_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;
if (string.IsNullOrEmpty(keyword))
{
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))
var result = new Result
{
var result = new Result
{
Title = subtitle,
SubTitle = string.Empty,
IcoPath = searchSource.IconPath
};
results.Add(result);
}
else
{
var result = new Result
{
Title = title,
SubTitle = subtitle,
Score = 6,
IcoPath = searchSource.IconPath,
ActionKeywordAssigned = searchSource.ActionKeyword == SearchSourceGlobalPluginWildCardSign ? string.Empty : searchSource.ActionKeyword,
Action = c =>
{
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;
}
};
results.Add(result);
ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs
{
Results = results,
Query = query
});
UpdateResultsFromSuggestion(results, keyword, subtitle, searchSource, query);
}
Title = subtitle,
SubTitle = string.Empty,
IcoPath = searchSource.IconPath
};
results.Add(result);
}
else
{
var result = new Result
{
Title = title,
SubTitle = subtitle,
Score = 6,
IcoPath = searchSource.IconPath,
ActionKeywordAssigned = searchSource.ActionKeyword == SearchSourceGlobalPluginWildCardSign ? string.Empty : searchSource.ActionKeyword,
Action = c =>
{
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;
}
};
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;
}
private void UpdateResultsFromSuggestion(List<Result> results, string keyword, string subtitle,
SearchSource searchSource, Query query)
private async Task UpdateResultsFromSuggestionAsync(List<Result> 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);
if (token.IsCancellationRequested || !suggestions.Any())
return;
if (!task.Wait(waittime))
{
task.ContinueWith(_ => ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs
{
Results = results,
Query = query
}), _updateToken);
}
results.AddRange(suggestions);
token.ThrowIfCancellationRequested();
}
}
private async Task<IEnumerable<Result>> Suggestions(string keyword, string subtitle, SearchSource searchSource)
private async Task<IEnumerable<Result>> 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);
if (token.IsCancellationRequested)
return null;
var resultsFromSuggestion = suggestions.Select(o => new Result
{
Title = o,
@ -169,19 +160,24 @@ namespace Flow.Launcher.Plugin.WebSearch
_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);
return Task.Run(Init);
// 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");
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

View file

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
using System.Net.Http;
using System.Threading;
namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
@ -15,14 +16,18 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
private readonly Regex _reg = new Regex("window.baidu.sug\\((.*)\\)");
public override async Task<List<string>> Suggestions(string query)
public override async Task<List<string>> 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 (TaskCanceledException)
{
return null;
}
catch (HttpRequestException e)
{

View file

@ -9,33 +9,37 @@ using System.Text.RegularExpressions;
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<List<string>> Suggestions(string query)
public override async Task<List<string>> 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)).ConfigureAwait(false);
using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeUriString(query), token).ConfigureAwait(false);
if (resultStream.Length == 0)
return new List<string>(); // this handles the cancellation
json = (await JsonDocument.ParseAsync(resultStream, cancellationToken: token)).RootElement.GetProperty("AS");
}
catch (TaskCanceledException)
{
return null;
}
catch (HttpRequestException e)
{
Log.Exception("|Bing.Suggestions|Can't get suggestion from Bing", e);
return new List<string>();
}
if (resultStream.Length == 0) return new List<string>();
JsonElement json;
try
{
json = (await JsonDocument.ParseAsync(resultStream)).RootElement.GetProperty("AS");
}
catch (JsonException e)
{
Log.Exception("|Bing.Suggestions|can't parse suggestions", e);

View file

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
using System.Net.Http;
using System.Threading;
using System.Text.Json;
using System.IO;
@ -13,25 +14,31 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{
public class Google : SuggestionSource
{
public override async Task<List<string>> Suggestions(string query)
public override async Task<List<string>> 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<string>();
json = await JsonDocument.ParseAsync(resultStream);
}
catch (TaskCanceledException)
{
return null;
}
catch (HttpRequestException e)
{
Log.Exception("|Google.Suggestions|Can't get suggestion from google", e);
return new List<string>();
}
if (resultStream.Length == 0) return new List<string>();
JsonDocument json;
try
{
json = await JsonDocument.ParseAsync(resultStream);
}
catch (JsonException e)
{
Log.Exception("|Google.Suggestions|can't parse suggestions", e);
@ -41,6 +48,7 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
var results = json?.RootElement.EnumerateArray().ElementAt(1);
return results?.EnumerateArray().Select(o => o.GetString()).ToList() ?? new List<string>();
}
public override string ToString()

View file

@ -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<List<string>> Suggestions(string query);
public abstract Task<List<string>> Suggestions(string query, CancellationToken token);
}
}
}