feat(ui): single-instance enforcement via per-user named mutex
Two simultaneous TeamsISO processes contend over the NDI runtime, the same default sender names, and %APPDATA%\\TeamsISO\\config.json — observed during testing when launchers / shortcuts produced duplicate windows. Add a Local namespace per-user-keyed mutex (Local\\WildDragon.TeamsISO.SingleInstance.<username>) at startup; if a second instance can't claim it, broadcast a registered window message ('WildDragon.TeamsISO.BringToFront') and Shutdown(0). The running instance subscribes to that message via ComponentDispatcher.ThreadFilterMessage and surfaces its main window when received.
Per-user keying lets two different Windows users on the same machine each run their own TeamsISO. Mutex is released and disposed on OnExit.
Verified: Start-Process the exe twice in a row -> only one process remains, with the original window surfaced.
This commit is contained in:
parent
b542d01835
commit
53c06a9af9
1 changed files with 55 additions and 0 deletions
|
|
@ -1,5 +1,7 @@
|
|||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TeamsISO.App.ViewModels;
|
||||
|
|
@ -14,15 +16,63 @@ namespace TeamsISO.App;
|
|||
|
||||
public partial class App : Application
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-user mutex name. Including the SID-equivalent (the username) ensures two
|
||||
/// different Windows users can each run TeamsISO on the same machine, while one
|
||||
/// user can't spawn duplicate instances that would contend over the NDI runtime
|
||||
/// and the shared %APPDATA%\TeamsISO\config.json.
|
||||
/// </summary>
|
||||
private static readonly string SingleInstanceMutexName =
|
||||
$"Local\\WildDragon.TeamsISO.SingleInstance.{Environment.UserName}";
|
||||
|
||||
private System.Threading.Mutex? _singleInstanceMutex;
|
||||
private ILoggerFactory? _loggerFactory;
|
||||
private NdiInteropPInvoke? _interop;
|
||||
private IsoController? _controller;
|
||||
private MainViewModel? _viewModel;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint RegisterWindowMessageW(string lpString);
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int PostMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int SendNotifyMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
|
||||
private const IntPtr HWND_BROADCAST = -1;
|
||||
|
||||
protected override async void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
// Single-instance gate: if another TeamsISO is already running for this user,
|
||||
// broadcast the bring-to-front message and exit silently. This prevents the
|
||||
// NDI/config contention seen during testing where two finders, two senders
|
||||
// with the same default name, and two writers to config.json all raced.
|
||||
bool createdNew;
|
||||
_singleInstanceMutex = new System.Threading.Mutex(initiallyOwned: true, SingleInstanceMutexName, out createdNew);
|
||||
if (!createdNew)
|
||||
{
|
||||
var bringToFront = RegisterWindowMessageW("WildDragon.TeamsISO.BringToFront");
|
||||
if (bringToFront != 0)
|
||||
SendNotifyMessageW(HWND_BROADCAST, bringToFront, IntPtr.Zero, IntPtr.Zero);
|
||||
Shutdown(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for the broadcast — if a *new* instance launches and finds us already
|
||||
// running, it'll send this message; we surface our window in response.
|
||||
var bringToFrontMsg = RegisterWindowMessageW("WildDragon.TeamsISO.BringToFront");
|
||||
ComponentDispatcher.ThreadFilterMessage += (ref System.Windows.Interop.MSG msg, ref bool handled) =>
|
||||
{
|
||||
if (msg.message == (int)bringToFrontMsg && MainWindow is not null)
|
||||
{
|
||||
if (MainWindow.WindowState == WindowState.Minimized) MainWindow.WindowState = WindowState.Normal;
|
||||
MainWindow.Activate();
|
||||
MainWindow.Topmost = true;
|
||||
MainWindow.Topmost = false;
|
||||
handled = true;
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_loggerFactory = EngineLogging.CreateConsole(LogLevel.Information);
|
||||
|
|
@ -102,6 +152,11 @@ public partial class App : Application
|
|||
{
|
||||
// Best-effort shutdown
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { _singleInstanceMutex?.ReleaseMutex(); } catch { /* not owned */ }
|
||||
_singleInstanceMutex?.Dispose();
|
||||
}
|
||||
base.OnExit(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue