Compare commits
5 Commits
bc4fc97652
...
a326a8cbde
| Author | SHA1 | Date | |
|---|---|---|---|
| a326a8cbde | |||
| a59d4ad76c | |||
| b6408726bc | |||
| c96e71c83c | |||
| fa33e1acf1 |
@@ -8,12 +8,13 @@ Two instances of the LmxProxy v2 Host service are deployed on windev (10.100.0.4
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Service Name** | `ZB.MOM.WW.LmxProxy.Host.V2` | `ZB.MOM.WW.LmxProxy.Host.V2B` |
|
| **Service Name** | `ZB.MOM.WW.LmxProxy.Host.V2` | `ZB.MOM.WW.LmxProxy.Host.V2B` |
|
||||||
| **Display Name** | SCADA Bridge LMX Proxy V2 | SCADA Bridge LMX Proxy V2B |
|
| **Display Name** | SCADA Bridge LMX Proxy V2 | SCADA Bridge LMX Proxy V2B |
|
||||||
|
| **MxAccess Client Name** | `LmxProxy-A` | `LmxProxy-B` |
|
||||||
| **Publish Directory** | `C:\publish-v2\` | `C:\publish-v2b\` |
|
| **Publish Directory** | `C:\publish-v2\` | `C:\publish-v2b\` |
|
||||||
| **gRPC Port** | 50100 | 50101 |
|
| **gRPC Port** | 50100 | 50101 |
|
||||||
| **HTTP Status Port** | 8081 | 8082 |
|
| **HTTP Status Port** | 8081 | 8082 |
|
||||||
| **Log File Prefix** | `lmxproxy-v2-` | `lmxproxy-v2b-` |
|
| **Log File Prefix** | `lmxproxy-v2-` | `lmxproxy-v2b-` |
|
||||||
| **Log Directory** | `C:\publish-v2\logs\` | `C:\publish-v2b\logs\` |
|
| **Log Directory** | `C:\publish-v2\logs\` | `C:\publish-v2b\logs\` |
|
||||||
| **Health Probe Tag** | `DevAppEngine.Scheduler.ScanTime` | `DevAppEngine.Scheduler.ScanTime` |
|
| **Health Probe Tag** | `DevPlatform.Scheduler.ScanTime` | `DevPlatform.Scheduler.ScanTime` |
|
||||||
| **API Keys File** | `C:\publish-v2\apikeys.json` | `C:\publish-v2b\apikeys.json` |
|
| **API Keys File** | `C:\publish-v2\apikeys.json` | `C:\publish-v2b\apikeys.json` |
|
||||||
| **Auto-Start** | Yes | Yes |
|
| **Auto-Start** | Yes | Yes |
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ namespace ZB.MOM.WW.LmxProxy.Host.Configuration
|
|||||||
/// <summary>Path to API key configuration file. Default: apikeys.json.</summary>
|
/// <summary>Path to API key configuration file. Default: apikeys.json.</summary>
|
||||||
public string ApiKeyConfigFile { get; set; } = "apikeys.json";
|
public string ApiKeyConfigFile { get; set; } = "apikeys.json";
|
||||||
|
|
||||||
|
/// <summary>Unique client name for MxAccess Register(). Must be unique per instance. Default: auto-generated.</summary>
|
||||||
|
public string? ClientName { get; set; }
|
||||||
|
|
||||||
/// <summary>MxAccess connection settings.</summary>
|
/// <summary>MxAccess connection settings.</summary>
|
||||||
public ConnectionConfiguration Connection { get; set; } = new ConnectionConfiguration();
|
public ConnectionConfiguration Connection { get; set; } = new ConnectionConfiguration();
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ namespace ZB.MOM.WW.LmxProxy.Host
|
|||||||
probeTestTagAddress: _config.HealthCheck.TestTagAddress,
|
probeTestTagAddress: _config.HealthCheck.TestTagAddress,
|
||||||
probeTimeoutMs: _config.HealthCheck.ProbeTimeoutMs,
|
probeTimeoutMs: _config.HealthCheck.ProbeTimeoutMs,
|
||||||
maxConsecutiveTransportFailures: _config.HealthCheck.MaxConsecutiveTransportFailures,
|
maxConsecutiveTransportFailures: _config.HealthCheck.MaxConsecutiveTransportFailures,
|
||||||
degradedProbeIntervalMs: _config.HealthCheck.DegradedProbeIntervalMs);
|
degradedProbeIntervalMs: _config.HealthCheck.DegradedProbeIntervalMs,
|
||||||
|
clientName: _config.ClientName);
|
||||||
|
|
||||||
// 5. Connect to MxAccess synchronously (with timeout)
|
// 5. Connect to MxAccess synchronously (with timeout)
|
||||||
Log.Information("Connecting to MxAccess (timeout: {Timeout}s)...",
|
Log.Information("Connecting to MxAccess (timeout: {Timeout}s)...",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
public sealed partial class MxAccessClient
|
public sealed partial class MxAccessClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connects to MxAccess via Task.Run (thread pool).
|
/// Connects to MxAccess on the dedicated STA thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task ConnectAsync(CancellationToken ct = default)
|
public async Task ConnectAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -23,7 +23,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Run(() => ConnectInternal(), ct);
|
await _staThread.RunAsync(() => ConnectInternal());
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
@@ -46,7 +46,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Disconnects from MxAccess via Task.Run (thread pool).
|
/// Disconnects from MxAccess on the dedicated STA thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task DisconnectAsync(CancellationToken ct = default)
|
public async Task DisconnectAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -56,7 +56,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Run(() => DisconnectInternal());
|
await _staThread.RunAsync(() => DisconnectInternal());
|
||||||
|
|
||||||
SetState(ConnectionState.Disconnected);
|
SetState(ConnectionState.Disconnected);
|
||||||
Log.Information("Disconnected from MxAccess");
|
Log.Information("Disconnected from MxAccess");
|
||||||
@@ -107,8 +107,9 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
_lmxProxy.OnDataChange += OnDataChange;
|
_lmxProxy.OnDataChange += OnDataChange;
|
||||||
_lmxProxy.OnWriteComplete += OnWriteComplete;
|
_lmxProxy.OnWriteComplete += OnWriteComplete;
|
||||||
|
|
||||||
// Register with MxAccess
|
// Register with MxAccess using unique client name
|
||||||
_connectionHandle = _lmxProxy.Register("ZB.MOM.WW.LmxProxy.Host");
|
_connectionHandle = _lmxProxy.Register(_clientName);
|
||||||
|
Log.Information("Registered with MxAccess as '{ClientName}'", _clientName);
|
||||||
|
|
||||||
if (_connectionHandle <= 0)
|
if (_connectionHandle <= 0)
|
||||||
{
|
{
|
||||||
@@ -346,13 +347,13 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cleans up COM objects via Task.Run after a failed connection.
|
/// Cleans up COM objects on the dedicated STA thread after a failed connection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CleanupComObjectsAsync()
|
private async Task CleanupComObjectsAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Run(() =>
|
await _staThread.RunAsync(() =>
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using ArchestrA.MxAccess;
|
using ArchestrA.MxAccess;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using ZB.MOM.WW.LmxProxy.Host.Domain;
|
using ZB.MOM.WW.LmxProxy.Host.Domain;
|
||||||
@@ -27,6 +28,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Log.Information("OnDataChange FIRED: handle={Handle}", phItemHandle);
|
||||||
var quality = MapQuality(pwItemQuality);
|
var quality = MapQuality(pwItemQuality);
|
||||||
var timestamp = ConvertTimestamp(pftItemTimeStamp);
|
var timestamp = ConvertTimestamp(pftItemTimeStamp);
|
||||||
|
|
||||||
|
|||||||
@@ -183,14 +183,14 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Internal write implementation using Task.Run for COM calls.
|
/// Internal write implementation dispatched on the STA thread.
|
||||||
/// MxAccess completes supervisory writes synchronously — the Write() call
|
/// MxAccess completes supervisory writes synchronously — the Write() call
|
||||||
/// succeeding (not throwing) confirms the write. The OnWriteComplete callback
|
/// succeeding (not throwing) confirms the write. The OnWriteComplete callback
|
||||||
/// is kept wired for diagnostic logging but is not awaited.
|
/// is kept wired for diagnostic logging but is not awaited.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task WriteInternalAsync(string address, object value, CancellationToken ct)
|
private async Task WriteInternalAsync(string address, object value, CancellationToken ct)
|
||||||
{
|
{
|
||||||
await Task.Run(() =>
|
await _staThread.RunAsync(() =>
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
@@ -243,7 +243,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, ct);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Subscribes to value changes for the specified addresses.
|
/// Subscribes to value changes for the specified addresses.
|
||||||
/// Stores subscription state for reconnect replay.
|
/// Stores subscription state for reconnect replay.
|
||||||
/// COM calls dispatched via Task.Run.
|
/// COM calls dispatched on the dedicated STA thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<IAsyncDisposable> SubscribeAsync(
|
public async Task<IAsyncDisposable> SubscribeAsync(
|
||||||
IEnumerable<string> addresses,
|
IEnumerable<string> addresses,
|
||||||
@@ -25,7 +25,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
|
|
||||||
var addressList = addresses.ToList();
|
var addressList = addresses.ToList();
|
||||||
|
|
||||||
await Task.Run(() =>
|
await _staThread.RunAsync(() =>
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
@@ -40,7 +40,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
_storedSubscriptions[address] = callback;
|
_storedSubscriptions[address] = callback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, ct);
|
});
|
||||||
|
|
||||||
Log.Information("Subscribed to {Count} tags", addressList.Count);
|
Log.Information("Subscribed to {Count} tags", addressList.Count);
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
{
|
{
|
||||||
var addressList = addresses.ToList();
|
var addressList = addresses.ToList();
|
||||||
|
|
||||||
await Task.Run(() =>
|
await _staThread.RunAsync(() =>
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
@@ -93,7 +93,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
|
|
||||||
Log.Information("Recreating {Count} stored subscriptions after reconnect", subscriptions.Count);
|
Log.Information("Recreating {Count} stored subscriptions after reconnect", subscriptions.Count);
|
||||||
|
|
||||||
await Task.Run(() =>
|
await _staThread.RunAsync(() =>
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wraps the ArchestrA MXAccess COM API. All COM operations
|
/// Wraps the ArchestrA MXAccess COM API. All COM operations
|
||||||
/// execute via Task.Run (thread pool / MTA), relying on COM
|
/// execute on a dedicated STA thread with a Windows message pump
|
||||||
/// marshaling to handle cross-apartment calls.
|
/// so that COM callbacks (OnDataChange, OnWriteComplete) are
|
||||||
|
/// delivered correctly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class MxAccessClient : IScadaClient
|
public sealed partial class MxAccessClient : IScadaClient
|
||||||
{
|
{
|
||||||
@@ -25,11 +26,15 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
private readonly bool _autoReconnect;
|
private readonly bool _autoReconnect;
|
||||||
private readonly string? _nodeName;
|
private readonly string? _nodeName;
|
||||||
private readonly string? _galaxyName;
|
private readonly string? _galaxyName;
|
||||||
|
private readonly string _clientName;
|
||||||
|
|
||||||
private readonly SemaphoreSlim _readSemaphore;
|
private readonly SemaphoreSlim _readSemaphore;
|
||||||
private readonly SemaphoreSlim _writeSemaphore;
|
private readonly SemaphoreSlim _writeSemaphore;
|
||||||
|
|
||||||
// COM objects
|
// STA thread for COM interop
|
||||||
|
private readonly StaComThread _staThread;
|
||||||
|
|
||||||
|
// COM objects — only accessed on the STA thread
|
||||||
private LMXProxyServer? _lmxProxy;
|
private LMXProxyServer? _lmxProxy;
|
||||||
private int _connectionHandle;
|
private int _connectionHandle;
|
||||||
|
|
||||||
@@ -77,7 +82,8 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
string? probeTestTagAddress = null,
|
string? probeTestTagAddress = null,
|
||||||
int probeTimeoutMs = 5000,
|
int probeTimeoutMs = 5000,
|
||||||
int maxConsecutiveTransportFailures = 3,
|
int maxConsecutiveTransportFailures = 3,
|
||||||
int degradedProbeIntervalMs = 30000)
|
int degradedProbeIntervalMs = 30000,
|
||||||
|
string? clientName = null)
|
||||||
{
|
{
|
||||||
_maxConcurrentOperations = maxConcurrentOperations;
|
_maxConcurrentOperations = maxConcurrentOperations;
|
||||||
_readTimeoutMs = readTimeoutSeconds * 1000;
|
_readTimeoutMs = readTimeoutSeconds * 1000;
|
||||||
@@ -90,9 +96,13 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
_probeTimeoutMs = probeTimeoutMs;
|
_probeTimeoutMs = probeTimeoutMs;
|
||||||
_maxConsecutiveTransportFailures = maxConsecutiveTransportFailures;
|
_maxConsecutiveTransportFailures = maxConsecutiveTransportFailures;
|
||||||
_degradedProbeIntervalMs = degradedProbeIntervalMs;
|
_degradedProbeIntervalMs = degradedProbeIntervalMs;
|
||||||
|
_clientName = clientName ?? "LmxProxy-" + Guid.NewGuid().ToString("N").Substring(0, 8);
|
||||||
|
|
||||||
_readSemaphore = new SemaphoreSlim(maxConcurrentOperations, maxConcurrentOperations);
|
_readSemaphore = new SemaphoreSlim(maxConcurrentOperations, maxConcurrentOperations);
|
||||||
_writeSemaphore = new SemaphoreSlim(maxConcurrentOperations, maxConcurrentOperations);
|
_writeSemaphore = new SemaphoreSlim(maxConcurrentOperations, maxConcurrentOperations);
|
||||||
|
|
||||||
|
_staThread = new StaComThread();
|
||||||
|
_staThread.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsConnected
|
public bool IsConnected
|
||||||
@@ -152,6 +162,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
_readSemaphore.Dispose();
|
_readSemaphore.Dispose();
|
||||||
_writeSemaphore.Dispose();
|
_writeSemaphore.Dispose();
|
||||||
_reconnectCts?.Dispose();
|
_reconnectCts?.Dispose();
|
||||||
|
_staThread.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dedicated STA thread with a raw Win32 message pump for COM interop.
|
||||||
|
/// All MxAccess COM objects must be created and called on this thread
|
||||||
|
/// so that COM callbacks (OnDataChange, OnWriteComplete) are delivered
|
||||||
|
/// via the message loop.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StaComThread : IDisposable
|
||||||
|
{
|
||||||
|
private const uint WM_APP = 0x8000;
|
||||||
|
private const uint PM_NOREMOVE = 0x0000;
|
||||||
|
|
||||||
|
private static readonly ILogger Log = Serilog.Log.ForContext<StaComThread>();
|
||||||
|
|
||||||
|
private readonly Thread _thread;
|
||||||
|
private readonly TaskCompletionSource<bool> _ready = new TaskCompletionSource<bool>();
|
||||||
|
private readonly ConcurrentQueue<Action> _workItems = new ConcurrentQueue<Action>();
|
||||||
|
private volatile uint _nativeThreadId;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public StaComThread()
|
||||||
|
{
|
||||||
|
_thread = new Thread(ThreadEntry)
|
||||||
|
{
|
||||||
|
Name = "MxAccess-STA",
|
||||||
|
IsBackground = true
|
||||||
|
};
|
||||||
|
_thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the STA thread and waits until the message pump is running.
|
||||||
|
/// </summary>
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_thread.Start();
|
||||||
|
_ready.Task.GetAwaiter().GetResult();
|
||||||
|
Log.Information("STA COM thread started (ThreadId={ThreadId})", _thread.ManagedThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marshals a synchronous action onto the STA thread and returns a Task
|
||||||
|
/// that completes when the action finishes.
|
||||||
|
/// </summary>
|
||||||
|
public Task RunAsync(Action action)
|
||||||
|
{
|
||||||
|
if (_disposed) throw new ObjectDisposedException(nameof(StaComThread));
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
_workItems.Enqueue(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
tcs.TrySetResult(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
tcs.TrySetException(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero);
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marshals a synchronous function onto the STA thread and returns
|
||||||
|
/// a Task<T> with the result.
|
||||||
|
/// </summary>
|
||||||
|
public Task<T> RunAsync<T>(Func<T> func)
|
||||||
|
{
|
||||||
|
if (_disposed) throw new ObjectDisposedException(nameof(StaComThread));
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<T>();
|
||||||
|
_workItems.Enqueue(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
tcs.TrySetResult(func());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
tcs.TrySetException(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero);
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_nativeThreadId != 0)
|
||||||
|
PostThreadMessage(_nativeThreadId, WM_APP + 1, IntPtr.Zero, IntPtr.Zero);
|
||||||
|
_thread.Join(TimeSpan.FromSeconds(5));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Error shutting down STA COM thread");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Information("STA COM thread stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThreadEntry()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_nativeThreadId = GetCurrentThreadId();
|
||||||
|
|
||||||
|
// Force message queue creation by peeking
|
||||||
|
MSG msg;
|
||||||
|
PeekMessage(out msg, IntPtr.Zero, 0, 0, PM_NOREMOVE);
|
||||||
|
|
||||||
|
_ready.TrySetResult(true);
|
||||||
|
|
||||||
|
// Run the message loop — blocks until WM_QUIT
|
||||||
|
while (GetMessage(out msg, IntPtr.Zero, 0, 0) > 0)
|
||||||
|
{
|
||||||
|
if (msg.message == WM_APP)
|
||||||
|
{
|
||||||
|
DrainQueue();
|
||||||
|
}
|
||||||
|
else if (msg.message == WM_APP + 1)
|
||||||
|
{
|
||||||
|
// Shutdown signal — drain remaining work then quit
|
||||||
|
DrainQueue();
|
||||||
|
PostQuitMessage(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TranslateMessage(ref msg);
|
||||||
|
DispatchMessage(ref msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "STA COM thread crashed");
|
||||||
|
_ready.TrySetException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrainQueue()
|
||||||
|
{
|
||||||
|
while (_workItems.TryDequeue(out var workItem))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
workItem();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Unhandled exception in STA work item");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Win32 PInvoke
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct MSG
|
||||||
|
{
|
||||||
|
public IntPtr hwnd;
|
||||||
|
public uint message;
|
||||||
|
public IntPtr wParam;
|
||||||
|
public IntPtr lParam;
|
||||||
|
public uint time;
|
||||||
|
public POINT pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct POINT
|
||||||
|
{
|
||||||
|
public int x;
|
||||||
|
public int y;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool TranslateMessage(ref MSG lpMsg);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr DispatchMessage(ref MSG lpMsg);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool PostThreadMessage(uint idThread, uint Msg, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern void PostQuitMessage(int nExitCode);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern uint GetCurrentThreadId();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user