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) => {