Fix OS-blind apphost probe in MTP bridge on Unix (regression from #16201)
Problem
MtpServerConnection.BuildLaunch (added in #16201) probes for a test app's native apphost by unconditionally swapping the source extension to .exe and gating on File.Exists:
string apphost = Path.ChangeExtension(source, ".exe");
return File.Exists(apphost) ? (apphost, ...) : ("dotnet", ...);
The apphost file name is OS-specific: Foo.exe on Windows, but extension-less Foo on Unix. On a normal Linux box the .exe doesn't exist and this harmlessly falls back to dotnet <dll>. But when a test payload is built on Windows and run on Unix (e.g. tests built on a Windows agent, zipped, and executed on a Linux Helix machine), the Windows PE Foo.exe travels alongside the dll — and zip carries no Unix permission bits. File.Exists then finds it and Process.Start fails:
Microsoft.Testing.Platform run failed for '.../Foo.dll':
An error occurred trying to start process '.../Foo.exe' ... Permission denied
(chmod +x would only turn this into Exec format error — it's a Windows binary.)
This surfaced in dotnet/aspnetcore's CI (Windows build agents + Linux Helix queues) on an xunit.v3 (MTP-capable) test assembly, but it affects any repo that builds MTP tests (xunit.v3 / MSTest-on-MTP / TUnit) on Windows and runs them on Linux.
Fix
Make the apphost probe OS-aware, and on Unix require the candidate to actually be executable before launching it (otherwise fall back to dotnet <dll>):
- Windows →
Foo.exe; Unix → extension-lessFoo. - New
IsUsableApphost: file must exist, and on Unix must carry an execute bit (File.GetUnixFileMode), so a stray non-native sibling (e.g. a Windows PE copied onto Unix) is never selected.
The OS check is #if NETFRAMEWORK-guarded (net462 has no RuntimeInformation and is Windows-only); the Unix-mode check is #if NET-guarded (File.GetUnixFileMode is .NET 7+). On netstandard2.0 it degrades to existence-only, which is still correct because the OS-aware path selection already resolves the bug.
Tests
Adds MtpServerConnectionTests covering: .exe source passes through; dll with a sibling Windows-only .exe on Unix is not selected (falls back to dotnet <dll>); dll with no apphost falls back; and IsUsableApphost execute-bit semantics on Unix.
Verification
Reproduced end-to-end with a source-built runner, A/B-swapping only CrossPlatEngine.dll against a real xunit.v3 app plus a dummy sibling .exe, under the same runtime:
- Before:
... start process '.../MtpRepro.exe' ... Permission denied→ Test Run Aborted. - After:
Passed! 1— the Windows.exeis ignored. A genuine executable Unix apphost is still selected and launched, and a classic (non-MTP) xUnit v2 project is unaffected.
Regression from #16201.