diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 29d91dc8d28..52d6fd736db 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -37,7 +37,7 @@ public static class PluginManager private static PluginsSettings Settings; private static List _metadatas; - private static List _modifiedPlugins = new(); + private static readonly List _modifiedPlugins = new(); /// /// Directories that will hold Flow Launcher plugin directory @@ -61,10 +61,17 @@ private static void DeletePythonBinding() /// public static void Save() { - foreach (var plugin in AllPlugins) + foreach (var pluginPair in AllPlugins) { - var savable = plugin.Plugin as ISavable; - savable?.Save(); + var savable = pluginPair.Plugin as ISavable; + try + { + savable?.Save(); + } + catch (Exception e) + { + API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e); + } } API.SavePluginSettings(); @@ -81,14 +88,21 @@ public static async ValueTask DisposePluginsAsync() private static async Task DisposePluginAsync(PluginPair pluginPair) { - switch (pluginPair.Plugin) + try + { + switch (pluginPair.Plugin) + { + case IDisposable disposable: + disposable.Dispose(); + break; + case IAsyncDisposable asyncDisposable: + await asyncDisposable.DisposeAsync(); + break; + } + } + catch (Exception e) { - case IDisposable disposable: - disposable.Dispose(); - break; - case IAsyncDisposable asyncDisposable: - await asyncDisposable.DisposeAsync(); - break; + API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e); } } @@ -292,7 +306,7 @@ public static async Task> QueryForPluginAsync(PluginPair pair, Quer { Title = $"{metadata.Name}: Failed to respond!", SubTitle = "Select this result for more info", - IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon, + IcoPath = Constant.ErrorIcon, PluginDirectory = metadata.PluginDirectory, ActionKeywordAssigned = query.ActionKeyword, PluginID = metadata.ID, @@ -369,8 +383,8 @@ 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); + return actionKeyword != Query.GlobalPluginWildcardSign + && NonGlobalPlugins.ContainsKey(actionKeyword); } /// diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index ffa17ab4d5f..df841dbbe0e 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -22,8 +22,8 @@ public class Internationalization private const string DefaultFile = "en.xaml"; private const string Extension = ".xaml"; private readonly Settings _settings; - private readonly List _languageDirectories = new List(); - private readonly List _oldResources = new List(); + private readonly List _languageDirectories = new(); + private readonly List _oldResources = new(); private readonly string SystemLanguageCode; public Internationalization(Settings settings) @@ -144,7 +144,7 @@ public void ChangeLanguage(string languageCode) _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; } - private Language GetLanguageByLanguageCode(string languageCode) + private static Language GetLanguageByLanguageCode(string languageCode) { var lowercase = languageCode.ToLower(); var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase); @@ -239,7 +239,7 @@ public List LoadAvailableLanguages() return list; } - public string GetTranslation(string key) + public static string GetTranslation(string key) { var translation = Application.Current.TryFindResource(key); if (translation is string) @@ -257,8 +257,7 @@ private void UpdatePluginMetadataTranslations() { foreach (var p in PluginManager.GetPluginsForInterface()) { - var pluginI18N = p.Plugin as IPluginI18n; - if (pluginI18N == null) return; + if (p.Plugin is not IPluginI18n pluginI18N) return; try { p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); @@ -272,11 +271,11 @@ private void UpdatePluginMetadataTranslations() } } - public string LanguageFile(string folder, string language) + private static string LanguageFile(string folder, string language) { if (Directory.Exists(folder)) { - string path = Path.Combine(folder, language); + var path = Path.Combine(folder, language); if (File.Exists(path)) { return path; @@ -284,7 +283,7 @@ public string LanguageFile(string folder, string language) else { Log.Error($"|Internationalization.LanguageFile|Language path can't be found <{path}>"); - string english = Path.Combine(folder, DefaultFile); + var english = Path.Combine(folder, DefaultFile); if (File.Exists(english)) { return english; diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs index 64f80918199..8ff10816cca 100644 --- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs @@ -45,10 +45,10 @@ public BinaryStorage(string filename) [Obsolete("This constructor is obsolete. Use BinaryStorage(string filename) instead.")] public BinaryStorage(string filename, string directoryPath = null!) { - directoryPath ??= DataLocation.CacheDirectory; - FilesFolders.ValidateDirectory(directoryPath); + DirectoryPath = directoryPath ?? DataLocation.CacheDirectory; + FilesFolders.ValidateDirectory(DirectoryPath); - FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); + FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); } public async ValueTask TryLoadAsync(T defaultData) diff --git a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs index 8b4062b6b24..ca78b2f200a 100644 --- a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs @@ -17,11 +17,11 @@ namespace Flow.Launcher.Infrastructure.Storage public FlowLauncherJsonStorage() { - var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); - FilesFolders.ValidateDirectory(directoryPath); + DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); + FilesFolders.ValidateDirectory(DirectoryPath); var filename = typeof(T).Name; - FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); + FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); } public new void Save() diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 1de5841a530..6c506cfc06c 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -264,12 +264,12 @@ public static string GetPreviousExistingDirectory(Func locationExi var index = path.LastIndexOf('\\'); if (index > 0 && index < (path.Length - 1)) { - string previousDirectoryPath = path.Substring(0, index + 1); - return locationExists(previousDirectoryPath) ? previousDirectoryPath : ""; + string previousDirectoryPath = path[..(index + 1)]; + return locationExists(previousDirectoryPath) ? previousDirectoryPath : string.Empty; } else { - return ""; + return string.Empty; } } @@ -285,7 +285,7 @@ public static string ReturnPreviousDirectoryIfIncompleteString(string path) // not full path, get previous level directory string var indexOfSeparator = path.LastIndexOf('\\'); - return path.Substring(0, indexOfSeparator + 1); + return path[..(indexOfSeparator + 1)]; } return path; diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 89faa105ea7..a9cd1a8b986 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -153,6 +153,7 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => RegisterAppDomainExceptions(); RegisterDispatcherUnhandledException(); + RegisterTaskSchedulerUnhandledException(); var imageLoadertask = ImageLoader.InitializeAsync(); @@ -284,6 +285,15 @@ private static void RegisterAppDomainExceptions() AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; } + /// + /// let exception throw as normal is better for Debug + /// + [Conditional("RELEASE")] + private static void RegisterTaskSchedulerUnhandledException() + { + TaskScheduler.UnobservedTaskException += ErrorReporting.TaskSchedulerUnobservedTaskException; + } + #endregion #region IDisposable diff --git a/Flow.Launcher/Helper/ErrorReporting.cs b/Flow.Launcher/Helper/ErrorReporting.cs index 5b79c520d60..b1ddba7179a 100644 --- a/Flow.Launcher/Helper/ErrorReporting.cs +++ b/Flow.Launcher/Helper/ErrorReporting.cs @@ -1,8 +1,10 @@ using System; +using System.Threading.Tasks; +using System.Windows; using System.Windows.Threading; -using NLog; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Exception; +using NLog; namespace Flow.Launcher.Helper; @@ -30,6 +32,13 @@ public static void DispatcherUnhandledException(object sender, DispatcherUnhandl e.Handled = true; } + public static void TaskSchedulerUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + //handle unobserved task exceptions + Application.Current.Dispatcher.Invoke(() => Report(e.Exception)); + //prevent application exit, so the user can copy the prompted error info + } + public static string RuntimeInfo() { var info = diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 30afe67a1f3..bf7a45b1d25 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -291,15 +291,15 @@ private async void OnClosing(object sender, CancelEventArgs e) { if (!CanClose) { + CanClose = true; _notifyIcon.Visible = false; App.API.SaveAppAllSettings(); e.Cancel = true; await ImageLoader.WaitSaveAsync(); await PluginManager.DisposePluginsAsync(); Notification.Uninstall(); - // After plugins are all disposed, we can close the main window - CanClose = true; - // Use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event + // After plugins are all disposed, we shutdown application to close app + // We use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event Application.Current.Shutdown(); } } diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 95ef6c9f3bd..5438eac7dac 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -38,20 +38,23 @@ namespace Flow.Launcher public class PublicAPIInstance : IPublicAPI, IRemovable { private readonly Settings _settings; - private readonly Internationalization _translater; private readonly MainViewModel _mainVM; + // Must use getter to access Application.Current.Resources.MergedDictionaries so earlier private Theme _theme; private Theme Theme => _theme ??= Ioc.Default.GetRequiredService(); + // Must use getter to avoid circular dependency + private Updater _updater; + private Updater Updater => _updater ??= Ioc.Default.GetRequiredService(); + private readonly object _saveSettingsLock = new(); #region Constructor - public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM) + public PublicAPIInstance(Settings settings, MainViewModel mainVM) { _settings = settings; - _translater = translater; _mainVM = mainVM; GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback; WebRequest.RegisterPrefix("data", new DataWebRequestFactory()); @@ -100,8 +103,7 @@ public event VisibilityChangedEventHandler VisibilityChanged remove => _mainVM.VisibilityChanged -= value; } - // Must use Ioc.Default.GetRequiredService() to avoid circular dependency - public void CheckForNewUpdate() => _ = Ioc.Default.GetRequiredService().UpdateAppAsync(false); + public void CheckForNewUpdate() => _ = Updater.UpdateAppAsync(false); public void SaveAppAllSettings() { @@ -178,7 +180,7 @@ public void CopyToClipboard(string stringToCopy, bool directCopy = false, bool s public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed; - public string GetTranslation(string key) => _translater.GetTranslation(key); + public string GetTranslation(string key) => Internationalization.GetTranslation(key); public List GetAllPlugins() => PluginManager.AllPlugins.ToList();