Initial MXAccess gateway design docs
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
# MXAccess Gateway Agent Guide
|
||||
|
||||
Repository: https://gitea.dohertylan.com/dohertj2/mxaccessgw
|
||||
|
||||
This project builds a gateway that gives modern clients full MXAccess parity
|
||||
without requiring those clients to load MXAccess COM, run as x86, or own an STA
|
||||
message pump. Treat the installed MXAccess COM component as the compatibility
|
||||
baseline.
|
||||
|
||||
## Core Contract
|
||||
|
||||
Preserve MXAccess behavior first:
|
||||
|
||||
- public MXAccess command semantics,
|
||||
- native MXAccess event families,
|
||||
- STA/message-pump delivery behavior,
|
||||
- installed-provider quirks,
|
||||
- HRESULT/status/value marshaling,
|
||||
- per-client isolation.
|
||||
|
||||
Do not simplify, normalize, or "fix" MXAccess behavior unless an explicit
|
||||
non-parity mode is being implemented and tested. `MxAsbClient` and managed NMX
|
||||
are future acceleration paths only; they do not define the parity contract.
|
||||
|
||||
## Architecture
|
||||
|
||||
The intended split is:
|
||||
|
||||
```text
|
||||
client
|
||||
-> gRPC over TCP
|
||||
-> .NET 10 x64 gateway
|
||||
-> session manager
|
||||
-> per-session .NET Framework 4.8 x86 worker process
|
||||
-> dedicated STA thread
|
||||
-> MXAccess COM instance
|
||||
-> Windows/COM message pump
|
||||
-> command queue
|
||||
-> event sink
|
||||
```
|
||||
|
||||
The gateway must never instantiate or call MXAccess directly. All MXAccess COM
|
||||
interaction belongs in the worker process on its dedicated STA thread.
|
||||
|
||||
The worker must not host public gRPC. Gateway-to-worker communication should use
|
||||
a small local IPC protocol, with named pipes and protobuf-framed messages as the
|
||||
default design.
|
||||
|
||||
## Runtime Targets
|
||||
|
||||
- Gateway: .NET 10, C#, x64 preferred, ASP.NET Core gRPC.
|
||||
- Worker: .NET Framework 4.8, C#, x86 by default.
|
||||
- Worker IPC: one bidirectional named pipe per worker.
|
||||
- Worker process model: one external client session maps to one worker by
|
||||
default.
|
||||
|
||||
## Expected Layout
|
||||
|
||||
Prefer this structure unless there is a strong reason to adjust it:
|
||||
|
||||
```text
|
||||
src/MxGateway.Contracts/
|
||||
Protos/
|
||||
mxaccess_gateway.proto
|
||||
mxaccess_worker.proto
|
||||
Generated/
|
||||
|
||||
src/MxGateway.Server/
|
||||
Program.cs
|
||||
Sessions/
|
||||
Workers/
|
||||
Grpc/
|
||||
Metrics/
|
||||
|
||||
src/MxGateway.Worker/
|
||||
Program.cs
|
||||
Ipc/
|
||||
Sta/
|
||||
MxAccess/
|
||||
Conversion/
|
||||
|
||||
src/MxGateway.Tests/
|
||||
contract tests
|
||||
gateway session tests
|
||||
fake worker tests
|
||||
|
||||
src/MxGateway.Worker.Tests/
|
||||
value/status conversion tests
|
||||
STA queue tests
|
||||
|
||||
src/MxGateway.IntegrationTests/
|
||||
optional live MXAccess tests
|
||||
```
|
||||
|
||||
The contracts project may multi-target, or the `.proto` files may be shared as
|
||||
source inputs to both gateway and worker builds.
|
||||
|
||||
## Public API Shape
|
||||
|
||||
The external API should be session-oriented. Initial rollout should prefer
|
||||
unary `OpenSession`, `CloseSession`, and `Invoke`, plus server-streaming
|
||||
`StreamEvents`. Add a bidirectional `Session` stream after the command and event
|
||||
model is stable.
|
||||
|
||||
Do not compress MXAccess into generic verbs too early. Use a command enum with
|
||||
method-specific payloads so parity can be tested method by method.
|
||||
|
||||
Core MXAccess commands to represent:
|
||||
|
||||
- `Register`
|
||||
- `Unregister`
|
||||
- `AddItem`
|
||||
- `AddItem2`
|
||||
- `RemoveItem`
|
||||
- `Advise`
|
||||
- `UnAdvise`
|
||||
- `AdviseSupervisory`
|
||||
- `AddBufferedItem`
|
||||
- `SetBufferedUpdateInterval`
|
||||
- `Suspend`
|
||||
- `Activate`
|
||||
- `Write`
|
||||
- `Write2`
|
||||
- `WriteSecured`
|
||||
- `WriteSecured2`
|
||||
- `AuthenticateUser`
|
||||
- `ArchestrAUserToId`
|
||||
|
||||
Diagnostics may include `Ping`, `GetSessionState`, `GetWorkerInfo`,
|
||||
`DrainEvents`, and `ShutdownWorker`.
|
||||
|
||||
## Event Requirements
|
||||
|
||||
Represent every public MXAccess event family:
|
||||
|
||||
- `OnDataChange`
|
||||
- `OnWriteComplete`
|
||||
- `OperationComplete`
|
||||
- `OnBufferedDataChange`
|
||||
|
||||
Preserve per-worker event order. The gateway must not reorder events emitted by
|
||||
the same MXAccess instance.
|
||||
|
||||
Event DTOs should carry event family, session id, server handle, item handle,
|
||||
value, quality, timestamp, `MXSTATUS_PROXY[]` equivalent, raw HRESULT/status
|
||||
fields when available, event sequence, worker timestamp, and gateway receive
|
||||
timestamp.
|
||||
|
||||
## Value And Status Rules
|
||||
|
||||
Use a protobuf value union that can represent COM `VARIANT` values and arrays.
|
||||
When a value cannot be losslessly converted, preserve both the best typed
|
||||
projection and enough raw diagnostic metadata to reproduce the case.
|
||||
|
||||
Represent `MXSTATUS_PROXY` explicitly. Do not collapse status arrays into a
|
||||
single success flag.
|
||||
|
||||
Command replies should include protocol status, COM HRESULT if available,
|
||||
MXAccess return values, method-specific out parameters, and status arrays where
|
||||
the MXAccess method emits them.
|
||||
|
||||
## Worker Rules
|
||||
|
||||
Each worker owns:
|
||||
|
||||
- one process,
|
||||
- one MXAccess session,
|
||||
- one dedicated STA thread,
|
||||
- one MXAccess COM object,
|
||||
- one inbound command queue,
|
||||
- one outbound event queue.
|
||||
|
||||
All MXAccess operations must run on the STA. A plain blocking queue is not
|
||||
enough for the STA; the STA loop must pump Windows/COM messages and service
|
||||
queued commands.
|
||||
|
||||
Do not block the STA on pipe writes, gRPC calls, or slow consumers. Event
|
||||
handlers should convert event args, enqueue outbound events, and return to
|
||||
pumping messages.
|
||||
|
||||
On graceful shutdown, reject new commands, optionally clean up active MXAccess
|
||||
handles, detach events, release the COM object, uninitialize COM, and exit. If
|
||||
graceful shutdown exceeds the configured timeout, the gateway may kill the
|
||||
worker.
|
||||
|
||||
## IPC Rules
|
||||
|
||||
Default pipe name shape:
|
||||
|
||||
```text
|
||||
mxaccess-gateway-{gatewayProcessId}-{sessionId}
|
||||
```
|
||||
|
||||
Frame messages as:
|
||||
|
||||
```text
|
||||
uint32 little-endian payload_length
|
||||
payload_length bytes protobuf WorkerEnvelope
|
||||
```
|
||||
|
||||
Every envelope should include protocol version, session id, monotonic sender
|
||||
sequence, correlation id, and a typed body. Protocol version mismatch should
|
||||
fail session creation.
|
||||
|
||||
Pipe security should be local-machine only, with ACLs restricted to the gateway
|
||||
identity and launched worker identity. Prefer a per-session nonce handshake.
|
||||
|
||||
## Gateway Rules
|
||||
|
||||
The gateway is responsible for:
|
||||
|
||||
- public TCP/gRPC API,
|
||||
- authn/authz when needed,
|
||||
- session creation and teardown,
|
||||
- worker launch and lifecycle management,
|
||||
- command routing,
|
||||
- event streaming,
|
||||
- leases, heartbeats, timeouts, and quotas,
|
||||
- worker kill/restart policy,
|
||||
- metrics and structured logs.
|
||||
|
||||
The gRPC layer should stay thin: validate request, find session, call the
|
||||
session worker client, map worker replies to public replies, and stream events.
|
||||
Keep MXAccess-specific translation logic testable outside the gRPC handlers.
|
||||
|
||||
Gateway restart should not try to reattach old workers in the first version.
|
||||
Terminate orphaned workers on startup if that behavior is implemented.
|
||||
|
||||
## Command, Timeout, And Cancellation Semantics
|
||||
|
||||
Command lifecycle:
|
||||
|
||||
```text
|
||||
client gRPC command
|
||||
gateway validates session and payload
|
||||
gateway assigns correlation id
|
||||
gateway writes WorkerCommand to pipe
|
||||
worker queues command to STA
|
||||
STA executes MXAccess method
|
||||
worker captures return/out/status/HRESULT
|
||||
worker sends WorkerCommandReply
|
||||
gateway completes gRPC response
|
||||
```
|
||||
|
||||
Canceling a gRPC call should stop waiting in the gateway, but it cannot safely
|
||||
abort an in-flight COM call on the STA. Hard cancellation means killing the
|
||||
worker process.
|
||||
|
||||
If a command wedges the STA beyond a configured grace period, the gateway should
|
||||
kill the worker and fail the session.
|
||||
|
||||
## Backpressure Policy
|
||||
|
||||
Worker outbound events must use a bounded queue. For parity testing, prefer
|
||||
fail-fast behavior over silent drops. Production coalescing or drop policies
|
||||
must be explicit and observable.
|
||||
|
||||
The gateway should preserve per-session event order, apply backpressure from
|
||||
slow gRPC streams, and disconnect or coalesce only according to an explicit
|
||||
policy.
|
||||
|
||||
## Security And Logging
|
||||
|
||||
Use TLS for remote gRPC when crossing machine boundaries. Authentication may be
|
||||
Windows auth, mTLS, or a deployment-specific token.
|
||||
|
||||
Commands that write, authenticate users, or alter runtime state need explicit
|
||||
authorization design.
|
||||
|
||||
Never log passwords or raw credential values for `AuthenticateUser`,
|
||||
`WriteSecured`, or related secured operations. Do not log full values by
|
||||
default; make value logging opt-in and redacted.
|
||||
|
||||
## Testing Expectations
|
||||
|
||||
Use focused tests for:
|
||||
|
||||
- contract/protobuf compatibility,
|
||||
- gateway session state and worker lifecycle,
|
||||
- gateway behavior with a fake worker,
|
||||
- worker value/status conversion,
|
||||
- STA queue and message-pump behavior.
|
||||
|
||||
Live MXAccess integration tests are optional but should be isolated because they
|
||||
depend on installed COM components and provider behavior.
|
||||
|
||||
Parity tests should compare direct MXAccess behavior against the gateway:
|
||||
|
||||
- return values,
|
||||
- HRESULTs and exceptions,
|
||||
- event sequence,
|
||||
- value projection,
|
||||
- quality/status arrays,
|
||||
- invalid handle behavior,
|
||||
- cross-server handle behavior,
|
||||
- cleanup behavior.
|
||||
|
||||
Known important parity areas:
|
||||
|
||||
- `WriteSecured` may fail before a value-bearing NMX body is emitted.
|
||||
- `WriteSecured2` can succeed in observed native paths.
|
||||
- `OperationComplete` is distinct from write completion.
|
||||
- `OnBufferedDataChange` has a distinct public event shape.
|
||||
- Invalid handles and cross-server handles have specific exception/status
|
||||
behavior.
|
||||
- STA message pumping is required for event delivery.
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
Build the smallest end-to-end slice first:
|
||||
|
||||
1. .NET 10 gateway starts.
|
||||
2. Client calls `OpenSession`.
|
||||
3. Gateway launches .NET Framework 4.8 x86 worker.
|
||||
4. Worker creates STA and MXAccess COM object.
|
||||
5. Client calls `Register`.
|
||||
6. Client calls `AddItem`.
|
||||
7. Client calls `Advise`.
|
||||
8. Worker forwards one `OnDataChange` event to the gateway.
|
||||
9. Gateway streams the event to the client.
|
||||
10. Client calls `CloseSession`.
|
||||
11. Gateway shuts down the worker.
|
||||
|
||||
That slice proves the high-risk requirements: process isolation, STA ownership,
|
||||
message pumping, command routing, and event streaming.
|
||||
|
||||
+897
@@ -0,0 +1,897 @@
|
||||
# MXAccess Gateway Design
|
||||
|
||||
## Goal
|
||||
|
||||
Provide full MXAccess parity to modern clients without forcing those clients to
|
||||
load MXAccess COM, run as x86, or own an STA message pump.
|
||||
|
||||
The gateway must preserve MXAccess behavior first:
|
||||
|
||||
- public MXAccess command semantics,
|
||||
- native MXAccess event families,
|
||||
- STA/message-pump delivery behavior,
|
||||
- installed-provider quirks,
|
||||
- HRESULT/status/value marshaling,
|
||||
- per-client isolation.
|
||||
|
||||
`MxAsbClient` and the managed NMX client remain useful future acceleration
|
||||
paths, but they should not define the parity contract. The installed MXAccess
|
||||
COM component is the compatibility baseline.
|
||||
|
||||
## Architecture
|
||||
|
||||
Use a .NET 10 C# gateway for external clients and per-session .NET Framework
|
||||
4.8 x86 C# worker processes for MXAccess.
|
||||
|
||||
```text
|
||||
client
|
||||
-> gRPC over TCP
|
||||
-> .NET 10 x64 gateway
|
||||
-> session manager
|
||||
-> per-session .NET Framework 4.8 x86 worker process
|
||||
-> dedicated STA thread
|
||||
-> MXAccess COM instance
|
||||
-> Windows/COM message pump
|
||||
-> command queue
|
||||
-> event sink
|
||||
```
|
||||
|
||||
The worker does not host gRPC. The gateway talks to workers through a small
|
||||
local IPC protocol. Named pipes with protobuf-framed messages are the default
|
||||
transport.
|
||||
|
||||
## Process Split
|
||||
|
||||
### Gateway Process
|
||||
|
||||
Runtime:
|
||||
|
||||
- .NET 10
|
||||
- C#
|
||||
- x64 preferred
|
||||
- ASP.NET Core gRPC server
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- expose the public TCP/gRPC API,
|
||||
- authenticate/authorize remote clients if needed,
|
||||
- create one worker per client session,
|
||||
- route commands to the owning worker,
|
||||
- stream worker events to the owning client,
|
||||
- enforce session leases, heartbeats, timeouts, and quotas,
|
||||
- kill/restart workers when they hang or crash,
|
||||
- collect metrics and structured logs,
|
||||
- optionally route selected future operations to ASB or managed NMX only after
|
||||
parity tests prove equivalent behavior.
|
||||
|
||||
The gateway must never instantiate or call MXAccess directly.
|
||||
|
||||
### Worker Process
|
||||
|
||||
Runtime:
|
||||
|
||||
- .NET Framework 4.8
|
||||
- C#
|
||||
- x86 build by default
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- own one MXAccess COM instance,
|
||||
- create and preserve one dedicated STA thread,
|
||||
- pump Windows/COM messages on that STA thread,
|
||||
- execute every MXAccess method call on that STA thread,
|
||||
- subscribe to MXAccess COM events,
|
||||
- convert command results and events into internal protobuf DTOs,
|
||||
- send events back to the gateway over the worker pipe,
|
||||
- shut down cleanly on request,
|
||||
- terminate quickly when the gateway kills the process.
|
||||
|
||||
The worker should be disposable. If MXAccess leaks state, faults, or wedges the
|
||||
STA, the gateway can kill the process without corrupting other clients.
|
||||
|
||||
## Why Not gRPC In The Worker
|
||||
|
||||
.NET Framework 4.8 does not have the same first-class gRPC stack as .NET 10.
|
||||
For the worker, a custom local protocol is simpler and more predictable:
|
||||
|
||||
- named pipes are Windows-native,
|
||||
- no HTTP/2 requirement,
|
||||
- fewer dependencies in the x86 process,
|
||||
- easier process lifetime control,
|
||||
- easier framed binary protocol,
|
||||
- sufficient throughput for command and event traffic.
|
||||
|
||||
The public API can still be modern gRPC because the gateway runs on .NET 10.
|
||||
|
||||
## Worker IPC
|
||||
|
||||
Default transport: one bidirectional named pipe per worker.
|
||||
|
||||
Pipe name:
|
||||
|
||||
```text
|
||||
mxaccess-gateway-{gatewayProcessId}-{sessionId}
|
||||
```
|
||||
|
||||
Message framing:
|
||||
|
||||
```text
|
||||
uint32 little-endian payload_length
|
||||
payload_length bytes protobuf WorkerEnvelope
|
||||
uint32 little-endian payload_length
|
||||
payload_length bytes protobuf WorkerEnvelope
|
||||
...
|
||||
```
|
||||
|
||||
The gateway creates the pipe server, starts the worker with the pipe name as an
|
||||
argument, then waits for the worker to connect and send `WorkerReady`.
|
||||
|
||||
Pipe security:
|
||||
|
||||
- local machine only,
|
||||
- ACL restricted to the gateway identity and the launched worker identity,
|
||||
- no anonymous access,
|
||||
- optionally add a per-session random handshake nonce passed by command line or
|
||||
inherited environment.
|
||||
|
||||
### Worker Envelope
|
||||
|
||||
Every IPC message uses a common envelope:
|
||||
|
||||
```protobuf
|
||||
message WorkerEnvelope {
|
||||
uint32 protocol_version = 1;
|
||||
string session_id = 2;
|
||||
uint64 sequence = 3;
|
||||
uint64 correlation_id = 4;
|
||||
oneof body {
|
||||
WorkerHello worker_hello = 10;
|
||||
GatewayHello gateway_hello = 11;
|
||||
WorkerReady worker_ready = 12;
|
||||
WorkerCommand command = 20;
|
||||
WorkerCommandReply command_reply = 21;
|
||||
WorkerEvent event = 22;
|
||||
WorkerHeartbeat heartbeat = 23;
|
||||
WorkerCancel cancel = 24;
|
||||
WorkerShutdown shutdown = 25;
|
||||
WorkerFault fault = 26;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `sequence` is monotonic per sender.
|
||||
- `correlation_id` links commands to replies.
|
||||
- Events use their own correlation id or zero.
|
||||
- Replies must preserve MXAccess HRESULT/status information even when the
|
||||
command is also represented as a protocol-level failure.
|
||||
- Protocol version mismatch fails session creation.
|
||||
|
||||
## Public gRPC API
|
||||
|
||||
The external API should be session-oriented. A bidirectional stream is the best
|
||||
long-term shape because it naturally carries commands, replies, events,
|
||||
heartbeats, and cancellation.
|
||||
|
||||
```protobuf
|
||||
service MxAccessGateway {
|
||||
rpc OpenSession(OpenSessionRequest) returns (OpenSessionReply);
|
||||
rpc CloseSession(CloseSessionRequest) returns (CloseSessionReply);
|
||||
rpc Invoke(MxCommandRequest) returns (MxCommandReply);
|
||||
rpc StreamEvents(StreamEventsRequest) returns (stream MxEvent);
|
||||
rpc Session(stream ClientMessage) returns (stream ServerMessage);
|
||||
}
|
||||
```
|
||||
|
||||
Recommended rollout:
|
||||
|
||||
1. Implement unary `OpenSession`, `CloseSession`, and `Invoke`.
|
||||
2. Implement server-streaming `StreamEvents`.
|
||||
3. Add bidirectional `Session` after the command/event model is stable.
|
||||
|
||||
The unary plus event-stream shape is easier to debug initially. The
|
||||
bidirectional stream can later reduce per-command overhead and improve
|
||||
backpressure.
|
||||
|
||||
## Public MXAccess Command Surface
|
||||
|
||||
The gateway contract should mirror MXAccess concepts without leaking COM types.
|
||||
Keep handles and statuses explicit.
|
||||
|
||||
Core commands:
|
||||
|
||||
- `Register`
|
||||
- `Unregister`
|
||||
- `AddItem`
|
||||
- `AddItem2`
|
||||
- `RemoveItem`
|
||||
- `Advise`
|
||||
- `UnAdvise`
|
||||
- `AdviseSupervisory`
|
||||
- `AddBufferedItem`
|
||||
- `SetBufferedUpdateInterval`
|
||||
- `Suspend`
|
||||
- `Activate`
|
||||
- `Write`
|
||||
- `Write2`
|
||||
- `WriteSecured`
|
||||
- `WriteSecured2`
|
||||
- `AuthenticateUser`
|
||||
- `ArchestrAUserToId`
|
||||
|
||||
Optional diagnostics:
|
||||
|
||||
- `Ping`
|
||||
- `GetSessionState`
|
||||
- `GetWorkerInfo`
|
||||
- `DrainEvents`
|
||||
- `ShutdownWorker`
|
||||
|
||||
Do not compress MXAccess semantics into generic verbs too early. A command enum
|
||||
with method-specific payloads is easier to test for parity.
|
||||
|
||||
## Event Surface
|
||||
|
||||
The gateway must represent every public MXAccess event family:
|
||||
|
||||
- `OnDataChange`
|
||||
- `OnWriteComplete`
|
||||
- `OperationComplete`
|
||||
- `OnBufferedDataChange`
|
||||
|
||||
The event DTO should include:
|
||||
|
||||
- event family,
|
||||
- session id,
|
||||
- server handle,
|
||||
- item handle,
|
||||
- value when present,
|
||||
- quality when present,
|
||||
- timestamp when present,
|
||||
- `MXSTATUS_PROXY[]` equivalent,
|
||||
- raw HRESULT/status fields when available,
|
||||
- event ordering sequence,
|
||||
- worker timestamp,
|
||||
- gateway receive timestamp.
|
||||
|
||||
Keep event order stable per worker. The gateway should not reorder events from
|
||||
the same MXAccess instance.
|
||||
|
||||
## Value Model
|
||||
|
||||
Use a protobuf value union that can represent COM `VARIANT` values and arrays.
|
||||
|
||||
```protobuf
|
||||
message MxValue {
|
||||
oneof kind {
|
||||
bool bool_value = 1;
|
||||
int32 int32_value = 2;
|
||||
int64 int64_value = 3;
|
||||
float float_value = 4;
|
||||
double double_value = 5;
|
||||
string string_value = 6;
|
||||
Timestamp timestamp_value = 7;
|
||||
MxArray array_value = 8;
|
||||
bytes raw_variant = 100;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Array support should include at least:
|
||||
|
||||
- bool array,
|
||||
- int32 array,
|
||||
- float array,
|
||||
- double array,
|
||||
- string array,
|
||||
- timestamp array,
|
||||
- raw fallback.
|
||||
|
||||
For full parity, unknown or awkward COM values should be preserved as raw
|
||||
metadata rather than dropped. If a value cannot be losslessly converted, the
|
||||
worker should return both the best typed projection and enough diagnostic
|
||||
metadata to reproduce the case.
|
||||
|
||||
## Status Model
|
||||
|
||||
Represent `MXSTATUS_PROXY` explicitly:
|
||||
|
||||
```protobuf
|
||||
message MxStatusProxy {
|
||||
int32 success = 1;
|
||||
uint32 category = 2;
|
||||
uint32 detail = 3;
|
||||
uint32 source = 4;
|
||||
uint32 raw_hresult = 5;
|
||||
string text = 6;
|
||||
}
|
||||
```
|
||||
|
||||
The exact field names should be adjusted to match the actual interop struct,
|
||||
but the design principle is important: do not collapse status arrays into a
|
||||
single success flag.
|
||||
|
||||
For command replies, return:
|
||||
|
||||
- protocol status,
|
||||
- COM HRESULT if available,
|
||||
- MXAccess return value if the method has one,
|
||||
- method-specific out parameters,
|
||||
- status array if the method emits one.
|
||||
|
||||
## STA Worker Thread Model
|
||||
|
||||
Each worker owns:
|
||||
|
||||
- one process,
|
||||
- one MXAccess session,
|
||||
- one dedicated STA thread,
|
||||
- one MXAccess COM object,
|
||||
- one inbound command queue,
|
||||
- one outbound event queue.
|
||||
|
||||
All MXAccess operations run on the STA:
|
||||
|
||||
```text
|
||||
pipe reader thread
|
||||
-> parse WorkerCommand
|
||||
-> enqueue StaCommand
|
||||
-> await task completion
|
||||
-> write WorkerCommandReply
|
||||
|
||||
STA thread
|
||||
-> CoInitializeEx(APARTMENTTHREADED)
|
||||
-> create MXAccess COM object
|
||||
-> wire events
|
||||
-> run message pump
|
||||
-> execute queued commands between message dispatches
|
||||
|
||||
MXAccess event handler on STA
|
||||
-> convert event args to WorkerEvent
|
||||
-> enqueue outbound event
|
||||
|
||||
pipe writer thread
|
||||
-> dequeue replies/events
|
||||
-> write framed protobuf messages
|
||||
```
|
||||
|
||||
Do not block the STA on pipe writes or gRPC calls. The STA should enqueue
|
||||
results/events and return to pumping messages.
|
||||
|
||||
### Message Pump
|
||||
|
||||
The STA loop must pump Windows messages and service command work. A typical
|
||||
shape:
|
||||
|
||||
```text
|
||||
while not shutdown:
|
||||
while command queue has work:
|
||||
execute one command on STA
|
||||
|
||||
MsgWaitForMultipleObjectsEx(
|
||||
command_event,
|
||||
timeout,
|
||||
QS_ALLINPUT,
|
||||
MWMO_INPUTAVAILABLE)
|
||||
|
||||
while PeekMessage:
|
||||
TranslateMessage
|
||||
DispatchMessage
|
||||
```
|
||||
|
||||
This is the critical piece for MXAccess event delivery. A plain blocking queue
|
||||
on an STA thread is not enough if it prevents COM/window messages from being
|
||||
pumped.
|
||||
|
||||
### COM Lifetime
|
||||
|
||||
Worker startup:
|
||||
|
||||
1. set apartment state to STA,
|
||||
2. initialize COM on the STA,
|
||||
3. instantiate `LMXProxyServerClass` or the installed MXAccess interop class,
|
||||
4. attach event handlers,
|
||||
5. send `WorkerReady`.
|
||||
|
||||
Worker shutdown:
|
||||
|
||||
1. reject new commands,
|
||||
2. optionally send `UnAdvise`/`RemoveItem`/`Unregister` for active handles,
|
||||
3. detach event handlers,
|
||||
4. release COM object until reference count reaches zero,
|
||||
5. uninitialize COM,
|
||||
6. exit process.
|
||||
|
||||
If graceful shutdown exceeds timeout, the gateway kills the worker.
|
||||
|
||||
## Session Model
|
||||
|
||||
One external client session maps to one worker process by default.
|
||||
|
||||
Session state in the gateway:
|
||||
|
||||
- session id,
|
||||
- client identity,
|
||||
- worker process id,
|
||||
- pipe name,
|
||||
- pipe connection,
|
||||
- open time,
|
||||
- last heartbeat,
|
||||
- active stream subscribers,
|
||||
- command timeout policy,
|
||||
- event queue metrics.
|
||||
|
||||
Session state in the worker:
|
||||
|
||||
- MXAccess COM object,
|
||||
- registered server handles,
|
||||
- item handles,
|
||||
- item definitions/context,
|
||||
- advise state,
|
||||
- buffered state,
|
||||
- authenticated user ids if needed,
|
||||
- event sequence number.
|
||||
|
||||
The gateway should treat worker state as authoritative for MXAccess handles.
|
||||
It can keep a shadow state for diagnostics and cleanup, but should not invent
|
||||
handles.
|
||||
|
||||
## Command Execution
|
||||
|
||||
Every command should follow the same lifecycle:
|
||||
|
||||
```text
|
||||
client sends gRPC command
|
||||
gateway validates session and payload
|
||||
gateway assigns correlation id
|
||||
gateway writes WorkerCommand to pipe
|
||||
worker pipe reader enqueues command to STA
|
||||
STA executes MXAccess method
|
||||
worker captures return value/out params/status/HRESULT
|
||||
worker sends WorkerCommandReply
|
||||
gateway completes gRPC response
|
||||
```
|
||||
|
||||
Timeouts:
|
||||
|
||||
- gateway command timeout bounds client waiting,
|
||||
- worker command timeout marks the command as stuck,
|
||||
- if the STA does not recover after a configurable grace period, kill the
|
||||
worker and fail the session.
|
||||
|
||||
Cancellation:
|
||||
|
||||
- canceling the gRPC call should stop waiting in the gateway,
|
||||
- it cannot safely abort an in-flight COM call on the STA,
|
||||
- the worker should finish the COM call and discard or log the late reply if
|
||||
the correlation was canceled,
|
||||
- hard cancellation means killing the worker process.
|
||||
|
||||
## Event Delivery And Backpressure
|
||||
|
||||
Events flow from worker to gateway, then gateway to client streams.
|
||||
|
||||
Worker policy:
|
||||
|
||||
- bounded outbound event channel,
|
||||
- never block MXAccess event handler on pipe writes,
|
||||
- if the outbound channel is full, apply configured policy:
|
||||
- disconnect session,
|
||||
- drop oldest low-priority data-change events,
|
||||
- coalesce data changes by item handle,
|
||||
- or block briefly then fault.
|
||||
|
||||
For full parity testing, default should be fail-fast rather than silent drop.
|
||||
For production high-rate telemetry, add explicit coalescing modes.
|
||||
|
||||
Gateway policy:
|
||||
|
||||
- one event sequencer per session,
|
||||
- preserve per-session event order,
|
||||
- support multiple client event subscribers only if explicitly required,
|
||||
- apply backpressure from slow gRPC streams,
|
||||
- disconnect or coalesce according to client-selected mode.
|
||||
|
||||
## Isolation And Fault Handling
|
||||
|
||||
Failure cases:
|
||||
|
||||
- worker fails startup,
|
||||
- worker pipe disconnects,
|
||||
- worker heartbeat expires,
|
||||
- worker process exits,
|
||||
- STA command times out,
|
||||
- MXAccess COM throws,
|
||||
- MXAccess event handler throws,
|
||||
- client disconnects,
|
||||
- gateway shuts down.
|
||||
|
||||
Policy:
|
||||
|
||||
- worker startup failure fails `OpenSession`,
|
||||
- worker crash emits terminal session fault to client,
|
||||
- command exceptions return structured command fault with HRESULT if known,
|
||||
- stale sessions are closed by lease timeout,
|
||||
- stuck workers are killed by process id,
|
||||
- gateway restart should not attempt to reattach old workers unless explicitly
|
||||
designed; first version should terminate orphaned workers on startup.
|
||||
|
||||
Because each client owns one worker, a crash or leak affects only that session.
|
||||
|
||||
## Security
|
||||
|
||||
External gateway:
|
||||
|
||||
- use TLS for remote gRPC if crossing machine boundaries,
|
||||
- authenticate clients with Windows auth, mTLS, or a deployment-specific token,
|
||||
- authorize access to commands that can write, authenticate users, or alter
|
||||
runtime state.
|
||||
|
||||
Internal worker IPC:
|
||||
|
||||
- local named pipes only,
|
||||
- restrictive pipe ACL,
|
||||
- per-session nonce handshake,
|
||||
- worker validates gateway hello before creating MXAccess,
|
||||
- gateway validates worker executable path and version,
|
||||
- no secrets in command line when avoidable.
|
||||
|
||||
Credential-sensitive commands such as `AuthenticateUser` and `WriteSecured`
|
||||
must not log passwords or raw credential values.
|
||||
|
||||
## Observability
|
||||
|
||||
Gateway metrics:
|
||||
|
||||
- sessions open,
|
||||
- workers running,
|
||||
- worker start latency,
|
||||
- command latency by method,
|
||||
- command failures by method/status,
|
||||
- event rate by session/event type,
|
||||
- event queue depth,
|
||||
- worker memory/CPU,
|
||||
- worker restarts/kills,
|
||||
- gRPC stream disconnects.
|
||||
|
||||
Worker logs:
|
||||
|
||||
- startup/shutdown,
|
||||
- MXAccess COM creation result,
|
||||
- command start/end with correlation id,
|
||||
- HRESULT/status summary,
|
||||
- event family and sequence number,
|
||||
- queue overflow,
|
||||
- STA watchdog warnings.
|
||||
|
||||
Do not log full values by default. Make value logging opt-in and redacted where
|
||||
credentials or secured writes are involved.
|
||||
|
||||
## Performance Strategy
|
||||
|
||||
First priority is parity. Performance comes from process isolation, batching,
|
||||
and avoiding unnecessary cross-process round trips.
|
||||
|
||||
Baseline choices:
|
||||
|
||||
- long-lived worker per session,
|
||||
- persistent pipe,
|
||||
- protobuf binary framing,
|
||||
- no gRPC inside worker,
|
||||
- no COM calls outside STA,
|
||||
- event streaming rather than event polling.
|
||||
|
||||
Optimizations after parity:
|
||||
|
||||
- batch commands where MXAccess semantics allow,
|
||||
- batch events from worker to gateway while preserving order,
|
||||
- optional data-change coalescing by item handle,
|
||||
- memory-mapped payload slabs for very large arrays,
|
||||
- shared schema for typed values to avoid raw COM marshaling at the gateway,
|
||||
- gateway-side route to `MxAsbClient` for proven high-volume read/write
|
||||
workloads only when caller opts into non-MXAccess-backed behavior or parity
|
||||
tests prove equivalence.
|
||||
|
||||
## Project Layout
|
||||
|
||||
Suggested additions:
|
||||
|
||||
```text
|
||||
src/MxGateway.Contracts/
|
||||
Protos/
|
||||
mxaccess_gateway.proto
|
||||
mxaccess_worker.proto
|
||||
Generated/
|
||||
|
||||
src/MxGateway.Server/
|
||||
Program.cs
|
||||
Sessions/
|
||||
Workers/
|
||||
Grpc/
|
||||
Metrics/
|
||||
|
||||
src/MxGateway.Worker/
|
||||
Program.cs
|
||||
Ipc/
|
||||
Sta/
|
||||
MxAccess/
|
||||
Conversion/
|
||||
|
||||
src/MxGateway.Tests/
|
||||
contract tests
|
||||
gateway session tests
|
||||
fake worker tests
|
||||
|
||||
src/MxGateway.Worker.Tests/
|
||||
value/status conversion tests
|
||||
STA queue tests
|
||||
|
||||
src/MxGateway.IntegrationTests/
|
||||
optional live MXAccess tests
|
||||
```
|
||||
|
||||
Build outputs:
|
||||
|
||||
- gateway: .NET 10 x64,
|
||||
- worker: .NET Framework 4.8 x86.
|
||||
|
||||
The contracts project can multi-target if needed, or the `.proto` files can be
|
||||
shared as source inputs to both gateway and worker builds.
|
||||
|
||||
## Worker Implementation Plan
|
||||
|
||||
### Phase 1: Minimal Worker Harness
|
||||
|
||||
- Create .NET Framework 4.8 x86 worker executable.
|
||||
- Parse pipe name/session id/nonce args.
|
||||
- Connect to gateway named pipe.
|
||||
- Exchange hello/ready messages.
|
||||
- Start STA thread.
|
||||
- Create MXAccess COM object on STA.
|
||||
- Pump messages.
|
||||
- Shut down cleanly.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- gateway can spawn worker,
|
||||
- worker reports ready,
|
||||
- worker exits on shutdown command,
|
||||
- STA remains responsive.
|
||||
|
||||
### Phase 2: Command Queue
|
||||
|
||||
- Add command DTOs for `Register`, `Unregister`, `AddItem`, `RemoveItem`.
|
||||
- Implement STA command dispatch.
|
||||
- Return method result, HRESULT, and structured fault.
|
||||
- Add command timeout handling in gateway.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- client can open a session and perform basic handle lifecycle through gRPC.
|
||||
|
||||
### Phase 3: Event Stream
|
||||
|
||||
- Wire MXAccess events in the worker.
|
||||
- Convert `OnDataChange`, `OnWriteComplete`, `OperationComplete`, and
|
||||
`OnBufferedDataChange` to protobuf events.
|
||||
- Add event sequence numbers.
|
||||
- Add gateway `StreamEvents`.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- advised item changes reach a .NET 10 client without the client owning an STA.
|
||||
|
||||
### Phase 4: Full Command Surface
|
||||
|
||||
Add remaining MXAccess methods:
|
||||
|
||||
- `Advise`
|
||||
- `UnAdvise`
|
||||
- `AdviseSupervisory`
|
||||
- `AddItem2`
|
||||
- `AddBufferedItem`
|
||||
- `SetBufferedUpdateInterval`
|
||||
- `Suspend`
|
||||
- `Activate`
|
||||
- `Write`
|
||||
- `Write2`
|
||||
- `WriteSecured`
|
||||
- `WriteSecured2`
|
||||
- `AuthenticateUser`
|
||||
- `ArchestrAUserToId`
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- gRPC command surface covers the installed MXAccess public method set.
|
||||
|
||||
### Phase 5: Parity Harness
|
||||
|
||||
- Reuse existing MXAccess trace harness scenarios.
|
||||
- Run each scenario against direct MXAccess and against the gateway.
|
||||
- Compare:
|
||||
- return values,
|
||||
- HRESULTs/exceptions,
|
||||
- event sequence,
|
||||
- value projection,
|
||||
- quality/status arrays,
|
||||
- invalid handle behavior,
|
||||
- cleanup behavior.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- documented parity matrix for all public methods and event families.
|
||||
|
||||
### Phase 6: Hardening
|
||||
|
||||
- Worker watchdog.
|
||||
- Heartbeats.
|
||||
- Process kill/restart.
|
||||
- Bounded queues.
|
||||
- Backpressure policy.
|
||||
- TLS/auth on public gateway.
|
||||
- Metrics.
|
||||
- Structured logging.
|
||||
- Installer/service packaging.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- gateway can run as a Windows service and recover from worker crashes.
|
||||
|
||||
## Gateway Implementation Plan
|
||||
|
||||
### Session Manager
|
||||
|
||||
Core operations:
|
||||
|
||||
- allocate session id,
|
||||
- choose worker executable,
|
||||
- create pipe name and nonce,
|
||||
- start worker process,
|
||||
- accept pipe connection,
|
||||
- verify worker hello,
|
||||
- track worker state,
|
||||
- close or kill worker.
|
||||
|
||||
State machine:
|
||||
|
||||
```text
|
||||
Creating
|
||||
-> StartingWorker
|
||||
-> WaitingForPipe
|
||||
-> InitializingWorker
|
||||
-> Ready
|
||||
-> Closing
|
||||
-> Closed
|
||||
-> Faulted
|
||||
```
|
||||
|
||||
### Worker Client
|
||||
|
||||
Gateway-side worker client owns:
|
||||
|
||||
- pipe stream,
|
||||
- read loop,
|
||||
- write loop,
|
||||
- pending command dictionary,
|
||||
- event channel,
|
||||
- heartbeat monitor,
|
||||
- process handle.
|
||||
|
||||
It should expose:
|
||||
|
||||
```csharp
|
||||
Task<WorkerCommandReply> InvokeAsync(WorkerCommand command, CancellationToken ct);
|
||||
IAsyncEnumerable<WorkerEvent> ReadEventsAsync(CancellationToken ct);
|
||||
Task ShutdownAsync(TimeSpan timeout);
|
||||
void Kill();
|
||||
```
|
||||
|
||||
### gRPC Layer
|
||||
|
||||
The gRPC layer should be thin:
|
||||
|
||||
- validate request,
|
||||
- find session,
|
||||
- call session worker client,
|
||||
- map worker reply to public reply,
|
||||
- stream events from session event channel.
|
||||
|
||||
Avoid embedding MXAccess-specific business logic in gRPC handlers. Keep the
|
||||
translation code testable.
|
||||
|
||||
## C# Worker Versus C++ Worker
|
||||
|
||||
Start with a C# .NET Framework 4.8 x86 worker.
|
||||
|
||||
Reasons:
|
||||
|
||||
- fastest implementation path,
|
||||
- easiest COM interop/event sink work,
|
||||
- straightforward named-pipe/protobuf implementation,
|
||||
- easier logging and diagnostics,
|
||||
- easier parity iteration.
|
||||
|
||||
C++/CLI or native C++ remains an escape hatch if C# COM interop proves
|
||||
insufficient. The pipe protocol should be language-neutral so a future C++
|
||||
worker can replace the C# worker without changing gateway or clients.
|
||||
|
||||
Use C++ only if evidence shows:
|
||||
|
||||
- C# event sinks cannot reliably pump MXAccess events,
|
||||
- COM `VARIANT`/`SAFEARRAY` conversion loses required data,
|
||||
- throughput is bottlenecked by .NET COM marshaling,
|
||||
- MXAccess requires ATL-style connection point behavior not reproducible from
|
||||
C#.
|
||||
|
||||
## Compatibility Baseline
|
||||
|
||||
The proxy should preserve direct MXAccess behavior, including surprising cases.
|
||||
|
||||
Known important parity areas from existing captures:
|
||||
|
||||
- `WriteSecured` may fail before a value-bearing NMX body is emitted.
|
||||
- `WriteSecured2` can succeed in observed native paths.
|
||||
- `OperationComplete` is distinct from write completion.
|
||||
- `OnBufferedDataChange` has a distinct public event shape.
|
||||
- Invalid handles and cross-server handles have specific exception/status
|
||||
behavior.
|
||||
- STA message pumping is required for event delivery.
|
||||
|
||||
The gateway should not "fix" these behaviors unless the client explicitly opts
|
||||
into a non-parity mode.
|
||||
|
||||
## Future Backend Routing
|
||||
|
||||
After the MXAccess-backed proxy is stable, the gateway can optionally support
|
||||
other backends behind the same public contract:
|
||||
|
||||
- `MxAsbClient` for high-volume basic read/write where poll-based subscription
|
||||
semantics are acceptable or proven equivalent for a workload,
|
||||
- managed NMX for native callback experiments and eventual MXAccess-free
|
||||
replacement work,
|
||||
- direct MXAccess worker as the default parity backend.
|
||||
|
||||
Routing must be explicit and observable:
|
||||
|
||||
- event/reply includes backend name,
|
||||
- tests assert backend choice,
|
||||
- no silent fallback that changes semantics.
|
||||
|
||||
Initial production mode should be:
|
||||
|
||||
```text
|
||||
backend = mxaccess-worker
|
||||
```
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Exact installed MXAccess COM ProgID/class used by production should be pinned
|
||||
from the existing trace harness.
|
||||
- Whether one gRPC client connection maps to one session or whether sessions can
|
||||
survive client reconnects.
|
||||
- Whether event streams can have multiple subscribers per session.
|
||||
- Required authentication model for remote clients.
|
||||
- Whether worker process identity should be the gateway identity or a restricted
|
||||
service account.
|
||||
- Maximum supported event rate before coalescing is required.
|
||||
- Whether command batching is needed for high-volume tag registration.
|
||||
|
||||
## Recommended Next Step
|
||||
|
||||
Build the smallest end-to-end slice:
|
||||
|
||||
1. .NET 10 gateway starts.
|
||||
2. Client calls `OpenSession`.
|
||||
3. Gateway launches .NET Framework 4.8 x86 worker.
|
||||
4. Worker creates STA and MXAccess COM object.
|
||||
5. Client calls `Register`.
|
||||
6. Client calls `AddItem`.
|
||||
7. Client calls `Advise`.
|
||||
8. Worker forwards one `OnDataChange` event to the gateway.
|
||||
9. Gateway streams the event to the client.
|
||||
10. Client calls `CloseSession`.
|
||||
11. Gateway shuts down the worker.
|
||||
|
||||
That slice proves the architecture's hardest requirements: process isolation,
|
||||
STA ownership, message pumping, command routing, and event streaming.
|
||||
Reference in New Issue
Block a user