Skip to content

Instantly share code, notes, and snippets.

@wbokkers
Last active November 23, 2022 20:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wbokkers/af326da5391bdb13b58529e720540178 to your computer and use it in GitHub Desktop.
Save wbokkers/af326da5391bdb13b58529e720540178 to your computer and use it in GitHub Desktop.
Single Instancing Solution for WinUI 3
...
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
}
}
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