| File: VisualStudioLocationHelper.cs | Web Access |
| Project: src\msbuild\src\Framework\Microsoft.Build.Framework.csproj (Microsoft.Build.Framework) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; #if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP using Microsoft.Build.Shared.VisualStudio; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.System.Com; #endif namespace Microsoft.Build.Shared { /// <summary> /// Helper class that queries the Visual Studio Setup Configuration COM API for instances /// of Visual Studio installed on the machine. Will not include anything before VS "15". /// </summary> /// <remarks> /// Uses manually-defined struct-based COM declarations under <c>Shared/VisualStudio/</c> /// (matching the CsWin32 struct-based COM pattern used elsewhere in the repo) so this /// project does not need a reference to the legacy /// <c>Microsoft.VisualStudio.Setup.Configuration.Interop</c> RCW package. All COM pointer /// lifetimes are managed via <c>ComScope<T></c>. /// </remarks> internal static unsafe class VisualStudioLocationHelper { /// <summary> /// Query the Visual Studio setup API to get instances of Visual Studio installed /// on the machine. Will not include anything before Visual Studio "15". /// </summary> /// <returns>Enumerable list of Visual Studio instances</returns> internal static IList<VisualStudioInstance> GetInstances() { var validInstances = new List<VisualStudioInstance>(); #if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP // No try/catch around the COM call chain by design: AcquireSetupConfiguration2 // and PopulateInstances check HRESULTs inline and return gracefully on failure. // If a future maintainer introduces a ThrowOnFailure() in this path, add the // matching catch — "empty list" is the documented behaviour for absent VS. try { using ComScope<ISetupConfiguration2> config = AcquireSetupConfiguration2(); if (!config.IsNull) { PopulateInstances(config.Pointer, validInstances); } } catch (DllNotFoundException) { // The Setup Configuration native helper isn't on disk — VS "15" or newer // is likely not installed. Fall through with whatever has been collected. } #endif return validInstances; } #if FEATURE_WINDOWSINTEROP && FEATURE_VISUALSTUDIOSETUP /// <summary> /// Try to obtain a top-level <see cref="ISetupConfiguration2"/> pointer. First attempts /// <c>CoCreateInstance</c> on the registered coclass; on /// <see cref="HRESULT.REGDB_E_CLASSNOTREG"/> falls back to the app-local /// <c>GetSetupConfiguration</c> entry point exported by /// <c>Microsoft.VisualStudio.Setup.Configuration.Native.dll</c> (which requires that /// helper DLL to be loadable but doesn't require COM registration). Any other COM /// failure is returned as a null scope — the caller treats absent VS Setup as an /// empty result, not an error. /// </summary> private static ComScope<ISetupConfiguration2> AcquireSetupConfiguration2() { Guid clsid = SetupConfiguration.CLSID_SetupConfiguration; Guid iidConfig2 = ISetupConfiguration2.IID_ISetupConfiguration2; ComScope<ISetupConfiguration2> config2 = new(); HRESULT hr = PInvoke.CoCreateInstance(&clsid, pUnkOuter: null, CLSCTX.CLSCTX_INPROC_SERVER, &iidConfig2, config2); if (hr.Succeeded) { return config2; } if (hr != HRESULT.REGDB_E_CLASSNOTREG) { // Some other COM failure. The caller swallows errors, so signal "no result" // by returning an empty scope rather than constructing a COMException only // to have it discarded. CoCreateInstance must null the out-parameter on // failure per the COM spec, but Dispose defensively so the ownership model // is explicit even against a buggy COM implementation. config2.Dispose(); return default; } // App-local fallback: the helper DLL is present but not COM-registered. using ComScope<ISetupConfiguration> config1 = new(); int rawHr = SetupConfiguration.GetSetupConfiguration(config1, IntPtr.Zero); if (rawHr < 0 || config1.IsNull) { return default; } HRESULT qiHr = config1.Pointer->QueryInterface(&iidConfig2, config2); if (qiHr.Failed) { config2.Dispose(); return default; } return config2; } /// <summary> /// Enumerate every instance the configuration exposes and append the complete ones /// to <paramref name="results"/>. Per-instance failures are silent — a partial/broken /// install simply does not appear in the output. /// </summary> private static void PopulateInstances(ISetupConfiguration2* config, List<VisualStudioInstance> results) { using ComScope<IEnumSetupInstances> enumInstances = new(); if (config->EnumAllInstances(enumInstances).Failed || enumInstances.IsNull) { return; } while (true) { using ComScope<ISetupInstance> instance = new(); uint fetched = 0; if (enumInstances.Pointer->Next(1, instance, &fetched).Failed || fetched == 0 || instance.IsNull) { break; } if (TryReadInstance(instance.Pointer, out VisualStudioInstance vs)) { results.Add(vs); } } } /// <summary> /// Read one <see cref="ISetupInstance"/>: filter by <see cref="InstanceState.Complete"/>, /// parse the version string, and capture the display name and installation path. Returns /// <see langword="false"/> when the instance should be skipped (incomplete install, /// missing v2 interface, or unparseable version). /// </summary> private static bool TryReadInstance(ISetupInstance* instance, out VisualStudioInstance result) { result = null!; // QI to ISetupInstance2 so we can read the install state. Instances that lack v2 are // ignored — only VS 15 and newer ship the v2 interface, which matches what the old // RCW-based code required for `GetState`. using ComScope<ISetupInstance2> instance2 = new(); Guid iidInstance2 = ISetupInstance2.IID_ISetupInstance2; if (instance->QueryInterface(&iidInstance2, instance2).Failed || instance2.IsNull) { return false; } InstanceState state = default; if (instance2.Pointer->GetState(&state).Failed || state != InstanceState.Complete) { return false; } // BSTR is IDisposable: `using` calls SysFreeString in place at scope exit, so we // don't need a try/finally around each of the three out-params. using BSTR versionBstr = default; using BSTR nameBstr = default; using BSTR pathBstr = default; if (instance->GetInstallationVersion(&versionBstr).Failed || !Version.TryParse(versionBstr.ToString(), out Version version)) { return false; } if (instance->GetDisplayName(0, &nameBstr).Failed || instance->GetInstallationPath(&pathBstr).Failed) { return false; } result = new VisualStudioInstance(nameBstr.ToString(), pathBstr.ToString(), version); return true; } #endif } /// <summary> /// Wrapper class to represent an installed instance of Visual Studio. /// </summary> internal class VisualStudioInstance { /// <summary> /// Version of the Visual Studio Instance /// </summary> internal Version Version { get; } /// <summary> /// Path to the Visual Studio installation /// </summary> internal string Path { get; } /// <summary> /// Full name of the Visual Studio instance with SKU name /// </summary> internal string Name { get; } internal VisualStudioInstance(string name, string path, Version version) { Name = name; Path = path; Version = version; } } }