Merge pull request #3854 from Flow-Launcher/plugin_initialization

Asynchronous Loading & Initialization Plugin Model to Improve Window Startup Speed
This commit is contained in:
Jeremy Wu 2025-10-16 22:08:42 +11:00 committed by GitHub
commit da4b961686
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 567 additions and 258 deletions

View file

@ -0,0 +1,12 @@
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Plugin;
public interface IResultUpdateRegister
{
/// <summary>
/// Register a plugin to receive results updated event.
/// </summary>
/// <param name="pair"></param>
void RegisterResultsUpdatedEvent(PluginPair pair);
}

View file

@ -7,6 +7,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
@ -24,44 +25,36 @@ namespace Flow.Launcher.Core.Plugin
{
private static readonly string ClassName = nameof(PluginManager);
public static List<PluginPair> AllPlugins { get; private set; }
public static readonly HashSet<PluginPair> GlobalPlugins = new();
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
private static readonly ConcurrentDictionary<string, PluginPair> _allLoadedPlugins = [];
private static readonly ConcurrentDictionary<string, PluginPair> _allInitializedPlugins = [];
private static readonly ConcurrentDictionary<string, PluginPair> _initFailedPlugins = [];
private static readonly ConcurrentDictionary<string, PluginPair> _globalPlugins = [];
private static readonly ConcurrentDictionary<string, PluginPair> _nonGlobalPlugins = [];
private static PluginsSettings Settings;
private static readonly ConcurrentBag<string> ModifiedPlugins = new();
private static readonly ConcurrentBag<string> ModifiedPlugins = [];
private static IEnumerable<PluginPair> _contextMenuPlugins;
private static IEnumerable<PluginPair> _homePlugins;
private static IEnumerable<PluginPair> _resultUpdatePlugin;
private static IEnumerable<PluginPair> _translationPlugins;
private static readonly List<DialogJumpExplorerPair> _dialogJumpExplorerPlugins = new();
private static readonly List<DialogJumpDialogPair> _dialogJumpDialogPlugins = new();
private static readonly ConcurrentBag<PluginPair> _contextMenuPlugins = [];
private static readonly ConcurrentBag<PluginPair> _homePlugins = [];
private static readonly ConcurrentBag<PluginPair> _translationPlugins = [];
private static readonly ConcurrentBag<PluginPair> _externalPreviewPlugins = [];
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
/// </summary>
public static readonly string[] Directories =
{
[
Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
};
];
private static void DeletePythonBinding()
{
const string binding = "flowlauncher.py";
foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory))
{
File.Delete(Path.Combine(subDirectory, binding));
}
}
#region Save & Dispose & Reload Plugin
/// <summary>
/// Save json and ISavable
/// </summary>
public static void Save()
{
foreach (var pluginPair in AllPlugins)
foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: false))
{
var savable = pluginPair.Plugin as ISavable;
try
@ -80,7 +73,8 @@ namespace Flow.Launcher.Core.Plugin
public static async ValueTask DisposePluginsAsync()
{
foreach (var pluginPair in AllPlugins)
// Still call dispose for all plugins even if initialization failed, so that we can clean up resources
foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: true))
{
await DisposePluginAsync(pluginPair);
}
@ -108,49 +102,53 @@ namespace Flow.Launcher.Core.Plugin
public static async Task ReloadDataAsync()
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
{
IReloadable p => Task.Run(p.ReloadData),
IAsyncReloadable p => p.ReloadDataAsync(),
_ => Task.CompletedTask,
}).ToArray());
})]);
}
#endregion
#region External Preview
public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
_ => Task.CompletedTask,
}).ToArray());
})]);
}
public static async Task CloseExternalPreviewAsync()
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.ClosePreviewAsync(),
_ => Task.CompletedTask,
}).ToArray());
})]);
}
public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
_ => Task.CompletedTask,
}).ToArray());
})]);
}
public static bool UseExternalPreview()
{
return GetPluginsForInterface<IAsyncExternalPreview>().Any(x => !x.Metadata.Disabled);
return GetExternalPreviewPlugins().Any(x => !x.Metadata.Disabled);
}
public static bool AllowAlwaysPreview()
{
var plugin = GetPluginsForInterface<IAsyncExternalPreview>().FirstOrDefault(x => !x.Metadata.Disabled);
var plugin = GetExternalPreviewPlugins().FirstOrDefault(x => !x.Metadata.Disabled);
if (plugin is null)
return false;
@ -158,6 +156,15 @@ namespace Flow.Launcher.Core.Plugin
return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview();
}
private static IList<PluginPair> GetExternalPreviewPlugins()
{
return [.. _externalPreviewPlugins.Where(p => !PluginModified(p.Metadata.ID))];
}
#endregion
#region Constructor
static PluginManager()
{
// validate user directory
@ -166,9 +173,28 @@ namespace Flow.Launcher.Core.Plugin
DeletePythonBinding();
}
private static void DeletePythonBinding()
{
const string binding = "flowlauncher.py";
foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory))
{
try
{
File.Delete(Path.Combine(subDirectory, binding));
}
catch (Exception e)
{
PublicApi.Instance.LogDebug(ClassName, $"Failed to delete {binding} in {subDirectory}: {e.Message}");
}
}
}
#endregion
#region Load & Initialize Plugins
/// <summary>
/// because InitializePlugins needs API, so LoadPlugins needs to be called first
/// todo happlebao The API should be removed
/// Load plugins from the directories specified in Directories.
/// </summary>
/// <param name="settings"></param>
public static void LoadPlugins(PluginsSettings settings)
@ -176,33 +202,22 @@ namespace Flow.Launcher.Core.Plugin
var metadatas = PluginConfig.Parse(Directories);
Settings = settings;
Settings.UpdatePluginSettings(metadatas);
AllPlugins = PluginsLoader.Plugins(metadatas, Settings);
// Load plugins
var allLoadedPlugins = PluginsLoader.Plugins(metadatas, Settings);
foreach (var plugin in allLoadedPlugins)
{
if (plugin != null)
{
if (!_allLoadedPlugins.TryAdd(plugin.Metadata.ID, plugin))
{
PublicApi.Instance.LogError(ClassName, $"Plugin with ID {plugin.Metadata.ID} already loaded");
}
}
}
// Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
UpdatePluginDirectory(metadatas);
// Initialize plugin enumerable after all plugins are initialized
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
_homePlugins = GetPluginsForInterface<IAsyncHomeQuery>();
_resultUpdatePlugin = GetPluginsForInterface<IResultUpdated>();
_translationPlugins = GetPluginsForInterface<IPluginI18n>();
// Initialize Dialog Jump plugin pairs
foreach (var pair in GetPluginsForInterface<IDialogJumpExplorer>())
{
_dialogJumpExplorerPlugins.Add(new DialogJumpExplorerPair
{
Plugin = (IDialogJumpExplorer)pair.Plugin,
Metadata = pair.Metadata
});
}
foreach (var pair in GetPluginsForInterface<IDialogJumpDialog>())
{
_dialogJumpDialogPlugins.Add(new DialogJumpDialogPair
{
Plugin = (IDialogJumpDialog)pair.Plugin,
Metadata = pair.Metadata
});
}
}
private static void UpdatePluginDirectory(List<PluginMetadata> metadatas)
@ -233,15 +248,19 @@ namespace Flow.Launcher.Core.Plugin
}
/// <summary>
/// Call initialize for all plugins
/// Initialize all plugins asynchronously.
/// </summary>
/// <param name="register">The register to register results updated event for each plugin.</param>
/// <returns>return the list of failed to init plugins or null for none</returns>
public static async Task InitializePluginsAsync()
public static async Task InitializePluginsAsync(IResultUpdateRegister register)
{
var failedPlugins = new ConcurrentQueue<PluginPair>();
var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate
var initTasks = _allLoadedPlugins.Select(x => Task.Run(async () =>
{
var pair = x.Value;
// Register plugin action keywords so that plugins can be queried in results
RegisterPluginActionKeywords(pair);
try
{
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>",
@ -264,35 +283,34 @@ namespace Flow.Launcher.Core.Plugin
{
pair.Metadata.Disabled = true;
pair.Metadata.HomeDisabled = true;
failedPlugins.Enqueue(pair);
PublicApi.Instance.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed");
}
// Even if the plugin cannot be initialized, we still need to add it in all plugin list so that
// we can remove the plugin from Plugin or Store page or Plugin Manager plugin.
_allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
_initFailedPlugins.TryAdd(pair.Metadata.ID, pair);
return;
}
// Register ResultsUpdated event so that plugin query can use results updated interface
register.RegisterResultsUpdatedEvent(pair);
// Update plugin metadata translation after the plugin is initialized with IPublicAPI instance
Internationalization.UpdatePluginMetadataTranslation(pair);
// Add plugin to Dialog Jump plugin list after the plugin is initialized
DialogJump.InitializeDialogJumpPlugin(pair);
// Add plugin to lists after the plugin is initialized
AddPluginToLists(pair);
}));
await Task.WhenAll(InitTasks);
await Task.WhenAll(initTasks);
foreach (var plugin in AllPlugins)
if (!_initFailedPlugins.IsEmpty)
{
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
// has multiple global and action keywords because we will only add them here once.
foreach (var actionKeyword in plugin.Metadata.ActionKeywords.Distinct())
{
switch (actionKeyword)
{
case Query.GlobalPluginWildcardSign:
GlobalPlugins.Add(plugin);
break;
default:
NonGlobalPlugins[actionKeyword] = plugin;
break;
}
}
}
if (!failedPlugins.IsEmpty)
{
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
var failed = string.Join(",", _initFailedPlugins.Values.Select(x => x.Metadata.Name));
PublicApi.Instance.ShowMsg(
Localize.failedToInitializePluginsTitle(),
Localize.failedToInitializePluginsMessage(failed),
@ -302,34 +320,74 @@ namespace Flow.Launcher.Core.Plugin
}
}
private static void RegisterPluginActionKeywords(PluginPair pair)
{
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
// has multiple global and action keywords because we will only add them here once.
foreach (var actionKeyword in pair.Metadata.ActionKeywords.Distinct())
{
switch (actionKeyword)
{
case Query.GlobalPluginWildcardSign:
_globalPlugins.TryAdd(pair.Metadata.ID, pair);
break;
default:
_nonGlobalPlugins.TryAdd(actionKeyword, pair);
break;
}
}
}
private static void AddPluginToLists(PluginPair pair)
{
if (pair.Plugin is IContextMenu)
{
_contextMenuPlugins.Add(pair);
}
if (pair.Plugin is IAsyncHomeQuery)
{
_homePlugins.Add(pair);
}
if (pair.Plugin is IPluginI18n)
{
_translationPlugins.Add(pair);
}
if (pair.Plugin is IAsyncExternalPreview)
{
_externalPreviewPlugins.Add(pair);
}
_allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
}
#endregion
#region Validate & Query Plugins
public static ICollection<PluginPair> ValidPluginsForQuery(Query query, bool dialogJump)
{
if (query is null)
return Array.Empty<PluginPair>();
if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
{
if (dialogJump)
return GlobalPlugins.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList();
return [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))];
else
return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
return [.. GetGlobalPlugins().Where(p => !PluginModified(p.Metadata.ID))];
}
if (dialogJump && plugin.Plugin is not IAsyncDialogJump)
return Array.Empty<PluginPair>();
if (PublicApi.Instance.PluginModified(plugin.Metadata.ID))
if (PluginModified(plugin.Metadata.ID))
return Array.Empty<PluginPair>();
return new List<PluginPair>
{
plugin
};
return [plugin];
}
public static ICollection<PluginPair> ValidPluginsForHomeQuery()
{
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
return [.. _homePlugins.Where(p => !PluginModified(p.Metadata.ID))];
}
public static async Task<List<Result>> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token)
@ -337,6 +395,28 @@ namespace Flow.Launcher.Core.Plugin
var results = new List<Result>();
var metadata = pair.Metadata;
if (IsPluginInitializing(metadata))
{
Result r = new()
{
Title = Localize.pluginStillInitializing(metadata.Name),
SubTitle = Localize.pluginStillInitializingSubtitle(),
AutoCompleteText = query.RawQuery,
IcoPath = metadata.IcoPath,
PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword,
PluginID = metadata.ID,
OriginQuery = query,
Action = _ =>
{
PublicApi.Instance.ReQuery();
return false;
}
};
results.Add(r);
return results;
}
try
{
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
@ -361,15 +441,15 @@ namespace Flow.Launcher.Core.Plugin
{
Result r = new()
{
Title = $"{metadata.Name}: Failed to respond!",
SubTitle = "Select this result for more info",
Title = Localize.pluginFailedToRespond(metadata.Name),
SubTitle = Localize.pluginFailedToRespondSubtitle(),
AutoCompleteText = query.RawQuery,
IcoPath = Constant.ErrorIcon,
PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword,
PluginID = metadata.ID,
OriginQuery = query,
Action = _ => { throw new FlowPluginException(metadata, e);},
Score = -100
Action = _ => { throw new FlowPluginException(metadata, e);}
};
results.Add(r);
}
@ -381,6 +461,28 @@ namespace Flow.Launcher.Core.Plugin
var results = new List<Result>();
var metadata = pair.Metadata;
if (IsPluginInitializing(metadata))
{
Result r = new()
{
Title = Localize.pluginStillInitializing(metadata.Name),
SubTitle = Localize.pluginStillInitializingSubtitle(),
AutoCompleteText = query.RawQuery,
IcoPath = metadata.IcoPath,
PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword,
PluginID = metadata.ID,
OriginQuery = query,
Action = _ =>
{
PublicApi.Instance.ReQuery();
return false;
}
};
results.Add(r);
return results;
}
try
{
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
@ -411,6 +513,12 @@ namespace Flow.Launcher.Core.Plugin
var results = new List<DialogJumpResult>();
var metadata = pair.Metadata;
if (IsPluginInitializing(metadata))
{
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
return null;
}
try
{
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
@ -436,6 +544,52 @@ namespace Flow.Launcher.Core.Plugin
return results;
}
private static bool IsPluginInitializing(PluginMetadata metadata)
{
return !_allInitializedPlugins.ContainsKey(metadata.ID);
}
#endregion
#region Get Plugin List
public static List<PluginPair> GetAllLoadedPlugins()
{
return [.. _allLoadedPlugins.Values];
}
public static List<PluginPair> GetAllInitializedPlugins(bool includeFailed)
{
if (includeFailed)
{
return [.. _allInitializedPlugins.Values];
}
else
{
return [.. _allInitializedPlugins.Values
.Where(p => !_initFailedPlugins.ContainsKey(p.Metadata.ID))];
}
}
private static List<PluginPair> GetGlobalPlugins()
{
return [.. _globalPlugins.Values];
}
public static Dictionary<string, PluginPair> GetNonGlobalPlugins()
{
return _nonGlobalPlugins.ToDictionary();
}
public static List<PluginPair> GetTranslationPlugins()
{
return [.. _translationPlugins.Where(p => !PluginModified(p.Metadata.ID))];
}
#endregion
#region Update Metadata & Get Plugin
public static void UpdatePluginMetadata(IReadOnlyList<Result> results, PluginMetadata metadata, Query query)
{
foreach (var r in results)
@ -454,28 +608,19 @@ namespace Flow.Launcher.Core.Plugin
/// <summary>
/// get specified plugin, return null if not found
/// </summary>
/// <remarks>
/// Plugin may not be initialized, so do not use its plugin model to execute any commands
/// </remarks>
/// <param name="id"></param>
/// <returns></returns>
public static PluginPair GetPluginForId(string id)
{
return AllPlugins.FirstOrDefault(o => o.Metadata.ID == id);
return GetAllLoadedPlugins().FirstOrDefault(o => o.Metadata.ID == id);
}
private static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
{
// Handle scenario where this is called before all plugins are instantiated, e.g. language change on startup
return AllPlugins?.Where(p => p.Plugin is T) ?? Array.Empty<PluginPair>();
}
#endregion
public static IList<PluginPair> GetResultUpdatePlugin()
{
return _resultUpdatePlugin.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static IList<PluginPair> GetTranslationPlugins()
{
return _translationPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
#region Get Context Menus
public static List<Result> GetContextMenusForPlugin(Result result)
{
@ -506,27 +651,82 @@ namespace Flow.Launcher.Core.Plugin
return results;
}
#endregion
#region Check Home Plugin
public static bool IsHomePlugin(string id)
{
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id);
}
public static IList<DialogJumpExplorerPair> GetDialogJumpExplorers()
#endregion
#region Check Initializing & Init Failed
public static bool IsInitializingOrInitFailed(string id)
{
return _dialogJumpExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
// Id does not exist in loaded plugins
if (!_allLoadedPlugins.ContainsKey(id)) return false;
// Plugin initialized already
if (_allInitializedPlugins.ContainsKey(id))
{
// Check if the plugin initialization failed
return _initFailedPlugins.ContainsKey(id);
}
// Plugin is still initializing
else
{
return true;
}
}
public static IList<DialogJumpDialogPair> GetDialogJumpDialogs()
public static bool IsInitializing(string id)
{
return _dialogJumpDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
// Id does not exist in loaded plugins
if (!_allLoadedPlugins.ContainsKey(id)) return false;
// Plugin initialized already
if (_allInitializedPlugins.ContainsKey(id))
{
return false;
}
// Plugin is still initializing
else
{
return true;
}
}
public static bool IsInitializationFailed(string id)
{
// Id does not exist in loaded plugins
if (!_allLoadedPlugins.ContainsKey(id)) return false;
// Plugin initialized already
if (_allInitializedPlugins.ContainsKey(id))
{
// Check if the plugin initialization failed
return _initFailedPlugins.ContainsKey(id);
}
// Plugin is still initializing
else
{
return false;
}
}
#endregion
#region Plugin Action Keyword
public static bool ActionKeywordRegistered(string actionKeyword)
{
// this method is only checking for action keywords (defined as not '*') registration
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic
return actionKeyword != Query.GlobalPluginWildcardSign
&& NonGlobalPlugins.ContainsKey(actionKeyword);
&& _nonGlobalPlugins.ContainsKey(actionKeyword);
}
/// <summary>
@ -538,11 +738,11 @@ namespace Flow.Launcher.Core.Plugin
var plugin = GetPluginForId(id);
if (newActionKeyword == Query.GlobalPluginWildcardSign)
{
GlobalPlugins.Add(plugin);
_globalPlugins.TryAdd(id, plugin);
}
else
{
NonGlobalPlugins[newActionKeyword] = plugin;
_nonGlobalPlugins.AddOrUpdate(newActionKeyword, plugin, (key, oldValue) => plugin);
}
// Update action keywords and action keyword in plugin metadata
@ -569,11 +769,13 @@ namespace Flow.Launcher.Core.Plugin
plugin.Metadata.ActionKeywords
.Count(x => x == Query.GlobalPluginWildcardSign) == 1)
{
GlobalPlugins.Remove(plugin);
_globalPlugins.TryRemove(id, out _);
}
if (oldActionkeyword != Query.GlobalPluginWildcardSign)
NonGlobalPlugins.Remove(oldActionkeyword);
{
_nonGlobalPlugins.TryRemove(oldActionkeyword, out _);
}
// Update action keywords and action keyword in plugin metadata
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
@ -587,6 +789,12 @@ namespace Flow.Launcher.Core.Plugin
}
}
#endregion
#region Plugin Install & Uninstall & Update
#region Private Functions
private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath)
{
var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length;
@ -612,12 +820,15 @@ namespace Flow.Launcher.Core.Plugin
if (!Version.TryParse(newMetadata.Version, out var newVersion))
return true; // If version is not valid, we assume it is lesser than any existing version
return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID
&& Version.TryParse(x.Metadata.Version, out var version)
&& newVersion <= version);
// Get all plugins even if initialization failed so that we can check if the plugin with the same ID exists
return GetAllInitializedPlugins(includeFailed: true).Any(x => x.Metadata.ID == newMetadata.ID
&& Version.TryParse(x.Metadata.Version, out var version)
&& newVersion <= version);
}
#region Public functions
#endregion
#region Public Functions
public static bool PluginModified(string id)
{
@ -655,7 +866,7 @@ namespace Flow.Launcher.Core.Plugin
#endregion
#region Internal functions
#region Internal Functions
internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
{
@ -752,7 +963,7 @@ namespace Flow.Launcher.Core.Plugin
// If we want to remove plugin from AllPlugins,
// we need to dispose them so that they can release file handles
// which can help FL to delete the plugin settings & cache folders successfully
var pluginPairs = AllPlugins.FindAll(p => p.Metadata.ID == plugin.ID);
var pluginPairs = GetAllInitializedPlugins(includeFailed: true).Where(p => p.Metadata.ID == plugin.ID).ToList();
foreach (var pluginPair in pluginPairs)
{
await DisposePluginAsync(pluginPair);
@ -797,12 +1008,22 @@ namespace Flow.Launcher.Core.Plugin
Localize.failedToRemovePluginCacheMessage(plugin.Name));
}
Settings.RemovePluginSettings(plugin.ID);
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
GlobalPlugins.RemoveWhere(p => p.Metadata.ID == plugin.ID);
var keysToRemove = NonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList();
{
_allLoadedPlugins.TryRemove(plugin.ID, out var _);
}
{
_allInitializedPlugins.TryRemove(plugin.ID, out var _);
}
{
_initFailedPlugins.TryRemove(plugin.ID, out var _);
}
{
_globalPlugins.TryRemove(plugin.ID, out var _);
}
var keysToRemove = _nonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList();
foreach (var key in keysToRemove)
{
NonGlobalPlugins.Remove(key);
_nonGlobalPlugins.TryRemove(key, out var _);
}
}
@ -818,5 +1039,7 @@ namespace Flow.Launcher.Core.Plugin
}
#endregion
#endregion
}
}

