diff --git a/src/modules/awake/Awake/ComServerHost.cs b/src/common/ManagedCommon/RotSingletonHost.cs
similarity index 52%
rename from src/modules/awake/Awake/ComServerHost.cs
rename to src/common/ManagedCommon/RotSingletonHost.cs
index 810bfe2d8b..2506f9353d 100644
--- a/src/modules/awake/Awake/ComServerHost.cs
+++ b/src/common/ManagedCommon/RotSingletonHost.cs
@@ -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
{
///
- /// 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.
///
- internal static class ComServerHost
+ public sealed class RotSingletonHost : IDisposable
{
- private const string DefaultMonikerName = "Awake.Automation";
+ private readonly Lock _sync = new();
+ private readonly Func _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)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Moniker name (logical unique id), e.g. "Awake.Automation".
+ /// Factory that creates the object to expose. Should return a COM-visible object.
+ /// Optional thread name for diagnostics.
+ public RotSingletonHost(string monikerName, Func 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]
diff --git a/src/modules/awake/Awake/Program.cs b/src/modules/awake/Awake/Program.cs
index e2ac22ae7f..3262c5c0df 100644
--- a/src/modules/awake/Awake/Program.cs
+++ b/src/modules/awake/Awake/Program.cs
@@ -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) =>
{