From 1b066a572487e21be6d62d1ffebb5064abc9c99c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 21 Mar 2025 16:49:22 +0800 Subject: [PATCH 1/9] Improve code quality --- Flow.Launcher/App.xaml.cs | 3 +- Flow.Launcher/Helper/SingleInstance.cs | 232 ++++++++++++------------- 2 files changed, 108 insertions(+), 127 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 23c77618f5d..19e932ea834 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -28,7 +28,6 @@ namespace Flow.Launcher public partial class App : IDisposable, ISingleInstanceApp { public static IPublicAPI API { get; private set; } - private const string Unique = "Flow.Launcher_Unique_Application_Mutex"; private static bool _disposed; private readonly Settings _settings; @@ -99,7 +98,7 @@ private static void ShowErrorMsgBoxAndFailFast(string message, Exception e) [STAThread] public static void Main() { - if (SingleInstance.InitializeAsFirstInstance(Unique)) + if (SingleInstance.InitializeAsFirstInstance()) { using var application = new App(); application.InitializeComponent(); diff --git a/Flow.Launcher/Helper/SingleInstance.cs b/Flow.Launcher/Helper/SingleInstance.cs index e0e3075f636..76c109a393f 100644 --- a/Flow.Launcher/Helper/SingleInstance.cs +++ b/Flow.Launcher/Helper/SingleInstance.cs @@ -6,155 +6,137 @@ // http://blogs.microsoft.co.il/arik/2010/05/28/wpf-single-instance-application/ // modified to allow single instace restart -namespace Flow.Launcher.Helper +namespace Flow.Launcher.Helper; + +public interface ISingleInstanceApp { - public interface ISingleInstanceApp - { - void OnSecondAppStarted(); - } + void OnSecondAppStarted(); +} + +/// +/// This class checks to make sure that only one instance of +/// this application is running at a time. +/// +/// +/// Note: this class should be used with some caution, because it does no +/// security checking. For example, if one instance of an app that uses this class +/// is running as Administrator, any other instance, even if it is not +/// running as Administrator, can activate it with command line arguments. +/// For most apps, this will not be much of an issue. +/// +public static class SingleInstance where TApplication: Application, ISingleInstanceApp +{ + #region Private Fields /// - /// This class checks to make sure that only one instance of - /// this application is running at a time. + /// String delimiter used in channel names. /// - /// - /// Note: this class should be used with some caution, because it does no - /// security checking. For example, if one instance of an app that uses this class - /// is running as Administrator, any other instance, even if it is not - /// running as Administrator, can activate it with command line arguments. - /// For most apps, this will not be much of an issue. - /// - public static class SingleInstance - where TApplication: Application , ISingleInstanceApp - - { - #region Private Fields + private const string Delimiter = ":"; - /// - /// String delimiter used in channel names. - /// - private const string Delimiter = ":"; + /// + /// Suffix to the channel name. + /// + private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; + private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex"; + + /// + /// Application mutex. + /// + internal static Mutex SingleInstanceMutex { get; set; } - /// - /// Suffix to the channel name. - /// - private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; + #endregion - /// - /// Application mutex. - /// - internal static Mutex singleInstanceMutex; + #region Public Methods - #endregion + /// + /// Checks if the instance of the application attempting to start is the first instance. + /// If not, activates the first instance. + /// + /// True if this is the first instance of the application. + public static bool InitializeAsFirstInstance() + { + // Build unique application Id and the IPC channel name. + string applicationIdentifier = InstanceMutexName + Environment.UserName; - #region Public Methods + string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); - /// - /// Checks if the instance of the application attempting to start is the first instance. - /// If not, activates the first instance. - /// - /// True if this is the first instance of the application. - public static bool InitializeAsFirstInstance( string uniqueName ) + // Create mutex based on unique application Id to check if this is the first instance of the application. + SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); + if (firstInstance) { - // Build unique application Id and the IPC channel name. - string applicationIdentifier = uniqueName + Environment.UserName; - - string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); - - // Create mutex based on unique application Id to check if this is the first instance of the application. - bool firstInstance; - singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance); - if (firstInstance) - { - _ = CreateRemoteService(channelName); - return true; - } - else - { - _ = SignalFirstInstance(channelName); - return false; - } + _ = CreateRemoteServiceAsync(channelName); + return true; } - - /// - /// Cleans up single-instance code, clearing shared resources, mutexes, etc. - /// - public static void Cleanup() + else { - singleInstanceMutex?.ReleaseMutex(); + _ = SignalFirstInstanceAsync(channelName); + return false; } + } - #endregion + /// + /// Cleans up single-instance code, clearing shared resources, mutexes, etc. + /// + public static void Cleanup() + { + SingleInstanceMutex?.ReleaseMutex(); + } - #region Private Methods + #endregion - /// - /// Creates a remote server pipe for communication. - /// Once receives signal from client, will activate first instance. - /// - /// Application's IPC channel name. - private static async Task CreateRemoteService(string channelName) - { - using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In)) - { - while(true) - { - // Wait for connection to the pipe - await pipeServer.WaitForConnectionAsync(); - if (Application.Current != null) - { - // Do an asynchronous call to ActivateFirstInstance function - Application.Current.Dispatcher.Invoke(ActivateFirstInstance); - } - // Disconect client - pipeServer.Disconnect(); - } - } - } + #region Private Methods - /// - /// Creates a client pipe and sends a signal to server to launch first instance - /// - /// Application's IPC channel name. - /// - /// Command line arguments for the second instance, passed to the first instance to take appropriate action. - /// - private static async Task SignalFirstInstance(string channelName) + /// + /// Creates a remote server pipe for communication. + /// Once receives signal from client, will activate first instance. + /// + /// Application's IPC channel name. + private static async Task CreateRemoteServiceAsync(string channelName) + { + using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); + while (true) { - // Create a client pipe connected to server - using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out)) - { - // Connect to the available pipe - await pipeClient.ConnectAsync(0); - } - } + // Wait for connection to the pipe + await pipeServer.WaitForConnectionAsync(); - /// - /// Callback for activating first instance of the application. - /// - /// Callback argument. - /// Always null. - private static object ActivateFirstInstanceCallback(object o) - { - ActivateFirstInstance(); - return null; + // Do an asynchronous call to ActivateFirstInstance function + Application.Current?.Dispatcher.Invoke(ActivateFirstInstance); + + // Disconect client + pipeServer.Disconnect(); } + } - /// - /// Activates the first instance of the application with arguments from a second instance. - /// - /// List of arguments to supply the first instance of the application. - private static void ActivateFirstInstance() - { - // Set main window state and process command line args - if (Application.Current == null) - { - return; - } + /// + /// Creates a client pipe and sends a signal to server to launch first instance + /// + /// Application's IPC channel name. + /// + /// Command line arguments for the second instance, passed to the first instance to take appropriate action. + /// + private static async Task SignalFirstInstanceAsync(string channelName) + { + // Create a client pipe connected to server + using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); - ((TApplication)Application.Current).OnSecondAppStarted(); + // Connect to the available pipe + await pipeClient.ConnectAsync(0); + } + + /// + /// Activates the first instance of the application with arguments from a second instance. + /// + /// List of arguments to supply the first instance of the application. + private static void ActivateFirstInstance() + { + // Set main window state and process command line args + if (Application.Current == null) + { + return; } - #endregion + ((TApplication)Application.Current).OnSecondAppStarted(); } + + #endregion } From 783cef6814c8731580ac99b691ec667ab854011b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 21 Mar 2025 20:55:57 +0800 Subject: [PATCH 2/9] Gracefully shutdown all threads when exiting --- Flow.Launcher/App.xaml.cs | 133 +++++++++++++++++++---- Flow.Launcher/MainWindow.xaml | 1 + Flow.Launcher/MainWindow.xaml.cs | 61 +++++++++-- Flow.Launcher/ViewModel/MainViewModel.cs | 26 ++++- 4 files changed, 187 insertions(+), 34 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 19e932ea834..8f7c8aec9ed 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -25,12 +25,29 @@ namespace Flow.Launcher { - public partial class App : IDisposable, ISingleInstanceApp + public partial class App : IAsyncDisposable, ISingleInstanceApp { + #region Public Properties + public static IPublicAPI API { get; private set; } + public static CancellationTokenSource NativeThreadCTS { get; private set; } + + #endregion + + #region Private Fields + private static bool _disposed; + private MainWindow _mainWindow; + private readonly MainViewModel _mainVM; private readonly Settings _settings; + // To prevent two disposals running at the same time. + private static readonly object _disposingLock = new(); + + #endregion + + #region Constructor + public App() { // Initialize settings @@ -78,34 +95,47 @@ public App() { API = Ioc.Default.GetRequiredService(); _settings.Initialize(); + _mainVM = Ioc.Default.GetRequiredService(); } catch (Exception e) { ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e); return; } - } - private static void ShowErrorMsgBoxAndFailFast(string message, Exception e) - { - // Firstly show users the message - MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error); + // Local function + static void ShowErrorMsgBoxAndFailFast(string message, Exception e) + { + // Firstly show users the message + MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error); - // Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info. - Environment.FailFast(message, e); + // Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info. + Environment.FailFast(message, e); + } } + #endregion + + #region Main + [STAThread] public static void Main() { + NativeThreadCTS = new CancellationTokenSource(); + if (SingleInstance.InitializeAsFirstInstance()) { - using var application = new App(); + var application = new App(); application.InitializeComponent(); application.Run(); + application.DisposeAsync().AsTask().GetAwaiter().GetResult(); } } + #endregion + + #region App Events + #pragma warning disable VSTHRD100 // Avoid async void methods private async void OnStartup(object sender, StartupEventArgs e) @@ -136,11 +166,11 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => await PluginManager.InitializePluginsAsync(); await imageLoadertask; - var window = new MainWindow(); + _mainWindow = new MainWindow(); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); - Current.MainWindow = window; + Current.MainWindow = _mainWindow; Current.MainWindow.Title = Constant.FlowLauncher; HotKeyMapper.Initialize(); @@ -157,8 +187,7 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => AutoUpdates(); API.SaveAppAllSettings(); - Log.Info( - "|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- "); + Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------"); }); } @@ -191,7 +220,6 @@ private void AutoStartup() } } - //[Conditional("RELEASE")] private void AutoUpdates() { _ = Task.Run(async () => @@ -209,11 +237,30 @@ private void AutoUpdates() }); } + #endregion + + #region Register Events + private void RegisterExitEvents() { - AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose(); - Current.Exit += (s, e) => Dispose(); - Current.SessionEnding += (s, e) => Dispose(); + AppDomain.CurrentDomain.ProcessExit += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Process Exit"); + _ = DisposeAsync(); + }; + + Current.Exit += (s, e) => + { + NativeThreadCTS.Cancel(); + Log.Info("|App.RegisterExitEvents|Application Exit"); + _ = DisposeAsync(); + }; + + Current.SessionEnding += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Session Ending"); + _ = DisposeAsync(); + }; } /// @@ -234,20 +281,62 @@ private static void RegisterAppDomainExceptions() AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; } - public void Dispose() + #endregion + + #region IAsyncDisposable + + protected virtual async ValueTask DisposeAsync(bool disposing) { - // if sessionending is called, exit proverbially be called when log off / shutdown - // but if sessionending is not called, exit won't be called when log off / shutdown - if (!_disposed) + // Prevent two disposes at the same time. + lock (_disposingLock) { - API.SaveAppAllSettings(); + if (!disposing) + { + return; + } + + if (_disposed) + { + return; + } + _disposed = true; } + + await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () => + { + Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); + + if (disposing) + { + API?.SaveAppAllSettings(); + await PluginManager.DisposePluginsAsync(); + + // Dispose needs to be called on the main Windows thread, since some resources owned by the thread need to be disposed. + await _mainWindow?.Dispatcher.InvokeAsync(DisposeAsync); + _mainVM?.Dispose(); + } + + Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------"); + }); } + public async ValueTask DisposeAsync() + { + // Do not change this code. Put cleanup code in 'DisposeAsync(bool disposing)' method + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + + #region ISingleInstanceApp + public void OnSecondAppStarted() { Ioc.Default.GetRequiredService().Show(); } + + #endregion } } diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 5b63303acf7..f5f3bac84bc 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -17,6 +17,7 @@ AllowDrop="True" AllowsTransparency="True" Background="Transparent" + Closed="OnClosed" Closing="OnClosing" Deactivated="OnDeactivated" Icon="Images/app.png" diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 2ce3d1e95e6..0af617a77b0 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -27,7 +27,7 @@ namespace Flow.Launcher { - public partial class MainWindow + public partial class MainWindow : IDisposable { #region Private Fields @@ -50,13 +50,17 @@ public partial class MainWindow private SoundPlayer animationSoundWPF; // Window WndProc + private HwndSource _hwndSource; private int _initialWidth; private int _initialHeight; // Window Animation private const double DefaultRightMargin = 66; //* this value from base.xaml private bool _animating; - private bool _isClockPanelAnimating = false; // 애니메이션 실행 중인지 여부 + private bool _isClockPanelAnimating = false; + + // IDisposable + private bool _disposedValue = false; #endregion @@ -85,8 +89,8 @@ public MainWindow() private void OnSourceInitialized(object sender, EventArgs e) { var handle = Win32Helper.GetWindowHandle(this, true); - var win = HwndSource.FromHwnd(handle); - win.AddHook(WndProc); + _hwndSource = HwndSource.FromHwnd(handle); + _hwndSource.AddHook(WndProc); Win32Helper.HideFromAltTab(this); Win32Helper.DisableControlBox(this); } @@ -227,20 +231,31 @@ private async void OnLoaded(object sender, RoutedEventArgs _) .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); } - private async void OnClosing(object sender, CancelEventArgs e) + private void OnClosing(object sender, CancelEventArgs e) { + _viewModel.Save(); _notifyIcon.Visible = false; - App.API.SaveAppAllSettings(); - e.Cancel = true; - await PluginManager.DisposePluginsAsync(); Notification.Uninstall(); - Environment.Exit(0); + } + + private void OnClosed(object sender, EventArgs e) + { + try + { + _hwndSource.RemoveHook(WndProc); + } + catch (Exception) + { + // Ignored + } + + _hwndSource = null; } private void OnLocationChanged(object sender, EventArgs e) { - if (_animating) - return; + if (_animating) return; + if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) { _settings.WindowLeft = Left; @@ -990,5 +1005,29 @@ private void QueryTextBox_OnPreviewDragOver(object sender, DragEventArgs e) } #endregion + + #region IDisposable + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _hwndSource?.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 46970a6a13f..18e61914d79 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -27,7 +27,7 @@ namespace Flow.Launcher.ViewModel { - public partial class MainViewModel : BaseModel, ISavable + public partial class MainViewModel : BaseModel, ISavable, IDisposable { #region Private Fields @@ -1542,5 +1542,29 @@ public void UpdateResultView(ICollection resultsForUpdates) } #endregion + + #region IDisposable + + private bool _disposed = false; + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _updateSource?.Dispose(); + _disposed = true; + } + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion } } From b71e7226e5bbc1ff852e4a71064c72afae3f7558 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 21 Mar 2025 21:17:31 +0800 Subject: [PATCH 3/9] Improve & Fix --- Flow.Launcher/App.xaml.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 8f7c8aec9ed..63dcdf3534c 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -243,23 +243,23 @@ private void AutoUpdates() private void RegisterExitEvents() { - AppDomain.CurrentDomain.ProcessExit += (s, e) => + AppDomain.CurrentDomain.ProcessExit += async (s, e) => { Log.Info("|App.RegisterExitEvents|Process Exit"); - _ = DisposeAsync(); + await DisposeAsync(); }; - Current.Exit += (s, e) => + Current.Exit += async (s, e) => { NativeThreadCTS.Cancel(); Log.Info("|App.RegisterExitEvents|Application Exit"); - _ = DisposeAsync(); + await DisposeAsync(); }; - Current.SessionEnding += (s, e) => + Current.SessionEnding += async (s, e) => { Log.Info("|App.RegisterExitEvents|Session Ending"); - _ = DisposeAsync(); + await DisposeAsync(); }; } @@ -303,6 +303,8 @@ protected virtual async ValueTask DisposeAsync(bool disposing) _disposed = true; } + await Task.Delay(10000); + await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () => { Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); @@ -313,7 +315,7 @@ await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () => await PluginManager.DisposePluginsAsync(); // Dispose needs to be called on the main Windows thread, since some resources owned by the thread need to be disposed. - await _mainWindow?.Dispatcher.InvokeAsync(DisposeAsync); + await _mainWindow?.Dispatcher.InvokeAsync(_mainWindow.Dispose); _mainVM?.Dispose(); } From 5b29dedcbebb255a22a39932049fb8f6df53da3d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 10:10:52 +0800 Subject: [PATCH 4/9] Remove useless cancellation token source --- Flow.Launcher/App.xaml.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 63dcdf3534c..7d417e036be 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -30,7 +30,6 @@ public partial class App : IAsyncDisposable, ISingleInstanceApp #region Public Properties public static IPublicAPI API { get; private set; } - public static CancellationTokenSource NativeThreadCTS { get; private set; } #endregion @@ -121,8 +120,6 @@ static void ShowErrorMsgBoxAndFailFast(string message, Exception e) [STAThread] public static void Main() { - NativeThreadCTS = new CancellationTokenSource(); - if (SingleInstance.InitializeAsFirstInstance()) { var application = new App(); @@ -251,7 +248,6 @@ private void RegisterExitEvents() Current.Exit += async (s, e) => { - NativeThreadCTS.Cancel(); Log.Info("|App.RegisterExitEvents|Application Exit"); await DisposeAsync(); }; From 09bc2bc48b5d0325421a6eab20856c80771dfc7d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 10:24:34 +0800 Subject: [PATCH 5/9] Cleanup & Improve --- Flow.Launcher/App.xaml.cs | 35 +++++++++++++++----------------- Flow.Launcher/MainWindow.xaml.cs | 3 +-- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 7d417e036be..016c2d06cca 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -25,7 +25,7 @@ namespace Flow.Launcher { - public partial class App : IAsyncDisposable, ISingleInstanceApp + public partial class App : IDisposable, ISingleInstanceApp { #region Public Properties @@ -122,10 +122,9 @@ public static void Main() { if (SingleInstance.InitializeAsFirstInstance()) { - var application = new App(); + using var application = new App(); application.InitializeComponent(); application.Run(); - application.DisposeAsync().AsTask().GetAwaiter().GetResult(); } } @@ -240,22 +239,22 @@ private void AutoUpdates() private void RegisterExitEvents() { - AppDomain.CurrentDomain.ProcessExit += async (s, e) => + AppDomain.CurrentDomain.ProcessExit += (s, e) => { Log.Info("|App.RegisterExitEvents|Process Exit"); - await DisposeAsync(); + Dispose(); }; - Current.Exit += async (s, e) => + Current.Exit += (s, e) => { Log.Info("|App.RegisterExitEvents|Application Exit"); - await DisposeAsync(); + Dispose(); }; - Current.SessionEnding += async (s, e) => + Current.SessionEnding += (s, e) => { Log.Info("|App.RegisterExitEvents|Session Ending"); - await DisposeAsync(); + Dispose(); }; } @@ -279,9 +278,9 @@ private static void RegisterAppDomainExceptions() #endregion - #region IAsyncDisposable + #region IDisposable - protected virtual async ValueTask DisposeAsync(bool disposing) + protected virtual void Dispose(bool disposing) { // Prevent two disposes at the same time. lock (_disposingLock) @@ -299,9 +298,7 @@ protected virtual async ValueTask DisposeAsync(bool disposing) _disposed = true; } - await Task.Delay(10000); - - await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () => + Stopwatch.Normal("|App.Dispose|Dispose cost", async () => { Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); @@ -310,8 +307,9 @@ await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () => API?.SaveAppAllSettings(); await PluginManager.DisposePluginsAsync(); - // Dispose needs to be called on the main Windows thread, since some resources owned by the thread need to be disposed. - await _mainWindow?.Dispatcher.InvokeAsync(_mainWindow.Dispose); + // Dispose needs to be called on the main Windows thread, + // since some resources owned by the thread need to be disposed. + _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); _mainVM?.Dispose(); } @@ -319,10 +317,9 @@ await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () => }); } - public async ValueTask DisposeAsync() + public void Dispose() { - // Do not change this code. Put cleanup code in 'DisposeAsync(bool disposing)' method - await DisposeAsync(disposing: true); + Dispose(disposing: true); GC.SuppressFinalize(this); } diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 0af617a77b0..e7be150811b 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -42,7 +42,7 @@ public partial class MainWindow : IDisposable private readonly ContextMenu contextMenu = new(); private readonly MainViewModel _viewModel; - // Window Event : Key Event + // Window Event: Key Event private bool isArrowKeyPressed = false; // Window Sound Effects @@ -233,7 +233,6 @@ private async void OnLoaded(object sender, RoutedEventArgs _) private void OnClosing(object sender, CancelEventArgs e) { - _viewModel.Save(); _notifyIcon.Visible = false; Notification.Uninstall(); } From 9d81e60813be042cbf12a9a5e58bd59094728fc3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:26:12 +0800 Subject: [PATCH 6/9] Improve dispose logic --- Flow.Launcher/App.xaml.cs | 6 +-- Flow.Launcher/MainWindow.xaml.cs | 57 ++++++++++++++---------- Flow.Launcher/ViewModel/MainViewModel.cs | 1 + 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 016c2d06cca..d78c6c47bb1 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -298,15 +298,12 @@ protected virtual void Dispose(bool disposing) _disposed = true; } - Stopwatch.Normal("|App.Dispose|Dispose cost", async () => + Stopwatch.Normal("|App.Dispose|Dispose cost", () => { Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); if (disposing) { - API?.SaveAppAllSettings(); - await PluginManager.DisposePluginsAsync(); - // Dispose needs to be called on the main Windows thread, // since some resources owned by the thread need to be disposed. _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); @@ -319,6 +316,7 @@ protected virtual void Dispose(bool disposing) public void Dispose() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index e7be150811b..adbb6f32987 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -39,11 +39,13 @@ public partial class MainWindow : IDisposable private NotifyIcon _notifyIcon; // Window Context Menu - private readonly ContextMenu contextMenu = new(); + 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; + private bool _isArrowKeyPressed = false; // Window Sound Effects private MediaPlayer animationSoundWMP; @@ -60,7 +62,7 @@ public partial class MainWindow : IDisposable private bool _isClockPanelAnimating = false; // IDisposable - private bool _disposedValue = false; + private bool _disposed = false; #endregion @@ -231,10 +233,18 @@ private async void OnLoaded(object sender, RoutedEventArgs _) .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); } - private void OnClosing(object sender, CancelEventArgs e) + private async void OnClosing(object sender, CancelEventArgs e) { - _notifyIcon.Visible = false; - Notification.Uninstall(); + if (!_canClose) + { + _notifyIcon.Visible = false; + App.API.SaveAppAllSettings(); + e.Cancel = true; + await PluginManager.DisposePluginsAsync(); + Notification.Uninstall(); + _canClose = true; + Close(); + } } private void OnClosed(object sender, EventArgs e) @@ -292,12 +302,12 @@ private void OnKeyDown(object sender, KeyEventArgs e) switch (e.Key) { case Key.Down: - isArrowKeyPressed = true; + _isArrowKeyPressed = true; _viewModel.SelectNextItemCommand.Execute(null); e.Handled = true; break; case Key.Up: - isArrowKeyPressed = true; + _isArrowKeyPressed = true; _viewModel.SelectPrevItemCommand.Execute(null); e.Handled = true; break; @@ -355,13 +365,13 @@ private void OnKeyUp(object sender, KeyEventArgs e) { if (e.Key == Key.Up || e.Key == Key.Down) { - isArrowKeyPressed = false; + _isArrowKeyPressed = false; } } private void OnPreviewMouseMove(object sender, MouseEventArgs e) { - if (isArrowKeyPressed) + if (_isArrowKeyPressed) { e.Handled = true; // Ignore Mouse Hover when press Arrowkeys } @@ -531,11 +541,11 @@ private void InitializeNotifyIcon() gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip"); positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip"); - contextMenu.Items.Add(open); - contextMenu.Items.Add(gamemode); - contextMenu.Items.Add(positionreset); - contextMenu.Items.Add(settings); - contextMenu.Items.Add(exit); + _contextMenu.Items.Add(open); + _contextMenu.Items.Add(gamemode); + _contextMenu.Items.Add(positionreset); + _contextMenu.Items.Add(settings); + _contextMenu.Items.Add(exit); _notifyIcon.MouseClick += (o, e) => { @@ -546,14 +556,14 @@ private void InitializeNotifyIcon() break; case MouseButtons.Right: - contextMenu.IsOpen = true; + _contextMenu.IsOpen = true; // Get context menu handle and bring it to the foreground - if (PresentationSource.FromVisual(contextMenu) is HwndSource hwndSource) + if (PresentationSource.FromVisual(_contextMenu) is HwndSource hwndSource) { Win32Helper.SetForegroundWindow(hwndSource.Handle); } - contextMenu.Focus(); + _contextMenu.Focus(); break; } }; @@ -561,7 +571,7 @@ private void InitializeNotifyIcon() private void UpdateNotifyIconText() { - var menu = contextMenu; + var menu = _contextMenu; ((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")"; ((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode"); @@ -757,7 +767,7 @@ private void WindowAnimation() if (_animating) return; - isArrowKeyPressed = true; + _isArrowKeyPressed = true; _animating = true; UpdatePosition(false); @@ -835,7 +845,7 @@ private void WindowAnimation() clocksb.Completed += (_, _) => _animating = false; _settings.WindowLeft = Left; - isArrowKeyPressed = false; + _isArrowKeyPressed = false; if (QueryTextBox.Text.Length == 0) { @@ -1009,14 +1019,15 @@ private void QueryTextBox_OnPreviewDragOver(object sender, DragEventArgs e) protected virtual void Dispose(bool disposing) { - if (!_disposedValue) + if (!_disposed) { if (disposing) { _hwndSource?.Dispose(); + _notifyIcon?.Dispose(); } - _disposedValue = true; + _disposed = true; } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 18e61914d79..1668bac3ae7 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1561,6 +1561,7 @@ protected virtual void Dispose(bool disposing) public void Dispose() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } From 48792b6a3bcb3d2b972b00eb39247f16ddaa535e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:29:47 +0800 Subject: [PATCH 7/9] Restore style --- Flow.Launcher/Helper/SingleInstance.cs | 225 +++++++++++++------------ 1 file changed, 113 insertions(+), 112 deletions(-) diff --git a/Flow.Launcher/Helper/SingleInstance.cs b/Flow.Launcher/Helper/SingleInstance.cs index 76c109a393f..de2579b6290 100644 --- a/Flow.Launcher/Helper/SingleInstance.cs +++ b/Flow.Launcher/Helper/SingleInstance.cs @@ -6,137 +6,138 @@ // http://blogs.microsoft.co.il/arik/2010/05/28/wpf-single-instance-application/ // modified to allow single instace restart -namespace Flow.Launcher.Helper; - -public interface ISingleInstanceApp -{ - void OnSecondAppStarted(); -} - -/// -/// This class checks to make sure that only one instance of -/// this application is running at a time. -/// -/// -/// Note: this class should be used with some caution, because it does no -/// security checking. For example, if one instance of an app that uses this class -/// is running as Administrator, any other instance, even if it is not -/// running as Administrator, can activate it with command line arguments. -/// For most apps, this will not be much of an issue. -/// -public static class SingleInstance where TApplication: Application, ISingleInstanceApp +namespace Flow.Launcher.Helper { - #region Private Fields - - /// - /// String delimiter used in channel names. - /// - private const string Delimiter = ":"; - - /// - /// Suffix to the channel name. - /// - private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; - private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex"; - - /// - /// Application mutex. - /// - internal static Mutex SingleInstanceMutex { get; set; } - - #endregion - - #region Public Methods + public interface ISingleInstanceApp + { + void OnSecondAppStarted(); + } /// - /// Checks if the instance of the application attempting to start is the first instance. - /// If not, activates the first instance. + /// This class checks to make sure that only one instance of + /// this application is running at a time. /// - /// True if this is the first instance of the application. - public static bool InitializeAsFirstInstance() + /// + /// Note: this class should be used with some caution, because it does no + /// security checking. For example, if one instance of an app that uses this class + /// is running as Administrator, any other instance, even if it is not + /// running as Administrator, can activate it with command line arguments. + /// For most apps, this will not be much of an issue. + /// + public static class SingleInstance where TApplication : Application, ISingleInstanceApp { - // Build unique application Id and the IPC channel name. - string applicationIdentifier = InstanceMutexName + Environment.UserName; - - string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); - - // Create mutex based on unique application Id to check if this is the first instance of the application. - SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); - if (firstInstance) + #region Private Fields + + /// + /// String delimiter used in channel names. + /// + private const string Delimiter = ":"; + + /// + /// Suffix to the channel name. + /// + private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; + private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex"; + + /// + /// Application mutex. + /// + internal static Mutex SingleInstanceMutex { get; set; } + + #endregion + + #region Public Methods + + /// + /// Checks if the instance of the application attempting to start is the first instance. + /// If not, activates the first instance. + /// + /// True if this is the first instance of the application. + public static bool InitializeAsFirstInstance() { - _ = CreateRemoteServiceAsync(channelName); - return true; + // Build unique application Id and the IPC channel name. + string applicationIdentifier = InstanceMutexName + Environment.UserName; + + string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); + + // Create mutex based on unique application Id to check if this is the first instance of the application. + SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); + if (firstInstance) + { + _ = CreateRemoteServiceAsync(channelName); + return true; + } + else + { + _ = SignalFirstInstanceAsync(channelName); + return false; + } } - else + + /// + /// Cleans up single-instance code, clearing shared resources, mutexes, etc. + /// + public static void Cleanup() { - _ = SignalFirstInstanceAsync(channelName); - return false; + SingleInstanceMutex?.ReleaseMutex(); } - } - /// - /// Cleans up single-instance code, clearing shared resources, mutexes, etc. - /// - public static void Cleanup() - { - SingleInstanceMutex?.ReleaseMutex(); - } + #endregion - #endregion + #region Private Methods - #region Private Methods - - /// - /// Creates a remote server pipe for communication. - /// Once receives signal from client, will activate first instance. - /// - /// Application's IPC channel name. - private static async Task CreateRemoteServiceAsync(string channelName) - { - using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); - while (true) + /// + /// Creates a remote server pipe for communication. + /// Once receives signal from client, will activate first instance. + /// + /// Application's IPC channel name. + private static async Task CreateRemoteServiceAsync(string channelName) { - // Wait for connection to the pipe - await pipeServer.WaitForConnectionAsync(); - - // Do an asynchronous call to ActivateFirstInstance function - Application.Current?.Dispatcher.Invoke(ActivateFirstInstance); - - // Disconect client - pipeServer.Disconnect(); + using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); + while (true) + { + // Wait for connection to the pipe + await pipeServer.WaitForConnectionAsync(); + + // Do an asynchronous call to ActivateFirstInstance function + Application.Current?.Dispatcher.Invoke(ActivateFirstInstance); + + // Disconect client + pipeServer.Disconnect(); + } } - } - /// - /// Creates a client pipe and sends a signal to server to launch first instance - /// - /// Application's IPC channel name. - /// - /// Command line arguments for the second instance, passed to the first instance to take appropriate action. - /// - private static async Task SignalFirstInstanceAsync(string channelName) - { - // Create a client pipe connected to server - using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); + /// + /// Creates a client pipe and sends a signal to server to launch first instance + /// + /// Application's IPC channel name. + /// + /// Command line arguments for the second instance, passed to the first instance to take appropriate action. + /// + private static async Task SignalFirstInstanceAsync(string channelName) + { + // Create a client pipe connected to server + using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); - // Connect to the available pipe - await pipeClient.ConnectAsync(0); - } + // Connect to the available pipe + await pipeClient.ConnectAsync(0); + } - /// - /// Activates the first instance of the application with arguments from a second instance. - /// - /// List of arguments to supply the first instance of the application. - private static void ActivateFirstInstance() - { - // Set main window state and process command line args - if (Application.Current == null) + /// + /// Activates the first instance of the application with arguments from a second instance. + /// + /// List of arguments to supply the first instance of the application. + private static void ActivateFirstInstance() { - return; + // Set main window state and process command line args + if (Application.Current == null) + { + return; + } + + ((TApplication)Application.Current).OnSecondAppStarted(); } - ((TApplication)Application.Current).OnSecondAppStarted(); + #endregion } - - #endregion } From adbef0d99438a31f3ff141654a0a968c2091b89f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:34:03 +0800 Subject: [PATCH 8/9] Improve disposable interface for mainvm --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 1668bac3ae7..ab67b21bbe0 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1554,6 +1554,8 @@ protected virtual void Dispose(bool disposing) if (disposing) { _updateSource?.Dispose(); + _resultsUpdateChannelWriter?.Complete(); + _resultsViewUpdateTask?.Dispose(); _disposed = true; } } From 93ccdee54add655af0384af0a436f3f01924922a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:40:33 +0800 Subject: [PATCH 9/9] Improve comments --- Flow.Launcher/MainWindow.xaml.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index adbb6f32987..c2b35ed12fd 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -219,15 +219,15 @@ private async void OnLoaded(object sender, RoutedEventArgs _) } }; - // ✅ QueryTextBox.Text 변경 감지 (글자 수 1 이상일 때만 동작하도록 수정) + // QueryTextBox.Text change detection (modified to only work when character count is 1 or higher) QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility(); - // ✅ ContextMenu.Visibility 변경 감지 + // Detecting ContextMenu.Visibility changes DependencyPropertyDescriptor .FromProperty(VisibilityProperty, typeof(ContextMenu)) .AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility()); - // ✅ History.Visibility 변경 감지 + // Detect History.Visibility changes DependencyPropertyDescriptor .FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정 .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); @@ -242,6 +242,7 @@ private async void OnClosing(object sender, CancelEventArgs e) e.Cancel = true; await PluginManager.DisposePluginsAsync(); Notification.Uninstall(); + // After plugins are all disposed, we can close the main window _canClose = true; Close(); }