View file

@ -48,7 +48,7 @@ namespace Flow.Launcher.Core.Plugin
return plugins;
}
private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
private static List<PluginPair> DotNetPlugins(List<PluginMetadata> source)
{
var erroredPlugins = new List<string>();
@ -58,55 +58,57 @@ namespace Flow.Launcher.Core.Plugin
foreach (var metadata in metadatas)
{
var milliseconds = PublicApi.Instance.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
{
Assembly assembly = null;
IAsyncPlugin plugin = null;
try
{
Assembly assembly = null;
IAsyncPlugin plugin = null;
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
assembly = assemblyLoader.LoadAssemblyAndDependencies();
try
{
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
assembly = assemblyLoader.LoadAssemblyAndDependencies();
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
typeof(IAsyncPlugin));
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
typeof(IAsyncPlugin));
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
metadata.AssemblyName = assembly.GetName().Name;
}
metadata.AssemblyName = assembly.GetName().Name;
}
#if DEBUG
catch (Exception)
{
throw;
}
catch (Exception)
{
throw;
}
#else
catch (Exception e) when (assembly == null)
{
PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
}
catch (InvalidOperationException e)
{
PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
}
catch (ReflectionTypeLoadException e)
{
PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
}
catch (Exception e) when (assembly == null)
{
PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
}
catch (InvalidOperationException e)
{
PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
}
catch (ReflectionTypeLoadException e)
{
PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
}
#endif
if (plugin == null)
{
erroredPlugins.Add(metadata.Name);
return;
}
if (plugin == null)
{
erroredPlugins.Add(metadata.Name);
return;
}
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
});
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
});
metadata.InitTime += milliseconds;
PublicApi.Instance.LogDebug(ClassName, $"Constructor cost for <{metadata.Name}> is <{metadata.InitTime}ms>");
}
if (erroredPlugins.Count > 0)

