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; using TeamsISO.Engine.Controller; using TeamsISO.Engine.Interop; using TeamsISO.Engine.Logging; using TeamsISO.Engine.NdiInterop; using TeamsISO.Engine.Persistence; using TeamsISO.Engine.Pipeline; 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 { // WPF host: write to both console (visible if attached) and a rolling daily // file under %LOCALAPPDATA%\TeamsISO\Logs so users have something to grab when // they file an issue. _loggerFactory = EngineLogging.CreateDefault(LogLevel.Information); var logger = _loggerFactory.CreateLogger(); logger.LogInformation( "TeamsISO.App starting up. Build: {Version}. Process: {Pid}.", typeof(App).Assembly.GetName().Version, Environment.ProcessId); // ---- Preflight: NDI runtime ---- try { _interop = new NdiInteropPInvoke(_loggerFactory.CreateLogger()); } catch (Exception ex) { MessageBox.Show( "TeamsISO could not initialize the NDI runtime.\n\n" + "Install the NDI Runtime from https://ndi.video/tools/ and try again.\n\n" + "Details: " + ex.Message, "TeamsISO — NDI runtime missing", MessageBoxButton.OK, MessageBoxImage.Error); Shutdown(2); return; } // ---- Engine wiring ---- var configPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TeamsISO", "config.json"); var configStore = new ConfigStore(configPath, _loggerFactory.CreateLogger()); var probe = new NdiRuntimeProbe(_interop, NdiVersion.ExpectedRuntimeVersionPrefix); var scaler = new ManagedNearestNeighborFrameScaler(); var loggerFactoryRef = _loggerFactory; var interopRef = _interop; IsoPipeline PipelineFactory(IsoPipelineConfig config) { var clock = new PeriodicTimerFrameClock(config.Settings.FramerateHz); return new IsoPipeline( config, interopRef, scaler, clock, ExponentialBackoff.Default, (delay, ct) => Task.Delay(delay, ct), loggerFactoryRef); } _controller = new IsoController( _interop, PipelineFactory, configStore, probe, _loggerFactory); _viewModel = new MainViewModel(_controller, Dispatcher); var window = new MainWindow(_viewModel); window.Show(); MainWindow = window; await _viewModel.InitializeAsync(CancellationToken.None); } catch (Exception ex) { MessageBox.Show( "TeamsISO failed to start.\n\nDetails: " + ex, "TeamsISO — startup error", MessageBoxButton.OK, MessageBoxImage.Error); Shutdown(1); } } protected override async void OnExit(ExitEventArgs e) { try { _viewModel?.Dispose(); if (_controller is not null) await _controller.DisposeAsync(); _interop?.Dispose(); _loggerFactory?.Dispose(); } catch { // Best-effort shutdown } finally { try { _singleInstanceMutex?.ReleaseMutex(); } catch { /* not owned */ } _singleInstanceMutex?.Dispose(); } base.OnExit(e); } }