This commit is contained in:
Leilei Zhang
2025-09-16 16:15:40 +08:00
parent f382b724a8
commit ba2d1fc3f5
2 changed files with 95 additions and 47 deletions

View File

@@ -5,58 +5,92 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using ManagedCommon;
namespace Awake
#nullable enable
#pragma warning disable IL2050 // Suppress COM interop trimming warnings for ROT hosting P/Invokes (desktop only scenario)
namespace ManagedCommon
{
/// <summary>
/// Background ROT host for the automation object. No registry / class factory registration; discovery is by moniker.
/// Generic helper to host a single COM-visible automation object in the Running Object Table (ROT)
/// without registry/CLSID class factory registration. Used for lightweight cross-process automation.
/// Pattern: create instance -> register with moniker -> wait until Stop.
/// Threading: spins up a dedicated STA thread so objects needing STA semantics are safe.
/// </summary>
internal static class ComServerHost
public sealed class RotSingletonHost : IDisposable
{
private const string DefaultMonikerName = "Awake.Automation";
private readonly Lock _sync = new();
private readonly Func<object> _factory;
private readonly string _monikerName;
private readonly string _threadName;
private readonly ManualResetEvent _shutdown = new(false);
private static readonly object SyncLock = new();
private static readonly ManualResetEvent ShutdownEvent = new(false);
private Thread? _thread;
private int _rotCookie;
private object? _instance; // keep alive
private IMoniker? _moniker;
private bool _disposed;
private static Thread? _rotThread;
private static int _rotCookie;
private static object? _automationInstance; // keep alive
private static IMoniker? _moniker;
public static void StartBackground(string? monikerName = null)
/// <summary>
/// Initializes a new instance of the <see cref="RotSingletonHost"/> class.
/// </summary>
/// <param name="monikerName">Moniker name (logical unique id), e.g. "Awake.Automation".</param>
/// <param name="factory">Factory that creates the object to expose. Should return a COM-visible object.</param>
/// <param name="threadName">Optional thread name for diagnostics.</param>
public RotSingletonHost(string monikerName, Func<object> factory, string? threadName = null)
{
lock (SyncLock)
_monikerName = string.IsNullOrWhiteSpace(monikerName) ? throw new ArgumentException("Moniker required", nameof(monikerName)) : monikerName;
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
_threadName = threadName ?? $"RotHost:{_monikerName}";
}
public bool IsRunning => _thread != null;
public string MonikerName => _monikerName;
public void Start()
{
lock (_sync)
{
if (_rotThread != null)
if (_disposed)
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
if (_thread != null)
{
return; // already running
}
_thread = new Thread(ThreadMain)
{
IsBackground = true,
Name = _threadName,
};
_thread.SetApartmentState(ApartmentState.STA);
_thread.Start();
Logger.LogInfo($"ROT host starting for moniker '{_monikerName}'");
}
}
public void Stop()
{
lock (_sync)
{
if (_thread == null)
{
return;
}
string name = string.IsNullOrWhiteSpace(monikerName) ? DefaultMonikerName : monikerName!;
_rotThread = new Thread(() => RotThreadMain(name))
{
Name = "AwakeAutomationRotThread",
IsBackground = true,
};
_rotThread.SetApartmentState(ApartmentState.STA);
_rotThread.Start();
Logger.LogInfo($"Starting Awake automation ROT host with moniker '{name}'");
}
}
public static void Stop()
{
lock (SyncLock)
{
ShutdownEvent.Set();
_shutdown.Set();
}
_rotThread?.Join(3000);
_rotThread = null;
_thread?.Join(3000);
_thread = null;
_shutdown.Reset();
}
private static void RotThreadMain(string monikerName)
private void ThreadMain()
{
int hr = Ole32.CoInitializeEx(IntPtr.Zero, Ole32.CoinitApartmentThreaded);
if (hr < 0)
@@ -74,18 +108,18 @@ namespace Awake
return;
}
hr = Ole32.CreateItemMoniker("!", monikerName, out _moniker);
hr = Ole32.CreateItemMoniker("!", _monikerName, out _moniker);
if (hr < 0 || _moniker == null)
{
Logger.LogError($"CreateItemMoniker failed: 0x{hr:X8}");
return;
}
_automationInstance = new AwakeAutomation();
var unk = Marshal.GetIUnknownForObject(_automationInstance);
_instance = _factory();
var unk = Marshal.GetIUnknownForObject(_instance);
try
{
hr = rot.Register(0x1 /* ROTFLAGS_REGISTRATIONKEEPSALIVE */, _automationInstance, _moniker, out _rotCookie);
hr = rot.Register(0x1 /* ROTFLAGS_REGISTRATIONKEEPSALIVE */, _instance, _moniker, out _rotCookie);
if (hr < 0)
{
Logger.LogError($"IRunningObjectTable.Register failed: 0x{hr:X8}");
@@ -97,20 +131,20 @@ namespace Awake
Marshal.Release(unk);
}
Logger.LogInfo("Awake automation registered in ROT.");
WaitHandle.WaitAny(new WaitHandle[] { ShutdownEvent });
Logger.LogInfo($"ROT registered: '{_monikerName}'");
WaitHandle.WaitAny(new WaitHandle[] { _shutdown });
}
catch (Exception ex)
{
Logger.LogError($"Automation ROT exception: {ex}");
Logger.LogError($"ROT host exception: {ex}");
}
finally
{
try
{
if (_rotCookie != 0 && Ole32.GetRunningObjectTable(0, out var rot) == 0 && rot != null)
if (_rotCookie != 0 && Ole32.GetRunningObjectTable(0, out var rot2) == 0 && rot2 != null)
{
rot.Revoke(_rotCookie);
rot2.Revoke(_rotCookie);
_rotCookie = 0;
}
}
@@ -120,14 +154,26 @@ namespace Awake
}
Ole32.CoUninitialize();
Logger.LogInfo("Awake automation ROT host stopped.");
Logger.LogInfo($"ROT host stopped: '{_monikerName}'");
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
Stop();
_disposed = true;
}
private static class Ole32
{
internal const int CoinitApartmentThreaded = 0x2;
#pragma warning disable IL2050 // Suppress trimming warnings for COM interop P/Invokes; ROT hosting not used in trimmed scenarios.
[DllImport("ole32.dll")]
internal static extern int CoInitializeEx(IntPtr pvReserved, int dwCoInit);
@@ -139,6 +185,7 @@ namespace Awake
[DllImport("ole32.dll")]
internal static extern int CreateItemMoniker([MarshalAs(UnmanagedType.LPWStr)] string lpszDelim, [MarshalAs(UnmanagedType.LPWStr)] string lpszItem, out IMoniker? ppmk);
#pragma warning restore IL2050
}
[ComImport]

View File

@@ -98,8 +98,9 @@ namespace Awake
// Start background COM automation host (ROT) so any startup path exposes the automation surface.
// Uses default moniker; could be extended with a --rotname parameter if needed later.
ComServerHost.StartBackground();
AppDomain.CurrentDomain.ProcessExit += (_, _) => ComServerHost.Stop();
var rotHost = new RotSingletonHost("Awake.Automation", () => new AwakeAutomation(), "AwakeAutomationRotThread");
rotHost.Start();
AppDomain.CurrentDomain.ProcessExit += (_, _) => rotHost.Stop();
TaskScheduler.UnobservedTaskException += (sender, args) =>
{