View file

@ -377,6 +377,22 @@ namespace Flow.Launcher.Core.Resource
}
}
public static void UpdatePluginMetadataTranslation(PluginPair p)
{
// Update plugin metadata name & description
if (p.Plugin is not IPluginI18n pluginI18N) return;
try
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
}
}
#endregion
#region IDisposable

View file

@ -13,6 +13,7 @@ using NHotkey;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Accessibility;
using System.Collections.Concurrent;
namespace Flow.Launcher.Infrastructure.DialogJump
{
@ -60,12 +61,12 @@ namespace Flow.Launcher.Infrastructure.DialogJump
private static HWND _mainWindowHandle = HWND.Null;
private static readonly Dictionary<DialogJumpExplorerPair, IDialogJumpExplorerWindow> _dialogJumpExplorers = new();
private static readonly ConcurrentDictionary<DialogJumpExplorerPair, IDialogJumpExplorerWindow> _dialogJumpExplorers = new();
private static DialogJumpExplorerPair _lastExplorer = null;
private static readonly Lock _lastExplorerLock = new();
private static readonly Dictionary<DialogJumpDialogPair, IDialogJumpDialogWindow> _dialogJumpDialogs = new();
private static readonly ConcurrentDictionary<DialogJumpDialogPair, IDialogJumpDialogWindow> _dialogJumpDialogs = new();
private static IDialogJumpDialogWindow _dialogWindow = null;
private static readonly Lock _dialogWindowLock = new();
@ -101,22 +102,13 @@ namespace Flow.Launcher.Infrastructure.DialogJump
#region Initialize & Setup
public static void InitializeDialogJump(IList<DialogJumpExplorerPair> dialogJumpExplorers,
IList<DialogJumpDialogPair> dialogJumpDialogs)
public static void InitializeDialogJump()
{
if (_initialized) return;
// Initialize Dialog Jump explorers & dialogs
_dialogJumpExplorers.Add(WindowsDialogJumpExplorer, null);
foreach (var explorer in dialogJumpExplorers)
{
_dialogJumpExplorers.Add(explorer, null);
}
_dialogJumpDialogs.Add(WindowsDialogJumpDialog, null);
foreach (var dialog in dialogJumpDialogs)
{
_dialogJumpDialogs.Add(dialog, null);
}
// Initialize preinstalled Dialog Jump explorers & dialogs
_dialogJumpExplorers.TryAdd(WindowsDialogJumpExplorer, null);
_dialogJumpDialogs.TryAdd(WindowsDialogJumpDialog, null);
// Initialize main window handle
_mainWindowHandle = Win32Helper.GetMainWindowHandle();
@ -131,6 +123,29 @@ namespace Flow.Launcher.Infrastructure.DialogJump
_initialized = true;
}
public static void InitializeDialogJumpPlugin(PluginPair pair)
{
// Add Dialog Jump explorers & dialogs
if (pair.Plugin is IDialogJumpExplorer explorer)
{
var dialogJumpExplorer = new DialogJumpExplorerPair
{
Plugin = explorer,
Metadata = pair.Metadata
};
_dialogJumpExplorers.TryAdd(dialogJumpExplorer, null);
}
if (pair.Plugin is IDialogJumpDialog dialog)
{
var dialogJumpDialog = new DialogJumpDialogPair
{
Plugin = dialog,
Metadata = pair.Metadata
};
_dialogJumpDialogs.TryAdd(dialogJumpDialog, null);
}
}
public static void SetupDialogJump(bool enabled)
{
if (enabled == _enabled) return;

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
@ -173,9 +173,21 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Get all loaded plugins
/// </summary>
/// <remarks>
/// Will also return any plugins not fully initialized yet
/// </remarks>
/// <returns></returns>
List<PluginPair> GetAllPlugins();
/// <summary>
/// Get all initialized plugins
/// </summary>
/// <param name="includeFailed">
/// Whether to include plugins that failed to initialize
/// </param>
/// <returns></returns>
List<PluginPair> GetAllInitializedPlugins(bool includeFailed);
/// <summary>
/// Registers a callback function for global keyboard events.
/// </summary>

View file

@ -187,12 +187,14 @@ namespace Flow.Launcher
// So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
// Setup log level before any logging is done
Log.SetLogLevel(_settings.LogLevel);
// Update dynamic resources base on settings
Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont);
Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont);
// Initialize notification system before any notification api is called
Notification.Install();
// Enable Win32 dark mode if the system is in dark mode before creating all windows
@ -201,6 +203,7 @@ namespace Flow.Launcher
// Initialize language before portable clean up since it needs translations
await _internationalization.InitializeLanguageAsync();
// Clean up after portability update
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
@ -210,32 +213,25 @@ namespace Flow.Launcher
RegisterDispatcherUnhandledException();
RegisterTaskSchedulerUnhandledException();
var imageLoadertask = ImageLoader.InitializeAsync();
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
PluginManager.LoadPlugins(_settings.PluginSettings);
// Register ResultsUpdated event after all plugins are loaded
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
var imageLoaderTask = ImageLoader.InitializeAsync();
Http.Proxy = _settings.Proxy;
// Initialize plugin manifest before initializing plugins so that they can use the manifest instantly
await API.UpdatePluginManifestAsync();
await PluginManager.InitializePluginsAsync();
// Update plugin titles after plugins are initialized with their api instances
Internationalization.UpdatePluginMetadataTranslations();
await imageLoadertask;
await imageLoaderTask;
_mainWindow = new MainWindow();
Current.MainWindow = _mainWindow;
Current.MainWindow.Title = Constant.FlowLauncher;
// Initialize Dialog Jump before hotkey mapper since hotkey mapper will register its hotkey
// Initialize Dialog Jump after main window is created so that it can access main window handle
DialogJump.InitializeDialogJump();
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
// Initialize hotkey mapper instantly after main window is created because
// it will steal focus from main window which causes window hide
HotKeyMapper.Initialize();
@ -243,19 +239,40 @@ namespace Flow.Launcher
// Initialize theme for main window
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs());
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
RegisterExitEvents();
AutoStartup();
AutoUpdates();
AutoPluginUpdates();
API.SaveAppAllSettings();
API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------");
API.LogInfo(ClassName, "End Flow Launcher startup ------------------------------------------------------");
_ = API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
{
API.LogInfo(ClassName, "Begin plugin initialization ----------------------------------------------------");
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
PluginManager.LoadPlugins(_settings.PluginSettings);
await PluginManager.InitializePluginsAsync(_mainVM);
// Refresh home page after plugins are initialized because users may open main window during plugin initialization
// And home page is created without full plugin list
if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText))
{
_mainVM.QueryResults();
}
AutoPluginUpdates();
// Save all settings since we possibly update the plugin environment paths
API.SaveAppAllSettings();
API.LogInfo(ClassName, "End plugin initialization ------------------------------------------------------");
});
});
}

