Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 276288ad87 | |||
| 76bd3de5a2 | |||
| 29455fc1f6 | |||
| 5511609880 | |||
| 451dccf7e3 |
@@ -289,6 +289,13 @@ The worker should reference the interop assembly and instantiate
|
|||||||
`LMXProxyServerClass` on the dedicated STA thread. Keep the ProgID and assembly
|
`LMXProxyServerClass` on the dedicated STA thread. Keep the ProgID and assembly
|
||||||
path configurable for diagnostics, but this COM class is the v1 default.
|
path configurable for diagnostics, but this COM class is the v1 default.
|
||||||
|
|
||||||
|
`MxAccessStaSession` owns the initial COM creation path. It starts `StaRuntime`,
|
||||||
|
creates `LMXProxyServerClass` through `MxAccessComObjectFactory` on the STA,
|
||||||
|
attaches `MxAccessBaseEventSink`, and returns `WorkerReady` only after those
|
||||||
|
steps succeed. `MxAccessSession` keeps the raw COM object private, records the
|
||||||
|
STA managed thread id that created it, detaches the base event sink during
|
||||||
|
disposal, and releases the COM reference on the STA.
|
||||||
|
|
||||||
Creation rules:
|
Creation rules:
|
||||||
|
|
||||||
- Create COM object only on the STA.
|
- Create COM object only on the STA.
|
||||||
@@ -306,6 +313,11 @@ If COM creation fails, the worker should send a structured fault with:
|
|||||||
- worker process id,
|
- worker process id,
|
||||||
- session id.
|
- session id.
|
||||||
|
|
||||||
|
`WorkerPipeSession` maps startup exceptions from this path to
|
||||||
|
`WorkerFaultCategory.MxaccessCreationFailed`, includes the captured HRESULT
|
||||||
|
when the exception exposes one, and does not send `WorkerReady` after a failed
|
||||||
|
COM creation attempt.
|
||||||
|
|
||||||
## Event Sink
|
## Event Sink
|
||||||
|
|
||||||
The worker must subscribe to every public MXAccess event family:
|
The worker must subscribe to every public MXAccess event family:
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
using MxGateway.Worker.Conversion;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.Tests.Conversion;
|
||||||
|
|
||||||
|
public sealed class HResultConverterTests
|
||||||
|
{
|
||||||
|
private readonly HResultConverter _converter = new();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Convert_WithComException_CapturesExceptionHResult()
|
||||||
|
{
|
||||||
|
COMException exception = new("Sensitive provider text should not be copied.", unchecked((int)0x80070057));
|
||||||
|
|
||||||
|
HResultConversion converted = _converter.Convert(exception);
|
||||||
|
|
||||||
|
Assert.Equal(unchecked((int)0x80070057), converted.HResult);
|
||||||
|
Assert.Equal(ProtocolStatusCode.MxaccessFailure, converted.ProtocolStatus.Code);
|
||||||
|
Assert.Contains("0x80070057", converted.ProtocolStatus.Message);
|
||||||
|
Assert.Contains(typeof(COMException).FullName!, converted.DiagnosticMessage);
|
||||||
|
Assert.DoesNotContain("Sensitive provider text", converted.DiagnosticMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateProtocolStatus_WithSuccessHResult_ReturnsOk()
|
||||||
|
{
|
||||||
|
ProtocolStatus status = _converter.CreateProtocolStatus(0);
|
||||||
|
|
||||||
|
Assert.Equal(ProtocolStatusCode.Ok, status.Code);
|
||||||
|
Assert.Equal("HRESULT 0x00000000", status.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Convert_WithNonComException_CapturesExceptionHResult()
|
||||||
|
{
|
||||||
|
InvalidOperationException exception = new("do not include this");
|
||||||
|
|
||||||
|
HResultConversion converted = _converter.Convert(exception);
|
||||||
|
|
||||||
|
Assert.Equal(exception.HResult, converted.HResult);
|
||||||
|
Assert.Equal(ProtocolStatusCode.MxaccessFailure, converted.ProtocolStatus.Code);
|
||||||
|
Assert.Contains("0x", converted.DiagnosticMessage);
|
||||||
|
Assert.DoesNotContain("do not include this", converted.DiagnosticMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
using MxGateway.Worker.Conversion;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.Tests.Conversion;
|
||||||
|
|
||||||
|
public sealed class MxStatusProxyConverterTests
|
||||||
|
{
|
||||||
|
private readonly MxStatusProxyConverter _converter = new();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Convert_WithStatusStruct_PreservesStatusFields()
|
||||||
|
{
|
||||||
|
FakeMxStatusProxy status = new()
|
||||||
|
{
|
||||||
|
success = 1,
|
||||||
|
category = 5,
|
||||||
|
detectedBy = 3,
|
||||||
|
detail = 21,
|
||||||
|
};
|
||||||
|
|
||||||
|
MxStatusProxy converted = _converter.Convert(status);
|
||||||
|
|
||||||
|
Assert.Equal(1, converted.Success);
|
||||||
|
Assert.Equal(MxStatusCategory.OperationalError, converted.Category);
|
||||||
|
Assert.Equal(MxStatusSource.RespondingNmx, converted.DetectedBy);
|
||||||
|
Assert.Equal(21, converted.Detail);
|
||||||
|
Assert.Equal(5, converted.RawCategory);
|
||||||
|
Assert.Equal(3, converted.RawDetectedBy);
|
||||||
|
Assert.Equal("Invalid reference", converted.DiagnosticText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConvertMany_WithStatusArray_DoesNotCollapseEntries()
|
||||||
|
{
|
||||||
|
FakeMxStatusProxy[] statuses =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
success = 1,
|
||||||
|
category = 0,
|
||||||
|
detectedBy = 0,
|
||||||
|
detail = 0,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
success = 0,
|
||||||
|
category = 6,
|
||||||
|
detectedBy = 5,
|
||||||
|
detail = 33,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
IReadOnlyList<MxStatusProxy> converted = _converter.ConvertMany(statuses);
|
||||||
|
|
||||||
|
Assert.Equal(2, converted.Count);
|
||||||
|
Assert.Equal(MxStatusCategory.Ok, converted[0].Category);
|
||||||
|
Assert.Equal(MxStatusCategory.SecurityError, converted[1].Category);
|
||||||
|
Assert.Equal(MxStatusSource.RespondingAutomationObject, converted[1].DetectedBy);
|
||||||
|
Assert.Equal("Write access denied", converted[1].DiagnosticText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Convert_WithUnknownCategoryAndSource_PreservesRawFields()
|
||||||
|
{
|
||||||
|
FakeMxStatusProxy status = new()
|
||||||
|
{
|
||||||
|
success = -1,
|
||||||
|
category = 99,
|
||||||
|
detectedBy = 42,
|
||||||
|
detail = 1234,
|
||||||
|
};
|
||||||
|
|
||||||
|
MxStatusProxy converted = _converter.Convert(status);
|
||||||
|
|
||||||
|
Assert.Equal(-1, converted.Success);
|
||||||
|
Assert.Equal(MxStatusCategory.Unknown, converted.Category);
|
||||||
|
Assert.Equal(MxStatusSource.Unknown, converted.DetectedBy);
|
||||||
|
Assert.Equal(99, converted.RawCategory);
|
||||||
|
Assert.Equal(42, converted.RawDetectedBy);
|
||||||
|
Assert.Equal(1234, converted.Detail);
|
||||||
|
Assert.Equal(string.Empty, converted.DiagnosticText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreserveCompletionOnlyStatusBytes_ReturnsRawHexMetadata()
|
||||||
|
{
|
||||||
|
string rawStatus = _converter.PreserveCompletionOnlyStatusBytes(
|
||||||
|
[0x00, 0x00, 0x50, 0x80, 0x00]);
|
||||||
|
|
||||||
|
Assert.Equal("completion_only_status_hex=0000508000", rawStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Convert_WithMissingStatusField_ThrowsConversionException()
|
||||||
|
{
|
||||||
|
MxStatusConversionException exception =
|
||||||
|
Assert.Throws<MxStatusConversionException>(() => _converter.Convert(new MissingFields()));
|
||||||
|
|
||||||
|
Assert.Contains("success", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct FakeMxStatusProxy
|
||||||
|
{
|
||||||
|
public short success;
|
||||||
|
|
||||||
|
public int category;
|
||||||
|
|
||||||
|
public int detectedBy;
|
||||||
|
|
||||||
|
public short detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class MissingFields
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MxGateway.Contracts;
|
using MxGateway.Contracts;
|
||||||
@@ -37,6 +38,10 @@ public sealed class WorkerPipeSessionTests
|
|||||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerHello, written[0].BodyCase);
|
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerHello, written[0].BodyCase);
|
||||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerReady, written[1].BodyCase);
|
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerReady, written[1].BodyCase);
|
||||||
Assert.Equal(Nonce, written[0].WorkerHello.Nonce);
|
Assert.Equal(Nonce, written[0].WorkerHello.Nonce);
|
||||||
|
Assert.Equal(1234, written[1].WorkerReady.WorkerProcessId);
|
||||||
|
Assert.Equal(MxGateway.Worker.MxAccess.MxAccessInteropInfo.ProgId, written[1].WorkerReady.MxaccessProgid);
|
||||||
|
Assert.Equal(MxGateway.Worker.MxAccess.MxAccessInteropInfo.Clsid, written[1].WorkerReady.MxaccessClsid);
|
||||||
|
Assert.NotNull(written[1].WorkerReady.ReadyTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -117,6 +122,31 @@ public sealed class WorkerPipeSessionTests
|
|||||||
Assert.Equal(WorkerFaultCategory.ProtocolViolation, fault.WorkerFault.Category);
|
Assert.Equal(WorkerFaultCategory.ProtocolViolation, fault.WorkerFault.Category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CompleteStartupHandshakeAsync_WhenMxAccessCreationFails_WritesFaultInsteadOfReady()
|
||||||
|
{
|
||||||
|
const int hresult = unchecked((int)0x80040154);
|
||||||
|
WorkerFrameProtocolOptions options = CreateOptions();
|
||||||
|
MemoryStream inbound = new();
|
||||||
|
await new WorkerFrameWriter(inbound, options).WriteAsync(CreateGatewayHelloEnvelope());
|
||||||
|
inbound.Position = 0;
|
||||||
|
MemoryStream outbound = new();
|
||||||
|
WorkerPipeSession session = CreateSession(inbound, outbound, options);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<COMException>(
|
||||||
|
async () => await session.CompleteStartupHandshakeAsync(
|
||||||
|
_ => Task.FromException<WorkerReady>(new COMException("Class not registered.", hresult))));
|
||||||
|
|
||||||
|
WorkerEnvelope[] written = ReadWrittenFrames(outbound, options);
|
||||||
|
Assert.Equal(2, written.Length);
|
||||||
|
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerHello, written[0].BodyCase);
|
||||||
|
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerFault, written[1].BodyCase);
|
||||||
|
Assert.Equal(WorkerFaultCategory.MxaccessCreationFailed, written[1].WorkerFault.Category);
|
||||||
|
Assert.Equal(hresult, written[1].WorkerFault.Hresult);
|
||||||
|
Assert.Equal(typeof(COMException).FullName, written[1].WorkerFault.ExceptionType);
|
||||||
|
Assert.Equal(ProtocolStatusCode.WorkerUnavailable, written[1].WorkerFault.ProtocolStatus.Code);
|
||||||
|
}
|
||||||
|
|
||||||
private static WorkerPipeSession CreateSession(
|
private static WorkerPipeSession CreateSession(
|
||||||
Stream inbound,
|
Stream inbound,
|
||||||
Stream outbound,
|
Stream outbound,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MxGateway.Worker.MxAccess;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.Tests.MxAccess;
|
||||||
|
|
||||||
|
public sealed class MxAccessLiveComCreationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task StartAsync_WhenOptedIn_CreatesInstalledMxAccessComObjectOnSta()
|
||||||
|
{
|
||||||
|
if (!string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("MXGATEWAY_RUN_LIVE_MXACCESS_TESTS"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using MxAccessStaSession session = new();
|
||||||
|
|
||||||
|
await session.StartAsync(workerProcessId: 1234);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
using MxGateway.Worker.MxAccess;
|
||||||
|
using MxGateway.Worker.Sta;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.Tests.MxAccess;
|
||||||
|
|
||||||
|
public sealed class MxAccessStaSessionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task StartAsync_CreatesComObjectAndAttachesEventSinkOnStaThread()
|
||||||
|
{
|
||||||
|
FakeMxAccessComObjectFactory factory = new();
|
||||||
|
FakeMxAccessEventSink eventSink = new();
|
||||||
|
using StaRuntime runtime = CreateRuntime();
|
||||||
|
using MxAccessStaSession session = new(runtime, factory, eventSink);
|
||||||
|
|
||||||
|
WorkerReady ready = await session.StartAsync(workerProcessId: 1234);
|
||||||
|
|
||||||
|
Assert.Equal(1234, ready.WorkerProcessId);
|
||||||
|
Assert.Equal(MxAccessInteropInfo.ProgId, ready.MxaccessProgid);
|
||||||
|
Assert.Equal(MxAccessInteropInfo.Clsid, ready.MxaccessClsid);
|
||||||
|
Assert.NotNull(ready.ReadyTimestamp);
|
||||||
|
Assert.Equal(runtime.StaThreadId, factory.CreateThreadId);
|
||||||
|
Assert.Equal(runtime.StaThreadId, eventSink.AttachThreadId);
|
||||||
|
Assert.Equal(ApartmentState.STA, factory.CreateApartmentState);
|
||||||
|
Assert.Same(factory.CreatedObject, eventSink.AttachedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StartAsync_WhenFactoryFails_MapsCreationExceptionWithHResult()
|
||||||
|
{
|
||||||
|
const int hresult = unchecked((int)0x80040154);
|
||||||
|
FakeMxAccessComObjectFactory factory = new(new COMException("Class not registered.", hresult));
|
||||||
|
FakeMxAccessEventSink eventSink = new();
|
||||||
|
using StaRuntime runtime = CreateRuntime();
|
||||||
|
using MxAccessStaSession session = new(runtime, factory, eventSink);
|
||||||
|
|
||||||
|
MxAccessCreationException exception = await Assert.ThrowsAsync<MxAccessCreationException>(
|
||||||
|
() => session.StartAsync(workerProcessId: 1234));
|
||||||
|
|
||||||
|
Assert.Equal(hresult, exception.CapturedHResult);
|
||||||
|
Assert.Equal(MxAccessInteropInfo.ProgId, exception.AttemptedProgId);
|
||||||
|
Assert.Equal(MxAccessInteropInfo.Clsid, exception.AttemptedClsid);
|
||||||
|
Assert.Null(eventSink.AttachedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Dispose_DetachesEventSinkOnStaThread()
|
||||||
|
{
|
||||||
|
FakeMxAccessComObjectFactory factory = new();
|
||||||
|
FakeMxAccessEventSink eventSink = new();
|
||||||
|
using StaRuntime runtime = CreateRuntime();
|
||||||
|
MxAccessStaSession session = new(runtime, factory, eventSink);
|
||||||
|
await session.StartAsync(workerProcessId: 1234);
|
||||||
|
|
||||||
|
session.Dispose();
|
||||||
|
|
||||||
|
Assert.Equal(runtime.StaThreadId, eventSink.DetachThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StaRuntime CreateRuntime()
|
||||||
|
{
|
||||||
|
return new StaRuntime(
|
||||||
|
new NoopComApartmentInitializer(),
|
||||||
|
new StaMessagePump(),
|
||||||
|
TimeSpan.FromMilliseconds(25));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeMxAccessComObjectFactory : IMxAccessComObjectFactory
|
||||||
|
{
|
||||||
|
private readonly Exception? exception;
|
||||||
|
|
||||||
|
public FakeMxAccessComObjectFactory(Exception? exception = null)
|
||||||
|
{
|
||||||
|
this.exception = exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object CreatedObject { get; } = new();
|
||||||
|
|
||||||
|
public int? CreateThreadId { get; private set; }
|
||||||
|
|
||||||
|
public ApartmentState? CreateApartmentState { get; private set; }
|
||||||
|
|
||||||
|
public object Create()
|
||||||
|
{
|
||||||
|
CreateThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
CreateApartmentState = Thread.CurrentThread.GetApartmentState();
|
||||||
|
|
||||||
|
if (exception is not null)
|
||||||
|
{
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreatedObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeMxAccessEventSink : IMxAccessEventSink
|
||||||
|
{
|
||||||
|
public object? AttachedObject { get; private set; }
|
||||||
|
|
||||||
|
public int? AttachThreadId { get; private set; }
|
||||||
|
|
||||||
|
public int? DetachThreadId { get; private set; }
|
||||||
|
|
||||||
|
public void Attach(object mxAccessComObject)
|
||||||
|
{
|
||||||
|
AttachedObject = mxAccessComObject;
|
||||||
|
AttachThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Detach()
|
||||||
|
{
|
||||||
|
DetachThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
AttachedObject = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NoopComApartmentInitializer : IStaComApartmentInitializer
|
||||||
|
{
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Uninitialize()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.Conversion;
|
||||||
|
|
||||||
|
public sealed class HResultConversion
|
||||||
|
{
|
||||||
|
public HResultConversion(
|
||||||
|
int hresult,
|
||||||
|
ProtocolStatus protocolStatus,
|
||||||
|
string diagnosticMessage)
|
||||||
|
{
|
||||||
|
HResult = hresult;
|
||||||
|
ProtocolStatus = protocolStatus;
|
||||||
|
DiagnosticMessage = diagnosticMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int HResult { get; }
|
||||||
|
|
||||||
|
public ProtocolStatus ProtocolStatus { get; }
|
||||||
|
|
||||||
|
public string DiagnosticMessage { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.Conversion;
|
||||||
|
|
||||||
|
public sealed class HResultConverter
|
||||||
|
{
|
||||||
|
public HResultConversion Convert(Exception exception)
|
||||||
|
{
|
||||||
|
if (exception is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
int hresult = exception is COMException comException
|
||||||
|
? comException.ErrorCode
|
||||||
|
: exception.HResult;
|
||||||
|
|
||||||
|
return new HResultConversion(
|
||||||
|
hresult,
|
||||||
|
CreateProtocolStatus(hresult, exception),
|
||||||
|
CreateSafeDiagnosticMessage(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProtocolStatus CreateProtocolStatus(
|
||||||
|
int hresult,
|
||||||
|
Exception? exception = null)
|
||||||
|
{
|
||||||
|
return new ProtocolStatus
|
||||||
|
{
|
||||||
|
Code = hresult == 0 ? ProtocolStatusCode.Ok : ProtocolStatusCode.MxaccessFailure,
|
||||||
|
Message = exception is null
|
||||||
|
? FormatHResult(hresult)
|
||||||
|
: $"{exception.GetType().Name}: {FormatHResult(hresult)}",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateSafeDiagnosticMessage(Exception exception)
|
||||||
|
{
|
||||||
|
return $"{exception.GetType().FullName}: {FormatHResult(exception.HResult)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatHResult(int hresult)
|
||||||
|
{
|
||||||
|
return $"HRESULT 0x{unchecked((uint)hresult):X8}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.Conversion;
|
||||||
|
|
||||||
|
public sealed class MxStatusConversionException : Exception
|
||||||
|
{
|
||||||
|
public MxStatusConversionException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.Conversion;
|
||||||
|
|
||||||
|
internal static class MxStatusDetailText
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyDictionary<int, string> KnownDetails = new Dictionary<int, string>
|
||||||
|
{
|
||||||
|
[16] = "Request timed out",
|
||||||
|
[17] = "Platform communication error",
|
||||||
|
[18] = "Invalid platform ID",
|
||||||
|
[19] = "Invalid engine ID",
|
||||||
|
[20] = "Engine communication error",
|
||||||
|
[21] = "Invalid reference",
|
||||||
|
[22] = "No Galaxy Repository",
|
||||||
|
[23] = "Invalid object ID",
|
||||||
|
[24] = "Object signature mismatch",
|
||||||
|
[25] = "Invalid primitive ID",
|
||||||
|
[26] = "Invalid attribute ID",
|
||||||
|
[27] = "Invalid property ID",
|
||||||
|
[28] = "Index out of range",
|
||||||
|
[29] = "Data out of range",
|
||||||
|
[30] = "Incorrect data type",
|
||||||
|
[31] = "Attribute not readable",
|
||||||
|
[32] = "Attribute not writeable",
|
||||||
|
[33] = "Write access denied",
|
||||||
|
[34] = "Unknown error",
|
||||||
|
[36] = "Wrong data type",
|
||||||
|
[37] = "Wrong number of dimensions",
|
||||||
|
[38] = "Invalid index",
|
||||||
|
[39] = "Index out of order",
|
||||||
|
[40] = "Dimension does not exist",
|
||||||
|
[41] = "Conversion not supported",
|
||||||
|
[42] = "Unable to convert string",
|
||||||
|
[43] = "Overflow",
|
||||||
|
[44] = "Attribute signature mismatch",
|
||||||
|
[47] = "Nmx version mismatch",
|
||||||
|
[48] = "Nmx command not valid",
|
||||||
|
[49] = "Lmx version mismatch",
|
||||||
|
[50] = "Lmx command not valid",
|
||||||
|
[56] = "Secured Write",
|
||||||
|
[57] = "Verified Write",
|
||||||
|
[60] = "User did not have the necessary permissions to write",
|
||||||
|
[61] = "Verifier did not have the necessary permissions to verify",
|
||||||
|
[541] = "Conversion to intended data type is not supported",
|
||||||
|
[542] = "Unable to convert the input string to intended data type",
|
||||||
|
[8017] = "Object must be offscan to modify attributes that have an MxSecurityConfigure security classification",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string Lookup(int detail)
|
||||||
|
{
|
||||||
|
return KnownDetails.TryGetValue(detail, out string text) ? text : string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.Conversion;
|
||||||
|
|
||||||
|
public sealed class MxStatusProxyConverter
|
||||||
|
{
|
||||||
|
public MxStatusProxy Convert(object status)
|
||||||
|
{
|
||||||
|
if (status is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
Type statusType = status.GetType();
|
||||||
|
int success = ReadInt32Field(status, statusType, "success");
|
||||||
|
int rawCategory = ReadInt32Field(status, statusType, "category");
|
||||||
|
int rawDetectedBy = ReadInt32Field(status, statusType, "detectedBy");
|
||||||
|
int detail = ReadInt32Field(status, statusType, "detail");
|
||||||
|
|
||||||
|
return new MxStatusProxy
|
||||||
|
{
|
||||||
|
Success = success,
|
||||||
|
Category = MapCategory(rawCategory),
|
||||||
|
DetectedBy = MapSource(rawDetectedBy),
|
||||||
|
Detail = detail,
|
||||||
|
RawCategory = rawCategory,
|
||||||
|
RawDetectedBy = rawDetectedBy,
|
||||||
|
DiagnosticText = MxStatusDetailText.Lookup(detail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<MxStatusProxy> ConvertMany(Array? statuses)
|
||||||
|
{
|
||||||
|
if (statuses is null)
|
||||||
|
{
|
||||||
|
return Array.Empty<MxStatusProxy>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MxStatusProxy> converted = new(statuses.Length);
|
||||||
|
foreach (object? status in statuses)
|
||||||
|
{
|
||||||
|
if (status is null)
|
||||||
|
{
|
||||||
|
converted.Add(new MxStatusProxy
|
||||||
|
{
|
||||||
|
Category = MxStatusCategory.Unknown,
|
||||||
|
DetectedBy = MxStatusSource.Unknown,
|
||||||
|
DiagnosticText = "Null MXSTATUS_PROXY entry.",
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
converted.Add(Convert(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PreserveCompletionOnlyStatusBytes(byte[] statusBytes)
|
||||||
|
{
|
||||||
|
if (statusBytes is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(statusBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"completion_only_status_hex={BitConverter.ToString(statusBytes).Replace("-", string.Empty)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadInt32Field(
|
||||||
|
object value,
|
||||||
|
Type valueType,
|
||||||
|
string fieldName)
|
||||||
|
{
|
||||||
|
FieldInfo? field = valueType.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public);
|
||||||
|
if (field is null)
|
||||||
|
{
|
||||||
|
throw new MxStatusConversionException(
|
||||||
|
$"Status object type '{valueType.FullName}' does not expose required field '{fieldName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
object? fieldValue = field.GetValue(value);
|
||||||
|
if (fieldValue is null)
|
||||||
|
{
|
||||||
|
throw new MxStatusConversionException(
|
||||||
|
$"Status object field '{fieldName}' on type '{valueType.FullName}' is null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return System.Convert.ToInt32(fieldValue, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MxStatusCategory MapCategory(int rawCategory)
|
||||||
|
{
|
||||||
|
return rawCategory switch
|
||||||
|
{
|
||||||
|
-1 => MxStatusCategory.Unknown,
|
||||||
|
0 => MxStatusCategory.Ok,
|
||||||
|
1 => MxStatusCategory.Pending,
|
||||||
|
2 => MxStatusCategory.Warning,
|
||||||
|
3 => MxStatusCategory.CommunicationError,
|
||||||
|
4 => MxStatusCategory.ConfigurationError,
|
||||||
|
5 => MxStatusCategory.OperationalError,
|
||||||
|
6 => MxStatusCategory.SecurityError,
|
||||||
|
7 => MxStatusCategory.SoftwareError,
|
||||||
|
8 => MxStatusCategory.OtherError,
|
||||||
|
_ => MxStatusCategory.Unknown,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MxStatusSource MapSource(int rawDetectedBy)
|
||||||
|
{
|
||||||
|
return rawDetectedBy switch
|
||||||
|
{
|
||||||
|
-1 => MxStatusSource.Unknown,
|
||||||
|
0 => MxStatusSource.RequestingLmx,
|
||||||
|
1 => MxStatusSource.RespondingLmx,
|
||||||
|
2 => MxStatusSource.RequestingNmx,
|
||||||
|
3 => MxStatusSource.RespondingNmx,
|
||||||
|
4 => MxStatusSource.RequestingAutomationObject,
|
||||||
|
5 => MxStatusSource.RespondingAutomationObject,
|
||||||
|
_ => MxStatusSource.Unknown,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ public sealed class WorkerPipeSession
|
|||||||
private readonly Func<int> _processIdProvider;
|
private readonly Func<int> _processIdProvider;
|
||||||
private readonly WorkerFrameReader _reader;
|
private readonly WorkerFrameReader _reader;
|
||||||
private readonly WorkerFrameWriter _writer;
|
private readonly WorkerFrameWriter _writer;
|
||||||
|
private MxAccessStaSession? _mxAccessStaSession;
|
||||||
private long _nextSequence;
|
private long _nextSequence;
|
||||||
|
|
||||||
public WorkerPipeSession(
|
public WorkerPipeSession(
|
||||||
@@ -42,7 +43,7 @@ public sealed class WorkerPipeSession
|
|||||||
|
|
||||||
public Task CompleteStartupHandshakeAsync(CancellationToken cancellationToken = default)
|
public Task CompleteStartupHandshakeAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return CompleteStartupHandshakeAsync(_ => Task.CompletedTask, cancellationToken);
|
return CompleteStartupHandshakeAsync(InitializeMxAccessAsync, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CompleteStartupHandshakeAsync(
|
public async Task CompleteStartupHandshakeAsync(
|
||||||
@@ -54,20 +55,44 @@ public sealed class WorkerPipeSession
|
|||||||
throw new ArgumentNullException(nameof(initializeMxAccessAsync));
|
throw new ArgumentNullException(nameof(initializeMxAccessAsync));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await CompleteStartupHandshakeAsync(
|
||||||
|
async innerCancellationToken =>
|
||||||
|
{
|
||||||
|
await initializeMxAccessAsync(innerCancellationToken).ConfigureAwait(false);
|
||||||
|
return CreateWorkerReady();
|
||||||
|
},
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CompleteStartupHandshakeAsync(
|
||||||
|
Func<CancellationToken, Task<WorkerReady>> initializeMxAccessAsync,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (initializeMxAccessAsync is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(initializeMxAccessAsync));
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
WorkerEnvelope envelope = await _reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
WorkerEnvelope envelope = await _reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||||
ValidateGatewayHello(envelope);
|
ValidateGatewayHello(envelope);
|
||||||
|
|
||||||
await WriteWorkerHelloAsync(cancellationToken).ConfigureAwait(false);
|
await WriteWorkerHelloAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await initializeMxAccessAsync(cancellationToken).ConfigureAwait(false);
|
WorkerReady ready = await initializeMxAccessAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await WriteWorkerReadyAsync(cancellationToken).ConfigureAwait(false);
|
await WriteWorkerReadyAsync(ready, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (WorkerFrameProtocolException exception)
|
catch (WorkerFrameProtocolException exception)
|
||||||
{
|
{
|
||||||
await TryWriteFaultAsync(exception, cancellationToken).ConfigureAwait(false);
|
await TryWriteFaultAsync(exception, cancellationToken).ConfigureAwait(false);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
await TryWriteFaultAsync(MxAccessCreationException.From(exception), cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateGatewayHello(WorkerEnvelope envelope)
|
private void ValidateGatewayHello(WorkerEnvelope envelope)
|
||||||
@@ -108,17 +133,11 @@ public sealed class WorkerPipeSession
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task WriteWorkerReadyAsync(CancellationToken cancellationToken)
|
private Task WriteWorkerReadyAsync(
|
||||||
|
WorkerReady ready,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return _writer.WriteAsync(
|
return _writer.WriteAsync(CreateEnvelope(ready), cancellationToken);
|
||||||
CreateEnvelope(new WorkerReady
|
|
||||||
{
|
|
||||||
WorkerProcessId = _processIdProvider(),
|
|
||||||
MxaccessProgid = MxAccessInteropInfo.ProgId,
|
|
||||||
MxaccessClsid = MxAccessInteropInfo.Clsid,
|
|
||||||
ReadyTimestamp = Timestamp.FromDateTime(DateTime.UtcNow),
|
|
||||||
}),
|
|
||||||
cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TryWriteFaultAsync(
|
private async Task TryWriteFaultAsync(
|
||||||
@@ -140,6 +159,25 @@ public sealed class WorkerPipeSession
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task TryWriteFaultAsync(
|
||||||
|
MxAccessCreationException exception,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _writer
|
||||||
|
.WriteAsync(CreateEnvelope(CreateFault(exception)), cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception faultWriteException) when (
|
||||||
|
faultWriteException is IOException
|
||||||
|
|| faultWriteException is ObjectDisposedException
|
||||||
|
|| faultWriteException is WorkerFrameProtocolException)
|
||||||
|
{
|
||||||
|
// The MXAccess creation failure is the actionable error.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private WorkerEnvelope CreateEnvelope(WorkerHello hello)
|
private WorkerEnvelope CreateEnvelope(WorkerHello hello)
|
||||||
{
|
{
|
||||||
return CreateBaseEnvelope(hello);
|
return CreateBaseEnvelope(hello);
|
||||||
@@ -191,6 +229,34 @@ public sealed class WorkerPipeSession
|
|||||||
return unchecked((ulong)Interlocked.Increment(ref _nextSequence));
|
return unchecked((ulong)Interlocked.Increment(ref _nextSequence));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<WorkerReady> InitializeMxAccessAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_mxAccessStaSession = new MxAccessStaSession();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _mxAccessStaSession
|
||||||
|
.StartAsync(_processIdProvider(), cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_mxAccessStaSession.Dispose();
|
||||||
|
_mxAccessStaSession = null;
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorkerReady CreateWorkerReady()
|
||||||
|
{
|
||||||
|
return new WorkerReady
|
||||||
|
{
|
||||||
|
WorkerProcessId = _processIdProvider(),
|
||||||
|
MxaccessProgid = MxAccessInteropInfo.ProgId,
|
||||||
|
MxaccessClsid = MxAccessInteropInfo.Clsid,
|
||||||
|
ReadyTimestamp = Timestamp.FromDateTime(DateTime.UtcNow),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static WorkerFault CreateFault(WorkerFrameProtocolException exception)
|
private static WorkerFault CreateFault(WorkerFrameProtocolException exception)
|
||||||
{
|
{
|
||||||
return new WorkerFault
|
return new WorkerFault
|
||||||
@@ -206,6 +272,29 @@ public sealed class WorkerPipeSession
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static WorkerFault CreateFault(MxAccessCreationException exception)
|
||||||
|
{
|
||||||
|
WorkerFault fault = new()
|
||||||
|
{
|
||||||
|
Category = WorkerFaultCategory.MxaccessCreationFailed,
|
||||||
|
ExceptionType = exception.InnerException?.GetType().FullName ?? exception.GetType().FullName ?? string.Empty,
|
||||||
|
DiagnosticMessage = exception.Message,
|
||||||
|
ProtocolStatus = new ProtocolStatus
|
||||||
|
{
|
||||||
|
Code = ProtocolStatusCode.WorkerUnavailable,
|
||||||
|
Message = exception.Message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
int? hresult = MxAccessCreationException.ExtractHResult(exception);
|
||||||
|
if (hresult.HasValue)
|
||||||
|
{
|
||||||
|
fault.Hresult = hresult.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fault;
|
||||||
|
}
|
||||||
|
|
||||||
private static WorkerFaultCategory MapFaultCategory(WorkerFrameProtocolErrorCode errorCode)
|
private static WorkerFaultCategory MapFaultCategory(WorkerFrameProtocolErrorCode errorCode)
|
||||||
{
|
{
|
||||||
return errorCode switch
|
return errorCode switch
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace MxGateway.Worker.MxAccess;
|
||||||
|
|
||||||
|
public interface IMxAccessComObjectFactory
|
||||||
|
{
|
||||||
|
object Create();
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace MxGateway.Worker.MxAccess;
|
||||||
|
|
||||||
|
public interface IMxAccessEventSink
|
||||||
|
{
|
||||||
|
void Attach(object mxAccessComObject);
|
||||||
|
|
||||||
|
void Detach();
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using ArchestrA.MxAccess;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.MxAccess;
|
||||||
|
|
||||||
|
public sealed class MxAccessBaseEventSink : IMxAccessEventSink
|
||||||
|
{
|
||||||
|
private LMXProxyServerClass? server;
|
||||||
|
|
||||||
|
public void Attach(object mxAccessComObject)
|
||||||
|
{
|
||||||
|
server = (LMXProxyServerClass)mxAccessComObject;
|
||||||
|
server.OnDataChange += OnDataChange;
|
||||||
|
server.OnWriteComplete += OnWriteComplete;
|
||||||
|
server.OperationComplete += OperationComplete;
|
||||||
|
server.OnBufferedDataChange += OnBufferedDataChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Detach()
|
||||||
|
{
|
||||||
|
if (server is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server.OnDataChange -= OnDataChange;
|
||||||
|
server.OnWriteComplete -= OnWriteComplete;
|
||||||
|
server.OperationComplete -= OperationComplete;
|
||||||
|
server.OnBufferedDataChange -= OnBufferedDataChange;
|
||||||
|
server = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnDataChange(
|
||||||
|
int hLMXServerHandle,
|
||||||
|
int phItemHandle,
|
||||||
|
object pvItemValue,
|
||||||
|
int pwItemQuality,
|
||||||
|
object pftItemTimeStamp,
|
||||||
|
ref MXSTATUS_PROXY[] pVars)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnWriteComplete(
|
||||||
|
int hLMXServerHandle,
|
||||||
|
int phItemHandle,
|
||||||
|
ref MXSTATUS_PROXY[] pVars)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OperationComplete(
|
||||||
|
int hLMXServerHandle,
|
||||||
|
int phItemHandle,
|
||||||
|
ref MXSTATUS_PROXY[] pVars)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnBufferedDataChange(
|
||||||
|
int hLMXServerHandle,
|
||||||
|
int phItemHandle,
|
||||||
|
MxDataType dtDataType,
|
||||||
|
object pvItemValue,
|
||||||
|
object pwItemQuality,
|
||||||
|
object pftItemTimeStamp,
|
||||||
|
ref MXSTATUS_PROXY[] pVars)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using ArchestrA.MxAccess;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.MxAccess;
|
||||||
|
|
||||||
|
public sealed class MxAccessComObjectFactory : IMxAccessComObjectFactory
|
||||||
|
{
|
||||||
|
public object Create()
|
||||||
|
{
|
||||||
|
return new LMXProxyServerClass();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.MxAccess;
|
||||||
|
|
||||||
|
public sealed class MxAccessCreationException : Exception
|
||||||
|
{
|
||||||
|
public MxAccessCreationException(Exception innerException)
|
||||||
|
: base(
|
||||||
|
$"Failed to create MXAccess COM object {MxAccessInteropInfo.ComClassName} ({MxAccessInteropInfo.ProgId}).",
|
||||||
|
innerException)
|
||||||
|
{
|
||||||
|
AttemptedProgId = MxAccessInteropInfo.ProgId;
|
||||||
|
AttemptedClsid = MxAccessInteropInfo.Clsid;
|
||||||
|
AttemptedComClassName = MxAccessInteropInfo.ComClassName;
|
||||||
|
HResult = innerException.HResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string AttemptedProgId { get; }
|
||||||
|
|
||||||
|
public string AttemptedClsid { get; }
|
||||||
|
|
||||||
|
public string AttemptedComClassName { get; }
|
||||||
|
|
||||||
|
public int? CapturedHResult => HResult == 0 ? null : HResult;
|
||||||
|
|
||||||
|
public static MxAccessCreationException From(Exception exception)
|
||||||
|
{
|
||||||
|
return exception is MxAccessCreationException creationException
|
||||||
|
? creationException
|
||||||
|
: new MxAccessCreationException(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int? ExtractHResult(Exception exception)
|
||||||
|
{
|
||||||
|
if (exception is MxAccessCreationException creationException)
|
||||||
|
{
|
||||||
|
return creationException.CapturedHResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception is COMException comException)
|
||||||
|
{
|
||||||
|
return comException.HResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exception.HResult == 0 ? null : exception.HResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.MxAccess;
|
||||||
|
|
||||||
|
public sealed class MxAccessSession : IDisposable
|
||||||
|
{
|
||||||
|
private readonly object mxAccessComObject;
|
||||||
|
private readonly IMxAccessEventSink eventSink;
|
||||||
|
private bool disposed;
|
||||||
|
|
||||||
|
private MxAccessSession(
|
||||||
|
object mxAccessComObject,
|
||||||
|
IMxAccessEventSink eventSink,
|
||||||
|
int creationThreadId)
|
||||||
|
{
|
||||||
|
this.mxAccessComObject = mxAccessComObject ?? throw new ArgumentNullException(nameof(mxAccessComObject));
|
||||||
|
this.eventSink = eventSink ?? throw new ArgumentNullException(nameof(eventSink));
|
||||||
|
CreationThreadId = creationThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CreationThreadId { get; }
|
||||||
|
|
||||||
|
public WorkerReady CreateWorkerReady(int workerProcessId)
|
||||||
|
{
|
||||||
|
return new WorkerReady
|
||||||
|
{
|
||||||
|
WorkerProcessId = workerProcessId,
|
||||||
|
MxaccessProgid = MxAccessInteropInfo.ProgId,
|
||||||
|
MxaccessClsid = MxAccessInteropInfo.Clsid,
|
||||||
|
ReadyTimestamp = Timestamp.FromDateTime(DateTime.UtcNow),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MxAccessSession Create(
|
||||||
|
IMxAccessComObjectFactory factory,
|
||||||
|
IMxAccessEventSink eventSink)
|
||||||
|
{
|
||||||
|
if (factory is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(factory));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventSink is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(eventSink));
|
||||||
|
}
|
||||||
|
|
||||||
|
object? mxAccessComObject = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mxAccessComObject = factory.Create();
|
||||||
|
if (mxAccessComObject is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("MXAccess COM factory returned null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSink.Attach(mxAccessComObject);
|
||||||
|
|
||||||
|
return new MxAccessSession(
|
||||||
|
mxAccessComObject,
|
||||||
|
eventSink,
|
||||||
|
Environment.CurrentManagedThreadId);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
eventSink.Detach();
|
||||||
|
|
||||||
|
if (mxAccessComObject is not null && Marshal.IsComObject(mxAccessComObject))
|
||||||
|
{
|
||||||
|
Marshal.FinalReleaseComObject(mxAccessComObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw MxAccessCreationException.From(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSink.Detach();
|
||||||
|
|
||||||
|
if (Marshal.IsComObject(mxAccessComObject))
|
||||||
|
{
|
||||||
|
Marshal.FinalReleaseComObject(mxAccessComObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MxGateway.Contracts.Proto;
|
||||||
|
using MxGateway.Worker.Sta;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.MxAccess;
|
||||||
|
|
||||||
|
public sealed class MxAccessStaSession : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IMxAccessComObjectFactory factory;
|
||||||
|
private readonly IMxAccessEventSink eventSink;
|
||||||
|
private readonly StaRuntime staRuntime;
|
||||||
|
private MxAccessSession? session;
|
||||||
|
private bool disposed;
|
||||||
|
|
||||||
|
public MxAccessStaSession()
|
||||||
|
: this(
|
||||||
|
new StaRuntime(),
|
||||||
|
new MxAccessComObjectFactory(),
|
||||||
|
new MxAccessBaseEventSink())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MxAccessStaSession(
|
||||||
|
StaRuntime staRuntime,
|
||||||
|
IMxAccessComObjectFactory factory,
|
||||||
|
IMxAccessEventSink eventSink)
|
||||||
|
{
|
||||||
|
this.staRuntime = staRuntime ?? throw new ArgumentNullException(nameof(staRuntime));
|
||||||
|
this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||||
|
this.eventSink = eventSink ?? throw new ArgumentNullException(nameof(eventSink));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<WorkerReady> StartAsync(
|
||||||
|
int workerProcessId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
staRuntime.Start();
|
||||||
|
|
||||||
|
return staRuntime.InvokeAsync(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (session is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("MXAccess COM session has already been created.");
|
||||||
|
}
|
||||||
|
|
||||||
|
session = MxAccessSession.Create(factory, eventSink);
|
||||||
|
return session.CreateWorkerReady(workerProcessId);
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session is not null)
|
||||||
|
{
|
||||||
|
staRuntime.InvokeAsync(() => session.Dispose()).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
staRuntime.Dispose();
|
||||||
|
disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user