2026-05-07 23:59:47 -04:00
|
|
|
using System.Runtime.InteropServices;
|
2026-05-07 11:09:56 -04:00
|
|
|
using System.Windows;
|
2026-05-07 23:59:47 -04:00
|
|
|
using System.Windows.Interop;
|
2026-05-07 11:41:58 -04:00
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using TeamsISO.App.ViewModels;
|
|
|
|
|
using TeamsISO.Engine.Controller;
|
|
|
|
|
using TeamsISO.Engine.Logging;
|
|
|
|
|
using TeamsISO.Engine.NdiInterop;
|
2026-05-07 11:09:56 -04:00
|
|
|
|
2026-05-10 09:41:29 -04:00
|
|
|
// Application + MessageBox aliases live in GlobalUsings.cs (project-wide).
|
|
|
|
|
// Don't redeclare here — Roslyn errors with CS1537 on duplicate alias.
|
|
|
|
|
|
2026-05-07 11:09:56 -04:00
|
|
|
namespace TeamsISO.App;
|
|
|
|
|
|
refactor(app): split App.xaml.cs into themed partial files
App.xaml.cs was 461 lines / 21KB and conflated four concerns: process-
level lifecycle (mutex / message pump filter / shutdown), engine bootstrap
(NDI runtime / IsoController / view model construction), crash handling
(three exception channels + log directory + dialog), and the background
update-checker kickoff.
Splits via partial-class into themed sibling files:
* App.xaml.cs (was 461L → now 219L) — class skeleton, fields, internal
property accessors, Win32 P/Invoke surface, OnStartup as a wiring
pipeline that calls the bootstrap steps in order, OnExit, CLI parser.
* App.Bootstrap.cs (250L, new) — linear startup steps:
TryAcquireSingleInstance, TryBootstrapNdiInterop, BootstrapEngine,
ConstructAndShowMainWindow, BootstrapControlSurfaceServices,
BootstrapTrayIcon, TryShowOnboarding, TryAutoLaunchTeams. Each
returns a signal (bool / window ref) when OnStartup needs it to
decide whether to continue.
* App.CrashHandlers.cs (93L, new) — OnAppDomainUnhandled,
OnDispatcherUnhandled, OnUnobservedTaskException, TryLogFatal,
TryShowCrashDialog, LogDirectory.
* App.UpdateCheckBootstrap.cs (42L, new) — StartBackgroundUpdateCheck
(24h-throttled, fire-and-forget).
OnStartup's body is now a 30-ish-line procedure that names each step,
which is what the original was trying to be. Comments inline the
"happened before, kept here for reason X" notes (theme.Apply before
window show; CLI args parsed before InitializeAsync). Behavior is
unchanged — Shutdown codes, error paths, and the side-effect order are
all preserved.
Build clean (0 warnings, 0 errors); 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:36:07 -04:00
|
|
|
// Split across partial files by responsibility:
|
|
|
|
|
// • App.xaml.cs — class skeleton, OnStartup (the wiring
|
|
|
|
|
// pipeline that calls into the partials),
|
|
|
|
|
// OnExit, CLI arg parser.
|
|
|
|
|
// • App.Bootstrap.cs — the linear setup steps OnStartup walks
|
|
|
|
|
// (single-instance gate, NDI interop, engine,
|
|
|
|
|
// main window, control surface, tray icon,
|
|
|
|
|
// onboarding, Teams auto-launch).
|
|
|
|
|
// • App.CrashHandlers.cs — AppDomain / Dispatcher / Task exception
|
|
|
|
|
// handlers + crash dialog + LogDirectory.
|
|
|
|
|
// • App.UpdateCheckBootstrap.cs — the background update-checker
|
|
|
|
|
// kickoff (24h-throttled).
|
2026-05-07 11:09:56 -04:00
|
|
|
public partial class App : Application
|
|
|
|
|
{
|
2026-05-07 23:59:47 -04:00
|
|
|
/// <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;
|
2026-05-08 01:01:00 -04:00
|
|
|
private bool _ownsSingleInstanceMutex;
|
|
|
|
|
private ThreadMessageEventHandler? _bringToFrontHandler;
|
2026-05-07 11:41:58 -04:00
|
|
|
private ILoggerFactory? _loggerFactory;
|
|
|
|
|
private NdiInteropPInvoke? _interop;
|
|
|
|
|
private IsoController? _controller;
|
|
|
|
|
private MainViewModel? _viewModel;
|
2026-05-10 09:41:29 -04:00
|
|
|
private TeamsISO.App.Services.ControlSurfaceServer? _controlSurface;
|
|
|
|
|
private TeamsISO.App.Services.OscBridge? _oscBridge;
|
2026-05-14 06:02:40 -04:00
|
|
|
// _diskSpaceWatcher removed — only existed to auto-disable recording at low free space.
|
2026-05-10 09:41:29 -04:00
|
|
|
private TeamsISO.App.Services.TrayIconHost? _trayIcon;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// REST control surface lifetime. Lives on App so the settings VM can flip
|
|
|
|
|
/// it on/off without us plumbing yet another DI dependency through MainViewModel.
|
|
|
|
|
/// Null between process startup and the OnStartup wire-up, and after OnExit.
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal TeamsISO.App.Services.ControlSurfaceServer? ControlSurface => _controlSurface;
|
|
|
|
|
|
|
|
|
|
/// <summary>OSC bridge (UDP) lifetime — same lifecycle pattern as the REST surface.</summary>
|
|
|
|
|
internal TeamsISO.App.Services.OscBridge? OscBridge => _oscBridge;
|
|
|
|
|
|
|
|
|
|
/// <summary>Tray-icon host. Exposed so the settings VM can flip the minimize-to-tray toggle.</summary>
|
|
|
|
|
internal TeamsISO.App.Services.TrayIconHost? TrayIcon => _trayIcon;
|
2026-05-07 11:41:58 -04:00
|
|
|
|
2026-05-07 23:59:47 -04:00
|
|
|
[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;
|
|
|
|
|
|
2026-05-07 11:41:58 -04:00
|
|
|
protected override async void OnStartup(StartupEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
base.OnStartup(e);
|
|
|
|
|
|
2026-05-10 09:41:29 -04:00
|
|
|
// Crash diagnostics — wire the three exception channels WPF leaves open by
|
|
|
|
|
// default to a single handler that logs Fatal to Serilog (which has the
|
|
|
|
|
// rolling-daily file sink at %LOCALAPPDATA%\TeamsISO\Logs) and then shows
|
|
|
|
|
// the user a dialog with the log path so they can attach it to a bug
|
|
|
|
|
// report. We deliberately don't catch StackOverflowException or
|
|
|
|
|
// ExecutionEngineException — both are uncatchable in modern .NET; if one
|
|
|
|
|
// fires the OS Watson dialog will take it from here.
|
|
|
|
|
AppDomain.CurrentDomain.UnhandledException += OnAppDomainUnhandled;
|
|
|
|
|
DispatcherUnhandledException += OnDispatcherUnhandled;
|
|
|
|
|
System.Threading.Tasks.TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
|
|
|
|
|
2026-05-14 12:46:24 -04:00
|
|
|
// Resolve and apply the theme BEFORE any window is shown so we don't
|
|
|
|
|
// paint a dark frame for one tick then flip to light (or vice versa).
|
|
|
|
|
// ThemeManager.Apply swaps Application.Resources.MergedDictionaries
|
|
|
|
|
// in place; DynamicResource refs in WildDragonTheme.xaml re-bind.
|
|
|
|
|
TeamsISO.App.Services.ThemeManager.Current.Apply();
|
|
|
|
|
|
refactor(app): split App.xaml.cs into themed partial files
App.xaml.cs was 461 lines / 21KB and conflated four concerns: process-
level lifecycle (mutex / message pump filter / shutdown), engine bootstrap
(NDI runtime / IsoController / view model construction), crash handling
(three exception channels + log directory + dialog), and the background
update-checker kickoff.
Splits via partial-class into themed sibling files:
* App.xaml.cs (was 461L → now 219L) — class skeleton, fields, internal
property accessors, Win32 P/Invoke surface, OnStartup as a wiring
pipeline that calls the bootstrap steps in order, OnExit, CLI parser.
* App.Bootstrap.cs (250L, new) — linear startup steps:
TryAcquireSingleInstance, TryBootstrapNdiInterop, BootstrapEngine,
ConstructAndShowMainWindow, BootstrapControlSurfaceServices,
BootstrapTrayIcon, TryShowOnboarding, TryAutoLaunchTeams. Each
returns a signal (bool / window ref) when OnStartup needs it to
decide whether to continue.
* App.CrashHandlers.cs (93L, new) — OnAppDomainUnhandled,
OnDispatcherUnhandled, OnUnobservedTaskException, TryLogFatal,
TryShowCrashDialog, LogDirectory.
* App.UpdateCheckBootstrap.cs (42L, new) — StartBackgroundUpdateCheck
(24h-throttled, fire-and-forget).
OnStartup's body is now a 30-ish-line procedure that names each step,
which is what the original was trying to be. Comments inline the
"happened before, kept here for reason X" notes (theme.Apply before
window show; CLI args parsed before InitializeAsync). Behavior is
unchanged — Shutdown codes, error paths, and the side-effect order are
all preserved.
Build clean (0 warnings, 0 errors); 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:36:07 -04:00
|
|
|
// Single-instance gate. Implementation in App.Bootstrap.cs; we
|
|
|
|
|
// bail silently if another instance already owns the mutex (the
|
|
|
|
|
// existing instance gets surfaced via the bring-to-front broadcast).
|
|
|
|
|
if (!TryAcquireSingleInstance())
|
2026-05-07 23:59:47 -04:00
|
|
|
{
|
|
|
|
|
Shutdown(0);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 11:41:58 -04:00
|
|
|
try
|
|
|
|
|
{
|
refactor(app): split App.xaml.cs into themed partial files
App.xaml.cs was 461 lines / 21KB and conflated four concerns: process-
level lifecycle (mutex / message pump filter / shutdown), engine bootstrap
(NDI runtime / IsoController / view model construction), crash handling
(three exception channels + log directory + dialog), and the background
update-checker kickoff.
Splits via partial-class into themed sibling files:
* App.xaml.cs (was 461L → now 219L) — class skeleton, fields, internal
property accessors, Win32 P/Invoke surface, OnStartup as a wiring
pipeline that calls the bootstrap steps in order, OnExit, CLI parser.
* App.Bootstrap.cs (250L, new) — linear startup steps:
TryAcquireSingleInstance, TryBootstrapNdiInterop, BootstrapEngine,
ConstructAndShowMainWindow, BootstrapControlSurfaceServices,
BootstrapTrayIcon, TryShowOnboarding, TryAutoLaunchTeams. Each
returns a signal (bool / window ref) when OnStartup needs it to
decide whether to continue.
* App.CrashHandlers.cs (93L, new) — OnAppDomainUnhandled,
OnDispatcherUnhandled, OnUnobservedTaskException, TryLogFatal,
TryShowCrashDialog, LogDirectory.
* App.UpdateCheckBootstrap.cs (42L, new) — StartBackgroundUpdateCheck
(24h-throttled, fire-and-forget).
OnStartup's body is now a 30-ish-line procedure that names each step,
which is what the original was trying to be. Comments inline the
"happened before, kept here for reason X" notes (theme.Apply before
window show; CLI args parsed before InitializeAsync). Behavior is
unchanged — Shutdown codes, error paths, and the side-effect order are
all preserved.
Build clean (0 warnings, 0 errors); 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:36:07 -04:00
|
|
|
// 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.
|
2026-05-08 00:47:25 -04:00
|
|
|
_loggerFactory = EngineLogging.CreateDefault(LogLevel.Information);
|
2026-05-07 11:41:58 -04:00
|
|
|
var logger = _loggerFactory.CreateLogger<App>();
|
2026-05-08 00:47:25 -04:00
|
|
|
logger.LogInformation(
|
|
|
|
|
"TeamsISO.App starting up. Build: {Version}. Process: {Pid}.",
|
|
|
|
|
typeof(App).Assembly.GetName().Version,
|
|
|
|
|
Environment.ProcessId);
|
2026-05-07 11:41:58 -04:00
|
|
|
|
refactor(app): split App.xaml.cs into themed partial files
App.xaml.cs was 461 lines / 21KB and conflated four concerns: process-
level lifecycle (mutex / message pump filter / shutdown), engine bootstrap
(NDI runtime / IsoController / view model construction), crash handling
(three exception channels + log directory + dialog), and the background
update-checker kickoff.
Splits via partial-class into themed sibling files:
* App.xaml.cs (was 461L → now 219L) — class skeleton, fields, internal
property accessors, Win32 P/Invoke surface, OnStartup as a wiring
pipeline that calls the bootstrap steps in order, OnExit, CLI parser.
* App.Bootstrap.cs (250L, new) — linear startup steps:
TryAcquireSingleInstance, TryBootstrapNdiInterop, BootstrapEngine,
ConstructAndShowMainWindow, BootstrapControlSurfaceServices,
BootstrapTrayIcon, TryShowOnboarding, TryAutoLaunchTeams. Each
returns a signal (bool / window ref) when OnStartup needs it to
decide whether to continue.
* App.CrashHandlers.cs (93L, new) — OnAppDomainUnhandled,
OnDispatcherUnhandled, OnUnobservedTaskException, TryLogFatal,
TryShowCrashDialog, LogDirectory.
* App.UpdateCheckBootstrap.cs (42L, new) — StartBackgroundUpdateCheck
(24h-throttled, fire-and-forget).
OnStartup's body is now a 30-ish-line procedure that names each step,
which is what the original was trying to be. Comments inline the
"happened before, kept here for reason X" notes (theme.Apply before
window show; CLI args parsed before InitializeAsync). Behavior is
unchanged — Shutdown codes, error paths, and the side-effect order are
all preserved.
Build clean (0 warnings, 0 errors); 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:36:07 -04:00
|
|
|
if (!TryBootstrapNdiInterop())
|
2026-05-07 11:41:58 -04:00
|
|
|
{
|
|
|
|
|
Shutdown(2);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
refactor(app): split App.xaml.cs into themed partial files
App.xaml.cs was 461 lines / 21KB and conflated four concerns: process-
level lifecycle (mutex / message pump filter / shutdown), engine bootstrap
(NDI runtime / IsoController / view model construction), crash handling
(three exception channels + log directory + dialog), and the background
update-checker kickoff.
Splits via partial-class into themed sibling files:
* App.xaml.cs (was 461L → now 219L) — class skeleton, fields, internal
property accessors, Win32 P/Invoke surface, OnStartup as a wiring
pipeline that calls the bootstrap steps in order, OnExit, CLI parser.
* App.Bootstrap.cs (250L, new) — linear startup steps:
TryAcquireSingleInstance, TryBootstrapNdiInterop, BootstrapEngine,
ConstructAndShowMainWindow, BootstrapControlSurfaceServices,
BootstrapTrayIcon, TryShowOnboarding, TryAutoLaunchTeams. Each
returns a signal (bool / window ref) when OnStartup needs it to
decide whether to continue.
* App.CrashHandlers.cs (93L, new) — OnAppDomainUnhandled,
OnDispatcherUnhandled, OnUnobservedTaskException, TryLogFatal,
TryShowCrashDialog, LogDirectory.
* App.UpdateCheckBootstrap.cs (42L, new) — StartBackgroundUpdateCheck
(24h-throttled, fire-and-forget).
OnStartup's body is now a 30-ish-line procedure that names each step,
which is what the original was trying to be. Comments inline the
"happened before, kept here for reason X" notes (theme.Apply before
window show; CLI args parsed before InitializeAsync). Behavior is
unchanged — Shutdown codes, error paths, and the side-effect order are
all preserved.
Build clean (0 warnings, 0 errors); 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:36:07 -04:00
|
|
|
BootstrapEngine();
|
|
|
|
|
var window = ConstructAndShowMainWindow();
|
|
|
|
|
BootstrapControlSurfaceServices();
|
|
|
|
|
BootstrapTrayIcon(window);
|
|
|
|
|
TryShowOnboarding(window);
|
2026-05-10 09:41:29 -04:00
|
|
|
|
refactor(app): split App.xaml.cs into themed partial files
App.xaml.cs was 461 lines / 21KB and conflated four concerns: process-
level lifecycle (mutex / message pump filter / shutdown), engine bootstrap
(NDI runtime / IsoController / view model construction), crash handling
(three exception channels + log directory + dialog), and the background
update-checker kickoff.
Splits via partial-class into themed sibling files:
* App.xaml.cs (was 461L → now 219L) — class skeleton, fields, internal
property accessors, Win32 P/Invoke surface, OnStartup as a wiring
pipeline that calls the bootstrap steps in order, OnExit, CLI parser.
* App.Bootstrap.cs (250L, new) — linear startup steps:
TryAcquireSingleInstance, TryBootstrapNdiInterop, BootstrapEngine,
ConstructAndShowMainWindow, BootstrapControlSurfaceServices,
BootstrapTrayIcon, TryShowOnboarding, TryAutoLaunchTeams. Each
returns a signal (bool / window ref) when OnStartup needs it to
decide whether to continue.
* App.CrashHandlers.cs (93L, new) — OnAppDomainUnhandled,
OnDispatcherUnhandled, OnUnobservedTaskException, TryLogFatal,
TryShowCrashDialog, LogDirectory.
* App.UpdateCheckBootstrap.cs (42L, new) — StartBackgroundUpdateCheck
(24h-throttled, fire-and-forget).
OnStartup's body is now a 30-ish-line procedure that names each step,
which is what the original was trying to be. Comments inline the
"happened before, kept here for reason X" notes (theme.Apply before
window show; CLI args parsed before InitializeAsync). Behavior is
unchanged — Shutdown codes, error paths, and the side-effect order are
all preserved.
Build clean (0 warnings, 0 errors); 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:36:07 -04:00
|
|
|
// Parse CLI args BEFORE InitializeAsync so any --apply-preset
|
|
|
|
|
// request overrides the persisted auto-apply preference cleanly.
|
2026-05-10 09:41:29 -04:00
|
|
|
ApplyCommandLineArgs(e.Args);
|
|
|
|
|
|
refactor(app): split App.xaml.cs into themed partial files
App.xaml.cs was 461 lines / 21KB and conflated four concerns: process-
level lifecycle (mutex / message pump filter / shutdown), engine bootstrap
(NDI runtime / IsoController / view model construction), crash handling
(three exception channels + log directory + dialog), and the background
update-checker kickoff.
Splits via partial-class into themed sibling files:
* App.xaml.cs (was 461L → now 219L) — class skeleton, fields, internal
property accessors, Win32 P/Invoke surface, OnStartup as a wiring
pipeline that calls the bootstrap steps in order, OnExit, CLI parser.
* App.Bootstrap.cs (250L, new) — linear startup steps:
TryAcquireSingleInstance, TryBootstrapNdiInterop, BootstrapEngine,
ConstructAndShowMainWindow, BootstrapControlSurfaceServices,
BootstrapTrayIcon, TryShowOnboarding, TryAutoLaunchTeams. Each
returns a signal (bool / window ref) when OnStartup needs it to
decide whether to continue.
* App.CrashHandlers.cs (93L, new) — OnAppDomainUnhandled,
OnDispatcherUnhandled, OnUnobservedTaskException, TryLogFatal,
TryShowCrashDialog, LogDirectory.
* App.UpdateCheckBootstrap.cs (42L, new) — StartBackgroundUpdateCheck
(24h-throttled, fire-and-forget).
OnStartup's body is now a 30-ish-line procedure that names each step,
which is what the original was trying to be. Comments inline the
"happened before, kept here for reason X" notes (theme.Apply before
window show; CLI args parsed before InitializeAsync). Behavior is
unchanged — Shutdown codes, error paths, and the side-effect order are
all preserved.
Build clean (0 warnings, 0 errors); 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:36:07 -04:00
|
|
|
await _viewModel!.InitializeAsync(CancellationToken.None);
|
2026-05-10 20:35:00 -04:00
|
|
|
|
refactor(app): split App.xaml.cs into themed partial files
App.xaml.cs was 461 lines / 21KB and conflated four concerns: process-
level lifecycle (mutex / message pump filter / shutdown), engine bootstrap
(NDI runtime / IsoController / view model construction), crash handling
(three exception channels + log directory + dialog), and the background
update-checker kickoff.
Splits via partial-class into themed sibling files:
* App.xaml.cs (was 461L → now 219L) — class skeleton, fields, internal
property accessors, Win32 P/Invoke surface, OnStartup as a wiring
pipeline that calls the bootstrap steps in order, OnExit, CLI parser.
* App.Bootstrap.cs (250L, new) — linear startup steps:
TryAcquireSingleInstance, TryBootstrapNdiInterop, BootstrapEngine,
ConstructAndShowMainWindow, BootstrapControlSurfaceServices,
BootstrapTrayIcon, TryShowOnboarding, TryAutoLaunchTeams. Each
returns a signal (bool / window ref) when OnStartup needs it to
decide whether to continue.
* App.CrashHandlers.cs (93L, new) — OnAppDomainUnhandled,
OnDispatcherUnhandled, OnUnobservedTaskException, TryLogFatal,
TryShowCrashDialog, LogDirectory.
* App.UpdateCheckBootstrap.cs (42L, new) — StartBackgroundUpdateCheck
(24h-throttled, fire-and-forget).
OnStartup's body is now a 30-ish-line procedure that names each step,
which is what the original was trying to be. Comments inline the
"happened before, kept here for reason X" notes (theme.Apply before
window show; CLI args parsed before InitializeAsync). Behavior is
unchanged — Shutdown codes, error paths, and the side-effect order are
all preserved.
Build clean (0 warnings, 0 errors); 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:36:07 -04:00
|
|
|
TryAutoLaunchTeams(logger);
|
|
|
|
|
StartBackgroundUpdateCheck(logger);
|
2026-05-07 11:41:58 -04:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
feat(wpf): v2 task 39+40 - studio table redesign + Ctrl+K command palette
Task 39: 5-column participants table - state LED, name+codec caption, 5-bar audio meter, mono output name, ISO pill. Row height 52, full-row active-speaker tint (no left stripe). New converter LevelThresholdConverter, OutputName property on ParticipantViewModel.
Task 40: Ctrl+K / Ctrl+P command palette - chromeless centered floating window, fuzzy Contains match across Label/Category/Keywords, arrow nav, Enter invoke, Esc close. Quick/Teams/Network/App categories cover top operator verbs and theme switching.
Also: log startup exceptions to Serilog before the modal MessageBox fires - much better triage signal than user-pasted dialog text.
2026-05-15 11:15:00 -04:00
|
|
|
// Log the full exception (incl. stack + inner) to Serilog BEFORE the
|
|
|
|
|
// modal MessageBox fires — diagnostic logs are far more useful than a
|
|
|
|
|
// user-pasted "TeamsISO failed to start..." line when triaging a
|
|
|
|
|
// startup crash. The logger may itself have been the failure target
|
|
|
|
|
// so guard the call.
|
|
|
|
|
try { _loggerFactory?.CreateLogger<App>().LogCritical(ex, "OnStartup failed before main loop"); }
|
|
|
|
|
catch { /* defensive */ }
|
2026-05-07 11:41:58 -04:00
|
|
|
MessageBox.Show(
|
|
|
|
|
"TeamsISO failed to start.\n\nDetails: " + ex,
|
|
|
|
|
"TeamsISO — startup error",
|
|
|
|
|
MessageBoxButton.OK,
|
|
|
|
|
MessageBoxImage.Error);
|
|
|
|
|
Shutdown(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 09:41:29 -04:00
|
|
|
/// <summary>
|
|
|
|
|
/// Parse the supported CLI flags. Currently:
|
|
|
|
|
/// <c>--apply-preset NAME</c> — apply the named preset once participants
|
|
|
|
|
/// populate. Equivalent to running TeamsISO and clicking Presets → select →
|
|
|
|
|
/// Apply, but driven from a desktop shortcut.
|
|
|
|
|
/// Unrecognized flags are silently ignored — operators using shortcut.lnk
|
|
|
|
|
/// files don't need to fight argument parsers.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void ApplyCommandLineArgs(string[] args)
|
|
|
|
|
{
|
|
|
|
|
if (_viewModel is null) return;
|
|
|
|
|
for (var i = 0; i < args.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
switch (args[i])
|
|
|
|
|
{
|
|
|
|
|
case "--apply-preset":
|
|
|
|
|
if (i + 1 < args.Length && !string.IsNullOrEmpty(args[i + 1]))
|
|
|
|
|
{
|
|
|
|
|
_viewModel.RequestApplyPresetOnStartup(args[i + 1]);
|
|
|
|
|
i++; // consume the value
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(app): split App.xaml.cs into themed partial files
App.xaml.cs was 461 lines / 21KB and conflated four concerns: process-
level lifecycle (mutex / message pump filter / shutdown), engine bootstrap
(NDI runtime / IsoController / view model construction), crash handling
(three exception channels + log directory + dialog), and the background
update-checker kickoff.
Splits via partial-class into themed sibling files:
* App.xaml.cs (was 461L → now 219L) — class skeleton, fields, internal
property accessors, Win32 P/Invoke surface, OnStartup as a wiring
pipeline that calls the bootstrap steps in order, OnExit, CLI parser.
* App.Bootstrap.cs (250L, new) — linear startup steps:
TryAcquireSingleInstance, TryBootstrapNdiInterop, BootstrapEngine,
ConstructAndShowMainWindow, BootstrapControlSurfaceServices,
BootstrapTrayIcon, TryShowOnboarding, TryAutoLaunchTeams. Each
returns a signal (bool / window ref) when OnStartup needs it to
decide whether to continue.
* App.CrashHandlers.cs (93L, new) — OnAppDomainUnhandled,
OnDispatcherUnhandled, OnUnobservedTaskException, TryLogFatal,
TryShowCrashDialog, LogDirectory.
* App.UpdateCheckBootstrap.cs (42L, new) — StartBackgroundUpdateCheck
(24h-throttled, fire-and-forget).
OnStartup's body is now a 30-ish-line procedure that names each step,
which is what the original was trying to be. Comments inline the
"happened before, kept here for reason X" notes (theme.Apply before
window show; CLI args parsed before InitializeAsync). Behavior is
unchanged — Shutdown codes, error paths, and the side-effect order are
all preserved.
Build clean (0 warnings, 0 errors); 56 + 104 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:36:07 -04:00
|
|
|
// Crash handlers (OnAppDomainUnhandled / OnDispatcherUnhandled /
|
|
|
|
|
// OnUnobservedTaskException / TryLogFatal / TryShowCrashDialog / LogDirectory)
|
|
|
|
|
// live in App.CrashHandlers.cs.
|
2026-05-10 09:41:29 -04:00
|
|
|
|
2026-05-07 11:41:58 -04:00
|
|
|
protected override async void OnExit(ExitEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-05-10 09:41:29 -04:00
|
|
|
_trayIcon?.Dispose();
|
|
|
|
|
if (_controlSurface is not null)
|
|
|
|
|
await _controlSurface.DisposeAsync();
|
|
|
|
|
if (_oscBridge is not null)
|
|
|
|
|
await _oscBridge.DisposeAsync();
|
2026-05-07 11:41:58 -04:00
|
|
|
_viewModel?.Dispose();
|
|
|
|
|
if (_controller is not null)
|
|
|
|
|
await _controller.DisposeAsync();
|
|
|
|
|
_interop?.Dispose();
|
|
|
|
|
_loggerFactory?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Best-effort shutdown
|
|
|
|
|
}
|
2026-05-07 23:59:47 -04:00
|
|
|
finally
|
|
|
|
|
{
|
2026-05-08 01:01:00 -04:00
|
|
|
// Unsubscribe the bring-to-front filter so the delegate doesn't outlive
|
|
|
|
|
// the App; ComponentDispatcher is process-static.
|
|
|
|
|
if (_bringToFrontHandler is not null)
|
|
|
|
|
{
|
|
|
|
|
ComponentDispatcher.ThreadFilterMessage -= _bringToFrontHandler;
|
|
|
|
|
_bringToFrontHandler = null;
|
|
|
|
|
}
|
|
|
|
|
// Release the Mutex iff we acquired it. The "lost the race" path above
|
|
|
|
|
// sets _ownsSingleInstanceMutex=false and we skip ReleaseMutex (which
|
|
|
|
|
// would throw ApplicationException on an unowned Mutex).
|
|
|
|
|
try { if (_ownsSingleInstanceMutex) _singleInstanceMutex?.ReleaseMutex(); }
|
|
|
|
|
catch { /* defensive: already-released or invalid handle */ }
|
2026-05-07 23:59:47 -04:00
|
|
|
_singleInstanceMutex?.Dispose();
|
|
|
|
|
}
|
2026-05-07 11:41:58 -04:00
|
|
|
base.OnExit(e);
|
|
|
|
|
}
|
2026-05-07 11:09:56 -04:00
|
|
|
}
|