Skip to content

Commit

Permalink
Add MutexContext
Browse files Browse the repository at this point in the history
Its possible for the single instance manager to throw an exception on
shutdown, when it was disposed on an other thread, the the original
`StartApplication` was made. We don't want to force the user to keep
track of the correct threads themself, so we pass all calls to the mutex
to a separate thread. This however imposes an other issue for special
cases like our unut tests.

Using a dedicated thread for locking is no problem, as long as one
process only ever attempts to lock the mutex once. But when we try to
lock multiple times from from within the same application, like we do in
our unit tests, then we run into the situation, that we lock twice on
the smae threads, which will succeed both times, since a mutex is
reentrant. This however is not thre result we want, as we still want the
second call to fail and signal, that there is already a running
instance. To circumvent this issue, we track how often the lock has been
taken, and release it immedialty, when the count is greater than 1.
After that we can pretent, the lock failed, and the rest works fine
again
  • Loading branch information
Pretasoc committed May 15, 2023
1 parent 980942b commit a0659a4
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 4 deletions.
77 changes: 77 additions & 0 deletions SingleInstanceManager/MutextContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SingleInstanceManager
{
internal class MutexContext : IDisposable
{
private readonly BlockingCollection<Action> _pendingOperations = new BlockingCollection<Action>();

public MutexContext()
{
Task.Factory.StartNew(
ProcessLoop,
default,
TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default);
}

public void Dispose()
{
_pendingOperations.CompleteAdding();
}

public void Execute(Action? action)
{
TaskCompletionSource<object?> tcs = new TaskCompletionSource<object?>();

void ExecuteInternal()
{
try
{
action?.Invoke();
tcs.SetResult(null);
}
catch (Exception e)
{
tcs.SetException(e);
}
}

_pendingOperations.Add(ExecuteInternal);
tcs.Task.GetAwaiter().GetResult();
}

public T Execute<T>(Func<T> action)
{
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();

void ExecuteInternal()
{
try
{
T result = action.Invoke();
tcs.SetResult(result);
}
catch (Exception e)
{
tcs.SetException(e);
}
}

_pendingOperations.Add(ExecuteInternal);
return tcs.Task.GetAwaiter().GetResult();
}

private void ProcessLoop()
{
IEnumerable<Action> operations = _pendingOperations.GetConsumingEnumerable();
foreach (Action operation in operations)
{
operation.Invoke();
}
}
}
}
54 changes: 50 additions & 4 deletions SingleInstanceManager/SingleInstanceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,18 @@ public class SingleInstanceManager : IDisposable
private static SingleInstanceManager? _instance;
private readonly CancellationTokenSource _cts;
private readonly Mutex _instanceLockerMutex;
private readonly MutexContext _mutexContext = new MutexContext();
private readonly string _pipeName;
private bool _disposed;
private int _reentranceCounter;

private SingleInstanceManager(string? guid, bool global = false)
{
// create a (hopefully) unique mutex name
string assemblyName = guid ?? Assembly.GetEntryAssembly()?.FullName ?? "SingleInstanceManager";

// this mutex will be shared across the system to signal an existing instance
_instanceLockerMutex = new Mutex(true, $"{(global ? GlobalNamespace : LocalNamespace)}\\{assemblyName}");
_instanceLockerMutex = new Mutex(false, $"{(global ? GlobalNamespace : LocalNamespace)}\\{assemblyName}");

// create a (hopefully) unique lock name
_pipeName = assemblyName + "argsStream";
Expand Down Expand Up @@ -80,8 +83,20 @@ private set

public void Dispose()
{
_instanceLockerMutex?.Dispose();
if (_disposed)
{
return;
}

_disposed = true;
_mutexContext.Execute(
() =>
{
_instanceLockerMutex?.Dispose();
Interlocked.Decrement(ref _reentranceCounter);
});

_mutexContext.Dispose();
_cts?.Dispose();
_instance = null;
}
Expand All @@ -96,7 +111,11 @@ public bool RunApplication(string[] args)
try
{
// if WaitOne returns true, no other instance has taken the mutex
if (_instanceLockerMutex.WaitOne(0))
// All calls to the mutex are made through _mutex context, to ensure
// we release the lock on the same thread, as we locked earlier
bool acquiredLock = _mutexContext.Execute(LockMutex);

if (acquiredLock)
{
Task.Factory.StartNew(() => ConnectionLoop(_cts.Token), TaskCreationOptions.LongRunning);
return true;
Expand Down Expand Up @@ -138,6 +157,7 @@ public void Shutdown()
private async void ConnectionLoop(CancellationToken cancellationToken)
{
SemaphoreSlim serverCounter = new SemaphoreSlim(2);

void DoConnection(BinaryReader reader)
{
try
Expand Down Expand Up @@ -175,11 +195,37 @@ void DoConnection(BinaryReader reader)
serverCounter.Release();
continue;
}

_ = Task.Run(() => DoConnection(reader), cancellationToken);
}
}

private void OnSecondInstanceStarted(string[] parameters, IReadOnlyDictionary<string, string> environmentalVariables, string workingDirectory)
private bool LockMutex()
{
if (!_instanceLockerMutex.WaitOne(0))
{
return false;
}

// In normal scenarios the WaitOne call above is sufficient,
// but in a scenario, like our unit cases, the forcing all mutex calls
// to a single thread causes a problem, because mutex is reentrant we
// could lock on the same mutex more than once. So we have to count
// on ourself and limit the lock count to one.
if (Interlocked.Increment(ref _reentranceCounter) > 1)
{
Interlocked.Decrement(ref _reentranceCounter);
_instanceLockerMutex.ReleaseMutex();
return false;
}

return true;
}

private void OnSecondInstanceStarted(
string[] parameters,
IReadOnlyDictionary<string, string> environmentalVariables,
string workingDirectory)
{
if (SecondInstanceStarted is not { } secondInstanceStarted)
{
Expand Down

0 comments on commit a0659a4

Please sign in to comment.