docs(next-steps): root cause was explorer-spawn elevation, fix shipped in 191b2c5
Some checks failed
CI / build-and-test (push) Failing after 27s

This commit is contained in:
Zac Gaetano 2026-05-16 11:39:31 -04:00
parent 191b2c5f52
commit 0e73746b58

View file

@ -1,101 +1,129 @@
# 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 "<exe path>" --relaunched <forwarded args>`
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 rc1rc5 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<date>.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, 19 / NumPad 19 toggle
Nth participant ISO.
## Build, install, run cheatsheet
## Build / install cheatsheet
```powershell
cd "C:\Users\zacga\Documents\Claude\Projects\Teams ISO"
@ -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.