diff --git a/src/modules/launcher/PowerLauncher/App.xaml.cs b/src/modules/launcher/PowerLauncher/App.xaml.cs index 3398f3bc45..25daee7578 100644 --- a/src/modules/launcher/PowerLauncher/App.xaml.cs +++ b/src/modules/launcher/PowerLauncher/App.xaml.cs @@ -37,7 +37,7 @@ namespace PowerLauncher private ThemeManager _themeManager; private SettingWindowViewModel _settingsVM; private StringMatcher _stringMatcher; - private SettingsWatcher _settingsWatcher; + private SettingsReader _settingsReader; [STAThread] public static void Main() @@ -103,6 +103,9 @@ namespace PowerLauncher _mainVM = new MainViewModel(_settings); _mainWindow = new MainWindow(_settings, _mainVM); API = new PublicAPIInstance(_settingsVM, _mainVM, _themeManager); + _settingsReader = new SettingsReader(_settings, _themeManager); + _settingsReader.ReadSettings(); + PluginManager.InitializePlugins(API); Current.MainWindow = _mainWindow; @@ -113,7 +116,7 @@ namespace PowerLauncher RegisterExitEvents(); - _settingsWatcher = new SettingsWatcher(_settings, _themeManager); + _settingsReader.ReadSettingsOnChange(); _mainVM.MainWindowVisibility = Visibility.Visible; _mainVM.ColdStartFix(); diff --git a/src/modules/launcher/PowerLauncher/Plugin/PluginManager.cs b/src/modules/launcher/PowerLauncher/Plugin/PluginManager.cs index 12e2a3cfd7..9b70756d61 100644 --- a/src/modules/launcher/PowerLauncher/Plugin/PluginManager.cs +++ b/src/modules/launcher/PowerLauncher/Plugin/PluginManager.cs @@ -10,6 +10,7 @@ using System.IO.Abstractions; using System.Linq; using System.Reflection; using System.Threading.Tasks; +using PowerLauncher.Properties; using Wox.Infrastructure; using Wox.Infrastructure.Storage; using Wox.Plugin; @@ -49,7 +50,10 @@ namespace PowerLauncher.Plugin { if (_allPlugins == null) { - _allPlugins = PluginsLoader.Plugins(PluginConfig.Parse(Directories)); + _allPlugins = PluginConfig.Parse(Directories) + .Where(x => x.Language.ToUpperInvariant() == AllowedLanguage.CSharp) + .Select(x => new PluginPair(x)) + .ToList(); } } } @@ -119,23 +123,15 @@ namespace PowerLauncher.Plugin var failedPlugins = new ConcurrentQueue(); Parallel.ForEach(AllPlugins, pair => { - try + if (pair.Metadata.Disabled) { - var milliseconds = Stopwatch.Debug($"PluginManager.InitializePlugins - Init method time cost for <{pair.Metadata.Name}>", () => - { - pair.Plugin.Init(new PluginInitContext - { - CurrentPluginMetadata = pair.Metadata, - API = API, - }); - }); - pair.Metadata.InitTime += milliseconds; - Log.Info($"Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>", MethodBase.GetCurrentMethod().DeclaringType); + return; } - catch (Exception e) + + pair.LoadPlugin(API); + + if (!pair.IsPluginLoaded) { - Log.Exception($"Fail to Init plugin: {pair.Metadata.Name}", e, MethodBase.GetCurrentMethod().DeclaringType); - pair.Metadata.Disabled = true; failedPlugins.Enqueue(pair); } }); @@ -145,7 +141,8 @@ namespace PowerLauncher.Plugin 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", string.Empty, false); + var description = string.Format(CultureInfo.CurrentCulture, Resources.FailedToInitializePluginsDescription, failed); + API.ShowMsg(Resources.FailedToInitializePluginsTitle, description, string.Empty, false); } } @@ -162,6 +159,11 @@ namespace PowerLauncher.Plugin throw new ArgumentNullException(nameof(pair)); } + if (!pair.IsPluginLoaded) + { + return new List(); + } + try { List results = null; diff --git a/src/modules/launcher/PowerLauncher/Plugin/PluginsLoader.cs b/src/modules/launcher/PowerLauncher/Plugin/PluginsLoader.cs deleted file mode 100644 index 2b51e114e0..0000000000 --- a/src/modules/launcher/PowerLauncher/Plugin/PluginsLoader.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.Loader; -using Wox.Infrastructure; -using Wox.Plugin; -using Wox.Plugin.Logger; - -namespace PowerLauncher.Plugin -{ - public static class PluginsLoader - { - public const string PATH = "PATH"; - - public static List Plugins(List metadatas) - { - var csharpPlugins = CSharpPlugins(metadatas).ToList(); - return csharpPlugins; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Suppressing this to enable FxCop. We are logging the exception, and going forward general exceptions should not be caught")] - public static IEnumerable CSharpPlugins(List source) - { - var plugins = new List(); - var metadatas = source - .Where(o => o.Language.ToUpperInvariant() == AllowedLanguage.CSharp) - .ToList(); - - foreach (var metadata in metadatas) - { - var milliseconds = Stopwatch.Debug($"PluginsLoader.CSharpPlugins - Constructor init cost for {metadata.Name}", () => - { -#if DEBUG - var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(metadata.ExecuteFilePath); - var types = assembly.GetTypes(); - var type = types.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Contains(typeof(IPlugin))); - var plugin = (IPlugin)Activator.CreateInstance(type); -#else - Assembly assembly; - try - { - assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(metadata.ExecuteFilePath); - } - catch (Exception e) - { - Log.Exception($"Couldn't load assembly for {metadata.Name}", e, MethodBase.GetCurrentMethod().DeclaringType); - return; - } - - var types = assembly.GetTypes(); - Type type; - try - { - type = types.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Contains(typeof(IPlugin))); - } - catch (InvalidOperationException e) - { - Log.Exception($"Can't find class implement IPlugin for <{metadata.Name}>", e, MethodBase.GetCurrentMethod().DeclaringType); - return; - } - - IPlugin plugin; - try - { - plugin = (IPlugin)Activator.CreateInstance(type); - } - catch (Exception e) - { - Log.Exception($"Can't create instance for <{metadata.Name}>", e, MethodBase.GetCurrentMethod().DeclaringType); - return; - } -#endif - PluginPair pair = new PluginPair - { - Plugin = plugin, - Metadata = metadata, - }; - plugins.Add(pair); - }); - metadata.InitTime += milliseconds; - } - - return plugins; - } - } -} diff --git a/src/modules/launcher/PowerLauncher/Properties/Resources.Designer.cs b/src/modules/launcher/PowerLauncher/Properties/Resources.Designer.cs index 589dffd5ca..e67df7b86b 100644 --- a/src/modules/launcher/PowerLauncher/Properties/Resources.Designer.cs +++ b/src/modules/launcher/PowerLauncher/Properties/Resources.Designer.cs @@ -132,6 +132,24 @@ namespace PowerLauncher.Properties { } } + /// + /// Looks up a localized string similar to Plugins: {0} - fail to load and would be disabled, please contact plugins creator for help. + /// + public static string FailedToInitializePluginsDescription { + get { + return ResourceManager.GetString("FailedToInitializePluginsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fail to initialize plugins. + /// + public static string FailedToInitializePluginsTitle { + get { + return ResourceManager.GetString("FailedToInitializePluginsTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Last execution time: {0}. /// diff --git a/src/modules/launcher/PowerLauncher/Properties/Resources.resx b/src/modules/launcher/PowerLauncher/Properties/Resources.resx index e0b6647201..6353c2dea9 100644 --- a/src/modules/launcher/PowerLauncher/Properties/Resources.resx +++ b/src/modules/launcher/PowerLauncher/Properties/Resources.resx @@ -185,4 +185,10 @@ Settings will be reset to default and program will continue to function. + + Plugins: {0} - fail to load and would be disabled, please contact plugins creator for help + + + Fail to initialize plugins + \ No newline at end of file diff --git a/src/modules/launcher/PowerLauncher/SettingsWatcher.cs b/src/modules/launcher/PowerLauncher/SettingsReader.cs similarity index 87% rename from src/modules/launcher/PowerLauncher/SettingsWatcher.cs rename to src/modules/launcher/PowerLauncher/SettingsReader.cs index 31bcaaf4d8..632334f1ba 100644 --- a/src/modules/launcher/PowerLauncher/SettingsWatcher.cs +++ b/src/modules/launcher/PowerLauncher/SettingsReader.cs @@ -22,29 +22,23 @@ using JsonException = System.Text.Json.JsonException; namespace PowerLauncher { // Watch for /Local/Microsoft/PowerToys/Launcher/Settings.json changes - public class SettingsWatcher : BaseModel + public class SettingsReader : BaseModel { private readonly ISettingsUtils _settingsUtils; private const int MaxRetries = 10; - private static readonly object _watcherSyncObject = new object(); - private readonly IFileSystemWatcher _watcher; + private static readonly object _readSyncObject = new object(); private readonly PowerToysRunSettings _settings; - private readonly ThemeManager _themeManager; - public SettingsWatcher(PowerToysRunSettings settings, ThemeManager themeManager) + private IFileSystemWatcher _watcher; + + public SettingsReader(PowerToysRunSettings settings, ThemeManager themeManager) { _settingsUtils = new SettingsUtils(); _settings = settings; _themeManager = themeManager; - // Set up watcher - _watcher = Microsoft.PowerToys.Settings.UI.Library.Utilities.Helper.GetFileWatcher(PowerLauncherSettings.ModuleName, "settings.json", OverloadSettings); - - // Load initial settings file - OverloadSettings(); - // Apply theme at startup _themeManager.ChangeTheme(_settings.Theme, true); } @@ -61,9 +55,14 @@ namespace PowerLauncher } } - public void OverloadSettings() + public void ReadSettingsOnChange() { - Monitor.Enter(_watcherSyncObject); + _watcher = Microsoft.PowerToys.Settings.UI.Library.Utilities.Helper.GetFileWatcher(PowerLauncherSettings.ModuleName, "settings.json", ReadSettings); + } + + public void ReadSettings() + { + Monitor.Enter(_readSyncObject); var retry = true; var retryCount = 0; while (retry) @@ -86,16 +85,7 @@ namespace PowerLauncher foreach (var setting in overloadSettings.Plugins) { var plugin = PluginManager.AllPlugins.FirstOrDefault(x => x.Metadata.ID == setting.Id); - if (plugin != null) - { - plugin.Metadata.Disabled = setting.Disabled; - plugin.Metadata.ActionKeyword = setting.ActionKeyword; - plugin.Metadata.IsGlobal = setting.IsGlobal; - if (plugin.Plugin is ISettingProvider) - { - (plugin.Plugin as ISettingProvider).UpdateSettings(setting); - } - } + plugin?.Update(setting, App.API); } } @@ -168,7 +158,7 @@ namespace PowerLauncher } } - Monitor.Exit(_watcherSyncObject); + Monitor.Exit(_readSyncObject); } private static string ConvertHotkey(HotkeySettings hotkey) diff --git a/src/modules/launcher/Wox.Plugin/PluginPair.cs b/src/modules/launcher/Wox.Plugin/PluginPair.cs index 6bb94815b0..2fe4a821ce 100644 --- a/src/modules/launcher/Wox.Plugin/PluginPair.cs +++ b/src/modules/launcher/Wox.Plugin/PluginPair.cs @@ -3,6 +3,14 @@ // See the LICENSE file in the project root for more information. using System; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Microsoft.PowerToys.Settings.UI.Library; +using Wox.Plugin.Logger; +using Wox.Plugin.Properties; namespace Wox.Plugin { @@ -12,6 +20,76 @@ namespace Wox.Plugin public PluginMetadata Metadata { get; internal set; } + public PluginPair(PluginMetadata metadata) + { + this.Metadata = metadata; + } + + public bool IsPluginLoaded { get; set; } + + public void LoadPlugin(IPublicAPI api) + { + if (Metadata.Disabled) + { + Log.Info($"Do not load {Metadata.Name} as it is disabled.", GetType()); + return; + } + + if (IsPluginLoaded) + { + Log.Info($"Plugin {Metadata.Name} is already loaded", GetType()); + return; + } + + var stopWatch = new Stopwatch(); + stopWatch.Start(); + if (!CreatePluginInstance()) + { + return; + } + + if (!InitPlugin(api)) + { + return; + } + + stopWatch.Stop(); + IsPluginLoaded = true; + Metadata.InitTime += stopWatch.ElapsedMilliseconds; + Log.Info($"Total load cost for <{Metadata.Name}> is <{Metadata.InitTime}ms>", GetType()); + return; + } + + public void Update(PowerLauncherPluginSettings setting, IPublicAPI api) + { + if (setting == null || api == null) + { + return; + } + + if (Metadata.Disabled && !setting.Disabled) + { + Metadata.Disabled = false; + LoadPlugin(api); + if (!IsPluginLoaded) + { + var title = string.Format(CultureInfo.CurrentCulture, Resources.FailedToLoadPluginTitle, Metadata.Name); + api.ShowMsg(title, Resources.FailedToLoadPluginDescription, string.Empty, false); + } + } + else + { + Metadata.Disabled = setting.Disabled; + } + + Metadata.ActionKeyword = setting.ActionKeyword; + Metadata.IsGlobal = setting.IsGlobal; + if (Plugin is ISettingProvider) + { + (Plugin as ISettingProvider).UpdateSettings(setting); + } + } + public override string ToString() { return Metadata.Name; @@ -36,5 +114,69 @@ namespace Wox.Plugin var hashcode = Metadata.ID?.GetHashCode(StringComparison.Ordinal) ?? 0; return hashcode; } + + private bool CreatePluginInstance() + { + try + { + _assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(Metadata.ExecuteFilePath); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + Log.Exception($"Couldn't load assembly for {Metadata.Name}", e, MethodBase.GetCurrentMethod().DeclaringType); + return false; + } + + var types = _assembly.GetTypes(); + Type type; + try + { + type = types.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Contains(typeof(IPlugin))); + } + catch (InvalidOperationException e) + { + Log.Exception($"Can't find class implement IPlugin for <{Metadata.Name}>", e, MethodBase.GetCurrentMethod().DeclaringType); + return false; + } + + try + { + Plugin = (IPlugin)Activator.CreateInstance(type); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + Log.Exception($"Can't create instance for <{Metadata.Name}>", e, MethodBase.GetCurrentMethod().DeclaringType); + return false; + } + + return true; + } + + private bool InitPlugin(IPublicAPI api) + { + try + { + Plugin.Init(new PluginInitContext + { + CurrentPluginMetadata = Metadata, + API = api, + }); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + Log.Exception($"Fail to Init plugin: {Metadata.Name}", e, GetType()); + return false; + } + + return true; + } + + private Assembly _assembly; } } diff --git a/src/modules/launcher/Wox.Plugin/Properties/Resources.Designer.cs b/src/modules/launcher/Wox.Plugin/Properties/Resources.Designer.cs index 9d6b31a320..13e4320c1e 100644 --- a/src/modules/launcher/Wox.Plugin/Properties/Resources.Designer.cs +++ b/src/modules/launcher/Wox.Plugin/Properties/Resources.Designer.cs @@ -59,5 +59,23 @@ namespace Wox.Plugin.Properties { resourceCulture = value; } } + + /// + /// Looks up a localized string similar to Please contact plugin creator for help. + /// + public static string FailedToLoadPluginDescription { + get { + return ResourceManager.GetString("FailedToLoadPluginDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fail to Load {0} Plugin. + /// + public static string FailedToLoadPluginTitle { + get { + return ResourceManager.GetString("FailedToLoadPluginTitle", resourceCulture); + } + } } } diff --git a/src/modules/launcher/Wox.Plugin/Properties/Resources.resx b/src/modules/launcher/Wox.Plugin/Properties/Resources.resx index 1af7de150c..469e7716b1 100644 --- a/src/modules/launcher/Wox.Plugin/Properties/Resources.resx +++ b/src/modules/launcher/Wox.Plugin/Properties/Resources.resx @@ -117,4 +117,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Please contact plugin creator for help + + + Fail to Load {0} Plugin + \ No newline at end of file diff --git a/src/modules/launcher/Wox.Test/PluginManagerTest.cs b/src/modules/launcher/Wox.Test/PluginManagerTest.cs index ac8b093384..ae0bfb47fc 100644 --- a/src/modules/launcher/Wox.Test/PluginManagerTest.cs +++ b/src/modules/launcher/Wox.Test/PluginManagerTest.cs @@ -45,10 +45,10 @@ namespace Wox.Test var results = new List() { result }; var pluginMock = new Mock(); pluginMock.Setup(r => r.Query(query)).Returns(results); - var pluginPair = new PluginPair + var pluginPair = new PluginPair(metadata) { Plugin = pluginMock.Object, - Metadata = metadata, + IsPluginLoaded = true, }; // Act diff --git a/src/modules/launcher/Wox.Test/QueryBuilderTest.cs b/src/modules/launcher/Wox.Test/QueryBuilderTest.cs index fee8756909..40707822d3 100644 --- a/src/modules/launcher/Wox.Test/QueryBuilderTest.cs +++ b/src/modules/launcher/Wox.Test/QueryBuilderTest.cs @@ -28,10 +28,7 @@ namespace Wox.Test // Arrange PluginManager.SetAllPlugins(new List() { - new PluginPair - { - Metadata = new PluginMetadata() { ActionKeyword = ">" }, - }, + new PluginPair(new PluginMetadata() { ActionKeyword = ">" }), }); string searchQuery = "> file.txt file2 file3"; @@ -51,7 +48,7 @@ namespace Wox.Test string searchQuery = "file.txt file2 file3"; PluginManager.SetAllPlugins(new List() { - new PluginPair { Metadata = new PluginMetadata() { Disabled = false, IsGlobal = true } }, + new PluginPair(new PluginMetadata() { Disabled = false, IsGlobal = true }), }); // Act @@ -66,7 +63,7 @@ namespace Wox.Test public void QueryBuildShouldGenerateSameSearchQueryWithOrWithoutSpaceAfterActionKeyword() { // Arrange - var plugin = new PluginPair { Metadata = new PluginMetadata() { ActionKeyword = "a" } }; + var plugin = new PluginPair(new PluginMetadata() { ActionKeyword = "a" }); PluginManager.SetAllPlugins(new List() { plugin, @@ -93,8 +90,8 @@ namespace Wox.Test { // Arrange string searchQuery = "abcdefgh"; - var firstPlugin = new PluginPair { Metadata = new PluginMetadata { ActionKeyword = "ab", ID = "plugin1" } }; - var secondPlugin = new PluginPair { Metadata = new PluginMetadata { ActionKeyword = "abcd", ID = "plugin2" } }; + var firstPlugin = new PluginPair(new PluginMetadata { ActionKeyword = "ab", ID = "plugin1" }); + var secondPlugin = new PluginPair(new PluginMetadata { ActionKeyword = "abcd", ID = "plugin2" }); PluginManager.SetAllPlugins(new List() { firstPlugin, @@ -117,8 +114,8 @@ namespace Wox.Test { // Arrange string searchQuery = "abcd efgh"; - var firstPlugin = new PluginPair { Metadata = new PluginMetadata { ActionKeyword = "ab", ID = "plugin1" } }; - var secondPlugin = new PluginPair { Metadata = new PluginMetadata { ActionKeyword = "abcd", ID = "plugin2" } }; + var firstPlugin = new PluginPair(new PluginMetadata { ActionKeyword = "ab", ID = "plugin1" }); + var secondPlugin = new PluginPair(new PluginMetadata { ActionKeyword = "abcd", ID = "plugin2" }); PluginManager.SetAllPlugins(new List() { firstPlugin, @@ -142,8 +139,8 @@ namespace Wox.Test { // Arrange string searchQuery = "!efgh"; - var firstPlugin = new PluginPair { Metadata = new PluginMetadata { ActionKeyword = "!", ID = "plugin1" } }; - var secondPlugin = new PluginPair { Metadata = new PluginMetadata { ActionKeyword = "!", ID = "plugin2" } }; + var firstPlugin = new PluginPair(new PluginMetadata { ActionKeyword = "!", ID = "plugin1" }); + var secondPlugin = new PluginPair(new PluginMetadata { ActionKeyword = "!", ID = "plugin2" }); PluginManager.SetAllPlugins(new List() { firstPlugin,