Merge pull request #3417 from Flow-Launcher/environment_exit

Fix environment exit & Wait image cache save before restarting & Add settings saving lock & Do not crash when caught exception saving
This commit is contained in:
Jack Ye 2025-04-04 15:30:33 +08:00 committed by GitHub
commit 67cc1e2c77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 168 additions and 50 deletions

View file

@ -22,7 +22,7 @@ namespace Flow.Launcher.Core.Configuration
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
/// </summary>
/// <returns></returns>
private UpdateManager NewUpdateManager()
private static UpdateManager NewUpdateManager()
{
var applicationFolderName = Constant.ApplicationDirectory
.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None)
@ -81,20 +81,16 @@ namespace Flow.Launcher.Core.Configuration
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 @@ namespace Flow.Launcher.Core.Configuration
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 @@ namespace Flow.Launcher.Core.Configuration
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);
}
///<summary>

View file

@ -23,6 +23,8 @@ namespace Flow.Launcher.Core.Plugin
protected ConcurrentDictionary<string, object?> Settings { get; set; } = null!;
public required IPublicAPI API { get; init; }
private static readonly string ClassName = nameof(JsonRPCPluginSettings);
private JsonStorage<ConcurrentDictionary<string, object?>> _storage = null!;
private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin");
@ -122,12 +124,26 @@ namespace Flow.Launcher.Core.Plugin
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()

View file

@ -4,10 +4,12 @@ using System.Net;
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.Http;
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 @@ namespace Flow.Launcher.Core
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 @@ namespace Flow.Launcher.Core
}
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
private async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
private static async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
{
var uri = new Uri(repository);
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
@ -145,9 +146,9 @@ namespace Flow.Launcher.Core
return manager;
}
public string NewVersionTips(string version)
private static string NewVersionTips(string version)
{
var translator = InternationalizationManager.Instance;
var translator = Ioc.Default.GetRequiredService<Internationalization>();
var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
return tips;

View file

@ -61,7 +61,7 @@ namespace Flow.Launcher.Infrastructure.Image
});
}
public static async Task Save()
public static async Task SaveAsync()
{
await storageLock.WaitAsync();
@ -71,12 +71,22 @@ namespace Flow.Launcher.Infrastructure.Image
.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<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
{
await storageLock.WaitAsync();

View file

@ -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<T> : JsonStorage<T> 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<IPublicAPI>();
public FlowLauncherJsonStorage()
{
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
@ -14,5 +23,29 @@ namespace Flow.Launcher.Infrastructure.Storage
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);
}
}
}
}

View file

@ -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<IPublicAPI>();
public PluginJsonStorage()
{
// C# related, add python related below
@ -24,5 +33,29 @@ namespace Flow.Launcher.Infrastructure.Storage
{
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);
}
}
}
}

View file

@ -488,5 +488,16 @@ namespace Flow.Launcher.Infrastructure
}
#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
}
}

View file

@ -304,6 +304,14 @@ namespace Flow.Launcher
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;
}

View file

@ -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 @@ namespace Flow.Launcher
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 @@ namespace Flow.Launcher
private async void OnClosing(object sender, CancelEventArgs e)
{
if (!_canClose)
if (!CanClose)
{
_notifyIcon.Visible = false;
App.API.SaveAppAllSettings();
@ -287,7 +292,7 @@ namespace Flow.Launcher
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();
}

View file

@ -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 = "<Pending>")]
internal static bool legacy = !Win32Helper.IsNotificationSupported();
internal static void Uninstall()
{
if (!legacy)
@ -25,7 +25,6 @@ namespace Flow.Launcher
});
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]
private static void ShowInternal(string title, string subTitle, string iconPath = null)
{
// Handle notification for win7/8/early win10

View file

@ -37,6 +37,8 @@ namespace Flow.Launcher
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 @@ namespace Flow.Launcher
_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 @@ namespace Flow.Launcher
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();