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();