View file

@ -20,7 +20,7 @@ public static class ResultHelper
{
var plugin = PluginManager.GetPluginForId(pluginId);
if (plugin == null) return null;
var query = QueryBuilder.Build(rawQuery, PluginManager.NonGlobalPlugins);
var query = QueryBuilder.Build(rawQuery, PluginManager.GetNonGlobalPlugins());
if (query == null) return null;
try
{

View file

@ -66,6 +66,10 @@
<system:String x:Key="PositionReset">Position Reset</system:String>
<system:String x:Key="PositionResetToolTip">Reset search window position</system:String>
<system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
<system:String x:Key="pluginStillInitializing">{0}: This plugin is still initializing...</system:String>
<system:String x:Key="pluginStillInitializingSubtitle">Select this result to requery</system:String>
<system:String x:Key="pluginFailedToRespond">{0}: Failed to respond!</system:String>
<system:String x:Key="pluginFailedToRespondSubtitle">Select this result for more info</system:String>
<!-- Setting General -->
<system:String x:Key="flowlauncher_settings">Settings</system:String>

View file

@ -477,7 +477,7 @@ namespace Flow.Launcher
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
{
var queryWithoutActionKeyword =
QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search;
QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.GetNonGlobalPlugins())?.Search;
if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword))
{

View file

@ -5,7 +5,6 @@ using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -248,7 +247,10 @@ namespace Flow.Launcher
public string GetTranslation(string key) => Internationalization.GetTranslation(key);
public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins.ToList();
public List<PluginPair> GetAllPlugins() => PluginManager.GetAllLoadedPlugins();
public List<PluginPair> GetAllInitializedPlugins(bool includeFailed) =>
PluginManager.GetAllInitializedPlugins(includeFailed);
public MatchResult FuzzySearch(string query, string stringToCompare) =>
StringMatcher.FuzzySearch(query, stringToCompare);

View file

@ -114,8 +114,11 @@ public partial class SettingsPanePluginsViewModel : BaseModel
}
}
private IList<PluginViewModel>? _pluginViewModels;
public IList<PluginViewModel> PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins
private List<PluginViewModel>? _pluginViewModels;
// Get all plugins: Initializing & Initialized & Init failed plugins
// Include init failed ones so that we can uninstall them
// Include initializing ones so that we can change related settings like action keywords, etc.
public List<PluginViewModel> PluginViewModels => _pluginViewModels ??= App.API.GetAllPlugins()
.OrderBy(plugin => plugin.Metadata.Disabled)
.ThenBy(plugin => plugin.Metadata.Name)
.Select(plugin => new PluginViewModel

View file

@ -29,7 +29,7 @@ using Microsoft.VisualStudio.Threading;
namespace Flow.Launcher.ViewModel
{
public partial class MainViewModel : BaseModel, ISavable, IDisposable
public partial class MainViewModel : BaseModel, ISavable, IDisposable, IResultUpdateRegister
{
#region Private Fields
@ -277,52 +277,50 @@ namespace Flow.Launcher.ViewModel
}
}
public void RegisterResultsUpdatedEvent()
public void RegisterResultsUpdatedEvent(PluginPair pair)
{
foreach (var pair in PluginManager.GetResultUpdatePlugin())
if (pair.Plugin is not IResultUpdated plugin) return;
plugin.ResultsUpdated += (s, e) =>
{
var plugin = (IResultUpdated)pair.Plugin;
plugin.ResultsUpdated += (s, e) =>
if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested)
{
if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested)
return;
}
var token = e.Token == default ? _updateToken : e.Token;
IReadOnlyList<Result> resultsCopy;
if (e.Results == null)
{
resultsCopy = _emptyResult;
}
else
{
// make a clone to avoid possible issue that plugin will also change the list and items when updating view model
resultsCopy = DeepCloneResults(e.Results, false, token);
}
foreach (var result in resultsCopy)
{
if (string.IsNullOrEmpty(result.BadgeIcoPath))
{
return;
result.BadgeIcoPath = pair.Metadata.IcoPath;
}
}
var token = e.Token == default ? _updateToken : e.Token;
PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query);
IReadOnlyList<Result> resultsCopy;
if (e.Results == null)
{
resultsCopy = _emptyResult;
}
else
{
// make a clone to avoid possible issue that plugin will also change the list and items when updating view model
resultsCopy = DeepCloneResults(e.Results, false, token);
}
if (token.IsCancellationRequested) return;
foreach (var result in resultsCopy)
{
if (string.IsNullOrEmpty(result.BadgeIcoPath))
{
result.BadgeIcoPath = pair.Metadata.IcoPath;
}
}
App.API.LogDebug(ClassName, $"Update results for plugin <{pair.Metadata.Name}>");
PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query);
if (token.IsCancellationRequested) return;
App.API.LogDebug(ClassName, $"Update results for plugin <{pair.Metadata.Name}>");
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
token)))
{
App.API.LogError(ClassName, "Unable to add item to Result Update Queue");
}
};
}
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
token)))
{
App.API.LogError(ClassName, "Unable to add item to Result Update Queue");
}
};
}
private async Task RegisterClockAndDateUpdateAsync()
@ -444,7 +442,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private void Backspace(object index)
{
var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins);
var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.GetNonGlobalPlugins());
// GetPreviousExistingDirectory does not require trailing '\', otherwise will return empty string
var path = FilesFolders.GetPreviousExistingDirectory((_) => true, query.Search.TrimEnd('\\'));
@ -1627,7 +1625,7 @@ namespace Flow.Launcher.ViewModel
{
if (string.IsNullOrWhiteSpace(queryText))
{
return QueryBuilder.Build(string.Empty, PluginManager.NonGlobalPlugins);
return QueryBuilder.Build(string.Empty, PluginManager.GetNonGlobalPlugins());
}
var queryBuilder = new StringBuilder(queryText);
@ -1647,7 +1645,7 @@ namespace Flow.Launcher.ViewModel
// Applying builtin shortcuts
await BuildQueryAsync(builtInShortcuts, queryBuilder, queryBuilderTmp);
return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins);
return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.GetNonGlobalPlugins());
}
private async Task BuildQueryAsync(IEnumerable<BaseBuiltinShortcutModel> builtInShortcuts,

View file

@ -131,8 +131,13 @@ namespace Flow.Launcher.ViewModel
private Control _bottomPart2;
public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null;
public bool HasSettingControl => PluginPair.Plugin is ISettingProvider &&
(PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || jsonRPCPluginBase.NeedCreateSettingPanel());
public bool HasSettingControl =>
// Here we do not check if the plugin is initialized successfully
// So we can let users change settings for initializing or initialization failed plugins
PluginPair.Plugin is ISettingProvider &&
(PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || // Is not JsonRPC plugin
jsonRPCPluginBase.NeedCreateSettingPanel()); // Is JsonRPC plugin and need to create setting panel
public Control SettingControl
=> IsExpanded
? _settingControl