dragon-iso/src/Dragon-ISO.App/App.xaml.cs

306 lines
14 KiB
C#
Raw Normal View History

using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using Microsoft.Extensions.Logging;
using DragonISO.App.ViewModels;
using DragonISO.Engine.Controller;
using DragonISO.Engine.Logging;
using DragonISO.Engine.NdiInterop;
// Application + MessageBox aliases live in GlobalUsings.cs (project-wide).
// Don't redeclare here — Roslyn errors with CS1537 on duplicate alias.
namespace DragonISO.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
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
// pipeline that calls into the partials),
// OnExit, CLI arg parser.
// • App.Bootstrap.cs — the linear setup steps OnStartup walks
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, NDI interop, engine,
// main window, control surface, tray icon,
// onboarding, Teams auto-launch).
// • App.CrashHandlers.cs — AppDomain / Dispatcher / Task exception
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
// handlers + crash dialog + LogDirectory.
// • App.UpdateCheckBootstrap.cs — the background update-checker
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
// kickoff (24h-throttled).
public partial class App : Application
{
/// <summary>
fix: cold-start discovery + installer shortcuts + single-instance hardening Three independent fixes bundled because all were chasing the same operator report: 'I just installed, launched from the shortcut, no participants.' 1) NdiDiscoveryService: poll immediately, then ramp from 200ms to the configured interval over the first 3 seconds. PeriodicTimer.WaitForNext- TickAsync waits the full interval before its first tick, so for a 500ms discovery interval the operator stared at 'no ndi sources yet' for half a second on every cold start. Force-poll up front (catches the runtime cache), then run a fast inner loop for ~3s while mDNS replies trickle in. Both loops share a try/finally so the NDI finder is always disposed. 2) MainViewModel.IsDiscovering: new boolean, true for 8s after engine start AS LONG AS no participants have arrived. MainWindow.xaml swaps the empty-state copy on this binding: IsDiscovering=true -> 'scanning for ndi sources...' (cyan dot) IsDiscovering=false -> 'no ndi sources visible -- is teams in a meeting?' + Refresh CTA The old copy ('no ndi sources yet -- open teams and start a meeting') was being shown immediately at launch even when discovery just hadn't run yet, making the app look broken. 3) App.xaml.cs: single-instance mutex moved from Local\ to Global\. On admin-user boxes with UAC disabled, launches from different parents (elevated File Explorer, non-elevated shell, etc.) can land in slightly different security contexts and a Local\ name can be invisible to the sibling. Global\ namespace closes that hole — both processes see the same mutex regardless of integrity. Belt-and-braces against future dual-instance file/port contention. 4) installer/Package.wxs: add a Desktop shortcut component (per-machine feature, HKCU keypath per ICE38/ICE43). Operators who can't find the Start Menu entry get the Desktop icon. Both shortcuts target the installed exe, NOT a stale path under publish/.
2026-05-16 11:23:19 -04:00
/// Per-user mutex name. Including the username (acting as a SID proxy) ensures two
/// different Windows users can each run Dragon-ISO on the same machine, while one
/// user can't spawn duplicate instances that would contend over the NDI runtime
/// and the shared %APPDATA%\Dragon-ISO\config.json.
fix: cold-start discovery + installer shortcuts + single-instance hardening Three independent fixes bundled because all were chasing the same operator report: 'I just installed, launched from the shortcut, no participants.' 1) NdiDiscoveryService: poll immediately, then ramp from 200ms to the configured interval over the first 3 seconds. PeriodicTimer.WaitForNext- TickAsync waits the full interval before its first tick, so for a 500ms discovery interval the operator stared at 'no ndi sources yet' for half a second on every cold start. Force-poll up front (catches the runtime cache), then run a fast inner loop for ~3s while mDNS replies trickle in. Both loops share a try/finally so the NDI finder is always disposed. 2) MainViewModel.IsDiscovering: new boolean, true for 8s after engine start AS LONG AS no participants have arrived. MainWindow.xaml swaps the empty-state copy on this binding: IsDiscovering=true -> 'scanning for ndi sources...' (cyan dot) IsDiscovering=false -> 'no ndi sources visible -- is teams in a meeting?' + Refresh CTA The old copy ('no ndi sources yet -- open teams and start a meeting') was being shown immediately at launch even when discovery just hadn't run yet, making the app look broken. 3) App.xaml.cs: single-instance mutex moved from Local\ to Global\. On admin-user boxes with UAC disabled, launches from different parents (elevated File Explorer, non-elevated shell, etc.) can land in slightly different security contexts and a Local\ name can be invisible to the sibling. Global\ namespace closes that hole — both processes see the same mutex regardless of integrity. Belt-and-braces against future dual-instance file/port contention. 4) installer/Package.wxs: add a Desktop shortcut component (per-machine feature, HKCU keypath per ICE38/ICE43). Operators who can't find the Start Menu entry get the Desktop icon. Both shortcuts target the installed exe, NOT a stale path under publish/.
2026-05-16 11:23:19 -04:00
///
/// The "Global\" prefix puts the named object in the system-wide namespace
/// (not session-local or integrity-isolated). This matters because when an
/// admin user has UAC effectively disabled, launches from different parents
/// (elevated File Explorer, non-elevated shell, etc.) can land in slightly
/// different security contexts. A "Local\" mutex was being created in
/// different views per integrity level on some boxes, letting two Dragon-ISO
/// instances run concurrently — the second's REST surface couldn't bind port
fix: cold-start discovery + installer shortcuts + single-instance hardening Three independent fixes bundled because all were chasing the same operator report: 'I just installed, launched from the shortcut, no participants.' 1) NdiDiscoveryService: poll immediately, then ramp from 200ms to the configured interval over the first 3 seconds. PeriodicTimer.WaitForNext- TickAsync waits the full interval before its first tick, so for a 500ms discovery interval the operator stared at 'no ndi sources yet' for half a second on every cold start. Force-poll up front (catches the runtime cache), then run a fast inner loop for ~3s while mDNS replies trickle in. Both loops share a try/finally so the NDI finder is always disposed. 2) MainViewModel.IsDiscovering: new boolean, true for 8s after engine start AS LONG AS no participants have arrived. MainWindow.xaml swaps the empty-state copy on this binding: IsDiscovering=true -> 'scanning for ndi sources...' (cyan dot) IsDiscovering=false -> 'no ndi sources visible -- is teams in a meeting?' + Refresh CTA The old copy ('no ndi sources yet -- open teams and start a meeting') was being shown immediately at launch even when discovery just hadn't run yet, making the app look broken. 3) App.xaml.cs: single-instance mutex moved from Local\ to Global\. On admin-user boxes with UAC disabled, launches from different parents (elevated File Explorer, non-elevated shell, etc.) can land in slightly different security contexts and a Local\ name can be invisible to the sibling. Global\ namespace closes that hole — both processes see the same mutex regardless of integrity. Belt-and-braces against future dual-instance file/port contention. 4) installer/Package.wxs: add a Desktop shortcut component (per-machine feature, HKCU keypath per ICE38/ICE43). Operators who can't find the Start Menu entry get the Desktop icon. Both shortcuts target the installed exe, NOT a stale path under publish/.
2026-05-16 11:23:19 -04:00
/// 9755 (already held) and its Serilog file sink couldn't open the daily log
/// (already held with shared=false), producing a window that looked like
/// the app but had no engine attached. Global\ closes that gap.
/// </summary>
private static readonly string SingleInstanceMutexName =
$"Global\\WildDragon.DragonISO.SingleInstance.{Environment.UserName}";
private System.Threading.Mutex? _singleInstanceMutex;
fix: address review findings on tonight's commits Code review on d14a33a..bab29b0 turned up three real issues, fixed here. 1. EngineLogging.CreateDefault no longer mutates Serilog.Log.Logger. The static set was a belt-and-suspenders attempt to catch any code path that reaches for the singleton, but it doesn't matter (engine code uses ILogger<T>, never Serilog.Log.*) and it raced under xUnit's parallel test execution. 2. IsoPipeline stops holding a RawFrame reference for stats. The receiver-side TappedChannelWriter callback now snapshots only Width/Height into volatile ints — frame's pixel buffer is allowed to GC on its normal schedule and a late stats poll can never resurrect a dropped frame. (Today the buffer is fully managed so a use-after-free wasn't actually possible, but the snapshot pattern is the right ownership shape.) 3. App.xaml.cs's ComponentDispatcher.ThreadFilterMessage subscription now lives in a field and is unsubscribed in OnExit. Mutex release is gated on a new _ownsSingleInstanceMutex flag so the 'lost the race; shut down silently' path doesn't accidentally try to release a handle it never owned. Plus a load-bearing comment in NdiInteropPInvoke.CreateFinder explaining why we free the UTF-8 group buffers right after the native call returns — same lifetime contract Phase B-2's CreateReceiver / CreateSender have always relied on; if it's wrong, those would fail too. The loopback discovery integration test would catch a regression. Tests: 74/74 unit + 9/9 NDI integration green.
2026-05-08 01:01:00 -04:00
private bool _ownsSingleInstanceMutex;
private ThreadMessageEventHandler? _bringToFrontHandler;
private ILoggerFactory? _loggerFactory;
private NdiInteropPInvoke? _interop;
private IsoController? _controller;
private MainViewModel? _viewModel;
private DragonISO.App.Services.ControlSurfaceServer? _controlSurface;
private DragonISO.App.Services.OscBridge? _oscBridge;
// _diskSpaceWatcher removed — only existed to auto-disable recording at low free space.
private DragonISO.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 DragonISO.App.Services.ControlSurfaceServer? ControlSurface => _controlSurface;
/// <summary>OSC bridge (UDP) lifetime — same lifecycle pattern as the REST surface.</summary>
internal DragonISO.App.Services.OscBridge? OscBridge => _oscBridge;
/// <summary>Tray-icon host. Exposed so the settings VM can flip the minimize-to-tray toggle.</summary>
internal DragonISO.App.Services.TrayIconHost? TrayIcon => _trayIcon;
[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)
{
// RAW TRACE — captures startup BEFORE Serilog comes up. Helps diagnose
// launches where the Serilog log stays empty (silent file-sink failure,
// pre-logger crash, weird parent-spawn environment, etc.). Writes to
// %LOCALAPPDATA%\Dragon-ISO\startup-trace.log.
var parentName = "(unknown)";
try { parentName = TryGetParentProcessName() ?? "(null)"; } catch { }
StartupTrace.Write($"OnStartup ENTER. exe={Environment.ProcessPath} parent={parentName} args=[{string.Join(' ', e.Args)}]");
try
{
using var id = System.Security.Principal.WindowsIdentity.GetCurrent();
var pr = new System.Security.Principal.WindowsPrincipal(id);
StartupTrace.Write($"identity user={id.Name} isAdmin={pr.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator)} integrity-token={id.User}");
}
catch (Exception ex) { StartupTrace.Write($"identity probe FAILED: {ex}"); }
base.OnStartup(e);
StartupTrace.Write("base.OnStartup returned");
fix(installer+wpf): REVERT runas /trustlevel demotion (it was the bug, not the fix) Massive misdiagnosis correction. The 2025-05-16 effort to 'fix elevation' has been actively breaking every Start Menu / Desktop shortcut launch since rc7. Empirical retrace: - Elevated PowerShell -> Process.Start(exe) -> elevated TeamsISO -> WORKS - Elevated PowerShell with --keep-elevation -> elevated TeamsISO -> WORKS (vm.Participants.Count=2) - Non-elevated PS Process.Start(exe) -> medium TeamsISO -> WORKS - ANY launch through runas /trustlevel:0x20000 -> SAFER-restricted TeamsISO -> BROKEN (window appears, zero managed code runs past BAML parse, no logs, no port binds) The SAFER-restricted token that runas /trustlevel produces breaks .NET 8 WPF apphost in a way that leaves the process apparently alive (with the MainWindow.xaml rendering the empty state from default property values) but executing zero managed code. So my StartupTrace, Serilog file sink, and ControlSurface bind all silently failed for every shortcut launch. Looked exactly like 'cold-start NDI Find stuck at zero' from the outside but had nothing to do with NDI. Revert: - installer/Package.wxs: shortcuts target the .exe directly, no runas wrapper - App.xaml.cs: removed ShouldDeElevate, TryDeElevateAndExit, RelaunchEnvVar, --keep-elevation/--relaunched handling. The check is gone, not just disabled, so future-me can't bring it back without re-discovering the same bug. Kept: - StartupTrace (still useful for any future startup mystery) - Self-healing NDI Find rebuild (c30a616) - still valuable for legitimate stuck-finder cases - System.Management PackageReference - TryGetParentProcessName still used in StartupTrace Verified post-revert: Start Menu shortcut click -> PID 43060 -> full trace -> REST 2 participants. 252/252 tests still passing.
2026-05-16 16:27:23 -04:00
// De-elevation via runas /trustlevel:0x20000 was tried (commits 191b2c5,
// 54ee578) on the theory that elevated Dragon-ISO can't discover NDI
// sources. THAT THEORY WAS WRONG — verified 2026-05-16 that elevated
// Dragon-ISO discovers NDI sources fine. The SAFER-restricted token
fix(installer+wpf): REVERT runas /trustlevel demotion (it was the bug, not the fix) Massive misdiagnosis correction. The 2025-05-16 effort to 'fix elevation' has been actively breaking every Start Menu / Desktop shortcut launch since rc7. Empirical retrace: - Elevated PowerShell -> Process.Start(exe) -> elevated TeamsISO -> WORKS - Elevated PowerShell with --keep-elevation -> elevated TeamsISO -> WORKS (vm.Participants.Count=2) - Non-elevated PS Process.Start(exe) -> medium TeamsISO -> WORKS - ANY launch through runas /trustlevel:0x20000 -> SAFER-restricted TeamsISO -> BROKEN (window appears, zero managed code runs past BAML parse, no logs, no port binds) The SAFER-restricted token that runas /trustlevel produces breaks .NET 8 WPF apphost in a way that leaves the process apparently alive (with the MainWindow.xaml rendering the empty state from default property values) but executing zero managed code. So my StartupTrace, Serilog file sink, and ControlSurface bind all silently failed for every shortcut launch. Looked exactly like 'cold-start NDI Find stuck at zero' from the outside but had nothing to do with NDI. Revert: - installer/Package.wxs: shortcuts target the .exe directly, no runas wrapper - App.xaml.cs: removed ShouldDeElevate, TryDeElevateAndExit, RelaunchEnvVar, --keep-elevation/--relaunched handling. The check is gone, not just disabled, so future-me can't bring it back without re-discovering the same bug. Kept: - StartupTrace (still useful for any future startup mystery) - Self-healing NDI Find rebuild (c30a616) - still valuable for legitimate stuck-finder cases - System.Management PackageReference - TryGetParentProcessName still used in StartupTrace Verified post-revert: Start Menu shortcut click -> PID 43060 -> full trace -> REST 2 participants. 252/252 tests still passing.
2026-05-16 16:27:23 -04:00
// produced by runas /trustlevel was the ACTUAL cause of every "no
// participants" report: it breaks .NET 8 WPF startup such that the
// process appears alive with a window but the managed code never gets
// past BAML parsing. No logs, no port binds. We now skip the check
// entirely. The --keep-elevation arg, originally an opt-out, is now
// accepted but no-op'd (kept to avoid breaking any operator scripts).
if (Array.IndexOf(e.Args, "--keep-elevation") >= 0)
StartupTrace.Write("--keep-elevation flag present (no-op now; de-elevation removed)");
// Crash diagnostics — wire the three exception channels WPF leaves open by
// default to a single handler that logs Fatal to Serilog.
AppDomain.CurrentDomain.UnhandledException += OnAppDomainUnhandled;
DispatcherUnhandledException += OnDispatcherUnhandled;
System.Threading.Tasks.TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
StartupTrace.Write("crash handlers registered");
try { DragonISO.App.Services.ThemeManager.Current.Apply(); StartupTrace.Write("ThemeManager.Apply OK"); }
catch (Exception ex) { StartupTrace.Write($"ThemeManager.Apply THREW: {ex}"); }
// Single-instance gate. Trace the mutex acquisition.
bool acquired = false;
try { acquired = TryAcquireSingleInstance(); } catch (Exception ex) { StartupTrace.Write($"TryAcquireSingleInstance THREW: {ex}"); }
StartupTrace.Write($"TryAcquireSingleInstance returned: {acquired}");
if (!acquired)
{
StartupTrace.Write("not first instance — Shutdown(0)");
Shutdown(0);
return;
}
try
{
StartupTrace.Write("Bootstrap try-block ENTER");
_loggerFactory = EngineLogging.CreateDefault(LogLevel.Information);
StartupTrace.Write("EngineLogging.CreateDefault OK");
var logger = _loggerFactory.CreateLogger<App>();
logger.LogInformation(
"DragonISO.App starting up. Build: {Version}. Process: {Pid}.",
typeof(App).Assembly.GetName().Version,
Environment.ProcessId);
StartupTrace.Write("Serilog first write attempted");
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())
{
StartupTrace.Write("TryBootstrapNdiInterop returned false — Shutdown(2)");
Shutdown(2);
return;
}
StartupTrace.Write("TryBootstrapNdiInterop OK");
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();
StartupTrace.Write("BootstrapEngine OK");
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
var window = ConstructAndShowMainWindow();
StartupTrace.Write("ConstructAndShowMainWindow OK (window shown)");
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
BootstrapControlSurfaceServices();
StartupTrace.Write("BootstrapControlSurfaceServices OK");
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
BootstrapTrayIcon(window);
StartupTrace.Write("BootstrapTrayIcon OK");
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
TryShowOnboarding(window);
StartupTrace.Write("TryShowOnboarding returned");
ApplyCommandLineArgs(e.Args);
StartupTrace.Write("ApplyCommandLineArgs OK");
StartupTrace.Write("about to await _viewModel.InitializeAsync");
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);
StartupTrace.Write("_viewModel.InitializeAsync COMPLETED");
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);
StartupTrace.Write("OnStartup COMPLETE");
// 5-second post-init participant probe — tells us whether discovery
// is actually producing rows once the engine is up.
_ = Task.Run(async () =>
{
await Task.Delay(5000);
try
{
var n = await Dispatcher.InvokeAsync(() => _viewModel?.Participants.Count ?? -1);
StartupTrace.Write($"+5s after init: vm.Participants.Count={n}");
}
catch (Exception ex) { StartupTrace.Write($"+5s probe THREW: {ex.Message}"); }
});
}
catch (Exception ex)
{
StartupTrace.Write($"OnStartup CATCH: {ex}");
try { _loggerFactory?.CreateLogger<App>().LogCritical(ex, "OnStartup failed before main loop"); }
catch { /* defensive */ }
MessageBox.Show(
"Dragon-ISO failed to start.\n\nDetails: " + ex,
"Dragon-ISO — startup error",
MessageBoxButton.OK,
MessageBoxImage.Error);
Shutdown(1);
}
}
fix(installer+wpf): REVERT runas /trustlevel demotion (it was the bug, not the fix) Massive misdiagnosis correction. The 2025-05-16 effort to 'fix elevation' has been actively breaking every Start Menu / Desktop shortcut launch since rc7. Empirical retrace: - Elevated PowerShell -> Process.Start(exe) -> elevated TeamsISO -> WORKS - Elevated PowerShell with --keep-elevation -> elevated TeamsISO -> WORKS (vm.Participants.Count=2) - Non-elevated PS Process.Start(exe) -> medium TeamsISO -> WORKS - ANY launch through runas /trustlevel:0x20000 -> SAFER-restricted TeamsISO -> BROKEN (window appears, zero managed code runs past BAML parse, no logs, no port binds) The SAFER-restricted token that runas /trustlevel produces breaks .NET 8 WPF apphost in a way that leaves the process apparently alive (with the MainWindow.xaml rendering the empty state from default property values) but executing zero managed code. So my StartupTrace, Serilog file sink, and ControlSurface bind all silently failed for every shortcut launch. Looked exactly like 'cold-start NDI Find stuck at zero' from the outside but had nothing to do with NDI. Revert: - installer/Package.wxs: shortcuts target the .exe directly, no runas wrapper - App.xaml.cs: removed ShouldDeElevate, TryDeElevateAndExit, RelaunchEnvVar, --keep-elevation/--relaunched handling. The check is gone, not just disabled, so future-me can't bring it back without re-discovering the same bug. Kept: - StartupTrace (still useful for any future startup mystery) - Self-healing NDI Find rebuild (c30a616) - still valuable for legitimate stuck-finder cases - System.Management PackageReference - TryGetParentProcessName still used in StartupTrace Verified post-revert: Start Menu shortcut click -> PID 43060 -> full trace -> REST 2 participants. 252/252 tests still passing.
2026-05-16 16:27:23 -04:00
// De-elevation helpers (ShouldDeElevate, TryDeElevateAndExit, the
// Dragon-ISO_RELAUNCHED env var) were removed 2026-05-16. The whole
fix(installer+wpf): REVERT runas /trustlevel demotion (it was the bug, not the fix) Massive misdiagnosis correction. The 2025-05-16 effort to 'fix elevation' has been actively breaking every Start Menu / Desktop shortcut launch since rc7. Empirical retrace: - Elevated PowerShell -> Process.Start(exe) -> elevated TeamsISO -> WORKS - Elevated PowerShell with --keep-elevation -> elevated TeamsISO -> WORKS (vm.Participants.Count=2) - Non-elevated PS Process.Start(exe) -> medium TeamsISO -> WORKS - ANY launch through runas /trustlevel:0x20000 -> SAFER-restricted TeamsISO -> BROKEN (window appears, zero managed code runs past BAML parse, no logs, no port binds) The SAFER-restricted token that runas /trustlevel produces breaks .NET 8 WPF apphost in a way that leaves the process apparently alive (with the MainWindow.xaml rendering the empty state from default property values) but executing zero managed code. So my StartupTrace, Serilog file sink, and ControlSurface bind all silently failed for every shortcut launch. Looked exactly like 'cold-start NDI Find stuck at zero' from the outside but had nothing to do with NDI. Revert: - installer/Package.wxs: shortcuts target the .exe directly, no runas wrapper - App.xaml.cs: removed ShouldDeElevate, TryDeElevateAndExit, RelaunchEnvVar, --keep-elevation/--relaunched handling. The check is gone, not just disabled, so future-me can't bring it back without re-discovering the same bug. Kept: - StartupTrace (still useful for any future startup mystery) - Self-healing NDI Find rebuild (c30a616) - still valuable for legitimate stuck-finder cases - System.Management PackageReference - TryGetParentProcessName still used in StartupTrace Verified post-revert: Start Menu shortcut click -> PID 43060 -> full trace -> REST 2 participants. 252/252 tests still passing.
2026-05-16 16:27:23 -04:00
// pattern was treating a symptom that wasn't actually the problem
// (elevation does NOT break NDI Find); the SAFER token produced by
// runas /trustlevel:0x20000 broke .NET 8 WPF startup itself, so the
// "fix" was the actual bug. See git log for the dead code, App.xaml.cs
// commit history around 191b2c5 / 54ee578 / removal.
/// <summary>
/// Look up our parent process's image name (without extension). Returns
/// null if it can't be determined (PID gone, denied, etc.).
/// </summary>
private static string? TryGetParentProcessName()
{
try
{
var pid = Environment.ProcessId;
using var search = new System.Management.ManagementObjectSearcher(
$"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId={pid}");
foreach (var m in search.Get())
{
var ppid = Convert.ToInt32(m["ParentProcessId"]);
using var parent = System.Diagnostics.Process.GetProcessById(ppid);
return parent.ProcessName;
}
}
catch { /* fall through */ }
return null;
}
fix(installer+wpf): REVERT runas /trustlevel demotion (it was the bug, not the fix) Massive misdiagnosis correction. The 2025-05-16 effort to 'fix elevation' has been actively breaking every Start Menu / Desktop shortcut launch since rc7. Empirical retrace: - Elevated PowerShell -> Process.Start(exe) -> elevated TeamsISO -> WORKS - Elevated PowerShell with --keep-elevation -> elevated TeamsISO -> WORKS (vm.Participants.Count=2) - Non-elevated PS Process.Start(exe) -> medium TeamsISO -> WORKS - ANY launch through runas /trustlevel:0x20000 -> SAFER-restricted TeamsISO -> BROKEN (window appears, zero managed code runs past BAML parse, no logs, no port binds) The SAFER-restricted token that runas /trustlevel produces breaks .NET 8 WPF apphost in a way that leaves the process apparently alive (with the MainWindow.xaml rendering the empty state from default property values) but executing zero managed code. So my StartupTrace, Serilog file sink, and ControlSurface bind all silently failed for every shortcut launch. Looked exactly like 'cold-start NDI Find stuck at zero' from the outside but had nothing to do with NDI. Revert: - installer/Package.wxs: shortcuts target the .exe directly, no runas wrapper - App.xaml.cs: removed ShouldDeElevate, TryDeElevateAndExit, RelaunchEnvVar, --keep-elevation/--relaunched handling. The check is gone, not just disabled, so future-me can't bring it back without re-discovering the same bug. Kept: - StartupTrace (still useful for any future startup mystery) - Self-healing NDI Find rebuild (c30a616) - still valuable for legitimate stuck-finder cases - System.Management PackageReference - TryGetParentProcessName still used in StartupTrace Verified post-revert: Start Menu shortcut click -> PID 43060 -> full trace -> REST 2 participants. 252/252 tests still passing.
2026-05-16 16:27:23 -04:00
// TryDeElevateAndExit removed 2026-05-16 (see comment above ShouldDeElevate).
/// <summary>
/// Parse the supported CLI flags. Currently:
/// <c>--apply-preset NAME</c> — apply the named preset once participants
/// populate. Equivalent to running Dragon-ISO 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.
protected override async void OnExit(ExitEventArgs e)
{
try
{
_trayIcon?.Dispose();
if (_controlSurface is not null)
await _controlSurface.DisposeAsync();
if (_oscBridge is not null)
await _oscBridge.DisposeAsync();
_viewModel?.Dispose();
if (_controller is not null)
await _controller.DisposeAsync();
_interop?.Dispose();
_loggerFactory?.Dispose();
}
catch
{
// Best-effort shutdown
}
finally
{
fix: address review findings on tonight's commits Code review on d14a33a..bab29b0 turned up three real issues, fixed here. 1. EngineLogging.CreateDefault no longer mutates Serilog.Log.Logger. The static set was a belt-and-suspenders attempt to catch any code path that reaches for the singleton, but it doesn't matter (engine code uses ILogger<T>, never Serilog.Log.*) and it raced under xUnit's parallel test execution. 2. IsoPipeline stops holding a RawFrame reference for stats. The receiver-side TappedChannelWriter callback now snapshots only Width/Height into volatile ints — frame's pixel buffer is allowed to GC on its normal schedule and a late stats poll can never resurrect a dropped frame. (Today the buffer is fully managed so a use-after-free wasn't actually possible, but the snapshot pattern is the right ownership shape.) 3. App.xaml.cs's ComponentDispatcher.ThreadFilterMessage subscription now lives in a field and is unsubscribed in OnExit. Mutex release is gated on a new _ownsSingleInstanceMutex flag so the 'lost the race; shut down silently' path doesn't accidentally try to release a handle it never owned. Plus a load-bearing comment in NdiInteropPInvoke.CreateFinder explaining why we free the UTF-8 group buffers right after the native call returns — same lifetime contract Phase B-2's CreateReceiver / CreateSender have always relied on; if it's wrong, those would fail too. The loopback discovery integration test would catch a regression. Tests: 74/74 unit + 9/9 NDI integration green.
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 */ }
_singleInstanceMutex?.Dispose();
}
base.OnExit(e);
}
}