diff --git a/Flow.Launcher.Core/Configuration/Portable.cs b/Flow.Launcher.Core/Configuration/Portable.cs index 069154364ab..2b570d2c088 100644 --- a/Flow.Launcher.Core/Configuration/Portable.cs +++ b/Flow.Launcher.Core/Configuration/Portable.cs @@ -22,7 +22,7 @@ public class Portable : IPortable /// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish /// /// - private UpdateManager NewUpdateManager() + private static UpdateManager NewUpdateManager() { var applicationFolderName = Constant.ApplicationDirectory .Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None) @@ -81,20 +81,16 @@ public void EnablePortableMode() public void RemoveShortcuts() { - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu); - portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop); - portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup); - } + using var portabilityUpdater = NewUpdateManager(); + portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu); + portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop); + portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup); } public void RemoveUninstallerEntry() { - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.RemoveUninstallerRegistryEntry(); - } + using var portabilityUpdater = NewUpdateManager(); + portabilityUpdater.RemoveUninstallerRegistryEntry(); } public void MoveUserDataFolder(string fromLocation, string toLocation) @@ -110,12 +106,10 @@ public void VerifyUserDataAfterMove(string fromLocation, string toLocation) public void CreateShortcuts() { - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false); - portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false); - portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false); - } + using var portabilityUpdater = NewUpdateManager(); + portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false); + portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false); + portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false); } public void CreateUninstallerEntry() @@ -129,18 +123,14 @@ public void CreateUninstallerEntry() subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String); } - using (var portabilityUpdater = NewUpdateManager()) - { - _ = portabilityUpdater.CreateUninstallerRegistryEntry(); - } + using var portabilityUpdater = NewUpdateManager(); + _ = portabilityUpdater.CreateUninstallerRegistryEntry(); } - internal void IndicateDeletion(string filePathTodelete) + private static void IndicateDeletion(string filePathTodelete) { var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile); - using (var _ = File.CreateText(deleteFilePath)) - { - } + using var _ = File.CreateText(deleteFilePath); } /// diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs index 944b2fd100d..e0a21725135 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -23,6 +23,8 @@ public class JsonRPCPluginSettings protected ConcurrentDictionary Settings { get; set; } = null!; public required IPublicAPI API { get; init; } + private static readonly string ClassName = nameof(JsonRPCPluginSettings); + private JsonStorage> _storage = null!; private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin"); @@ -122,12 +124,26 @@ public void UpdateSettings(IReadOnlyDictionary settings) public async Task SaveAsync() { - await _storage.SaveAsync(); + try + { + await _storage.SaveAsync(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e); + } } public void Save() { - _storage.Save(); + try + { + _storage.Save(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e); + } } public bool NeedCreateSettingPanel() diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 96efcacf600..20b884b3d0d 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -4,10 +4,12 @@ using System.Net.Http; using System.Net.Sockets; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; using System.Windows; -using JetBrains.Annotations; -using Squirrel; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.Resource; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Infrastructure; @@ -15,9 +17,8 @@ using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using System.Text.Json.Serialization; -using System.Threading; -using System.Text.Json; +using JetBrains.Annotations; +using Squirrel; namespace Flow.Launcher.Core { @@ -71,7 +72,7 @@ public async Task UpdateAppAsync(bool silentUpdate = true) if (DataLocation.PortableDataLocationInUse()) { - var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}"; + var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}"; FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)); if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s))) _api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), @@ -123,7 +124,7 @@ private class GithubRelease } // https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs - private async Task GitHubUpdateManagerAsync(string repository) + private static async Task GitHubUpdateManagerAsync(string repository) { var uri = new Uri(repository); var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases"; @@ -145,9 +146,9 @@ private async Task GitHubUpdateManagerAsync(string repository) return manager; } - public string NewVersionTips(string version) + private static string NewVersionTips(string version) { - var translator = InternationalizationManager.Instance; + var translator = Ioc.Default.GetRequiredService(); var tips = string.Format(translator.GetTranslation("newVersionTips"), version); return tips; diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index 6f7b1cd908d..1ee033821f0 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -61,7 +61,7 @@ await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async }); } - public static async Task Save() + public static async Task SaveAsync() { await storageLock.WaitAsync(); @@ -71,12 +71,22 @@ await _storage.SaveAsync(ImageCache.EnumerateEntries() .Select(x => x.Key) .ToList()); } + catch (System.Exception e) + { + Log.Exception($"|ImageLoader.SaveAsync|Failed to save image cache to file", e); + } finally { storageLock.Release(); } } + public static async Task WaitSaveAsync() + { + await storageLock.WaitAsync(); + storageLock.Release(); + } + private static async Task> LoadStorageToConcurrentDictionaryAsync() { await storageLock.WaitAsync(); diff --git a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs index 3669bb405ea..8b4062b6b24 100644 --- a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs @@ -1,11 +1,20 @@ using System.IO; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Infrastructure.Storage { public class FlowLauncherJsonStorage : JsonStorage where T : new() { + private static readonly string ClassName = "FlowLauncherJsonStorage"; + + // We should not initialize API in static constructor because it will create another API instance + private static IPublicAPI api = null; + private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + public FlowLauncherJsonStorage() { var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); @@ -14,5 +23,29 @@ public FlowLauncherJsonStorage() var filename = typeof(T).Name; FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); } + + public new void Save() + { + try + { + base.Save(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e); + } + } + + public new async Task SaveAsync() + { + try + { + await base.SaveAsync(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e); + } + } } } diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs index cc78bb8f6dc..e8cbd70fb98 100644 --- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs @@ -1,5 +1,8 @@ using System.IO; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Infrastructure.Storage @@ -9,6 +12,12 @@ namespace Flow.Launcher.Infrastructure.Storage // Use assembly name to check which plugin is using this storage public readonly string AssemblyName; + private static readonly string ClassName = "PluginJsonStorage"; + + // We should not initialize API in static constructor because it will create another API instance + private static IPublicAPI api = null; + private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + public PluginJsonStorage() { // C# related, add python related below @@ -24,5 +33,29 @@ public PluginJsonStorage(T data) : this() { Data = data; } + + public new void Save() + { + try + { + base.Save(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e); + } + } + + public new async Task SaveAsync() + { + try + { + await base.SaveAsync(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e); + } + } } } diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 7a3a0c36e26..c21849403ca 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -488,5 +488,16 @@ or PInvoke.LOCALE_TRANSIENT_KEYBOARD3 } #endregion + + #region Notification + + public static bool IsNotificationSupported() + { + // Notifications only supported on Windows 10 19041+ + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + Environment.OSVersion.Version.Build >= 19041; + } + + #endregion } } diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 9aee56bff81..f484d4dbadc 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -304,6 +304,14 @@ protected virtual void Dispose(bool disposing) return; } + // If we call Environment.Exit(0), the application dispose will be called before _mainWindow.Close() + // Accessing _mainWindow?.Dispatcher will cause the application stuck + // So here we need to check it and just return so that we will not acees _mainWindow?.Dispatcher + if (!_mainWindow.CanClose) + { + return; + } + _disposed = true; } diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 011d46d6b21..c62606743a6 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -32,6 +32,13 @@ namespace Flow.Launcher { public partial class MainWindow : IDisposable { + #region Public Property + + // Window Event: Close Event + public bool CanClose { get; set; } = false; + + #endregion + #region Private Fields // Dependency Injection @@ -45,8 +52,6 @@ public partial class MainWindow : IDisposable private readonly ContextMenu _contextMenu = new(); private readonly MainViewModel _viewModel; - // Window Event: Close Event - private bool _canClose = false; // Window Event: Key Event private bool _isArrowKeyPressed = false; @@ -279,7 +284,7 @@ private async void OnLoaded(object sender, RoutedEventArgs _) private async void OnClosing(object sender, CancelEventArgs e) { - if (!_canClose) + if (!CanClose) { _notifyIcon.Visible = false; App.API.SaveAppAllSettings(); @@ -287,7 +292,7 @@ private async void OnClosing(object sender, CancelEventArgs e) await PluginManager.DisposePluginsAsync(); Notification.Uninstall(); // After plugins are all disposed, we can close the main window - _canClose = true; + CanClose = true; // Use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event Application.Current.Shutdown(); } diff --git a/Flow.Launcher/Notification.cs b/Flow.Launcher/Notification.cs index cb1cbb729ec..30b3a067334 100644 --- a/Flow.Launcher/Notification.cs +++ b/Flow.Launcher/Notification.cs @@ -9,8 +9,8 @@ namespace Flow.Launcher { internal static class Notification { - internal static bool legacy = Environment.OSVersion.Version.Build < 19041; - [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "")] + internal static bool legacy = !Win32Helper.IsNotificationSupported(); + internal static void Uninstall() { if (!legacy) @@ -25,7 +25,6 @@ public static void Show(string title, string subTitle, string iconPath = null) }); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "")] private static void ShowInternal(string title, string subTitle, string iconPath = null) { // Handle notification for win7/8/early win10 diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index e19ad2fdcd4..d88eeb7c9e3 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -37,6 +37,8 @@ public class PublicAPIInstance : IPublicAPI private readonly Internationalization _translater; private readonly MainViewModel _mainVM; + private readonly object _saveSettingsLock = new(); + #region Constructor public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM) @@ -57,21 +59,28 @@ public void ChangeQuery(string query, bool requery = false) _mainVM.ChangeQueryText(query, requery); } - public void RestartApp() +#pragma warning disable VSTHRD100 // Avoid async void methods + + public async void RestartApp() { _mainVM.Hide(); - // we must manually save + // We must manually save // UpdateManager.RestartApp() will call Environment.Exit(0) // which will cause ungraceful exit SaveAppAllSettings(); + // Wait for all image caches to be saved before restarting + await ImageLoader.WaitSaveAsync(); + // Restart requires Squirrel's Update.exe to be present in the parent folder, // it is only published from the project's release pipeline. When debugging without it, // the project may not restart or just terminates. This is expected. UpdateManager.RestartApp(Constant.ApplicationFileName); } +#pragma warning restore VSTHRD100 // Avoid async void methods + public void ShowMainWindow() => _mainVM.Show(); public void HideMainWindow() => _mainVM.Hide(); @@ -85,10 +94,13 @@ public void RestartApp() public void SaveAppAllSettings() { - PluginManager.Save(); - _mainVM.Save(); - _settings.Save(); - _ = ImageLoader.Save(); + lock (_saveSettingsLock) + { + _settings.Save(); + PluginManager.Save(); + _mainVM.Save(); + } + _ = ImageLoader.SaveAsync(); } public Task ReloadAllPluginData() => PluginManager.ReloadDataAsync();