Summary
Add a supported in-process launch path to Microsoft.Testing.Platform.ServerMode.Client.Sources so embedded hosts can use the canonical MTP server-mode client without spawning a child process or manually constructing protocol internals.
Motivation
MtpServerClient.LaunchAsync(path) currently owns the loopback listener and starts the test application with Process.Start. That is the correct default for IDE and desktop tooling, but it cannot be used by embedded hosts such as MAUI applications, Android/iOS test apps, or other environments where the MTP application already runs in the caller's process.
Those hosts currently have to reproduce several implementation details themselves:
- create and manage the loopback listener;
- construct the correct
--server jsonrpc, client host, and client port arguments;
- race the connection against server startup failure, cancellation, and timeout;
- register client serializers in the required order;
- construct
TcpMessageHandler and MtpJsonRpcConnection;
- coordinate
exit, transport disposal, and completion of the in-process server task.
This defeats the purpose of shipping a canonical, wire-compatible client and makes it easy for consumers to reintroduce bugs already solved by the source client: partial frame writes, ignored JSON-RPC errors, missing $/cancelRequest, unbounded waits, and exception masking.
A concrete consumer is the DeviceRunners MSTest visual-runner work tracked by mattleibow/DeviceRunners#157 and #9809.
Proposed API
Provide a high-level callback-based factory, with names subject to normal API review:
using IMtpServerClient client = await MtpServerClient.LaunchInProcessAsync(
async (serverArgs, cancellationToken) =>
{
ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(serverArgs);
builder.AddMSTest(() => testAssemblies);
using ITestApplication app = await builder.BuildAsync();
return await app.RunAsync();
},
options,
cancellationToken);
The client should own:
- Listener creation and generation of the server-mode arguments passed to the callback.
- Connection timeout and the race among connection, callback failure/completion, and caller cancellation.
- Formatter creation, serializer registration,
TcpMessageHandler, and MtpJsonRpcConnection construction.
- Protocol initialization, request cancellation through
$/cancelRequest, and typed events.
- Shutdown ownership: send
exit, close the transport, await the callback within a bound, and preserve the primary failure when shutdown also fails.
- Existing one-shot/stateful behavior and capability negotiation.
A smaller building block such as ConnectAsync(TcpClient/Stream, options) may also be useful, but it should not be the only API because it leaves the difficult listener/server-task lifecycle to every embedded host.
Expected value
- Embedded and mobile hosts use the same tested client as IDE/desktop integrations.
- No
Process.Start requirement.
- No consumer dependency on internal formatter, serializer, transport, or JSON-RPC construction order.
- Framework-neutral: works with MSTest, NUnit, xUnit, and any MTP test framework.
- Preserves the MTP extension ecosystem and server-mode protocol evolution.
- Provides a low-risk near-term solution while a transport-free first-class in-process request-session API is evaluated separately.
Scope and limitations
This API still uses loopback TCP and JSON-RPC. It does not solve browser/WASM support and is not intended to replace a future transport-free in-process request API.
Acceptance criteria
- A test application can be launched in the current process through a callback and driven through
InitializeAsync, discovery, selected execution, cancellation, and ExitAsync.
- Callback failure before and after connection is propagated without hanging or leaking the listener/client/server task.
- Caller cancellation during connection and execution is bounded and preserves the primary exception.
- Disposal has explicit ownership semantics and completes within a documented bound.
- Existing external-process
LaunchAsync(path) behavior remains unchanged.
- Unit tests cover lifecycle races and protocol setup; an acceptance test uses a real in-process MTP application and at least one registered test framework.
- Documentation includes an embedded-host sample.
Summary
Add a supported in-process launch path to
Microsoft.Testing.Platform.ServerMode.Client.Sourcesso embedded hosts can use the canonical MTP server-mode client without spawning a child process or manually constructing protocol internals.Motivation
MtpServerClient.LaunchAsync(path)currently owns the loopback listener and starts the test application withProcess.Start. That is the correct default for IDE and desktop tooling, but it cannot be used by embedded hosts such as MAUI applications, Android/iOS test apps, or other environments where the MTP application already runs in the caller's process.Those hosts currently have to reproduce several implementation details themselves:
--server jsonrpc, client host, and client port arguments;TcpMessageHandlerandMtpJsonRpcConnection;exit, transport disposal, and completion of the in-process server task.This defeats the purpose of shipping a canonical, wire-compatible client and makes it easy for consumers to reintroduce bugs already solved by the source client: partial frame writes, ignored JSON-RPC errors, missing
$/cancelRequest, unbounded waits, and exception masking.A concrete consumer is the DeviceRunners MSTest visual-runner work tracked by mattleibow/DeviceRunners#157 and #9809.
Proposed API
Provide a high-level callback-based factory, with names subject to normal API review:
The client should own:
TcpMessageHandler, andMtpJsonRpcConnectionconstruction.$/cancelRequest, and typed events.exit, close the transport, await the callback within a bound, and preserve the primary failure when shutdown also fails.A smaller building block such as
ConnectAsync(TcpClient/Stream, options)may also be useful, but it should not be the only API because it leaves the difficult listener/server-task lifecycle to every embedded host.Expected value
Process.Startrequirement.Scope and limitations
This API still uses loopback TCP and JSON-RPC. It does not solve browser/WASM support and is not intended to replace a future transport-free in-process request API.
Acceptance criteria
InitializeAsync, discovery, selected execution, cancellation, andExitAsync.LaunchAsync(path)behavior remains unchanged.