fix(IPC-35): surface a handshake-phase WorkerFault with its category and diagnostic
The worker reports startup failures as structured WorkerFault frames --
MxAccessCreationException carries the COM HRESULT/ProgID context and a
WorkerFaultCategory -- so the gateway can tell "MXAccess is not installed / not
registered / access denied" apart from a pipe-level protocol problem.
ReadHandshakeEnvelopeAsync did not look inside: any body other than the expected
one became a ProtocolViolation naming only the body type, and that text is what
SessionManager puts in the operator log and the client's OpenFailed detail. The
information was lost exactly in the window where it matters most -- first
session open on a freshly deployed host, where the answer is almost always an
MXAccess installation or permission problem.
Special-case BodyCase == WorkerFault and throw WorkerClientException(
WorkerFaulted, CreateWorkerFaultMessage(...)) -- the same formatter the
steady-state fault path already uses, so both windows read alike. Every other
unexpected body still gets ProtocolViolation.
Test: StartAsync_WhenWorkerFaultsDuringHandshake_SurfacesFaultCategoryAndDiagnostic
has the fake worker answer the hello and then write a WorkerFault
{MxaccessCreationFailed} in place of WorkerReady; the existing
FakeWorkerHarness.EmitFaultAsync covered it, so the harness needed no change.
This commit is contained in:
@@ -950,6 +950,21 @@ public sealed class WorkerClient : IWorkerClient
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
WorkerEnvelope envelope = await _reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// IPC-35: a fault frame during the handshake is the worker reporting a structured startup
|
||||
// failure — most often MXAccess COM creation (not installed, not registered, access denied)
|
||||
// — not a protocol problem. Flattening it to ProtocolViolation discarded the category and
|
||||
// diagnostic exactly where they matter most: first session open on a freshly deployed host,
|
||||
// where the message reaches the operator log and the client's OpenFailed detail. Use the
|
||||
// same formatter the steady-state fault path uses so both windows read alike.
|
||||
if (envelope.BodyCase == WorkerEnvelope.BodyOneofCase.WorkerFault
|
||||
&& expectedBody != WorkerEnvelope.BodyOneofCase.WorkerFault)
|
||||
{
|
||||
throw new WorkerClientException(
|
||||
WorkerClientErrorCode.WorkerFaulted,
|
||||
CreateWorkerFaultMessage(envelope.WorkerFault));
|
||||
}
|
||||
|
||||
if (envelope.BodyCase != expectedBody)
|
||||
{
|
||||
throw new WorkerClientException(
|
||||
|
||||
@@ -322,6 +322,45 @@ public sealed class WorkerClientTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <c>WorkerFault</c> arriving in place of a handshake frame is the worker reporting a
|
||||
/// structured startup failure — most often MXAccess COM creation — not a protocol problem, so
|
||||
/// the gateway surfaces <see cref="WorkerClientErrorCode.WorkerFaulted"/> with the fault's
|
||||
/// category and diagnostic rather than a generic protocol violation naming only the body type
|
||||
/// (IPC-35). This is the window where the information matters most: first session open on a
|
||||
/// freshly deployed host, where the message reaches the operator log and the client's
|
||||
/// <c>OpenFailed</c> detail.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StartAsync_WhenWorkerFaultsDuringHandshake_SurfacesFaultCategoryAndDiagnostic()
|
||||
{
|
||||
const string diagnostic = "CoCreateInstance for LMXProxy.LMXProxyServer.1 failed (0x80040154).";
|
||||
|
||||
await using FakeWorkerHarness harness = await FakeWorkerHarness.CreateConnectedPairAsync();
|
||||
await using WorkerClient client = harness.CreateClient();
|
||||
|
||||
Task startTask = client.StartAsync(CancellationToken.None);
|
||||
|
||||
WorkerEnvelope gatewayHello = await harness.ReadGatewayEnvelopeAsync().WaitAsync(TestTimeout);
|
||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.GatewayHello, gatewayHello.BodyCase);
|
||||
|
||||
// The hello succeeds; the fault replaces the WorkerReady, which is where a COM creation
|
||||
// failure actually lands (the worker creates MXAccess after answering the hello).
|
||||
await harness.SendWorkerHelloAsync();
|
||||
await harness.EmitFaultAsync(WorkerFaultCategory.MxaccessCreationFailed, diagnostic);
|
||||
|
||||
WorkerClientException exception = await Assert.ThrowsAsync<WorkerClientException>(
|
||||
async () => await startTask.WaitAsync(TestTimeout));
|
||||
|
||||
Assert.Equal(WorkerClientErrorCode.WorkerFaulted, exception.ErrorCode);
|
||||
Assert.Contains(
|
||||
WorkerFaultCategory.MxaccessCreationFailed.ToString(),
|
||||
exception.Message,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(diagnostic, exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The envelope <c>sequence</c> is a monotonic per-sender counter (gateway.md), so the values
|
||||
/// observed on the pipe must be strictly increasing in wire order. Stamping the sequence when
|
||||
|
||||
Reference in New Issue
Block a user