LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL adapter files, and related docs to deprecated/. Removed LmxProxy registration from DataConnectionFactory, project reference from DCL, protocol option from UI, and cleaned up all requirement docs.
168 lines
8.4 KiB
Markdown
168 lines
8.4 KiB
Markdown
# STA Message Pump Gap — OnWriteComplete COM Callback
|
|
|
|
**Status**: Documented gap. Fire-and-forget workaround in place (deviation #7). Full fix deferred until secured/verified writes are needed.
|
|
|
|
## When This Matters
|
|
|
|
The current fire-and-forget write approach works for **supervisory writes** where:
|
|
- Security is handled at the LmxProxy API key level, not MxAccess attribute level
|
|
- Writes succeed synchronously (no secured/verified write requirements)
|
|
- Write confirmation is handled at the application level (read-back in `WriteBatchAndWait`)
|
|
|
|
This gap becomes a **blocking issue** if any of these scenarios arise:
|
|
- **Secured writes (MxAccess error 1012)**: Attribute requires ArchestrA user authentication. `OnWriteComplete` returns the error, and the caller must retry with `WriteSecured()`.
|
|
- **Verified writes (MxAccess error 1013)**: Attribute requires two-user verification. Same retry pattern.
|
|
- **Write failure detection**: MxAccess accepts the `Write()` call but can't complete it (e.g., downstream device failure). `OnWriteComplete` is the only notification of this — without it, the caller assumes success.
|
|
|
|
## Root Cause
|
|
|
|
The MxAccess documentation (Write() Method) states: *"Upon completion of the write, your program receives notification of the success/failure status through the OnWriteComplete() event"* and *"that item should not be taken off advise or removed from the internal tables until the OnWriteComplete() event is received."*
|
|
|
|
`OnWriteComplete` **should** fire after every `Write()` call. It doesn't in our service because:
|
|
- MxAccess is a COM component designed for Windows Forms apps with a UI message loop
|
|
- COM event callbacks are delivered via the Windows message pump
|
|
- Our Topshelf Windows service has no message pump — `Write()` is called from thread pool threads (`Task.Run`) with no message loop
|
|
- `OnDataChange` works because MxAccess fires it proactively on its own internal threads; `OnWriteComplete` is a response callback that needs message-pump-based marshaling
|
|
|
|
## Correct Solution: Dedicated STA Thread + `Application.Run()`
|
|
|
|
Based on research (Stephen Toub, MSDN Magazine 2007; Microsoft Learn COM interop docs; community patterns), the correct approach is a dedicated STA thread running a Windows Forms message pump via `Application.Run()`.
|
|
|
|
### Architecture
|
|
|
|
```
|
|
Service main thread (MTA)
|
|
│
|
|
├── gRPC server threads (handle client RPCs)
|
|
│ │
|
|
│ └── Marshal COM calls via Form.BeginInvoke() ──┐
|
|
│ │
|
|
└── Dedicated STA thread │
|
|
│ │
|
|
├── Creates LMXProxyServerClass COM object │
|
|
├── Wires event handlers (OnDataChange, │
|
|
│ OnWriteComplete, OperationComplete) │
|
|
├── Runs Application.Run() ← continuous │
|
|
│ message pump │
|
|
│ │
|
|
└── Hidden Form receives BeginInvoke calls ◄────┘
|
|
│
|
|
├── Executes COM operations (Read, Write,
|
|
│ AddItem, AdviseSupervisory, etc.)
|
|
│
|
|
└── COM callbacks delivered via message pump
|
|
(OnWriteComplete, OnDataChange, etc.)
|
|
```
|
|
|
|
### Implementation Pattern
|
|
|
|
```csharp
|
|
// In MxAccessClient constructor or Start():
|
|
var initDone = new ManualResetEventSlim(false);
|
|
|
|
_staThread = new Thread(() =>
|
|
{
|
|
// 1. Create hidden form for marshaling
|
|
_marshalForm = new Form();
|
|
_marshalForm.CreateHandle(); // force HWND creation without showing
|
|
|
|
// 2. Create COM objects ON THIS THREAD
|
|
_lmxProxy = new LMXProxyServerClass();
|
|
_lmxProxy.OnDataChange += OnDataChange;
|
|
_lmxProxy.OnWriteComplete += OnWriteComplete;
|
|
|
|
// 3. Signal that init is complete
|
|
initDone.Set();
|
|
|
|
// 4. Run message pump (blocks forever, pumps COM callbacks)
|
|
Application.Run();
|
|
});
|
|
_staThread.Name = "MxAccess-STA";
|
|
_staThread.IsBackground = true;
|
|
_staThread.SetApartmentState(ApartmentState.STA);
|
|
_staThread.Start();
|
|
|
|
initDone.Wait(); // wait for COM objects to be ready
|
|
```
|
|
|
|
### Dispatching Work to the STA Thread
|
|
|
|
```csharp
|
|
// All COM calls must go through the hidden form's invoke:
|
|
public Task<Vtq> ReadAsync(string address, CancellationToken ct)
|
|
{
|
|
var tcs = new TaskCompletionSource<Vtq>();
|
|
_marshalForm.BeginInvoke((Action)(() =>
|
|
{
|
|
try
|
|
{
|
|
// COM call executes on STA thread
|
|
int handle = _lmxProxy.AddItem(_connectionHandle, address);
|
|
_lmxProxy.AdviseSupervisory(_connectionHandle, handle);
|
|
// ... etc
|
|
tcs.SetResult(vtq);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
tcs.SetException(ex);
|
|
}
|
|
}));
|
|
return tcs.Task;
|
|
}
|
|
```
|
|
|
|
### Shutdown
|
|
|
|
```csharp
|
|
// To stop the message pump:
|
|
_marshalForm.BeginInvoke((Action)(() =>
|
|
{
|
|
// Clean up COM objects on STA thread
|
|
// ... UnAdvise, RemoveItem, Unregister ...
|
|
Marshal.ReleaseComObject(_lmxProxy);
|
|
Application.ExitThread(); // stops Application.Run()
|
|
}));
|
|
_staThread.Join(TimeSpan.FromSeconds(10));
|
|
```
|
|
|
|
### Why Our First Attempt Failed
|
|
|
|
Our original `StaDispatchThread` (Phase 2) used `BlockingCollection.Take()` to wait for work items, with `Application.DoEvents()` between items. This failed because:
|
|
|
|
| Our failed approach | Correct approach |
|
|
|---|---|
|
|
| `BlockingCollection.Take()` blocks the STA thread, preventing the message pump from running | `Application.Run()` runs continuously, pumping messages at all times |
|
|
| `Application.DoEvents()` only pumps messages already in the queue at that instant | Message pump runs an infinite loop, processing messages as they arrive |
|
|
| Work dispatched by enqueueing to `BlockingCollection` | Work dispatched via `Form.BeginInvoke()` which posts a Windows message to the STA thread's queue |
|
|
|
|
The key difference: `BeginInvoke` posts a `WM_` message that the message pump processes alongside COM callbacks. `BlockingCollection` bypasses the message pump entirely.
|
|
|
|
## Drawbacks of the STA Approach
|
|
|
|
### Performance
|
|
- **All COM calls serialize onto one thread.** Under load (batch reads of 100+ tags), operations queue up single-file. Current `Task.Run` approach allows MxAccess's internal marshaling to handle some concurrency.
|
|
- **Double context switch per operation.** Caller → STA thread (invoke) → wait → back to caller. Adds ~0.1-1ms per call. Negligible for single reads, noticeable for large batch operations.
|
|
|
|
### Safety
|
|
- **Single point of failure.** If the STA thread dies, all MxAccess operations stop. Recovery requires tearing down and recreating the thread + all COM objects.
|
|
- **Deadlock risk.** If STA thread code synchronously waits on something that needs the STA thread (circular dependency), the message pump freezes. All waits must be async/non-blocking.
|
|
- **Reentrancy.** While pumping messages, inbound COM callbacks can reenter your code during another COM call. Event handlers must be reentrant-safe.
|
|
|
|
### Complexity
|
|
- Every COM call needs `_marshalForm.BeginInvoke()` wrapping.
|
|
- COM object affinity to STA thread is hard to enforce at compile time.
|
|
- Unit tests need STA thread support or must use fakes.
|
|
|
|
## Decision
|
|
|
|
Fire-and-forget is the correct choice for now. Revisit when secured/verified writes are needed.
|
|
|
|
## References
|
|
|
|
- [.NET Matters: Handling Messages in Console Apps — Stephen Toub, MSDN Magazine 2007](https://learn.microsoft.com/en-us/archive/msdn-magazine/2007/june/net-matters-handling-messages-in-console-apps)
|
|
- [How to: Support COM Interop by Displaying Each Windows Form on Its Own Thread — Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/advanced/how-to-support-com-interop-by-displaying-each-windows-form-on-its-own-thread)
|
|
- [.NET Windows Service needs STAThread — hirenppatel](https://hirenppatel.wordpress.com/2012/11/24/net-windows-service-needs-to-use-stathread-instead-of-mtathread/)
|
|
- [Application.Run() In a Windows Service — PC Review](https://www.pcreview.co.uk/threads/application-run-in-a-windows-service.3087159/)
|
|
- [Build a message pump for a Windows service? — CodeProject](https://www.codeproject.com/Messages/1365966/Build-a-message-pump-for-a-Windows-service.aspx)
|
|
- MxAccess Toolkit User's Guide — Write() Method, OnWriteComplete Callback sections
|