2026-05-31 11:18:27 -04:00
|
|
|
|
using System.IO;
|
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;
|
2026-05-31 11:18:27 -04:00
|
|
|
|
using DragonISO.App.ViewModels;
|
|
|
|
|
|
using DragonISO.Engine.Controller;
|
|
|
|
|
|
using DragonISO.Engine.Logging;
|
|
|
|
|
|
using DragonISO.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).
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// Don't redeclare here — Roslyn errors with CS1537 on duplicate alias.
|
2026-05-10 09:41:29 -04:00
|
|
|
|
|
2026-05-31 11:18:27 -04:00
|
|
|
|
namespace DragonISO.App;
|
2026-05-07 11:09:56 -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
|
|
|
|
// Split across partial files by responsibility:
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// • 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.
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// • 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).
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// • 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.
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// • 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).
|
2026-05-07 11:09:56 -04:00
|
|
|
|
public partial class App : Application
|
|
|
|
|
|
{
|
2026-05-07 23:59:47 -04:00
|
|
|
|
/// <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
|
2026-05-31 11:18:27 -04:00
|
|
|
|
/// different Windows users can each run Dragon-ISO on the same machine, while one
|
2026-05-07 23:59:47 -04:00
|
|
|
|
/// user can't spawn duplicate instances that would contend over the NDI runtime
|
2026-05-31 11:18:27 -04:00
|
|
|
|
/// 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
|
2026-05-31 11:18:27 -04:00
|
|
|
|
/// 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.
|
2026-05-07 23:59:47 -04:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static readonly string SingleInstanceMutexName =
|
2026-05-31 11:18:27 -04:00
|
|
|
|
$"Global\\WildDragon.DragonISO.SingleInstance.{Environment.UserName}";
|
2026-05-07 23:59:47 -04:00
|
|
|
|
|
|
|
|
|
|
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-31 11:18:27 -04:00
|
|
|
|
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;
|
2026-05-10 09:41:29 -04:00
|
|
|
|
|
|
|
|
|
|
/// <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>
|
2026-05-31 11:18:27 -04:00
|
|
|
|
internal DragonISO.App.Services.ControlSurfaceServer? ControlSurface => _controlSurface;
|
2026-05-10 09:41:29 -04:00
|
|
|
|
|
2026-05-31 11:18:27 -04:00
|
|
|
|
/// <summary>OSC bridge (UDP) lifetime — same lifecycle pattern as the REST surface.</summary>
|
|
|
|
|
|
internal DragonISO.App.Services.OscBridge? OscBridge => _oscBridge;
|
2026-05-10 09:41:29 -04:00
|
|
|
|
|
|
|
|
|
|
/// <summary>Tray-icon host. Exposed so the settings VM can flip the minimize-to-tray toggle.</summary>
|
2026-05-31 11:18:27 -04:00
|
|
|
|
internal DragonISO.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)
|
|
|
|
|
|
{
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// RAW TRACE — captures startup BEFORE Serilog comes up. Helps diagnose
|
2026-05-16 12:16:55 -04:00
|
|
|
|
// launches where the Serilog log stays empty (silent file-sink failure,
|
|
|
|
|
|
// pre-logger crash, weird parent-spawn environment, etc.). Writes to
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// %LOCALAPPDATA%\Dragon-ISO\startup-trace.log.
|
2026-05-16 12:16:55 -04:00
|
|
|
|
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}"); }
|
|
|
|
|
|
|
2026-05-07 11:41:58 -04:00
|
|
|
|
base.OnStartup(e);
|
2026-05-16 12:16:55 -04:00
|
|
|
|
StartupTrace.Write("base.OnStartup returned");
|
2026-05-07 11:41:58 -04:00
|
|
|
|
|
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,
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// 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)");
|
2026-05-16 11:36:52 -04:00
|
|
|
|
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// Crash diagnostics — wire the three exception channels WPF leaves open by
|
2026-05-16 12:16:55 -04:00
|
|
|
|
// default to a single handler that logs Fatal to Serilog.
|
2026-05-10 09:41:29 -04:00
|
|
|
|
AppDomain.CurrentDomain.UnhandledException += OnAppDomainUnhandled;
|
|
|
|
|
|
DispatcherUnhandledException += OnDispatcherUnhandled;
|
|
|
|
|
|
System.Threading.Tasks.TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
2026-05-16 12:16:55 -04:00
|
|
|
|
StartupTrace.Write("crash handlers registered");
|
2026-05-10 09:41:29 -04:00
|
|
|
|
|
2026-05-31 11:18:27 -04:00
|
|
|
|
try { DragonISO.App.Services.ThemeManager.Current.Apply(); StartupTrace.Write("ThemeManager.Apply OK"); }
|
2026-05-16 12:16:55 -04:00
|
|
|
|
catch (Exception ex) { StartupTrace.Write($"ThemeManager.Apply THREW: {ex}"); }
|
2026-05-14 12:46:24 -04:00
|
|
|
|
|
2026-05-16 12:16:55 -04:00
|
|
|
|
// 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)
|
2026-05-07 23:59:47 -04:00
|
|
|
|
{
|
2026-05-31 11:18:27 -04:00
|
|
|
|
StartupTrace.Write("not first instance — Shutdown(0)");
|
2026-05-07 23:59:47 -04:00
|
|
|
|
Shutdown(0);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 11:41:58 -04:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-16 12:16:55 -04:00
|
|
|
|
StartupTrace.Write("Bootstrap try-block ENTER");
|
2026-05-08 00:47:25 -04:00
|
|
|
|
_loggerFactory = EngineLogging.CreateDefault(LogLevel.Information);
|
2026-05-16 12:16:55 -04:00
|
|
|
|
StartupTrace.Write("EngineLogging.CreateDefault OK");
|
2026-05-07 11:41:58 -04:00
|
|
|
|
var logger = _loggerFactory.CreateLogger<App>();
|
2026-05-08 00:47:25 -04:00
|
|
|
|
logger.LogInformation(
|
2026-05-31 11:18:27 -04:00
|
|
|
|
"DragonISO.App starting up. Build: {Version}. Process: {Pid}.",
|
2026-05-08 00:47:25 -04:00
|
|
|
|
typeof(App).Assembly.GetName().Version,
|
|
|
|
|
|
Environment.ProcessId);
|
2026-05-16 12:16:55 -04:00
|
|
|
|
StartupTrace.Write("Serilog first write attempted");
|
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
|
|
|
|
{
|
2026-05-31 11:18:27 -04:00
|
|
|
|
StartupTrace.Write("TryBootstrapNdiInterop returned false — Shutdown(2)");
|
2026-05-07 11:41:58 -04:00
|
|
|
|
Shutdown(2);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-16 12:16:55 -04:00
|
|
|
|
StartupTrace.Write("TryBootstrapNdiInterop OK");
|
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
|
|
|
|
BootstrapEngine();
|
2026-05-16 12:16:55 -04:00
|
|
|
|
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();
|
2026-05-16 12:16:55 -04:00
|
|
|
|
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();
|
2026-05-16 12:16:55 -04:00
|
|
|
|
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);
|
2026-05-16 12:16:55 -04:00
|
|
|
|
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);
|
2026-05-16 12:16:55 -04:00
|
|
|
|
StartupTrace.Write("TryShowOnboarding returned");
|
2026-05-10 09:41:29 -04:00
|
|
|
|
|
|
|
|
|
|
ApplyCommandLineArgs(e.Args);
|
2026-05-16 12:16:55 -04:00
|
|
|
|
StartupTrace.Write("ApplyCommandLineArgs OK");
|
2026-05-10 09:41:29 -04:00
|
|
|
|
|
2026-05-16 12:16:55 -04:00
|
|
|
|
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);
|
2026-05-16 12:16:55 -04:00
|
|
|
|
StartupTrace.Write("_viewModel.InitializeAsync COMPLETED");
|
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-16 12:16:55 -04:00
|
|
|
|
StartupTrace.Write("OnStartup COMPLETE");
|
|
|
|
|
|
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// 5-second post-init participant probe — tells us whether discovery
|
2026-05-16 12:16:55 -04:00
|
|
|
|
// 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}"); }
|
|
|
|
|
|
});
|
2026-05-07 11:41:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-05-16 12:16:55 -04:00
|
|
|
|
StartupTrace.Write($"OnStartup CATCH: {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
|
|
|
|
try { _loggerFactory?.CreateLogger<App>().LogCritical(ex, "OnStartup failed before main loop"); }
|
|
|
|
|
|
catch { /* defensive */ }
|
2026-05-07 11:41:58 -04:00
|
|
|
|
MessageBox.Show(
|
2026-05-31 11:18:27 -04:00
|
|
|
|
"Dragon-ISO failed to start.\n\nDetails: " + ex,
|
|
|
|
|
|
"Dragon-ISO — startup error",
|
2026-05-07 11:41:58 -04:00
|
|
|
|
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
|
2026-05-31 11:18:27 -04:00
|
|
|
|
// 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.
|
2026-05-16 11:36:52 -04:00
|
|
|
|
|
|
|
|
|
|
/// <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).
|
2026-05-16 11:36:52 -04:00
|
|
|
|
|
2026-05-10 09:41:29 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Parse the supported CLI flags. Currently:
|
2026-05-31 11:18:27 -04:00
|
|
|
|
/// <c>--apply-preset NAME</c> — apply the named preset once participants
|
|
|
|
|
|
/// populate. Equivalent to running Dragon-ISO and clicking Presets → select →
|
2026-05-10 09:41:29 -04:00
|
|
|
|
/// Apply, but driven from a desktop shortcut.
|
2026-05-31 11:18:27 -04:00
|
|
|
|
/// Unrecognized flags are silently ignored — operators using shortcut.lnk
|
2026-05-10 09:41:29 -04:00
|
|
|
|
/// 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
|
|
|
|
}
|