Merge pull request #3456 from Flow-Launcher/plugin_exception

Improve FL Exiting & Exception Handler
This commit is contained in:
Jack Ye 2025-04-12 14:50:12 +08:00 committed by GitHub
commit c8d16a8de4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 77 additions and 43 deletions

View file

@ -37,7 +37,7 @@ namespace Flow.Launcher.Core.Plugin
private static PluginsSettings Settings; private static PluginsSettings Settings;
private static List<PluginMetadata> _metadatas; private static List<PluginMetadata> _metadatas;
private static List<string> _modifiedPlugins = new(); private static readonly List<string> _modifiedPlugins = new();
/// <summary> /// <summary>
/// Directories that will hold Flow Launcher plugin directory /// Directories that will hold Flow Launcher plugin directory
@ -61,10 +61,17 @@ namespace Flow.Launcher.Core.Plugin
/// </summary> /// </summary>
public static void Save() public static void Save()
{ {
foreach (var plugin in AllPlugins) foreach (var pluginPair in AllPlugins)
{ {
var savable = plugin.Plugin as ISavable; var savable = pluginPair.Plugin as ISavable;
savable?.Save(); try
{
savable?.Save();
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e);
}
} }
API.SavePluginSettings(); API.SavePluginSettings();
@ -81,14 +88,21 @@ namespace Flow.Launcher.Core.Plugin
private static async Task DisposePluginAsync(PluginPair pluginPair) private static async Task DisposePluginAsync(PluginPair pluginPair)
{ {
switch (pluginPair.Plugin) try
{ {
case IDisposable disposable: switch (pluginPair.Plugin)
disposable.Dispose(); {
break; case IDisposable disposable:
case IAsyncDisposable asyncDisposable: disposable.Dispose();
await asyncDisposable.DisposeAsync(); break;
break; case IAsyncDisposable asyncDisposable:
await asyncDisposable.DisposeAsync();
break;
}
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e);
} }
} }
@ -292,7 +306,7 @@ namespace Flow.Launcher.Core.Plugin
{ {
Title = $"{metadata.Name}: Failed to respond!", Title = $"{metadata.Name}: Failed to respond!",
SubTitle = "Select this result for more info", SubTitle = "Select this result for more info",
IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon, IcoPath = Constant.ErrorIcon,
PluginDirectory = metadata.PluginDirectory, PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword, ActionKeywordAssigned = query.ActionKeyword,
PluginID = metadata.ID, PluginID = metadata.ID,
@ -369,8 +383,8 @@ namespace Flow.Launcher.Core.Plugin
{ {
// this method is only checking for action keywords (defined as not '*') registration // this method is only checking for action keywords (defined as not '*') registration
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic // hence the actionKeyword != Query.GlobalPluginWildcardSign logic
return actionKeyword != Query.GlobalPluginWildcardSign return actionKeyword != Query.GlobalPluginWildcardSign
&& NonGlobalPlugins.ContainsKey(actionKeyword); && NonGlobalPlugins.ContainsKey(actionKeyword);
} }
/// <summary> /// <summary>

View file

@ -22,8 +22,8 @@ namespace Flow.Launcher.Core.Resource
private const string DefaultFile = "en.xaml"; private const string DefaultFile = "en.xaml";
private const string Extension = ".xaml"; private const string Extension = ".xaml";
private readonly Settings _settings; private readonly Settings _settings;
private readonly List<string> _languageDirectories = new List<string>(); private readonly List<string> _languageDirectories = new();
private readonly List<ResourceDictionary> _oldResources = new List<ResourceDictionary>(); private readonly List<ResourceDictionary> _oldResources = new();
private readonly string SystemLanguageCode; private readonly string SystemLanguageCode;
public Internationalization(Settings settings) public Internationalization(Settings settings)
@ -144,7 +144,7 @@ namespace Flow.Launcher.Core.Resource
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
} }
private Language GetLanguageByLanguageCode(string languageCode) private static Language GetLanguageByLanguageCode(string languageCode)
{ {
var lowercase = languageCode.ToLower(); var lowercase = languageCode.ToLower();
var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase); var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase);
@ -239,7 +239,7 @@ namespace Flow.Launcher.Core.Resource
return list; return list;
} }
public string GetTranslation(string key) public static string GetTranslation(string key)
{ {
var translation = Application.Current.TryFindResource(key); var translation = Application.Current.TryFindResource(key);
if (translation is string) if (translation is string)
@ -257,8 +257,7 @@ namespace Flow.Launcher.Core.Resource
{ {
foreach (var p in PluginManager.GetPluginsForInterface<IPluginI18n>()) foreach (var p in PluginManager.GetPluginsForInterface<IPluginI18n>())
{ {
var pluginI18N = p.Plugin as IPluginI18n; if (p.Plugin is not IPluginI18n pluginI18N) return;
if (pluginI18N == null) return;
try try
{ {
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
@ -272,11 +271,11 @@ namespace Flow.Launcher.Core.Resource
} }
} }
public string LanguageFile(string folder, string language) private static string LanguageFile(string folder, string language)
{ {
if (Directory.Exists(folder)) if (Directory.Exists(folder))
{ {
string path = Path.Combine(folder, language); var path = Path.Combine(folder, language);
if (File.Exists(path)) if (File.Exists(path))
{ {
return path; return path;
@ -284,7 +283,7 @@ namespace Flow.Launcher.Core.Resource
else else
{ {
Log.Error($"|Internationalization.LanguageFile|Language path can't be found <{path}>"); 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)) if (File.Exists(english))
{ {
return english; return english;

View file

@ -45,10 +45,10 @@ namespace Flow.Launcher.Infrastructure.Storage
[Obsolete("This constructor is obsolete. Use BinaryStorage(string filename) instead.")] [Obsolete("This constructor is obsolete. Use BinaryStorage(string filename) instead.")]
public BinaryStorage(string filename, string directoryPath = null!) public BinaryStorage(string filename, string directoryPath = null!)
{ {
directoryPath ??= DataLocation.CacheDirectory; DirectoryPath = directoryPath ?? DataLocation.CacheDirectory;
FilesFolders.ValidateDirectory(directoryPath); FilesFolders.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
} }
public async ValueTask<T> TryLoadAsync(T defaultData) public async ValueTask<T> TryLoadAsync(T defaultData)

View file

@ -17,11 +17,11 @@ namespace Flow.Launcher.Infrastructure.Storage
public FlowLauncherJsonStorage() public FlowLauncherJsonStorage()
{ {
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
FilesFolders.ValidateDirectory(directoryPath); FilesFolders.ValidateDirectory(DirectoryPath);
var filename = typeof(T).Name; var filename = typeof(T).Name;
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
} }
public new void Save() public new void Save()

View file

@ -264,12 +264,12 @@ namespace Flow.Launcher.Plugin.SharedCommands
var index = path.LastIndexOf('\\'); var index = path.LastIndexOf('\\');
if (index > 0 && index < (path.Length - 1)) if (index > 0 && index < (path.Length - 1))
{ {
string previousDirectoryPath = path.Substring(0, index + 1); string previousDirectoryPath = path[..(index + 1)];
return locationExists(previousDirectoryPath) ? previousDirectoryPath : ""; return locationExists(previousDirectoryPath) ? previousDirectoryPath : string.Empty;
} }
else else
{ {
return ""; return string.Empty;
} }
} }
@ -285,7 +285,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
// not full path, get previous level directory string // not full path, get previous level directory string
var indexOfSeparator = path.LastIndexOf('\\'); var indexOfSeparator = path.LastIndexOf('\\');
return path.Substring(0, indexOfSeparator + 1); return path[..(indexOfSeparator + 1)];
} }
return path; return path;

View file

@ -153,6 +153,7 @@ namespace Flow.Launcher
RegisterAppDomainExceptions(); RegisterAppDomainExceptions();
RegisterDispatcherUnhandledException(); RegisterDispatcherUnhandledException();
RegisterTaskSchedulerUnhandledException();
var imageLoadertask = ImageLoader.InitializeAsync(); var imageLoadertask = ImageLoader.InitializeAsync();
@ -284,6 +285,15 @@ namespace Flow.Launcher
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle;
} }
/// <summary>
/// let exception throw as normal is better for Debug
/// </summary>
[Conditional("RELEASE")]
private static void RegisterTaskSchedulerUnhandledException()
{
TaskScheduler.UnobservedTaskException += ErrorReporting.TaskSchedulerUnobservedTaskException;
}
#endregion #endregion
#region IDisposable #region IDisposable

View file

@ -1,8 +1,10 @@
using System; using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading; using System.Windows.Threading;
using NLog;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Exception; using Flow.Launcher.Infrastructure.Exception;
using NLog;
namespace Flow.Launcher.Helper; namespace Flow.Launcher.Helper;
@ -30,6 +32,13 @@ public static class ErrorReporting
e.Handled = true; 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() public static string RuntimeInfo()
{ {
var info = var info =

View file

@ -291,15 +291,15 @@ namespace Flow.Launcher
{ {
if (!CanClose) if (!CanClose)
{ {
CanClose = true;
_notifyIcon.Visible = false; _notifyIcon.Visible = false;
App.API.SaveAppAllSettings(); App.API.SaveAppAllSettings();
e.Cancel = true; e.Cancel = true;
await ImageLoader.WaitSaveAsync(); await ImageLoader.WaitSaveAsync();
await PluginManager.DisposePluginsAsync(); await PluginManager.DisposePluginsAsync();
Notification.Uninstall(); Notification.Uninstall();
// After plugins are all disposed, we can close the main window // After plugins are all disposed, we shutdown application to close app
CanClose = true; // We use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event
// Use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event
Application.Current.Shutdown(); Application.Current.Shutdown();
} }
} }

View file

@ -38,20 +38,23 @@ namespace Flow.Launcher
public class PublicAPIInstance : IPublicAPI, IRemovable public class PublicAPIInstance : IPublicAPI, IRemovable
{ {
private readonly Settings _settings; private readonly Settings _settings;
private readonly Internationalization _translater;
private readonly MainViewModel _mainVM; private readonly MainViewModel _mainVM;
// Must use getter to access Application.Current.Resources.MergedDictionaries so earlier
private Theme _theme; private Theme _theme;
private Theme Theme => _theme ??= Ioc.Default.GetRequiredService<Theme>(); private Theme Theme => _theme ??= Ioc.Default.GetRequiredService<Theme>();
// Must use getter to avoid circular dependency
private Updater _updater;
private Updater Updater => _updater ??= Ioc.Default.GetRequiredService<Updater>();
private readonly object _saveSettingsLock = new(); private readonly object _saveSettingsLock = new();
#region Constructor #region Constructor
public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM) public PublicAPIInstance(Settings settings, MainViewModel mainVM)
{ {
_settings = settings; _settings = settings;
_translater = translater;
_mainVM = mainVM; _mainVM = mainVM;
GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback; GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback;
WebRequest.RegisterPrefix("data", new DataWebRequestFactory()); WebRequest.RegisterPrefix("data", new DataWebRequestFactory());
@ -100,8 +103,7 @@ namespace Flow.Launcher
remove => _mainVM.VisibilityChanged -= value; remove => _mainVM.VisibilityChanged -= value;
} }
// Must use Ioc.Default.GetRequiredService<Updater>() to avoid circular dependency public void CheckForNewUpdate() => _ = Updater.UpdateAppAsync(false);
public void CheckForNewUpdate() => _ = Ioc.Default.GetRequiredService<Updater>().UpdateAppAsync(false);
public void SaveAppAllSettings() public void SaveAppAllSettings()
{ {
@ -178,7 +180,7 @@ namespace Flow.Launcher
public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed; 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<PluginPair> GetAllPlugins() => PluginManager.AllPlugins.ToList(); public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins.ToList();