Add gateway implementation planning docs
This commit is contained in:
@@ -0,0 +1,636 @@
|
||||
# MXAccess Worker Instance Detailed Design
|
||||
|
||||
## Purpose
|
||||
|
||||
An MXAccess worker instance is the compatibility boundary around one installed
|
||||
MXAccess COM object. It runs as a disposable .NET Framework 4.8 x86 process,
|
||||
owns one dedicated STA thread, pumps Windows/COM messages, executes MXAccess
|
||||
commands on that STA, and forwards MXAccess events back to the gateway.
|
||||
|
||||
The worker's job is not to make MXAccess nicer. Its job is to preserve direct
|
||||
MXAccess behavior while making that behavior available to modern clients through
|
||||
the gateway.
|
||||
|
||||
## Runtime
|
||||
|
||||
- Target runtime: .NET Framework 4.8.
|
||||
- Language: C#.
|
||||
- Platform target: x86 by default.
|
||||
- Process lifetime: one worker per gateway session.
|
||||
- Public network listeners: none.
|
||||
- Gateway IPC: one named pipe with protobuf-framed messages.
|
||||
- COM apartment: one dedicated STA thread.
|
||||
|
||||
Style guides:
|
||||
|
||||
- [C# Style Guide](./style-guides/CSharpStyleGuide.md)
|
||||
- [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md)
|
||||
|
||||
## Responsibilities
|
||||
|
||||
The worker owns:
|
||||
|
||||
- connection to the gateway pipe,
|
||||
- protocol hello and readiness reporting,
|
||||
- STA thread creation and teardown,
|
||||
- COM initialization on the STA,
|
||||
- MXAccess COM object creation,
|
||||
- MXAccess event sink wiring,
|
||||
- command dispatch on the STA,
|
||||
- MXAccess handle and advise state tracking,
|
||||
- value/status/HRESULT capture,
|
||||
- conversion to worker protobuf DTOs,
|
||||
- event sequencing,
|
||||
- heartbeat reporting,
|
||||
- graceful shutdown.
|
||||
|
||||
The worker does not own:
|
||||
|
||||
- public gRPC API,
|
||||
- client authentication,
|
||||
- cross-session routing,
|
||||
- worker process supervision,
|
||||
- remote TLS,
|
||||
- policy decisions for other sessions.
|
||||
|
||||
## Process Bootstrap
|
||||
|
||||
Expected command-line arguments:
|
||||
|
||||
```text
|
||||
--session-id <sessionId>
|
||||
--pipe-name <pipeName>
|
||||
--protocol-version <version>
|
||||
```
|
||||
|
||||
Expected protected environment values:
|
||||
|
||||
```text
|
||||
MXGATEWAY_WORKER_NONCE=<random nonce>
|
||||
MXGATEWAY_WORKER_LOG_CONTEXT=<optional context>
|
||||
```
|
||||
|
||||
Startup sequence:
|
||||
|
||||
1. Parse command-line arguments.
|
||||
2. Configure minimal logging.
|
||||
3. Validate required values are present.
|
||||
4. Connect to the gateway named pipe.
|
||||
5. Exchange `WorkerHello` and `GatewayHello`.
|
||||
6. Validate protocol version, session id, and nonce.
|
||||
7. Start the STA runtime.
|
||||
8. Create the MXAccess COM object on the STA.
|
||||
9. Attach MXAccess event handlers on the STA.
|
||||
10. Send `WorkerReady`.
|
||||
11. Start pipe read, pipe write, heartbeat, and shutdown coordination loops.
|
||||
|
||||
If validation fails before MXAccess creation, exit quickly with a non-zero exit
|
||||
code. If MXAccess creation fails, send `WorkerFault` when possible and exit.
|
||||
|
||||
## Internal Components
|
||||
|
||||
```text
|
||||
MxGateway.Worker
|
||||
Program
|
||||
Bootstrap
|
||||
WorkerOptions
|
||||
WorkerHost
|
||||
Ipc
|
||||
PipeClient
|
||||
FrameReader
|
||||
FrameWriter
|
||||
WorkerProtocol
|
||||
Sta
|
||||
StaRuntime
|
||||
StaCommandQueue
|
||||
MessagePump
|
||||
StaWatchdog
|
||||
MxAccess
|
||||
MxAccessSession
|
||||
MxAccessCommandDispatcher
|
||||
MxAccessEventSink
|
||||
MxAccessHandleRegistry
|
||||
Conversion
|
||||
VariantConverter
|
||||
SafeArrayConverter
|
||||
StatusProxyConverter
|
||||
HResultMapper
|
||||
```
|
||||
|
||||
## Threading Model
|
||||
|
||||
```text
|
||||
main thread
|
||||
-> parse args
|
||||
-> configure host
|
||||
-> coordinate shutdown
|
||||
|
||||
pipe reader thread/task
|
||||
-> read WorkerEnvelope frames
|
||||
-> validate protocol
|
||||
-> enqueue commands or control messages
|
||||
|
||||
pipe writer thread/task
|
||||
-> serialize WorkerEnvelope frames
|
||||
-> write replies, events, heartbeats, faults
|
||||
|
||||
STA thread
|
||||
-> CoInitializeEx(APARTMENTTHREADED)
|
||||
-> create MXAccess COM object
|
||||
-> attach event handlers
|
||||
-> pump Windows/COM messages
|
||||
-> execute queued commands
|
||||
-> detach events and release COM on shutdown
|
||||
|
||||
watchdog/heartbeat task
|
||||
-> observe STA responsiveness
|
||||
-> send heartbeat or fault
|
||||
```
|
||||
|
||||
No MXAccess method may execute outside the STA thread. Do not use `Task.Run`
|
||||
around COM calls. Do not let event handlers perform pipe writes.
|
||||
|
||||
## STA Runtime
|
||||
|
||||
The STA runtime is the most important part of the worker.
|
||||
|
||||
Startup:
|
||||
|
||||
1. Create a dedicated `Thread`.
|
||||
2. Set apartment state to `ApartmentState.STA`.
|
||||
3. Start the thread.
|
||||
4. Inside the thread, initialize COM.
|
||||
5. Create the MXAccess COM object.
|
||||
6. Attach event handlers.
|
||||
7. Signal ready to the worker host.
|
||||
8. Enter the message pump.
|
||||
|
||||
Shutdown:
|
||||
|
||||
1. Mark the command queue as completing.
|
||||
2. Drain or reject pending commands according to shutdown mode.
|
||||
3. Optionally issue MXAccess cleanup calls for active handles.
|
||||
4. Detach event handlers.
|
||||
5. Release COM references.
|
||||
6. Uninitialize COM.
|
||||
7. Exit the thread.
|
||||
|
||||
## Message Pump
|
||||
|
||||
The STA must pump Windows messages while also processing queued commands. A
|
||||
blocking queue that prevents message pumping is not acceptable.
|
||||
|
||||
Required loop 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
|
||||
```
|
||||
|
||||
The command queue should signal a Win32 event or equivalent wait handle so the
|
||||
STA can wake without busy-waiting.
|
||||
|
||||
The loop should update a heartbeat timestamp after:
|
||||
|
||||
- successfully pumping messages,
|
||||
- starting a command,
|
||||
- finishing a command,
|
||||
- processing an MXAccess event.
|
||||
|
||||
## COM Creation
|
||||
|
||||
The MXAccess analysis source at `C:\Users\dohertj2\Desktop\mxaccess` identifies
|
||||
the installed COM target:
|
||||
|
||||
- interop assembly:
|
||||
`C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`
|
||||
- assembly identity:
|
||||
`ArchestrA.MxAccess, Version=3.2.0.0, PublicKeyToken=23106a86e706d0ae`
|
||||
- COM class:
|
||||
`ArchestrA.MxAccess.LMXProxyServerClass`
|
||||
- CLSID:
|
||||
`{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}`
|
||||
- ProgID:
|
||||
`LMXProxy.LMXProxyServer.1`
|
||||
- version-independent ProgID:
|
||||
`LMXProxy.LMXProxyServer`
|
||||
- registered server:
|
||||
`C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll`
|
||||
- registry view:
|
||||
`HKCR\Wow6432Node\CLSID\{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}`
|
||||
- threading model:
|
||||
`Apartment`
|
||||
|
||||
The worker should reference the interop assembly and instantiate
|
||||
`LMXProxyServerClass` on the dedicated STA thread. Keep the ProgID and assembly
|
||||
path configurable for diagnostics, but this COM class is the v1 default.
|
||||
|
||||
Creation rules:
|
||||
|
||||
- Create COM object only on the STA.
|
||||
- Attach event handlers only on the STA.
|
||||
- Keep the COM reference private to the STA runtime.
|
||||
- Never marshal the raw COM object to pipe reader/writer threads.
|
||||
- Capture COM creation HRESULT or exception details.
|
||||
|
||||
If COM creation fails, the worker should send a structured fault with:
|
||||
|
||||
- fault category,
|
||||
- exception type,
|
||||
- HRESULT when available,
|
||||
- COM class or ProgID attempted,
|
||||
- worker process id,
|
||||
- session id.
|
||||
|
||||
## Event Sink
|
||||
|
||||
The worker must subscribe to every public MXAccess event family:
|
||||
|
||||
- `OnDataChange`
|
||||
- `OnWriteComplete`
|
||||
- `OperationComplete`
|
||||
- `OnBufferedDataChange`
|
||||
|
||||
Forward these event families only when the native MXAccess COM object raises
|
||||
them. Do not synthesize `OperationComplete` from write completion or command
|
||||
status. `OnBufferedDataChange` must be represented in the protocol now, but
|
||||
multi-sample payload conversion should remain capture-validated; preserve raw
|
||||
metadata whenever conversion is incomplete.
|
||||
|
||||
Event handling rules:
|
||||
|
||||
- Event handlers are expected to run on the STA.
|
||||
- Assign a monotonic worker event sequence.
|
||||
- Convert event args to `WorkerEvent`.
|
||||
- Include value, quality, timestamp, handles, status arrays, and raw status
|
||||
details when available.
|
||||
- Preserve raw event payload metadata for unsupported buffered or
|
||||
completion-only shapes.
|
||||
- Enqueue to the outbound event queue.
|
||||
- Return quickly to preserve message pumping.
|
||||
|
||||
If event conversion throws, catch it inside the event handler, enqueue a
|
||||
structured `WorkerFault` or diagnostic event, and keep the worker alive only if
|
||||
the fault policy allows it.
|
||||
|
||||
## Command Queue
|
||||
|
||||
The pipe reader converts `WorkerCommand` messages into `StaCommand` entries.
|
||||
|
||||
Each entry should include:
|
||||
|
||||
- correlation id,
|
||||
- method name,
|
||||
- method-specific request payload,
|
||||
- enqueue timestamp,
|
||||
- cancellation marker,
|
||||
- reply completion path.
|
||||
|
||||
The STA command dispatcher:
|
||||
|
||||
1. Dequeues one command.
|
||||
2. Checks whether shutdown has started.
|
||||
3. Calls the matching MXAccess method.
|
||||
4. Captures return values, out parameters, status arrays, and HRESULT.
|
||||
5. Converts results to `WorkerCommandReply`.
|
||||
6. Enqueues the reply to the pipe writer.
|
||||
|
||||
The STA should execute one command at a time. MXAccess command ordering must be
|
||||
preserved for one worker.
|
||||
|
||||
## Command Dispatch Surface
|
||||
|
||||
Phase 1 commands:
|
||||
|
||||
- `Register`
|
||||
- `Unregister`
|
||||
- `AddItem`
|
||||
- `RemoveItem`
|
||||
|
||||
Phase 2 event commands:
|
||||
|
||||
- `Advise`
|
||||
- `UnAdvise`
|
||||
- `AdviseSupervisory`
|
||||
|
||||
Full surface:
|
||||
|
||||
- `AddItem2`
|
||||
- `AddBufferedItem`
|
||||
- `SetBufferedUpdateInterval`
|
||||
- `Suspend`
|
||||
- `Activate`
|
||||
- `Write`
|
||||
- `Write2`
|
||||
- `WriteSecured`
|
||||
- `WriteSecured2`
|
||||
- `AuthenticateUser`
|
||||
- `ArchestrAUserToId`
|
||||
|
||||
Diagnostics:
|
||||
|
||||
- `Ping`
|
||||
- `GetSessionState`
|
||||
- `GetWorkerInfo`
|
||||
- `DrainEvents`
|
||||
- `ShutdownWorker`
|
||||
|
||||
Implement method-specific dispatch instead of a generic string method invoker.
|
||||
Parity tests need stable command-specific request and reply shapes.
|
||||
|
||||
## Handle Registry
|
||||
|
||||
The worker should track MXAccess state for diagnostics and cleanup, while still
|
||||
treating MXAccess as the authority.
|
||||
|
||||
Suggested tracked state:
|
||||
|
||||
- registered server handles,
|
||||
- item handles,
|
||||
- item names and context,
|
||||
- server handle for each item,
|
||||
- advise state,
|
||||
- buffered item state,
|
||||
- authenticated user ids if needed,
|
||||
- last command touching each handle.
|
||||
|
||||
Rules:
|
||||
|
||||
- Do not invent handles.
|
||||
- Do not rewrite handles returned by MXAccess.
|
||||
- Preserve invalid-handle behavior from MXAccess.
|
||||
- Preserve cross-server handle behavior from MXAccess.
|
||||
- Use registry state for cleanup and diagnostics, not semantic correction.
|
||||
|
||||
## Value Conversion
|
||||
|
||||
`VariantConverter` should convert COM values into the protobuf `MxValue` union.
|
||||
|
||||
Supported scalar projections:
|
||||
|
||||
- bool,
|
||||
- int32,
|
||||
- int64,
|
||||
- float,
|
||||
- double,
|
||||
- string,
|
||||
- timestamp,
|
||||
- raw fallback.
|
||||
|
||||
Supported arrays:
|
||||
|
||||
- bool array,
|
||||
- int32 array,
|
||||
- float array,
|
||||
- double array,
|
||||
- string array,
|
||||
- timestamp array,
|
||||
- raw fallback.
|
||||
|
||||
Rules:
|
||||
|
||||
- Preserve null and empty values distinctly when MXAccess exposes a distinction.
|
||||
- Preserve array rank and dimensions when available.
|
||||
- Preserve original variant type metadata.
|
||||
- If conversion is lossy, include the best typed value plus raw diagnostic
|
||||
metadata.
|
||||
- Do not throw away values just because they are awkward.
|
||||
|
||||
Credential-bearing values must not be logged.
|
||||
|
||||
## Status And HRESULT Capture
|
||||
|
||||
`MXSTATUS_PROXY` arrays must be represented explicitly. Do not collapse status
|
||||
arrays into a single success flag.
|
||||
|
||||
For every command reply, capture:
|
||||
|
||||
- protocol success/failure,
|
||||
- method name,
|
||||
- correlation id,
|
||||
- COM HRESULT if available,
|
||||
- thrown exception HRESULT if available,
|
||||
- MXAccess return value if any,
|
||||
- method-specific out parameters,
|
||||
- status array,
|
||||
- diagnostic message safe for logs.
|
||||
|
||||
If a COM call throws, map the exception into a command reply instead of
|
||||
crashing the worker, unless the exception indicates process corruption or the
|
||||
configured policy says to fail the session.
|
||||
|
||||
## Cancellation
|
||||
|
||||
Worker cancellation is cooperative at the queue boundary.
|
||||
|
||||
Rules:
|
||||
|
||||
- If a `WorkerCancel` arrives before a command starts, mark the command
|
||||
canceled and reply or drop according to protocol policy.
|
||||
- If a command is already executing on the STA, do not attempt to abort the COM
|
||||
call.
|
||||
- When the COM call returns after gateway cancellation, send the reply only if
|
||||
the gateway still wants late replies; otherwise log and discard.
|
||||
- Hard cancellation is process kill by the gateway.
|
||||
|
||||
## Outbound Queues
|
||||
|
||||
The worker should use bounded outbound queues for replies, events, heartbeats,
|
||||
and faults.
|
||||
|
||||
Priority order when writing:
|
||||
|
||||
1. faults,
|
||||
2. command replies,
|
||||
3. shutdown acknowledgements,
|
||||
4. heartbeats,
|
||||
5. events.
|
||||
|
||||
Event overflow policy defaults to fail-fast for parity testing. If the event
|
||||
queue fills:
|
||||
|
||||
1. Capture overflow metrics.
|
||||
2. Send `WorkerFault` if possible.
|
||||
3. Stop accepting new commands.
|
||||
4. Let the gateway close or kill the worker.
|
||||
|
||||
Production coalescing may be added later, but it must be explicit and tested.
|
||||
Do not drop or coalesce events in v1.
|
||||
|
||||
## Heartbeat And Watchdog
|
||||
|
||||
The worker heartbeat should prove that:
|
||||
|
||||
- pipe writer is alive,
|
||||
- worker host is alive,
|
||||
- STA has recently pumped or completed work.
|
||||
|
||||
Heartbeat payload should include:
|
||||
|
||||
- worker process id,
|
||||
- session id,
|
||||
- current state,
|
||||
- last STA activity timestamp,
|
||||
- pending command count,
|
||||
- outbound event queue depth,
|
||||
- event sequence,
|
||||
- current command correlation id if any.
|
||||
|
||||
The STA watchdog should warn when:
|
||||
|
||||
- one command exceeds its expected duration,
|
||||
- the STA has not pumped messages within the heartbeat grace period,
|
||||
- event queue depth remains high.
|
||||
|
||||
The worker can report the problem, but the gateway owns the final kill decision.
|
||||
|
||||
## Shutdown
|
||||
|
||||
Graceful shutdown sequence:
|
||||
|
||||
1. Pipe reader receives `WorkerShutdown`.
|
||||
2. Worker host marks shutdown requested.
|
||||
3. Reject new commands.
|
||||
4. Let current STA command finish if within timeout.
|
||||
5. Optionally run MXAccess cleanup:
|
||||
- `UnAdvise`,
|
||||
- `RemoveItem`,
|
||||
- `Unregister`.
|
||||
6. Detach event handlers.
|
||||
7. Release COM object until reference count reaches zero when possible.
|
||||
8. Stop pipe reader and writer.
|
||||
9. Exit process with success code.
|
||||
|
||||
If shutdown wedges, the gateway kills the process. The worker should be written
|
||||
so process kill does not corrupt other sessions.
|
||||
|
||||
## Fault Handling
|
||||
|
||||
Worker fault categories:
|
||||
|
||||
- `InvalidArguments`
|
||||
- `GatewayAuthenticationFailed`
|
||||
- `ProtocolMismatch`
|
||||
- `ProtocolViolation`
|
||||
- `PipeDisconnected`
|
||||
- `MxAccessCreationFailed`
|
||||
- `MxAccessCommandFailed`
|
||||
- `MxAccessEventConversionFailed`
|
||||
- `StaHung`
|
||||
- `QueueOverflow`
|
||||
- `ShutdownTimeout`
|
||||
|
||||
Fault payload should include:
|
||||
|
||||
- category,
|
||||
- session id,
|
||||
- correlation id when command-specific,
|
||||
- command method when command-specific,
|
||||
- HRESULT when available,
|
||||
- exception type when available,
|
||||
- safe diagnostic message.
|
||||
|
||||
Do not include raw credentials or full secured-write values.
|
||||
|
||||
## Security
|
||||
|
||||
The worker should trust only the launching gateway after validating:
|
||||
|
||||
- expected session id,
|
||||
- expected protocol version,
|
||||
- nonce,
|
||||
- pipe identity where available.
|
||||
|
||||
It should not expose any network listener. It should not accept commands from
|
||||
arbitrary local processes.
|
||||
|
||||
Credential-bearing commands must keep credential data out of:
|
||||
|
||||
- command line,
|
||||
- logs,
|
||||
- metrics labels,
|
||||
- exception messages,
|
||||
- crash dumps when avoidable.
|
||||
|
||||
## Observability
|
||||
|
||||
Worker logs should include:
|
||||
|
||||
- startup arguments except secrets,
|
||||
- protocol version,
|
||||
- gateway handshake result,
|
||||
- MXAccess COM creation result,
|
||||
- command start/end with correlation id,
|
||||
- HRESULT/status summary,
|
||||
- event family and sequence,
|
||||
- queue overflow,
|
||||
- STA watchdog warnings,
|
||||
- shutdown path.
|
||||
|
||||
Metrics can be emitted through the gateway or exposed as worker heartbeat
|
||||
fields. The worker does not need its own public metrics endpoint.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Worker tests that do not require installed MXAccess:
|
||||
|
||||
- frame reader/writer,
|
||||
- protocol validation,
|
||||
- command queue ordering,
|
||||
- STA command scheduling with a fake COM object,
|
||||
- message-pump wake behavior where practical,
|
||||
- value conversion,
|
||||
- status conversion,
|
||||
- event conversion from fake event args,
|
||||
- shutdown state transitions,
|
||||
- queue overflow behavior.
|
||||
|
||||
Live MXAccess tests:
|
||||
|
||||
- COM creation on STA,
|
||||
- `Register` and `Unregister`,
|
||||
- `AddItem` and `RemoveItem`,
|
||||
- `Advise` and one `OnDataChange`,
|
||||
- write completion behavior,
|
||||
- secured write behavior,
|
||||
- buffered data-change behavior,
|
||||
- invalid handle behavior.
|
||||
- no synthesized `OperationComplete` when native MXAccess does not raise it.
|
||||
- raw metadata preservation for buffered payloads that cannot yet be fully
|
||||
converted.
|
||||
|
||||
Live tests should be opt-in and clearly marked because they depend on installed
|
||||
MXAccess COM and provider state.
|
||||
|
||||
## Initial Implementation Slice
|
||||
|
||||
The first worker slice should implement:
|
||||
|
||||
1. Argument parsing and pipe connection.
|
||||
2. Protocol hello and nonce validation.
|
||||
3. STA thread startup.
|
||||
4. COM initialization and MXAccess object creation.
|
||||
5. Message pump with command wake event.
|
||||
6. `WorkerReady`.
|
||||
7. Shutdown command.
|
||||
8. `Register`, `AddItem`, and `Advise`.
|
||||
9. Event sink for one `OnDataChange`.
|
||||
10. Basic value/status conversion.
|
||||
11. Event model coverage for `OperationComplete` and `OnBufferedDataChange`
|
||||
without synthesized events.
|
||||
12. Fault reporting.
|
||||
|
||||
This slice proves the worker can preserve the core MXAccess requirements:
|
||||
single-process isolation, STA ownership, message pumping, command execution,
|
||||
and event delivery.
|
||||
Reference in New Issue
Block a user