Last active
November 23, 2022 20:57
-
-
Save wbokkers/af326da5391bdb13b58529e720540178 to your computer and use it in GitHub Desktop.
Single Instancing Solution for WinUI 3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
... | |
public partial class App : Application | |
{ | |
private readonly SingleInstanceDesktopApp _singleInstanceApp; | |
public App() | |
{ | |
InitializeComponent(); | |
_singleInstanceApp = new SingleInstanceDesktopApp("SOME-IDENTIFICATION-STRING-FOR-YOUR-APP"); | |
_singleInstanceApp.Launched += OnSingleInstanceLaunched; | |
} | |
// Redirect the OnLaunched event to the single app instance | |
protected override void OnLaunched(LaunchActivatedEventArgs args) | |
{ | |
_singleInstanceApp.Launch(args.Arguments); | |
} | |
private void OnSingleInstanceLaunched(object? sender, SingleInstanceLaunchEventArgs e) | |
{ | |
if(e.IsFirstLaunch) | |
{ | |
// TODO: do things on the first launch, like creating your main window | |
// and processing arguments (e.Arguments) | |
} | |
else | |
{ | |
// TODO: do things on subsequent launches, like processing arguments from e.Arguments | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Diagnostics; | |
using System.IO; | |
using System.IO.Pipes; | |
using System.Linq; | |
using System.Threading; | |
namespace AppInstancing | |
{ | |
public class SingleInstanceLaunchEventArgs : EventArgs | |
{ | |
public SingleInstanceLaunchEventArgs(string arguments, bool isFirstLaunch) | |
{ | |
Arguments = arguments; | |
IsFirstLaunch = isFirstLaunch; | |
} | |
public string Arguments { get; private set; } = ""; | |
public bool IsFirstLaunch { get; private set; } | |
} | |
public sealed class SingleInstanceDesktopApp : IDisposable | |
{ | |
private readonly string _mutexName = ""; | |
private readonly string _pipeName = ""; | |
private readonly object _namedPiperServerThreadLock = new(); | |
private bool _isDisposed = false; | |
private bool _isFirstInstance; | |
private Mutex? _mutexApplication; | |
private NamedPipeServerStream? _namedPipeServerStream; | |
public event EventHandler<SingleInstanceLaunchEventArgs>? Launched; | |
public SingleInstanceDesktopApp(string appId) | |
{ | |
_mutexName = "MUTEX_" + appId; | |
_pipeName = "PIPE_" + appId; | |
} | |
public void Launch(string arguments) | |
{ | |
if (string.IsNullOrEmpty(arguments)) | |
{ | |
// The arguments from LaunchActivatedEventArgs can be empty, when | |
// the user specified arguments (e.g. when using an execution alias). For this reason we | |
// alternatively check for arguments using a different API. | |
var argList = System.Environment.GetCommandLineArgs(); | |
if (argList.Length > 1) | |
{ | |
arguments = string.Join(' ', argList.Skip(1)); | |
} | |
} | |
if (IsFirstApplicationInstance()) | |
{ | |
CreateNamedPipeServer(); | |
Launched?.Invoke(this, new SingleInstanceLaunchEventArgs(arguments, isFirstLaunch: true)); | |
} | |
else | |
{ | |
SendArgumentsToRunningInstance(arguments); | |
Process.GetCurrentProcess().Kill(); | |
// Note: needed to kill the process in WinAppSDK 1.0, since Application.Current.Exit() does not work there. | |
// OR: Application.Current.Exit(); | |
} | |
} | |
public void Dispose() | |
{ | |
if (_isDisposed) | |
return; | |
_isDisposed = true; | |
_namedPipeServerStream?.Dispose(); | |
_mutexApplication?.Dispose(); | |
} | |
private bool IsFirstApplicationInstance() | |
{ | |
// Allow for multiple runs but only try and get the mutex once | |
if (_mutexApplication == null) | |
{ | |
_mutexApplication = new Mutex(true, _mutexName, out _isFirstInstance); | |
} | |
return _isFirstInstance; | |
} | |
/// <summary> | |
/// Starts a new pipe server if one isn't already active. | |
/// </summary> | |
private void CreateNamedPipeServer() | |
{ | |
_namedPipeServerStream = new NamedPipeServerStream( | |
_pipeName, PipeDirection.In, | |
maxNumberOfServerInstances: 1, | |
PipeTransmissionMode.Byte, | |
PipeOptions.Asynchronous, | |
inBufferSize: 0, | |
outBufferSize: 0); | |
_namedPipeServerStream.BeginWaitForConnection(OnNamedPipeServerConnected, _namedPipeServerStream); | |
} | |
private void SendArgumentsToRunningInstance(string arguments) | |
{ | |
try | |
{ | |
using var namedPipeClientStream = new NamedPipeClientStream(".", _pipeName, PipeDirection.Out); | |
namedPipeClientStream.Connect(3000); // Maximum wait 3 seconds | |
using var sw = new StreamWriter(namedPipeClientStream); | |
sw.Write(arguments); | |
sw.Flush(); | |
} | |
catch (Exception) | |
{ | |
// Error connecting or sending | |
} | |
} | |
private void OnNamedPipeServerConnected(IAsyncResult asyncResult) | |
{ | |
try | |
{ | |
if (_namedPipeServerStream == null) | |
return; | |
_namedPipeServerStream.EndWaitForConnection(asyncResult); | |
// Read the arguments from the pipe | |
lock (_namedPiperServerThreadLock) | |
{ | |
using var sr = new StreamReader(_namedPipeServerStream); | |
var args = sr.ReadToEnd(); | |
Launched?.Invoke(this, new SingleInstanceLaunchEventArgs(args, isFirstLaunch: false)); | |
} | |
} | |
catch (ObjectDisposedException) | |
{ | |
// EndWaitForConnection will throw when the pipe closes before there is a connection. | |
// In that case, we don't create more pipes and just return. | |
// This will happen when the app is closed and therefor the pipe is closed as well. | |
return; | |
} | |
catch (Exception) | |
{ | |
// ignored | |
} | |
finally | |
{ | |
// Close the original pipe (we will create a new one each time) | |
_namedPipeServerStream?.Dispose(); | |
} | |
// Create a new pipe for next connection | |
CreateNamedPipeServer(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment