Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graceful shutdown #3367

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 103 additions & 22 deletions Flow.Launcher/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -79,34 +94,44 @@ public App()
{
API = Ioc.Default.GetRequiredService<IPublicAPI>();
_settings.Initialize();
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
}
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<App>.InitializeAsFirstInstance(Unique))
if (SingleInstance<App>.InitializeAsFirstInstance())
{
using var application = new App();
application.InitializeComponent();
application.Run();
}
}

#endregion

#region App Events

#pragma warning disable VSTHRD100 // Avoid async void methods

private async void OnStartup(object sender, StartupEventArgs e)
Expand Down Expand Up @@ -137,11 +162,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();
Expand All @@ -158,8 +183,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 ----------------------------------------------------");
});
}

Expand Down Expand Up @@ -192,7 +216,6 @@ private void AutoStartup()
}
}

//[Conditional("RELEASE")]
private void AutoUpdates()
{
_ = Task.Run(async () =>
Expand All @@ -210,11 +233,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();
};
}

/// <summary>
Expand All @@ -235,20 +276,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<MainViewModel>().Show();
}

#endregion
}
}
75 changes: 29 additions & 46 deletions Flow.Launcher/Helper/SingleInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
// modified to allow single instace restart
namespace Flow.Launcher.Helper
{
public interface ISingleInstanceApp
{
void OnSecondAppStarted();
}
public interface ISingleInstanceApp
{
void OnSecondAppStarted();
}

/// <summary>
/// This class checks to make sure that only one instance of
Expand All @@ -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.
/// </remarks>
public static class SingleInstance<TApplication>
where TApplication: Application , ISingleInstanceApp

public static class SingleInstance<TApplication> where TApplication : Application, ISingleInstanceApp
{
#region Private Fields

Expand All @@ -39,11 +37,12 @@ public static class SingleInstance<TApplication>
/// Suffix to the channel name.
/// </summary>
private const string ChannelNameSuffix = "SingeInstanceIPCChannel";
private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex";

/// <summary>
/// Application mutex.
/// </summary>
internal static Mutex singleInstanceMutex;
internal static Mutex SingleInstanceMutex { get; set; }

#endregion

Expand All @@ -54,24 +53,23 @@ public static class SingleInstance<TApplication>
/// If not, activates the first instance.
/// </summary>
/// <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.
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;
}
}
Expand All @@ -81,7 +79,7 @@ public static bool InitializeAsFirstInstance( string uniqueName )
/// </summary>
public static void Cleanup()
{
singleInstanceMutex?.ReleaseMutex();
SingleInstanceMutex?.ReleaseMutex();
}

#endregion
Expand All @@ -93,22 +91,19 @@ public static void Cleanup()
/// Once receives signal from client, will activate first instance.
/// </summary>
/// <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();
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();
}
}

Expand All @@ -119,25 +114,13 @@ private static async Task CreateRemoteService(string channelName)
/// <param name="args">
/// Command line arguments for the second instance, passed to the first instance to take appropriate action.
/// </param>
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);

/// <summary>
/// Callback for activating first instance of the application.
/// </summary>
/// <param name="arg">Callback argument.</param>
/// <returns>Always null.</returns>
private static object ActivateFirstInstanceCallback(object o)
{
ActivateFirstInstance();
return null;
// Connect to the available pipe
await pipeClient.ConnectAsync(0);
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions Flow.Launcher/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
AllowDrop="True"
AllowsTransparency="True"
Background="Transparent"
Closed="OnClosed"
Closing="OnClosing"
Deactivated="OnDeactivated"
Icon="Images/app.png"
Expand Down
Loading
Loading