Skip to content

Add in-process launch support to the MTP server-mode client #10890

Description

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:

  1. Listener creation and generation of the server-mode arguments passed to the callback.
  2. Connection timeout and the race among connection, callback failure/completion, and caller cancellation.
  3. Formatter creation, serializer registration, TcpMessageHandler, and MtpJsonRpcConnection construction.
  4. Protocol initialization, request cancellation through $/cancelRequest, and typed events.
  5. Shutdown ownership: send exit, close the transport, await the callback within a bound, and preserve the primary failure when shutdown also fails.
  6. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions