Skip to content

Add server-initiated session cancellation to the dotnet test IPC protocol (#8691)#9549

Draft
Evangelink wants to merge 2 commits into
mainfrom
dev/amauryleve/server-session-cancellation-design
Draft

Add server-initiated session cancellation to the dotnet test IPC protocol (#8691)#9549
Evangelink wants to merge 2 commits into
mainfrom
dev/amauryleve/server-session-cancellation-design

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Closes #8691.

Implements the RFC's recommended Option B — a reverse "server control" pipe — so dotnet test-level features (global --maximum-failed-tests, --timeout, graceful Ctrl+C drain) can ask a Microsoft.Testing.Platform test app to stop cooperatively instead of only letting it run to completion or Process.Kill-ing it.

How it works

sequenceDiagram
    participant SDK as dotnet test (SDK)
    participant App as MTP test app
    App->>SDK: Handshake (advertises 1.4.0)
    SDK-->>App: Handshake reply (ServerControlPipeName=<pipe>)
    App->>SDK: WaitForServerControlRequest (parks on control pipe)
    Note over SDK: global budget reached
    SDK-->>App: ServerControlMessage(CancelSession)
    App->>App: IGracefulStopTestExecutionCapability.StopTestExecutionAsync()
    App->>SDK: final results + TestSessionEnd (data pipe)
    App-->>SDK: exit
Loading
  • The SDK advertises a control-pipe name in its handshake reply. Its presence is the capability signal (no separate boolean), gated per-connection — an older SDK never advertises it, so the feature stays off.
  • The test host opens a second NamedPipeClient and parks a long-poll WaitForServerControlRequest; the SDK completes it with a ServerControlMessage. This is a true server push, so it works even when the app is silent/hung — the crux for --timeout.
  • On CancelSession the host stops gracefully via IGracefulStopTestExecutionCapability (the same path --maximum-failed-tests uses), so trx/logs/artifacts for anything that completed are still emitted. It falls back to hard token cancellation only when the running framework has no graceful-stop capability.
  • The connect + long-poll run entirely on a background task, so test start is never blocked on the auxiliary channel. A dropped control pipe is treated as "host went away → cancel"; the new NamedPipeClient.exitProcessOnConnectionLoss flag keeps that from killing the host (the shared data-pipe behavior is unchanged). On exit the parked read is force-aborted (dispose-before-await), so shutdown can't hang — including on .NET Framework where cancelling an in-flight named-pipe read is unreliable.

Protocol changes (mirror in dotnet/sdk in lockstep)

  • ProtocolConstants bumped to add 1.4.0.
  • Handshake property ServerControlPipeName = 12.
  • Serializers WaitForServerControlRequest = 13, ServerControlMessage = 14 (Kind byte, CancelSession = 1). Note the RFC's "id 11 is next free" was stale — 11/12 are already AzureDevOpsLogMessage/DisplayMessage.
  • The ServerControlPipeName doc spells out the SDK-side contract: accept one control connection per connecting process, and keep the pipe open until the data session ends (an early drop is read as cancel).

Notes vs. the RFC

  • The version-negotiation "latent bug" is already effectively fixed in the current tree (the SDK returns a single negotiated version and the host tracks per-connection capability), so it's out of scope here.
  • The reaction is a graceful stop, not CancellationTokenSource.Cancel() as the RFC sketched, precisely so reporting survives.

Tests

  • Contract + round-trip unit tests for the two new serializers, plus id/property/version stability (ProtocolTests, DotnetTestProtocolContractTests).
  • End-to-end acceptance tests (DotnetTestPipeServerCancellationTests): a graceful server-initiated cancel still reports a result + TestSessionEnd and exits cleanly; an SDK that doesn't advertise the pipe is a no-op.
  • Extended the FakeDotnetTestSdk harness to stand up the control pipe and push CancelSession.

All 13 DotnetTestPipe acceptance tests, the protocol unit tests, and the contract tests pass; the strict -pack build is green.

An expert review pass was run and its findings addressed (background connect, force-abort-on-exit, protocol-contract docs) in the second commit.

⚠️ The protocol files are duplicated in dotnet/sdk — a matching PR must land there in coordination.

amauryleve and others added 2 commits July 1, 2026 16:49
…ocol (#8691)

Implements Option B from the RFC: a reverse "server control" pipe. The SDK
advertises a control pipe name in its handshake reply; the test host connects
back and parks a long-poll WaitForServerControlRequest that the SDK completes
with a ServerControlMessage (CancelSession). On cancel the host stops
gracefully via IGracefulStopTestExecutionCapability so trx/artifacts survive,
falling back to hard cancellation when no graceful capability exists. A dropped
control pipe is treated as "host gone => cancel".

- Protocol 1.4.0; handshake property ServerControlPipeName=12; serializer ids
  WaitForServerControlRequest=13, ServerControlMessage=14.
- NamedPipeClient gains exitProcessOnConnectionLoss so the control channel does
  not kill the host on disconnect.
- Contract + round-trip unit tests and end-to-end acceptance tests (graceful
  cancel still reports; no-advertise is a no-op).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… the parked read on exit

- StartServerControlChannelAsync no longer blocks test start on the control-pipe
  connect; connect + long-poll now run entirely on the background listener task
  (fixes up-to-30s startup stall and de-risks multi-process contention).
- OnExitAsync now cancels then disposes the control pipe client before a bounded
  wait, so the parked long-poll read is force-aborted even on .NET Framework where
  cancelling an in-flight named-pipe read is unreliable (fixes potential exit hang).
- Documented the SDK-side contract for ServerControlPipeName (one control
  connection per connecting process; SDK must keep the pipe open for the whole
  data session, since an early drop is treated as cancel).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 1, 2026 15:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds server-initiated session cancellation support to the dotnet test IPC protocol (MTP “server mode”) by introducing a reverse “server control” named pipe that the SDK can use to push a cooperative CancelSession signal to the test host, allowing graceful shutdown with reporting preserved.

Changes:

  • Extend the dotnet-test pipe protocol with v1.4.0, a new handshake property (ServerControlPipeName), and two new message types (WaitForServerControlRequest, ServerControlMessage).
  • Implement host-side listening/handling for server control messages (including a graceful-stop path via IGracefulStopTestExecutionCapability, with a cancellation fallback).
  • Add protocol/unit/contract coverage plus an end-to-end acceptance test and enhance the fake SDK harness to drive the control channel.
Show a summary per file
File Description
test/UnitTests/Microsoft.Testing.Platform.UnitTests/IPC/ProtocolTests.cs Adds round-trip tests for new serializers, updates stable IDs/handshake property list, and bumps supported protocol versions to include 1.4.0.
test/UnitTests/Microsoft.Testing.Platform.DotnetTestProtocolContract.UnitTests/DotnetTestProtocolContractTests.cs Mirrors the protocol contract stability assertions for new IDs/properties and version list.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DotnetTestPipe/FakeDotnetTestSdkResult.cs Tracks control-pipe observations (connected + cancel sent) for acceptance assertions.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DotnetTestPipe/FakeDotnetTestSdk.cs Adds optional reverse control pipe server and drives CancelSession message in the fake SDK harness.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DotnetTestPipe/DotnetTestPipeServerCancellationTests.cs New end-to-end acceptance tests validating graceful server-initiated cancellation behavior and the no-advertisement case.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DotnetTestPipe/DotnetTestPipeProtocol.cs Extends black-box protocol contract helper with new serializer IDs, handshake property ID, and server-control message encoding.
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/DotnetTestPipe/DotnetTestPipeBaselineTests.cs Updates baseline advertised protocol versions to include 1.4.0.
src/Platform/Microsoft.Testing.Platform/ServerMode/IPushOnlyProtocol.cs Adds control-channel capability flag and a start method to begin listening for server control signals.
src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/Serializers/WaitForServerControlRequestSerializer.cs New serializer for an empty long-poll request used on the control pipe.
src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/Serializers/ServerControlMessageSerializer.cs New serializer for a server-pushed control message (currently Kind only).
src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/ObjectFieldIds.cs Defines stable serializer IDs/field IDs for the new control messages.
src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/Models/WaitForServerControlRequest.cs New model representing the parked long-poll request.
src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/Models/ServerControlMessage.cs New model representing the server’s control reply (e.g., CancelSession).
src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/Constants.cs Adds handshake property constant for ServerControlPipeName, defines ServerControlKinds, and bumps supported versions to include 1.4.0.
src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/DotnetTestConnection.cs Implements control-pipe negotiation, background connect/listen loop, and one-time cancel dispatch + teardown behavior.
src/Platform/Microsoft.Testing.Platform/IPC/Serializers/RegisterSerializers.cs Registers the two new serializers on named-pipe endpoints.
src/Platform/Microsoft.Testing.Platform/IPC/NamedPipeClient.cs Adds exitProcessOnConnectionLoss flag to allow auxiliary channels to surface disconnects without terminating the process.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostOchestratorHost.cs Wires control-channel cancel to application token cancellation for orchestrator host scenario.
src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs Starts server-control listener after a successful handshake and maps cancel to graceful-stop capability (or token cancellation fallback).

Review details

  • Files reviewed: 19/19 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment on lines +56 to +62
string? controlOsPipeName = null;
NamedPipeServerStream? controlStream = null;
if (advertiseServerControlPipe)
{
controlOsPipeName = DotnetTestPipeProtocol.GetPipeName(Guid.NewGuid().ToString("N"));
controlStream = new(controlOsPipeName, PipeDirection.InOut, maxNumberOfServerInstances: 1, PipeTransmissionMode.Byte, options);
}
if (advertiseServerControlPipe)
{
controlOsPipeName = DotnetTestPipeProtocol.GetPipeName(Guid.NewGuid().ToString("N"));
controlStream = new(controlOsPipeName, PipeDirection.InOut, maxNumberOfServerInstances: 1, PipeTransmissionMode.Byte, options);
bool.TryParse(isIDEValue, out bool isIDE) &&
isIDE;

if (response.Properties?.TryGetValue(HandshakeMessagePropertyNames.ServerControlPipeName, out string? serverControlPipeName) == true &&
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[RFC] Server-initiated session cancellation in the dotnet test IPC protocol

2 participants