Merge pull request #3367 from Jack251970/graceful_shutdown

Graceful shutdown
This commit is contained in:
Jack Ye 2025-03-26 11:31:09 +08:00 committed by GitHub
commit e5ee8567e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 247 additions and 102 deletions

View file

@ -27,11 +27,26 @@ namespace Flow.Launcher
{ {
public partial class App : IDisposable, ISingleInstanceApp public partial class App : IDisposable, ISingleInstanceApp
{ {
#region Public Properties
public static IPublicAPI API { get; private set; } 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 static bool _disposed;
private MainWindow _mainWindow;
private readonly MainViewModel _mainVM;
private readonly Settings _settings; private readonly Settings _settings;
// To prevent two disposals running at the same time.
private static readonly object _disposingLock = new();
#endregion
#region Constructor
public App() public App()
{ {
// Initialize settings // Initialize settings
@ -79,27 +94,33 @@ namespace Flow.Launcher
{ {
API = Ioc.Default.GetRequiredService<IPublicAPI>(); API = Ioc.Default.GetRequiredService<IPublicAPI>();
_settings.Initialize(); _settings.Initialize();
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
} }
catch (Exception e) catch (Exception e)
{ {
ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e); ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e);
return; return;
} }
// 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);
}
} }
private static void ShowErrorMsgBoxAndFailFast(string message, Exception e) #endregion
{
// 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. #region Main
Environment.FailFast(message, e);
}
[STAThread] [STAThread]
public static void Main() public static void Main()
{ {
if (SingleInstance<App>.InitializeAsFirstInstance(Unique)) if (SingleInstance<App>.InitializeAsFirstInstance())
{ {
using var application = new App(); using var application = new App();
application.InitializeComponent(); application.InitializeComponent();
@ -107,6 +128,10 @@ namespace Flow.Launcher
} }
} }
#endregion
#region App Events
#pragma warning disable VSTHRD100 // Avoid async void methods #pragma warning disable VSTHRD100 // Avoid async void methods
private async void OnStartup(object sender, StartupEventArgs e) private async void OnStartup(object sender, StartupEventArgs e)
@ -142,11 +167,11 @@ namespace Flow.Launcher
await imageLoadertask; await imageLoadertask;
var window = new MainWindow(); _mainWindow = new MainWindow();
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
Current.MainWindow = window; Current.MainWindow = _mainWindow;
Current.MainWindow.Title = Constant.FlowLauncher; Current.MainWindow.Title = Constant.FlowLauncher;
HotKeyMapper.Initialize(); HotKeyMapper.Initialize();
@ -163,8 +188,7 @@ namespace Flow.Launcher
AutoUpdates(); AutoUpdates();
API.SaveAppAllSettings(); API.SaveAppAllSettings();
Log.Info( Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------");
"|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
}); });
} }
@ -197,7 +221,6 @@ namespace Flow.Launcher
} }
} }
//[Conditional("RELEASE")]
private void AutoUpdates() private void AutoUpdates()
{ {
_ = Task.Run(async () => _ = Task.Run(async () =>
@ -215,11 +238,29 @@ namespace Flow.Launcher
}); });
} }
#endregion
#region Register Events
private void RegisterExitEvents() private void RegisterExitEvents()
{ {
AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose(); AppDomain.CurrentDomain.ProcessExit += (s, e) =>
Current.Exit += (s, e) => Dispose(); {
Current.SessionEnding += (s, e) => Dispose(); 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();
};
} }
/// <summary> /// <summary>
@ -240,20 +281,60 @@ namespace Flow.Launcher
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; 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 // Prevent two disposes at the same time.
// but if sessionending is not called, exit won't be called when log off / shutdown lock (_disposingLock)
if (!_disposed)
{ {
API.SaveAppAllSettings(); if (!disposing)
{
return;
}
if (_disposed)
{
return;
}
_disposed = true; _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() public void OnSecondAppStarted()
{ {
Ioc.Default.GetRequiredService<MainViewModel>().Show(); Ioc.Default.GetRequiredService<MainViewModel>().Show();
} }
#endregion
} }
} }

View file

@ -8,10 +8,10 @@ using System.Windows;
// modified to allow single instace restart // modified to allow single instace restart
namespace Flow.Launcher.Helper namespace Flow.Launcher.Helper
{ {
public interface ISingleInstanceApp public interface ISingleInstanceApp
{ {
void OnSecondAppStarted(); void OnSecondAppStarted();
} }
/// <summary> /// <summary>
/// This class checks to make sure that only one instance of /// This class checks to make sure that only one instance of
@ -24,9 +24,7 @@ namespace Flow.Launcher.Helper
/// running as Administrator, can activate it with command line arguments. /// running as Administrator, can activate it with command line arguments.
/// For most apps, this will not be much of an issue. /// For most apps, this will not be much of an issue.
/// </remarks> /// </remarks>
public static class SingleInstance<TApplication> public static class SingleInstance<TApplication> where TApplication : Application, ISingleInstanceApp
where TApplication: Application , ISingleInstanceApp
{ {
#region Private Fields #region Private Fields
@ -39,11 +37,12 @@ namespace Flow.Launcher.Helper
/// Suffix to the channel name. /// Suffix to the channel name.
/// </summary> /// </summary>
private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; private const string ChannelNameSuffix = "SingeInstanceIPCChannel";
private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex";
/// <summary> /// <summary>
/// Application mutex. /// Application mutex.
/// </summary> /// </summary>
internal static Mutex singleInstanceMutex; internal static Mutex SingleInstanceMutex { get; set; }
#endregion #endregion
@ -54,24 +53,23 @@ namespace Flow.Launcher.Helper
/// If not, activates the first instance. /// If not, activates the first instance.
/// </summary> /// </summary>
/// <returns>True if this is the first instance of the application.</returns> /// <returns>True if this is the first instance of the application.</returns>
public static bool InitializeAsFirstInstance( string uniqueName ) public static bool InitializeAsFirstInstance()
{ {
// Build unique application Id and the IPC channel name. // 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. // 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 var firstInstance);
singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance);
if (firstInstance) if (firstInstance)
{ {
_ = CreateRemoteService(channelName); _ = CreateRemoteServiceAsync(channelName);
return true; return true;
} }
else else
{ {
_ = SignalFirstInstance(channelName); _ = SignalFirstInstanceAsync(channelName);
return false; return false;
} }
} }
@ -81,7 +79,7 @@ namespace Flow.Launcher.Helper
/// </summary> /// </summary>
public static void Cleanup() public static void Cleanup()
{ {
singleInstanceMutex?.ReleaseMutex(); SingleInstanceMutex?.ReleaseMutex();
} }
#endregion #endregion
@ -93,22 +91,19 @@ namespace Flow.Launcher.Helper
/// Once receives signal from client, will activate first instance. /// Once receives signal from client, will activate first instance.
/// </summary> /// </summary>
/// <param name="channelName">Application's IPC channel name.</param> /// <param name="channelName">Application's IPC channel name.</param>
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();
// Wait for connection to the pipe
await pipeServer.WaitForConnectionAsync(); // Do an asynchronous call to ActivateFirstInstance function
if (Application.Current != null) Application.Current?.Dispatcher.Invoke(ActivateFirstInstance);
{
// Do an asynchronous call to ActivateFirstInstance function // Disconect client
Application.Current.Dispatcher.Invoke(ActivateFirstInstance); pipeServer.Disconnect();
}
// Disconect client
pipeServer.Disconnect();
}
} }
} }
@ -119,25 +114,13 @@ namespace Flow.Launcher.Helper
/// <param name="args"> /// <param name="args">
/// Command line arguments for the second instance, passed to the first instance to take appropriate action. /// Command line arguments for the second instance, passed to the first instance to take appropriate action.
/// </param> /// </param>
private static async Task SignalFirstInstance(string channelName) private static async Task SignalFirstInstanceAsync(string channelName)
{ {
// Create a client pipe connected to server // Create a client pipe connected to server
using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out)) using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out);
{
// Connect to the available pipe
await pipeClient.ConnectAsync(0);
}
}
/// <summary> // Connect to the available pipe
/// Callback for activating first instance of the application. await pipeClient.ConnectAsync(0);
/// </summary>
/// <param name="arg">Callback argument.</param>
/// <returns>Always null.</returns>
private static object ActivateFirstInstanceCallback(object o)
{
ActivateFirstInstance();
return null;
} }
/// <summary> /// <summary>

View file

@ -17,6 +17,7 @@
AllowDrop="True" AllowDrop="True"
AllowsTransparency="True" AllowsTransparency="True"
Background="Transparent" Background="Transparent"
Closed="OnClosed"
Closing="OnClosing" Closing="OnClosing"
Deactivated="OnDeactivated" Deactivated="OnDeactivated"
Icon="Images/app.png" Icon="Images/app.png"

View file

@ -27,7 +27,7 @@ using Screen = System.Windows.Forms.Screen;
namespace Flow.Launcher namespace Flow.Launcher
{ {
public partial class MainWindow public partial class MainWindow : IDisposable
{ {
#region Private Fields #region Private Fields
@ -39,24 +39,30 @@ namespace Flow.Launcher
private NotifyIcon _notifyIcon; private NotifyIcon _notifyIcon;
// Window Context Menu // Window Context Menu
private readonly ContextMenu contextMenu = new(); private readonly ContextMenu _contextMenu = new();
private readonly MainViewModel _viewModel; private readonly MainViewModel _viewModel;
// Window Event : Key Event // Window Event: Close Event
private bool isArrowKeyPressed = false; private bool _canClose = false;
// Window Event: Key Event
private bool _isArrowKeyPressed = false;
// Window Sound Effects // Window Sound Effects
private MediaPlayer animationSoundWMP; private MediaPlayer animationSoundWMP;
private SoundPlayer animationSoundWPF; private SoundPlayer animationSoundWPF;
// Window WndProc // Window WndProc
private HwndSource _hwndSource;
private int _initialWidth; private int _initialWidth;
private int _initialHeight; private int _initialHeight;
// Window Animation // Window Animation
private const double DefaultRightMargin = 66; //* this value from base.xaml private const double DefaultRightMargin = 66; //* this value from base.xaml
private bool _animating; private bool _animating;
private bool _isClockPanelAnimating = false; // 애니메이션 실행 중인지 여부 private bool _isClockPanelAnimating = false;
// IDisposable
private bool _disposed = false;
#endregion #endregion
@ -85,8 +91,8 @@ namespace Flow.Launcher
private void OnSourceInitialized(object sender, EventArgs e) private void OnSourceInitialized(object sender, EventArgs e)
{ {
var handle = Win32Helper.GetWindowHandle(this, true); var handle = Win32Helper.GetWindowHandle(this, true);
var win = HwndSource.FromHwnd(handle); _hwndSource = HwndSource.FromHwnd(handle);
win.AddHook(WndProc); _hwndSource.AddHook(WndProc);
Win32Helper.HideFromAltTab(this); Win32Helper.HideFromAltTab(this);
Win32Helper.DisableControlBox(this); Win32Helper.DisableControlBox(this);
} }
@ -218,15 +224,15 @@ namespace Flow.Launcher
} }
}; };
// ✅ QueryTextBox.Text 변경 감지 (글자 수 1 이상일 때만 동작하도록 수정) // QueryTextBox.Text change detection (modified to only work when character count is 1 or higher)
QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility(); QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility();
// ✅ ContextMenu.Visibility 변경 감지 // Detecting ContextMenu.Visibility changes
DependencyPropertyDescriptor DependencyPropertyDescriptor
.FromProperty(VisibilityProperty, typeof(ContextMenu)) .FromProperty(VisibilityProperty, typeof(ContextMenu))
.AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility()); .AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility());
// ✅ History.Visibility 변경 감지 // Detect History.Visibility changes
DependencyPropertyDescriptor DependencyPropertyDescriptor
.FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정 .FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정
.AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility());
@ -234,18 +240,37 @@ namespace Flow.Launcher
private async void OnClosing(object sender, CancelEventArgs e) private async void OnClosing(object sender, CancelEventArgs e)
{ {
_notifyIcon.Visible = false; if (!_canClose)
App.API.SaveAppAllSettings(); {
e.Cancel = true; _notifyIcon.Visible = false;
await PluginManager.DisposePluginsAsync(); App.API.SaveAppAllSettings();
Notification.Uninstall(); e.Cancel = true;
Environment.Exit(0); 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) private void OnLocationChanged(object sender, EventArgs e)
{ {
if (_animating) if (_animating) return;
return;
if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation)
{ {
_settings.WindowLeft = Left; _settings.WindowLeft = Left;
@ -283,12 +308,12 @@ namespace Flow.Launcher
switch (e.Key) switch (e.Key)
{ {
case Key.Down: case Key.Down:
isArrowKeyPressed = true; _isArrowKeyPressed = true;
_viewModel.SelectNextItemCommand.Execute(null); _viewModel.SelectNextItemCommand.Execute(null);
e.Handled = true; e.Handled = true;
break; break;
case Key.Up: case Key.Up:
isArrowKeyPressed = true; _isArrowKeyPressed = true;
_viewModel.SelectPrevItemCommand.Execute(null); _viewModel.SelectPrevItemCommand.Execute(null);
e.Handled = true; e.Handled = true;
break; break;
@ -346,13 +371,13 @@ namespace Flow.Launcher
{ {
if (e.Key == Key.Up || e.Key == Key.Down) if (e.Key == Key.Up || e.Key == Key.Down)
{ {
isArrowKeyPressed = false; _isArrowKeyPressed = false;
} }
} }
private void OnPreviewMouseMove(object sender, MouseEventArgs e) private void OnPreviewMouseMove(object sender, MouseEventArgs e)
{ {
if (isArrowKeyPressed) if (_isArrowKeyPressed)
{ {
e.Handled = true; // Ignore Mouse Hover when press Arrowkeys e.Handled = true; // Ignore Mouse Hover when press Arrowkeys
} }
@ -522,11 +547,11 @@ namespace Flow.Launcher
gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip"); gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip");
positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip"); positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip");
contextMenu.Items.Add(open); _contextMenu.Items.Add(open);
contextMenu.Items.Add(gamemode); _contextMenu.Items.Add(gamemode);
contextMenu.Items.Add(positionreset); _contextMenu.Items.Add(positionreset);
contextMenu.Items.Add(settings); _contextMenu.Items.Add(settings);
contextMenu.Items.Add(exit); _contextMenu.Items.Add(exit);
_notifyIcon.MouseClick += (o, e) => _notifyIcon.MouseClick += (o, e) =>
{ {
@ -537,14 +562,14 @@ namespace Flow.Launcher
break; break;
case MouseButtons.Right: case MouseButtons.Right:
contextMenu.IsOpen = true; _contextMenu.IsOpen = true;
// Get context menu handle and bring it to the foreground // 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); Win32Helper.SetForegroundWindow(hwndSource.Handle);
} }
contextMenu.Focus(); _contextMenu.Focus();
break; break;
} }
}; };
@ -552,7 +577,7 @@ namespace Flow.Launcher
private void UpdateNotifyIconText() private void UpdateNotifyIconText()
{ {
var menu = contextMenu; var menu = _contextMenu;
((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") + ((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") +
" (" + _settings.Hotkey + ")"; " (" + _settings.Hotkey + ")";
((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode"); ((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode");
@ -748,7 +773,7 @@ namespace Flow.Launcher
if (_animating) if (_animating)
return; return;
isArrowKeyPressed = true; _isArrowKeyPressed = true;
_animating = true; _animating = true;
UpdatePosition(false); UpdatePosition(false);
@ -826,7 +851,7 @@ namespace Flow.Launcher
clocksb.Completed += (_, _) => _animating = false; clocksb.Completed += (_, _) => _animating = false;
_settings.WindowLeft = Left; _settings.WindowLeft = Left;
isArrowKeyPressed = false; _isArrowKeyPressed = false;
if (QueryTextBox.Text.Length == 0) if (QueryTextBox.Text.Length == 0)
{ {
@ -995,5 +1020,30 @@ namespace Flow.Launcher
} }
#endregion #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
} }
} }

View file

@ -27,7 +27,7 @@ using Microsoft.VisualStudio.Threading;
namespace Flow.Launcher.ViewModel namespace Flow.Launcher.ViewModel
{ {
public partial class MainViewModel : BaseModel, ISavable public partial class MainViewModel : BaseModel, ISavable, IDisposable
{ {
#region Private Fields #region Private Fields
@ -1551,5 +1551,35 @@ namespace Flow.Launcher.ViewModel
} }
#endregion #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
} }
} }