32f26272ae
Five tools under one repo, all docs organized per DOCS-GUIDE.md: - aalogcli: .NET 4.8 / x86 CliFx CLI for reading System Platform binary logs (*.aaLGX) for LLM debugging, built on aaOpenSource/aaLog. Commands: last, tail, range, unread, fields. Stable JSON envelope under --llm-json. Build template under lib/build/ for rebuilding aaLogReader.dll. - aot: ArchestrA Object Toolkit 2014 v4.0 reference material. Dev guide (Markdown converted from CHM), API reference for the ArchestrA.Toolkit namespace, and the Monitor / Watchdog VS sample solutions. - graccesscli: .NET 4.8 / x86 CliFx CLI that automates Galaxy configuration via the ArchestrA GRAccess COM interop. Includes session daemon, IPC protocol, and llm-json envelope contract. - grdb: SQL/DDL exploration of the Galaxy Repository database. DDL captures, reusable queries, hierarchy / contained-name <-> tag-name translation notes. - histdb: LLM-oriented reference for AVEVA Historian retrieval. INSQL linked-server, extension tables, every wwXxx time-domain extension, every retrieval mode, alarm/event SQL recipes, REST API. Distilled from the 243-page Historian Retrieval Guide. Root contains: - CLAUDE.md: thin index pointing into each tool's README. - DOCS-GUIDE.md: doctrine for organizing docs for LLM consumption. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
216 lines
6.7 KiB
C#
216 lines
6.7 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Serilog;
|
|
|
|
namespace ZB.MOM.WW.GRAccess.Cli.Session
|
|
{
|
|
/// <summary>
|
|
/// Dedicated STA thread with a raw Win32 message pump for COM interop.
|
|
/// All GRAccess COM objects must be created and called on this thread.
|
|
/// </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 = "GRAccess-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
|
|
}
|
|
}
|