diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index b102e384e70..833c63ddff8 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -27,11 +27,26 @@ namespace Flow.Launcher { public partial class App : IDisposable, ISingleInstanceApp { + #region Public Properties + public static IPublicAPI API { get; private set; } - private const string Unique = "Flow.Launcher_Unique_Application_Mutex"; + + #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 @@ -79,27 +94,33 @@ 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() { - if (SingleInstance.InitializeAsFirstInstance(Unique)) + if (SingleInstance.InitializeAsFirstInstance()) { using var application = new App(); application.InitializeComponent(); @@ -107,6 +128,10 @@ public static void Main() } } + #endregion + + #region App Events + #pragma warning disable VSTHRD100 // Avoid async void methods private async void OnStartup(object sender, StartupEventArgs e) @@ -142,11 +167,11 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => 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(); @@ -163,8 +188,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 ----------------------------------------------------"); }); } @@ -197,7 +221,6 @@ private void AutoStartup() } } - //[Conditional("RELEASE")] private void AutoUpdates() { _ = Task.Run(async () => @@ -215,11 +238,29 @@ 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"); + Dispose(); + }; + + Current.Exit += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Application Exit"); + Dispose(); + }; + + Current.SessionEnding += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Session Ending"); + Dispose(); + }; } /// @@ -240,20 +281,60 @@ private static void RegisterAppDomainExceptions() AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; } - public void Dispose() + #endregion + + #region IDisposable + + protected virtual void Dispose(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; } + + Stopwatch.Normal("|App.Dispose|Dispose cost", () => + { + Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); + + if (disposing) + { + // 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(); + } + + Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------"); + }); } + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + + #region ISingleInstanceApp + public void OnSecondAppStarted() { Ioc.Default.GetRequiredService().Show(); } + + #endregion } } diff --git a/Flow.Launcher/Helper/SingleInstance.cs b/Flow.Launcher/Helper/SingleInstance.cs index e0e3075f636..de2579b6290 100644 --- a/Flow.Launcher/Helper/SingleInstance.cs +++ b/Flow.Launcher/Helper/SingleInstance.cs @@ -8,10 +8,10 @@ // modified to allow single instace restart namespace Flow.Launcher.Helper { - public interface ISingleInstanceApp - { - void OnSecondAppStarted(); - } + public interface ISingleInstanceApp + { + void OnSecondAppStarted(); + } /// /// This class checks to make sure that only one instance of @@ -24,9 +24,7 @@ public interface ISingleInstanceApp /// 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 - + public static class SingleInstance where TApplication : Application, ISingleInstanceApp { #region Private Fields @@ -39,11 +37,12 @@ public static class SingleInstance /// 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; + internal static Mutex SingleInstanceMutex { get; set; } #endregion @@ -54,24 +53,23 @@ public static class SingleInstance /// If not, activates the first instance. /// /// True if this is the first instance of the application. - public static bool InitializeAsFirstInstance( string uniqueName ) + public static bool InitializeAsFirstInstance() { // Build unique application Id and the IPC channel name. - string applicationIdentifier = uniqueName + Environment.UserName; + string applicationIdentifier = InstanceMutexName + Environment.UserName; - string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); + 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); + SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); if (firstInstance) { - _ = CreateRemoteService(channelName); + _ = CreateRemoteServiceAsync(channelName); return true; } else { - _ = SignalFirstInstance(channelName); + _ = SignalFirstInstanceAsync(channelName); return false; } } @@ -81,7 +79,7 @@ public static bool InitializeAsFirstInstance( string uniqueName ) /// public static void Cleanup() { - singleInstanceMutex?.ReleaseMutex(); + SingleInstanceMutex?.ReleaseMutex(); } #endregion @@ -93,22 +91,19 @@ public static void Cleanup() /// Once receives signal from client, will activate first instance. /// /// Application's IPC channel name. - private static async Task CreateRemoteService(string channelName) + private static async Task CreateRemoteServiceAsync(string channelName) { - using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In)) + using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); + while (true) { - 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(); - } + // 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(); } } @@ -119,25 +114,13 @@ private static async Task CreateRemoteService(string channelName) /// /// Command line arguments for the second instance, passed to the first instance to take appropriate action. /// - private static async Task SignalFirstInstance(string channelName) + 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); - } - } + using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); - /// - /// Callback for activating first instance of the application. - /// - /// Callback argument. - /// Always null. - private static object ActivateFirstInstanceCallback(object o) - { - ActivateFirstInstance(); - return null; + // Connect to the available pipe + await pipeClient.ConnectAsync(0); } /// 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 ec8649efcbb..33654c4cf24 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 @@ -39,24 +39,30 @@ public partial class MainWindow private NotifyIcon _notifyIcon; // Window Context Menu - private readonly ContextMenu contextMenu = new(); + private readonly ContextMenu _contextMenu = new(); private readonly MainViewModel _viewModel; - // Window Event : Key Event - private bool isArrowKeyPressed = false; + // Window Event: Close Event + private bool _canClose = false; + // Window Event: Key Event + private bool _isArrowKeyPressed = false; // Window Sound Effects private MediaPlayer animationSoundWMP; 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 _disposed = false; #endregion @@ -85,8 +91,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); } @@ -218,15 +224,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()); @@ -234,18 +240,37 @@ private async void OnLoaded(object sender, RoutedEventArgs _) private async void OnClosing(object sender, CancelEventArgs e) { - _notifyIcon.Visible = false; - App.API.SaveAppAllSettings(); - e.Cancel = true; - await PluginManager.DisposePluginsAsync(); - Notification.Uninstall(); - Environment.Exit(0); + if (!_canClose) + { + _notifyIcon.Visible = false; + App.API.SaveAppAllSettings(); + e.Cancel = true; + await PluginManager.DisposePluginsAsync(); + Notification.Uninstall(); + // After plugins are all disposed, we can close the main window + _canClose = true; + Close(); + } + } + + 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; @@ -283,12 +308,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; @@ -346,13 +371,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 } @@ -522,11 +547,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) => { @@ -537,14 +562,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; } }; @@ -552,7 +577,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"); @@ -748,7 +773,7 @@ private void WindowAnimation() if (_animating) return; - isArrowKeyPressed = true; + _isArrowKeyPressed = true; _animating = true; UpdatePosition(false); @@ -826,7 +851,7 @@ private void WindowAnimation() clocksb.Completed += (_, _) => _animating = false; _settings.WindowLeft = Left; - isArrowKeyPressed = false; + _isArrowKeyPressed = false; if (QueryTextBox.Text.Length == 0) { @@ -995,5 +1020,30 @@ private void QueryTextBox_OnPreviewDragOver(object sender, DragEventArgs e) } #endregion + + #region IDisposable + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _hwndSource?.Dispose(); + _notifyIcon?.Dispose(); + } + + _disposed = 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 3ce0ba2f0b6..340dce1484b 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 @@ -1551,5 +1551,35 @@ public void UpdateResultView(ICollection resultsForUpdates) } #endregion + + #region IDisposable + + private bool _disposed = false; + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _updateSource?.Dispose(); + _resultsUpdateChannelWriter?.Complete(); + if (_resultsViewUpdateTask?.IsCompleted == true) + { + _resultsViewUpdateTask.Dispose(); + } + _disposed = true; + } + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion } }