File: Instance\RunningObjectTable.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
#if FEATURE_WINDOWSINTEROP
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
#endif
 
#nullable disable
 
namespace Microsoft.Build.Execution
{
    /// <summary>
    /// Wrapper for the COM Running Object Table.
    /// </summary>
    /// <remarks>
    /// See https://docs.microsoft.com/en-us/windows/desktop/api/objidl/nn-objidl-irunningobjecttable.
    /// </remarks>
    internal class RunningObjectTable : IRunningObjectTableWrapper
    {
#if FEATURE_WINDOWSINTEROP
        // Cached pointer to the IRunningObjectTable obtained on an MTA thread. Stored as nint
        // because ComScope<T> is a ref struct and cannot be a field. Released by the finalizer.
        private readonly Task<nint> _rotTask;
#endif
 
        public RunningObjectTable()
        {
#if FEATURE_WINDOWSINTEROP
            if (!NativeMethodsShared.IsWindows)
            {
                return;
            }
 
            if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA)
            {
                _rotTask = Task.FromResult(GetRunningObjectTablePointer());
            }
            else
            {
                // To avoid deadlock, create ROT on a threadpool thread which guarantees MTA.
                _rotTask = Task.Run(GetRunningObjectTablePointer);
            }
#endif
        }
 
#if FEATURE_WINDOWSINTEROP
        ~RunningObjectTable()
        {
            if (_rotTask is not null && _rotTask.Status == TaskStatus.RanToCompletion)
            {
                ReleasePointer(_rotTask.Result);
            }
        }
 
        [SupportedOSPlatform("windows5.0")]
        private static unsafe nint GetRunningObjectTablePointer()
        {
            IRunningObjectTable* rot;
            HRESULT hr = PInvoke.GetRunningObjectTable(0, &rot);
            return hr.Succeeded ? (nint)rot : 0;
        }
 
        private static unsafe void ReleasePointer(nint p)
        {
            if (p != 0)
            {
                ((IUnknown*)p)->Release();
            }
        }
#endif
 
        /// <summary>
        /// Attempts to retrieve an item from the ROT.
        /// </summary>
#if FEATURE_WINDOWSINTEROP
        [SupportedOSPlatform("windows5.0")]
#endif
        public object GetObject(string itemName)
        {
#if FEATURE_WINDOWSINTEROP
            return GetObjectWindows(itemName);
#else
            throw new PlatformNotSupportedException();
#endif
        }
 
#if FEATURE_WINDOWSINTEROP
        [SupportedOSPlatform("windows5.0")]
        private unsafe object GetObjectWindows(string itemName)
        {
            nint rotPtr = _rotTask.GetAwaiter().GetResult();
            if (rotPtr == 0)
            {
                throw new COMException("Failed to obtain the Running Object Table.");
            }
 
            // The IRunningObjectTable pointer was obtained on an MTA thread; calling through
            // it from an STA caller would bypass COM apartment marshalling. Run all ROT calls
            // on an MTA thread so the moniker creation and GetObject invocation happen in the
            // same apartment as the pointer's owner.
            if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA)
            {
                return GetObjectCore(rotPtr, itemName);
            }
 
            return Task.Run(() => GetObjectCore(rotPtr, itemName)).GetAwaiter().GetResult();
        }
 
        [SupportedOSPlatform("windows5.0")]
        private static unsafe object GetObjectCore(nint rotPtr, string itemName)
        {
            IRunningObjectTable* rot = (IRunningObjectTable*)rotPtr;
 
            IMoniker* monikerRaw;
            HRESULT hr = PInvoke.CreateItemMoniker("!", itemName, &monikerRaw);
            if (hr.Failed)
            {
                throw new COMException("CreateItemMoniker failed", hr);
            }
 
            using ComScope<IMoniker> moniker = new(monikerRaw);
 
            IUnknown* pUnk = null;
            hr = rot->GetObject(moniker.Pointer, &pUnk);
            if (hr.Failed)
            {
                ThrowComExceptionWithDetails(hr, itemName);
            }
 
            try
            {
                return Marshal.GetObjectForIUnknown((IntPtr)pUnk);
            }
            finally
            {
                if (pUnk is not null)
                {
                    pUnk->Release();
                }
            }
        }
 
        /// <summary>
        /// Throws an exception with detailed COM error information if available.
        /// </summary>
        [SupportedOSPlatform("windows5.0")]
        private static unsafe void ThrowComExceptionWithDetails(int hr, string itemName)
        {
            StringBuilder errorMessage = new();
            errorMessage.AppendLine($"Failed to get object '{itemName}' from Running Object Table.");
            errorMessage.AppendLine($"HRESULT: 0x{hr:X8} ({hr})");
 
            IErrorInfo* pErrorInfo = null;
            HRESULT errInfoHr = PInvoke.GetErrorInfo(0, &pErrorInfo);
            if (errInfoHr == HRESULT.S_OK && pErrorInfo is not null)
            {
                BSTR description = default;
                BSTR source = default;
                BSTR helpFile = default;
                uint helpContext = 0;
 
                try
                {
                    pErrorInfo->GetDescription(&description);
                    pErrorInfo->GetSource(&source);
                    pErrorInfo->GetHelpFile(&helpFile);
                    pErrorInfo->GetHelpContext(out helpContext);
 
                    AppendIfNotEmpty("description", description.ToString());
                    AppendIfNotEmpty("source", source.ToString());
                    AppendIfNotEmpty("help file", helpFile.ToString());
                    if (helpContext != 0)
                    {
                        errorMessage.AppendLine($"help context: {helpContext}");
                    }
                }
                catch
                {
                    // If we can't get error info details, just continue with the basic code.
                }
                finally
                {
                    if (description.Value is not null)
                    {
                        PInvoke.SysFreeString(description);
                    }
                    if (source.Value is not null)
                    {
                        PInvoke.SysFreeString(source);
                    }
                    if (helpFile.Value is not null)
                    {
                        PInvoke.SysFreeString(helpFile);
                    }
                    pErrorInfo->Release();
                }
            }
 
            throw new COMException(errorMessage.ToString(), hr);
 
            void AppendIfNotEmpty(string label, string value)
            {
                if (!string.IsNullOrEmpty(value))
                {
                    errorMessage.AppendLine($"{label}: {value}");
                }
            }
        }
#endif
    }
}