diff --git a/src/TeamsISO.App/App.xaml.cs b/src/TeamsISO.App/App.xaml.cs index 0360e00..c384870 100644 --- a/src/TeamsISO.App/App.xaml.cs +++ b/src/TeamsISO.App/App.xaml.cs @@ -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 { + /// + /// 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. + /// + 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); } }