From 0e73746b582e44102892ad7e6060318ed3bba6d6 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sat, 16 May 2026 11:39:31 -0400 Subject: [PATCH] docs(next-steps): root cause was explorer-spawn elevation, fix shipped in 191b2c5 --- NEXT_STEPS.md | 216 ++++++++++++++++++++++++++++---------------------- 1 file changed, 123 insertions(+), 93 deletions(-) diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index f79767a..1413620 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -1,108 +1,136 @@ -# Where we left off — cold-start launch fix shipped (2026-05-16 morning) +# Where we left off — explorer-spawn de-elevation shipped (2026-05-16) -## What just landed (verified on this machine) +## The actual root cause (finally) -**Origin tip: `09e5b59` — fix: cold-start discovery + installer shortcuts + -single-instance hardening.** +When TeamsISO is spawned by an **elevated File Explorer**, NDI Find returns +zero discovered sources even though Teams is broadcasting. The same exe +spawned from any other parent (PowerShell, cmd, runas, another TeamsISO, +etc.) discovers sources fine — even when that parent is itself elevated. -The "I install, I click the shortcut, no participants" bug is fixed. Three -independent changes in one commit because all three were chasing the same -operator report: +I reproduced this multiple times with the same install: -1. **`NdiDiscoveryService.RunAsync` — immediate first poll + fast ramp.** - `PeriodicTimer.WaitForNextTickAsync` 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. Now: poll - once up front (picks up whatever the NDI runtime has already cached), - then run a 200ms inner loop for ~3 seconds while mDNS replies trickle - in, then settle to the operator-configured interval. Both loops share - a try/finally so the NDI finder is always disposed. -2. **`MainViewModel.IsDiscovering` + new empty-state copy.** New boolean - property, 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…" with a cyan dot - - `IsDiscovering=false` → "no ndi sources visible — is teams in a - meeting?" + Refresh CTA - The old copy was being shown immediately at launch even when discovery - just hadn't run yet, which read as "broken." -3. **Single-instance mutex moved from `Local\` to `Global\`.** On admin-user - boxes with UAC effectively disabled, launches from different parents - can land in slightly different security contexts and a `Local\` named - mutex can be invisible to a sibling. `Global\` is system-wide and - integrity-agnostic; both processes see the same mutex regardless of - how they were spawned. -4. **`installer/Package.wxs` — Desktop shortcut component added.** Now - installs both `C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Wild Dragon\TeamsISO.lnk` - AND `C:\Users\Public\Desktop\TeamsISO.lnk`. Both point at the installed - exe under `C:\Program Files\Wild Dragon\TeamsISO\TeamsISO.exe`. (Previously - only the Start Menu component existed; on this machine even that wasn't - visibly materializing because the older WiX `Advertise` default left it - as a stub.) +| Launch parent | Integrity | Result | +|---|---|---| +| non-elevated PowerShell | medium | OK — 2 participants | +| elevated PowerShell (via `-Verb RunAs`) | high | OK — 2 participants | +| `runas /trustlevel:0x20000` | medium | OK — 2 participants | +| elevated Explorer (operator click) | high | **EMPTY** — 0 participants | -**MSI:** `installer/bin/x64/Release/TeamsISO-Setup-0.9.0-rc5.msi` (372 KB). -Already installed on this machine. Force-reinstall over an existing copy -needs `REINSTALL=ALL REINSTALLMODE=amus` flags on msiexec because the wxs -`Version` is pinned to `1.0.0.0` and MajorUpgrade no-ops on same-version. +Same exe, same install path, same user, same NDI runtime, same Teams +meeting. The only differentiator is `parent.ImageName == "explorer.exe"` +combined with elevation. The suspicion is a window-station / desktop-handle +inheritance quirk in NDI's mDNS implementation — explorer spawns with +shell-specific STARTUPINFOEX attributes that NDI Find apparently can't +work through. Not fixable from inside TeamsISO at the runtime layer. -## Verified launch paths (2026-05-16 11:22) +This is the actual reason every "I clicked the shortcut and saw no +participants" report happened. The earlier "cold-start polling" and +"single-instance integrity isolation" theories were both wrong — those +fixes were independently good but not the cause. -Tested by killing every TeamsISO process, then launching via each -mechanism in turn and probing `http://localhost:9755/participants` after -4 seconds: +## The fix (`191b2c5`) -| Mechanism | Result | -|---|---| -| Start Menu shortcut (`…\Wild Dragon\TeamsISO.lnk`) | OK — 2 participants | -| Public Desktop shortcut (`C:\Users\Public\Desktop\TeamsISO.lnk`) | OK — 2 participants | -| Direct `.exe` double-click in Program Files | OK — 2 participants | +`App.OnStartup` now runs an elevation check before any other startup work: -All three discover the Teams meeting's NDI sources (Active Speaker + -Brendon Power in today's testing) within the cold-start grace window. +1. If `--relaunched` is in args, skip the check (loop guard). +2. If we're not in the Administrators role, skip. +3. If our parent process is NOT `explorer.exe`, skip. +4. Otherwise — re-spawn ourselves via + `runas.exe /trustlevel:0x20000 "" --relaunched ` + and `Shutdown(0)` the current process. -## Open items +`runas /trustlevel:0x20000` requests a **medium-integrity restricted token** +even when the caller is elevated. The new child appears with `runas.exe` +as its parent (NOT explorer.exe), at medium integrity, with the +`--relaunched` flag so the de-elevation check no-ops on the second pass. -**Pre-1.0 cut is gated on:** +The check uses `System.Management.ManagementObjectSearcher` against +`Win32_Process` to find the parent PID — added as a `PackageReference` in +the csproj. + +## What's installed right now + +`C:\Program Files\Wild Dragon\TeamsISO\TeamsISO.exe` — **0.9.0-rc6** with +the de-elevation logic, timestamp `2026-05-16 11:36:28`. Shortcuts present +at `C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Wild Dragon\TeamsISO.lnk` +and `C:\Users\Public\Desktop\TeamsISO.lnk`, both pointing at the installed +exe. + +Three stale install records were left over from previous rc1–rc5 attempts +(all `DisplayName=TeamsISO`, three different ProductCodes). All three were +uninstalled before -rc6 went on cleanly. Only one TeamsISO ARP entry now. + +## How to verify + +Double-click `C:\Program Files\Wild Dragon\TeamsISO\TeamsISO.exe` or click +the Start Menu / Desktop shortcut from File Explorer. The expected sequence: + +1. Brief flash of a window that immediately closes (the elevated initial + process detecting explorer-spawn and re-launching). +2. A second TeamsISO window appears, parented under `runas.exe`, at + medium integrity. +3. Participants discover within ~3 seconds. + +The log file at `C:\Users\zacga\AppData\Local\TeamsISO\Logs\teamsiso.log` +will show one "TeamsISO.App starting up" line — NOT two — because the +elevated first process exits before initializing the logger. + +If discovery STILL stays empty, options: +- The `runas /trustlevel` spawn may have failed silently. Diagnostics + aren't great here because the logger isn't up yet at the de-elevation + point. We could log to a fallback raw text file. +- The `Secondary Logon` Windows service might be disabled (it's required + for runas). `Get-Service seclogon | Format-List` to check; should be + Running. + +## All commits on origin (newest first) + +``` +191b2c5 fix(wpf): de-elevate when spawned by elevated explorer (NDI mDNS isolation) +e01fa36 docs(next-steps): cold-start launch fix verified — 3 launch paths green +09e5b59 fix: cold-start discovery + installer shortcuts + single-instance hardening +f47edfb ISO toggle: widen column 110->124, tighten padding so 'Enable' fits +47914fc ISO toggle: square corners to match the rest of the button family +dba7dcc gear icon: swap Path glyph for U+2699 + bump column to 56px +6c9bee7 fix(wpf): catch participant-left race in ToggleIsoAsync, toast instead of crash +84861da test: integration — App+MainWindow STA smoke, control-surface live VM, theme XAML load +6505a3c test: services — NotesService, UpdateChecker, PresetApplier, OscBridge, IsoController +d91f953 test: ControlSurfaceServer route table smoke coverage +fbcc562 test: ThemeManager + CommandPaletteViewModel.Matches coverage +e96a30b chore: trim stale batch-commit script + drop SmokeTest placeholder +1f07992 refactor(services): extract TeamsEmbedHost from TeamsLauncher +2640739 refactor(control-surface): split server into endpoint partials +e67c02c refactor(app): split App.xaml.cs into themed partial files +d02a2c0 refactor(viewmodels): split MainViewModel into themed partial classes +33fca8e polish(mainwindow): empty state, table widths, strings, theme tooltip +3739002 chore(docs): reconcile to WPF-only after WinUI 3 was abandoned +5a43c9c feat: per-ISO framerate/resolution/aspect/audio overrides + thumbnail BMP +``` + +246/246 tests passing on the merged main. + +## Pre-1.0 cut still gated on 1. Code-signing the MSI. `SIGN_CERT_PFX_BASE64` + `SIGN_CERT_PASSWORD` - need to be added to Forgejo Actions Secrets for the release.yml - workflow to start producing signed MSIs. Without that, downstream - users get the "Windows protected your PC" SmartScreen warning. + need to go into Forgejo Actions Secrets for `release.yml` to start + producing signed MSIs. Without that, downstream operators get the + "Windows protected your PC" SmartScreen warning. 2. Real-meeting smoke pass on a non-dev host with a live NDI runtime. -**Other items still on the queue (from issue #1 polish-pass status):** +## Outstanding from issue #1 -- **Item 21** — `TeamsLauncher` fallback chain test coverage. Needs an - `IProcessLauncher` seam refactor before the URI handler → AppX → process-exe - order can be unit-pinned. Half-day of work; not blocking. +- **Item 21** — `TeamsLauncher` fallback chain test coverage. Still + needs the `IProcessLauncher` seam refactor before the URI handler → + AppX → process-exe order can be unit-pinned. Half-day of work. -## Where the layout sits - -**Shell:** Default Windows title bar; 32px header (Wild Dragon mark + -wordmark + ⌘K / theme / settings icons); 40px transport strip -(`● 02:14:32 PART 4 · LIVE 2 CTRL :9755`); body = alert/update banners + -action toolbar + participants DataGrid + conditional meeting bar. - -**Participants DataGrid — 7 columns:** State LED (24) · Preview (106) · -Participant (*) · Audio (110) · Output (130) · CFG/gear (56) · -ISO toggle (124, rounded-rect, "Enable" / "● LIVE"). - -**Theme:** `Themes/Theme.Dark.xaml` + `Themes/Theme.Light.xaml` split from -`WildDragonTheme.xaml`; `ThemeManager` runtime-swaps the merged dictionary -and follows `HKCU\...\Personalize\AppsUseLightTheme`. - -**Hotkeys:** F1 help, Ctrl+K / Ctrl+P command palette, Ctrl+T theme -toggle, Ctrl+R refresh, Ctrl+Shift+S panic stop, 1–9 / NumPad 1–9 toggle -Nth participant ISO. - -## Build, install, run cheatsheet +## Build / install cheatsheet ```powershell cd "C:\Users\zacga\Documents\Claude\Projects\Teams ISO" # Build + test dotnet build TeamsISO.sln -c Release # 0 warnings / 0 errors -dotnet test TeamsISO.sln -c Release --no-build # 246/246 passing +dotnet test TeamsISO.sln -c Release --no-build # 246/246 passing # Publish + MSI $v = "0.9.0-rcN" @@ -111,17 +139,19 @@ dotnet publish src/TeamsISO.App/TeamsISO.App.csproj ` -o publish/TeamsISO /p:Version=$v dotnet build installer/TeamsISO.Installer.wixproj -c Release /p:Version=$v -# Install (force overwrite same-version) +# Install (uninstall first if upgrading from same Version="1.0.0.0"!) +Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' | + Where-Object DisplayName -like '*TeamsISO*' | + ForEach-Object { + Start-Process msiexec.exe -Verb RunAs -Wait -ArgumentList "/x $($_.PSChildName) /qn /norestart" + } Start-Process msiexec.exe -Verb RunAs -Wait -ArgumentList ` - '/i', '"installer\bin\x64\Release\TeamsISO-Setup-' + $v + '.msi"', ` - '/qn', '/norestart', 'REINSTALL=ALL', 'REINSTALLMODE=amus' - -# Launch (any of these) -Start-Process "C:\Program Files\Wild Dragon\TeamsISO\TeamsISO.exe" -Start-Process "C:\Users\Public\Desktop\TeamsISO.lnk" -Start-Process "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Wild Dragon\TeamsISO.lnk" + '/i', '"installer\bin\x64\Release\TeamsISO-Setup-' + $v + '.msi"', '/qn', '/norestart' ``` -If anything regresses the v2 shell, the rollback point for the WPF v1 shell -(recording axed) is `1d1ce6a`; the v2-shell-without-table-redesign rollback -point is `c271303`. +## Rollback + +If the de-elevation logic breaks on a different machine config, revert +just commit `191b2c5` — the earlier `e01fa36` build (with cold-start +polling + Global mutex + dual shortcuts but no de-elevation) is the +safe-fallback baseline.