diff --git a/src/TeamsISO.App/App.xaml.cs b/src/TeamsISO.App/App.xaml.cs
index 8399309..50b13bd 100644
--- a/src/TeamsISO.App/App.xaml.cs
+++ b/src/TeamsISO.App/App.xaml.cs
@@ -83,6 +83,23 @@ public partial class App : Application
{
base.OnStartup(e);
+ // Re-launch detection: when explorer.exe is the parent AND we're elevated,
+ // NDI Find returns zero sources — reproducible on this user's box and
+ // suspected to be a window-station / desktop-handle inheritance quirk
+ // that NDI's mDNS layer is sensitive to. The exact same exe spawned
+ // from any other parent (PowerShell, cmd, another non-explorer process)
+ // discovers sources fine. Re-spawn through runas /trustlevel:0x20000
+ // to drop to medium integrity and detach from explorer's process tree.
+ //
+ // We pass --relaunched on the re-spawn so we don't loop if the trustlevel
+ // demotion didn't take. CLI args that the operator passed (e.g.
+ // --apply-preset NAME) are forwarded verbatim to the relaunched child.
+ if (ShouldDeElevate(e.Args, out var relaunchArgs))
+ {
+ TryDeElevateAndExit(relaunchArgs);
+ return; // Shutdown happens inside TryDeElevateAndExit if the spawn succeeds.
+ }
+
// Crash diagnostics — wire the three exception channels WPF leaves open by
// default to a single handler that logs Fatal to Serilog (which has the
// rolling-daily file sink at %LOCALAPPDATA%\TeamsISO\Logs) and then shows
@@ -160,6 +177,118 @@ public partial class App : Application
}
}
+ ///
+ /// Returns true when we need to re-spawn ourselves with a non-elevated
+ /// medium-integrity token. This is the case when:
+ ///
+ /// - We haven't already been relaunched (--relaunched guard
+ /// prevents infinite loops if the demotion didn't take).
+ /// - The current process token has the Administrators group
+ /// elevated (UAC "split-token" — admin SID is present and active).
+ /// - Our parent process is explorer.exe — that's the spawn
+ /// path that triggers the NDI mDNS-isolation bug. Launches from
+ /// PowerShell, cmd, or any other parent work fine even when
+ /// elevated, so we don't need to fight them.
+ ///
+ ///
+ private static bool ShouldDeElevate(string[] args, out string[] forwardArgs)
+ {
+ forwardArgs = args;
+ // Already relaunched once — don't loop.
+ if (Array.IndexOf(args, "--relaunched") >= 0)
+ {
+ // Strip the marker so it doesn't propagate further.
+ forwardArgs = args.Where(a => a != "--relaunched").ToArray();
+ return false;
+ }
+ // Not elevated — nothing to demote from.
+ try
+ {
+ using var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
+ var principal = new System.Security.Principal.WindowsPrincipal(identity);
+ if (!principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator))
+ return false;
+ }
+ catch { return false; }
+ // Check parent process; if anything but explorer.exe we leave well alone.
+ try
+ {
+ var parentName = TryGetParentProcessName();
+ if (!string.Equals(parentName, "explorer", StringComparison.OrdinalIgnoreCase))
+ return false;
+ }
+ catch { return false; }
+ return true;
+ }
+
+ ///
+ /// Look up our parent process's image name (without extension). Returns
+ /// null if it can't be determined (PID gone, denied, etc.).
+ ///
+ 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;
+ }
+
+ ///
+ /// Re-launch TeamsISO via runas.exe /trustlevel:0x20000. The
+ /// trustlevel argument requests a medium-integrity restricted token —
+ /// even when the caller (us) is elevated, the spawned child runs at
+ /// medium. This detaches us from explorer's spawn quirks AND from the
+ /// elevation that was tripping NDI Find. We then
+ /// the current process so only the medium-integrity child remains.
+ ///
+ /// If the spawn fails for any reason (runas missing, permission denied,
+ /// etc.) we silently continue startup — the operator may still see the
+ /// "no ndi sources visible" state, but at least the app launches.
+ ///
+ private void TryDeElevateAndExit(string[] forwardArgs)
+ {
+ try
+ {
+ var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
+ if (string.IsNullOrEmpty(exePath)) return; // can't relaunch what we can't find
+
+ var quotedExe = "\"" + exePath + "\"";
+ var forwarded = string.Join(" ", forwardArgs.Select(a => "\"" + a + "\""));
+ var trustArg = string.IsNullOrEmpty(forwarded)
+ ? quotedExe + " --relaunched"
+ : quotedExe + " --relaunched " + forwarded;
+
+ var psi = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = "runas.exe",
+ Arguments = "/trustlevel:0x20000 " + trustArg,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden,
+ };
+ System.Diagnostics.Process.Start(psi);
+ }
+ catch
+ {
+ // Relaunch failed — let normal startup proceed. Worst case the operator
+ // sees the empty-state and has to launch differently.
+ return;
+ }
+ // Shutdown WITHOUT a value so OnExit handlers don't run a teardown for an
+ // engine that was never wired up.
+ Shutdown(0);
+ }
+
///
/// Parse the supported CLI flags. Currently:
/// --apply-preset NAME — apply the named preset once participants
diff --git a/src/TeamsISO.App/TeamsISO.App.csproj b/src/TeamsISO.App/TeamsISO.App.csproj
index c430347..eda516d 100644
--- a/src/TeamsISO.App/TeamsISO.App.csproj
+++ b/src/TeamsISO.App/TeamsISO.App.csproj
@@ -25,6 +25,14 @@
+
+