diff --git a/.editorconfig b/.editorconfig index 24cb45298a2..4ba5450b178 100644 --- a/.editorconfig +++ b/.editorconfig @@ -457,3 +457,12 @@ dotnet_diagnostic.xUnit1031.severity = none # The latter brings incosistency in the codebase and some times in one test case. # So we are disabling this rule with respect to the above mentioned reasons. dotnet_diagnostic.xUnit2013.severity = none + +# Attributes needed for interop are not CLS-compliant, which produces CS3016 warnings. Disabling here doesn't +# actually work, but when we apply the [CLSCompliant(false)] attribute to the partial declarations of the generated +# types it then fires CS3019 warnings about it not making sense on internal types. We disable these warnings for +# anything in the CsWin32 subfolders. Other options are to disable CS3016 entirely in the project, but that would +# suppress the warning for all code, not just the interop code. Hopefully https://github.com/dotnet/roslyn/issues/68526 +# is addressed so we can remove all of this complication. +[{**/Windows/**/*.cs}] +dotnet_diagnostic.CS3019.severity = none diff --git a/.github/skills/cswin32-com/SKILL.md b/.github/skills/cswin32-com/SKILL.md new file mode 100644 index 00000000000..10dfc149ff4 --- /dev/null +++ b/.github/skills/cswin32-com/SKILL.md @@ -0,0 +1,106 @@ +--- +name: cswin32-com +description: 'Guides struct-based COM interop in MSBuild using CsWin32 patterns. Consult when working with ComScope, ComClassFactory, IComIID, IID.Get(), delegate* unmanaged vtables, CoCreateInstance, or manually defining COM interfaces not in Win32 metadata (e.g. WMI IWbemLocator, IWbemServices).' +argument-hint: 'Describe the COM interface or activation pattern you are working with.' +--- + +# CsWin32 COM Interop Guide + +Struct-based COM interop using CsWin32 patterns — AOT-compatible, no `[ComImport]` or built-in marshalling. + +## Workflow + +1. **Determine if the interface is in Win32 metadata.** If yes, add the name to `src/Framework/NativeMethods.txt` — CsWin32 generates it. If no (e.g. WMI), define a manual struct (see below). +2. **Create a `ComScope`** for lifetime management: `using ComScope scope = new();` +3. **Activate the COM object** via `ComClassFactory.TryCreate(CLSID, ...)` or `PInvoke.CoCreateInstance` with `IID.Get()`. +4. **Call methods** via `scope.Pointer->Method(...)`. Pass `ComScope` directly as `T**` output parameters. +5. **Guard with `#if FEATURE_WINDOWSINTEROP`** (or `&& NET` for manual structs needing `delegate* unmanaged`). + +## COM Interfaces in Win32 Metadata + +Add the interface name to `src/Framework/NativeMethods.txt` → CsWin32 generates it → use `ComScope`: + +```csharp +#if FEATURE_WINDOWSINTEROP +using ComScope rot = new(); +HRESULT hr = PInvoke.GetRunningObjectTable(0, rot); +if (hr.Failed) return; +rot.Pointer->SomeMethod(...); +#endif +``` + +## Manual COM Structs (Not in Metadata) + +For interfaces not in Win32 metadata (e.g. WMI), define struct-based implementations in their own files, excluded via `` in source builds. Guard with `#if FEATURE_WINDOWSINTEROP && NET` (needs `delegate* unmanaged`). + +```csharp +[SupportedOSPlatform("windows6.1")] +internal unsafe struct IWbemLocator : IComIID +{ + public static Guid Guid { get; } = new(0xDC12A687, ...); + + // .NET 7+ static abstract IComIID implementation + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ReadOnlySpan data = [ /* 16 GUID bytes */ ]; + return ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + } + } + + private readonly void** _lpVtbl; + + // IUnknown (vtable 0-2) + interface methods at correct indices + public HRESULT ConnectServer(char* strNetworkResource, ...) { + fixed (IWbemLocator* pThis = &this) + return ((delegate* unmanaged[Stdcall])_lpVtbl[3])(pThis, ...); + } + + public static Guid CLSID { get; } = new(0x4590F811, ...); +} +``` + +**Requirements:** +- `delegate* unmanaged[Stdcall]` — needs .NET 5+ +- Exact vtable indices — unused slots can be omitted as long as used method indices are correct +- Dual `IComIID` — static abstract on .NET 7+, instance-based on net472 (polyfill in `src/Framework/Framework/`) +- `char*` with `fixed` for BSTR string parameters +- CS0592 prevents `[SupportedOSPlatform]` on structs — put on individual methods instead + +## Activation + +```csharp +// Via ComClassFactory (AOT-compatible) +if (ComClassFactory.TryCreate(IWbemLocator.CLSID, out var factory, out HRESULT hr)) + using ComScope instance = factory.TryCreateInstance(out hr); + +// Via CoCreateInstance — use IID.Get() for the IID +Guid clsid = IWbemLocator.CLSID; +using ComScope locator = new(); +hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.Get(), locator); +``` + +**Key points:** +- Use `IID.Get()` — do not take `&localGuid` +- Initialize `ComScope` with `new()`. It implicitly converts to `T**` / `void**` output parameters + +## Lifetime & Access + +- `ComScope` is a `ref struct` — use with `using`. Calls `Release()` on dispose. +- Access methods via `scope.Pointer->Method(...)`. +- Pass `ComScope` directly as `T**` or `void**` output parameter (implicit conversion). + +## File Organization + +| Location | Contents | +|----------|----------| +| `src/Framework/Windows/Win32/System/Com/` | `ComScope.cs`, `ComClassFactory.cs` | +| `src/Framework/Windows/Win32/IID.cs` | Generic IID lookup | +| `src/Framework/Utilities/Wmi/` | Manual WMI structs (.NET-only) | +| `src/Framework/Framework/` | net472 `IComIID` polyfill | + +## CS3016 CLS Compliance + +CsWin32 COM structs trigger CS3016 under `[assembly: CLSCompliant(true)]`. Handled via `[CLSCompliant(false)]` partial declarations in `GeneratedInteropClsCompliance.cs`. CS3019 warnings suppressed in `.editorconfig` for `{**/Windows/**/*.cs}` — do not add per-file suppressions. See https://github.com/dotnet/roslyn/issues/68526. \ No newline at end of file diff --git a/.github/skills/cswin32-interop/SKILL.md b/.github/skills/cswin32-interop/SKILL.md new file mode 100644 index 00000000000..af95264a4b5 --- /dev/null +++ b/.github/skills/cswin32-interop/SKILL.md @@ -0,0 +1,112 @@ +--- +name: cswin32-interop +description: 'Guides CsWin32 P/Invoke interop in MSBuild. Consult when working with the PInvoke class, Windows.Win32 namespaces, FEATURE_WINDOWSINTEROP, HANDLE/HMODULE/HRESULT types, BufferScope, replacing [DllImport] with CsWin32, or conditioning Windows-only code for source builds.' +argument-hint: 'Describe the Windows API or interop code you are migrating or adding.' +--- + +# CsWin32 Interop Guide + +[CsWin32](https://github.com/microsoft/CsWin32) replaces `[DllImport]` with source-generated `PInvoke.*` calls. `FEATURE_WINDOWSINTEROP` is the compile-time gate; source builds disable it. + +## Rules + +1. **Replace `[DllImport]` with `PInvoke.*`**. Delete old declarations and hand-written structs/enums/constants. +2. **Gate with `#if FEATURE_WINDOWSINTEROP`**, add runtime `IsWindows` check inside. Both required. +3. **Use CsWin32 types directly** (`HANDLE`, `HMODULE`, `HRESULT.S_OK`, `FILE_FLAGS_AND_ATTRIBUTES`, etc.). +4. **Call `PInvoke.*` directly** — no wrappers. Types flow via `InternalsVisibleTo`. +5. **Prefer CsWin32 for Windows APIs**. Use `[LibraryImport]` only for non-Windows native calls (e.g. `libc`), guarded with `#if NET`. + +### Dual Guard Pattern + +```csharp +#if FEATURE_WINDOWSINTEROP + if (IsWindows) + { + PInvoke.GetFileAttributesEx(fullPath, out WIN32_FILE_ATTRIBUTE_DATA data); + } +#endif + // Cross-platform fallback +``` + +**WRONG**: `if (IsWindows) { #if FEATURE_WINDOWSINTEROP ... #endif }` — dead code in source builds. + +Windows-only files are excluded via `` instead — no `#if` inside needed. + +## Infrastructure + +**Define**: `src/Directory.BeforeCommon.targets` sets `FEATURE_WINDOWSINTEROP` + `$(FeatureWindowsInterop)` when `DotNetBuildSourceOnly != true`. Use `$(FeatureWindowsInterop)` in `.csproj` for ``/``. + +**CsWin32 config**: `src/Framework/NativeMethods.txt` (API list) + `NativeMethods.json` (`allowMarshaling: false`, `useSafeHandles: false`). Lives in Framework; other projects consume via `InternalsVisibleTo`. Do not add CsWin32 to other projects. + +**Guard selection**: + +| Guard | When | Runtime check? | +|-------|------|----------------| +| `#if FEATURE_WINDOWSINTEROP` | Multi-TFM Windows calls | Yes | +| `#if FEATURE_WINDOWSINTEROP && NET` | `delegate* unmanaged`, `ComScope` | Yes | +| `#if FEATURE_WINDOWSINTEROP && !NETSTANDARD` | CsWin32 types without `static abstract` (net472 + net10) | Yes | +| `#if !NET` / `#if FEATURE_MSCOREE` | net472-only = inherently Windows | No | + +**Namespace imports** must be inside `#if FEATURE_WINDOWSINTEROP`. WDK APIs use `Windows.Wdk` namespace. + +**Files**: `src/Framework/Windows/` (CsWin32 partials), `src/Shared/Win32/` (COM helpers), `src/Framework/Utilities/Wmi/` (.NET-only COM structs), `src/Framework/Framework/` (net472 polyfills). + +### Constant Replacements + +`NativeMethodsShared.S_OK` → `HRESULT.S_OK`, `InvalidHandle` → `HANDLE.INVALID_HANDLE_VALUE`, `FILE_ATTRIBUTE_DIRECTORY` → `FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY`, `STD_OUTPUT_HANDLE` → `STD_HANDLE.STD_OUTPUT_HANDLE`, `GENERIC_READ` → `FILE_ACCESS_RIGHTS.FILE_GENERIC_READ`. Pattern: `CsWin32EnumType.ORIGINAL_NAME` — check generated types in `obj/`. + +## BufferScope + +`BufferScope` (`src/Framework/Utilities/BufferScope.cs`) — stackalloc initial buffer with `ArrayPool` fallback. Lives in Framework, available to all projects via `InternalsVisibleTo`. + +```csharp +using BufferScope buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]); +int length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan()); +if (length > buffer.Length) +{ + buffer.EnsureCapacity(length); + length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan()); +} +if (length > 0) path = buffer.Slice(0, length).ToString(); +``` + +- `ref struct` — always use with `using`. Never stack-allocate more than 1024 bytes. +- Check CsWin32 convenience overloads (e.g. `GetShortPathName(string, Span)`) before writing `fixed` blocks. + +## Gotchas + +### CA1416 Platform Compatibility + +No blanket `NoWarn` — handle semantically: +- `if (IsWindows)` satisfies `[SupportedOSPlatform]` — no pragma needed +- `if (IsUnixLike)` satisfies `[UnsupportedOSPlatform("windows")]` +- **Never use `!IsWindows`** — use `else if (IsUnixLike)`. See `documentation/specs/CA1416-analyzer-analysis.md` +- Use versioned `[SupportedOSPlatform("windows6.1")]` on methods calling CsWin32 APIs +- `#pragma warning disable CA1416` only for **static local functions** (analyzer limitation) +- CS0592 prevents `[SupportedOSPlatform]` on `partial struct` — put on individual members instead + +### Type Conversions + +- `HANDLE ↔ IntPtr`: `(HANDLE)intPtr` / `(IntPtr)h.Value`. Sentinels: `HANDLE.Null`, `HANDLE.INVALID_HANDLE_VALUE` +- `FILETIME → long`: `data.ftLastWriteTime.ToLong()` — CsWin32 uses `ComTypes.FILETIME` (int fields), not `Win32.Foundation.FILETIME` +- `SafeFileHandle`: `new SafeFileHandle((IntPtr)h.Value, true)`, pass with `(HANDLE)handle.DangerousGetHandle()` +- Nullable structs: `(SECURITY_ATTRIBUTES?)null` +- Enum flags: use bitwise `&` — `HasFlag()` boxes on .NET Framework +- Anonymous unions: `systemInfo.Anonymous.Anonymous.wProcessorArchitecture` — check generated source in `obj/` + +### Source-Build Verification (REQUIRED before pushing) + +Source builds (`DotNetBuildSourceOnly=true`) disable `FEATURE_WINDOWSINTEROP`. CI treats **all warnings as errors**. Run both builds before every push: + +```shell +# Normal build +dotnet msbuild MSBuild.Dev.slnf -v:q + +# Source-build — catches unused usings/members/docs from #if guards +dotnet msbuild MSBuild.SourceBuild.slnf /p:DotNetBuildSourceOnly=true -v:q +``` + +**Everything** only referenced inside `#if FEATURE_WINDOWSINTEROP` must also be guarded: +- **IDE0005**: `using` directives — most common failure +- **IDE0051/IDE0052**: Private members (methods, fields) +- **CS1587**: XML doc comments (move inside `#if`, not before) \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 318ff794fd1..91a0a70fb36 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,14 +15,14 @@ Instructions for GitHub Copilot and other AI coding agents working with the MSBu ### Technology Stack - .NET 10.0 and .NET Framework 4.7.2 -- C# 13 features (especially collection expressions) +- C# 14 features (especially collection expressions) - xUnit with Shouldly for testing - Multi-platform support (Windows, Linux, macOS) ## General * Performance is the top priority - minimize allocations, avoid LINQ in hot paths, use efficient algorithms. -* Always use the latest C# features, currently C# 13, especially collection expressions (`[]` over `new Type[]`). +* Always use the latest C# features, currently C# 14, especially collection expressions (`[]` over `new Type[]`). * Match the style of surrounding code when making edits, but modernize aggressively for substantial changes. ## Code Review Instructions diff --git a/eng/dependabot/Directory.Packages.props b/eng/dependabot/Directory.Packages.props index 5681ba3675c..ff076b7a94b 100644 --- a/eng/dependabot/Directory.Packages.props +++ b/eng/dependabot/Directory.Packages.props @@ -63,6 +63,15 @@ + + + + + + + + + diff --git a/src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs b/src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs index 37b31c7cd0a..8fee963e09b 100644 --- a/src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs @@ -15,6 +15,9 @@ using Microsoft.Build.Shared; using Microsoft.Win32.SafeHandles; using Xunit; +#if FEATURE_WINDOWSINTEROP +using Windows.Win32.Storage.FileSystem; +#endif #nullable disable @@ -960,7 +963,7 @@ private void IsAnyOutOfDateTestHelper( [Fact(Skip = "Creating a symlink on Windows requires elevation.")] [SkipOnPlatform(TestPlatforms.AnyUnix, "Windows-specific test")] - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] public void NewSymlinkOldDestinationIsUpToDate() { SimpleSymlinkInputCheck(symlinkWriteTime: New, @@ -971,7 +974,7 @@ public void NewSymlinkOldDestinationIsUpToDate() [Fact(Skip = "Creating a symlink on Windows requires elevation.")] [SkipOnPlatform(TestPlatforms.AnyUnix, "Windows-specific test")] - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] public void OldSymlinkOldDestinationIsUpToDate() { SimpleSymlinkInputCheck(symlinkWriteTime: Old, @@ -982,7 +985,7 @@ public void OldSymlinkOldDestinationIsUpToDate() [Fact(Skip = "Creating a symlink on Windows requires elevation.")] [SkipOnPlatform(TestPlatforms.AnyUnix, "Windows-specific test")] - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] public void OldSymlinkNewDestinationIsNotUpToDate() { SimpleSymlinkInputCheck(symlinkWriteTime: Old, @@ -993,7 +996,7 @@ public void OldSymlinkNewDestinationIsNotUpToDate() [Fact(Skip = "Creating a symlink on Windows requires elevation.")] [SkipOnPlatform(TestPlatforms.AnyUnix, "Windows-specific test")] - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] public void NewSymlinkNewDestinationIsNotUpToDate() { SimpleSymlinkInputCheck(symlinkWriteTime: Middle, @@ -1004,15 +1007,26 @@ public void NewSymlinkNewDestinationIsNotUpToDate() [DllImport("kernel32.dll")] [return: MarshalAs(UnmanagedType.Bool)] - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] private static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, UInt32 dwFlags); [DllImport("kernel32.dll", SetLastError = true)] - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] private static extern bool SetFileTime(SafeFileHandle hFile, ref long creationTime, ref long lastAccessTime, ref long lastWriteTime); - [SupportedOSPlatform("windows")] + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "CreateFileW")] + [SupportedOSPlatform("windows6.1")] + private static extern SafeFileHandle CreateFileForSymlink( + string lpFileName, + uint dwDesiredAccess, + uint dwShareMode, + IntPtr lpSecurityAttributes, + uint dwCreationDisposition, + uint dwFlagsAndAttributes, + IntPtr hTemplateFile); + + [SupportedOSPlatform("windows6.1")] private void SimpleSymlinkInputCheck(DateTime symlinkWriteTime, DateTime targetWriteTime, DateTime outputWriteTime, bool expectedOutOfDate) { @@ -1038,11 +1052,13 @@ private void SimpleSymlinkInputCheck(DateTime symlinkWriteTime, DateTime targetW // File.SetLastWriteTime on the symlink sets the target write time, // so set the symlink's write time the hard way - using (SafeFileHandle handle = - NativeMethodsShared.CreateFile( - inputSymlink, NativeMethodsShared.GENERIC_READ | 0x100 /* FILE_WRITE_ATTRIBUTES */, - NativeMethodsShared.FILE_SHARE_READ, IntPtr.Zero, NativeMethodsShared.OPEN_EXISTING, - NativeMethodsShared.FILE_ATTRIBUTE_NORMAL | NativeMethodsShared.FILE_FLAG_OPEN_REPARSE_POINT, + using (SafeFileHandle handle = CreateFileForSymlink( + inputSymlink, + (uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | 0x100 /* FILE_WRITE_ATTRIBUTES */, + (uint)FILE_SHARE_MODE.FILE_SHARE_READ, + IntPtr.Zero, + (uint)FILE_CREATION_DISPOSITION.OPEN_EXISTING, + (uint)(FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL | FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_OPEN_REPARSE_POINT), IntPtr.Zero)) { if (handle.IsInvalid) diff --git a/src/Build.UnitTests/ConsoleLogger_Tests.cs b/src/Build.UnitTests/ConsoleLogger_Tests.cs index 3456745feb2..1f04286b23b 100644 --- a/src/Build.UnitTests/ConsoleLogger_Tests.cs +++ b/src/Build.UnitTests/ConsoleLogger_Tests.cs @@ -18,6 +18,12 @@ using Shouldly; using Xunit; using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +#if FEATURE_WINDOWSINTEROP +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Storage.FileSystem; +using Windows.Win32.System.Console; +#endif #nullable disable @@ -1959,18 +1965,16 @@ public void TestPrintTargetNamePerMessage() /// Check to see what kind of device we are outputting the log to, is it a character device, a file, or something else /// this can be used by loggers to modify their outputs based on the device they are writing to /// - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] internal bool IsRunningWithCharacterFileType() { // Get the std out handle - IntPtr stdHandle = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); + HANDLE stdHandle = PInvoke.GetStdHandle(STD_HANDLE.STD_OUTPUT_HANDLE); - if (stdHandle != Microsoft.Build.BackEnd.NativeMethods.InvalidHandle) + if (stdHandle != HANDLE.INVALID_HANDLE_VALUE) { - uint fileType = NativeMethodsShared.GetFileType(stdHandle); - // The std out is a char type(LPT or Console) - return fileType == NativeMethodsShared.FILE_TYPE_CHAR; + return PInvoke.GetFileType(stdHandle) == FILE_TYPE.FILE_TYPE_CHAR; } else { diff --git a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs index baccd0cdf14..b5368c1f8c2 100644 --- a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs +++ b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs @@ -16,6 +16,9 @@ using Microsoft.Build.Shared.Debugging; using Microsoft.Build.TelemetryInfra; using Microsoft.NET.StringTools; +#if FEATURE_WINDOWSINTEROP +using Windows.Win32.System.SystemInformation; +#endif using BuildAbortedException = Microsoft.Build.Exceptions.BuildAbortedException; #nullable disable @@ -126,10 +129,12 @@ internal class BuildRequestEngine : IBuildRequestEngine, IBuildComponent /// private readonly string _debugDumpPath; +#if FEATURE_WINDOWSINTEROP /// /// Forces caching of all configurations and results. /// private readonly bool _debugForceCaching; +#endif /// /// Constructor @@ -138,7 +143,9 @@ internal BuildRequestEngine() { _debugDumpState = Traits.Instance.DebugScheduler; _debugDumpPath = FrameworkDebugUtils.DebugPath; +#if FEATURE_WINDOWSINTEROP _debugForceCaching = Environment.GetEnvironmentVariable("MSBUILDDEBUGFORCECACHING") == "1"; +#endif if (String.IsNullOrEmpty(_debugDumpPath)) { @@ -885,6 +892,7 @@ private void EvaluateRequestStates() [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.GC.Collect", Justification = "We're trying to get rid of memory because we're running low, so we need to collect NOW in order to free it up ASAP")] private void CheckMemoryUsage() { +#if FEATURE_WINDOWSINTEROP if (!NativeMethodsShared.IsWindows || BuildEnvironmentHelper.Instance.RunningInVisualStudio) { // Since this causes synchronous I/O and a stop-the-world GC, it can be very expensive. If @@ -901,18 +909,17 @@ private void CheckMemoryUsage() // Jeffrey Richter suggests that when the memory load in the system exceeds 80% it is a good // idea to start finding ways to unload unnecessary data to prevent memory starvation. We use this metric in // our calculations below. - NativeMethodsShared.MemoryStatus memoryStatus = NativeMethodsShared.GetMemoryStatus(); - if (memoryStatus != null) + if (NativeMethodsShared.TryGetMemoryStatus(out MEMORYSTATUSEX memoryStatus)) { try { // The minimum limit must be no more than 80% of the virtual memory limit to reduce the chances of a single unfortunately // large project resulting in allocations which exceed available VM space between calls to this function. This situation // is more likely on 32-bit machines where VM space is only 2 gigs. - ulong memoryUseLimit = Convert.ToUInt64(memoryStatus.TotalVirtual * 0.8); + ulong memoryUseLimit = Convert.ToUInt64(memoryStatus.ullTotalVirtual * 0.8); // See how much memory we are using and compart that to our limit. - ulong memoryInUse = memoryStatus.TotalVirtual - memoryStatus.AvailableVirtual; + ulong memoryInUse = memoryStatus.ullTotalVirtual - memoryStatus.ullAvailVirtual; while ((memoryInUse > memoryUseLimit) || _debugForceCaching) { TraceEngine( @@ -936,8 +943,13 @@ private void CheckMemoryUsage() break; } - memoryStatus = NativeMethodsShared.GetMemoryStatus(); - memoryInUse = memoryStatus.TotalVirtual - memoryStatus.AvailableVirtual; + if (!NativeMethodsShared.TryGetMemoryStatus(out memoryStatus)) + { + TraceEngine("Failed to get memory status."); + break; + } + + memoryInUse = memoryStatus.ullTotalVirtual - memoryStatus.ullAvailVirtual; TraceEngine("Memory usage now at {0}", memoryInUse); } } @@ -949,6 +961,7 @@ private void CheckMemoryUsage() throw new BuildAbortedException(e.Message, e); } } +#endif } /// @@ -1477,6 +1490,7 @@ private void QueueAction(Action action, bool isLastTask) } } +#if FEATURE_WINDOWSINTEROP private void TraceEngine(string format, ulong arg) { if (_debugDumpState) @@ -1485,31 +1499,32 @@ private void TraceEngine(string format, ulong arg) } } - private void TraceEngine(string format, int arg) + private void TraceEngine(string format, ulong arg1, ulong arg2) { if (_debugDumpState) { - TraceEngine(format, [arg]); + TraceEngine(format, [arg1, arg2]); } } +#endif - private void TraceEngine(string format, int arg1, BuildRequestEngineStatus arg2) + private void TraceEngine(string format, int arg) { if (_debugDumpState) { - TraceEngine(format, [arg1, arg2.Box()]); + TraceEngine(format, [arg]); } } - private void TraceEngine(string format, int arg1, int arg2) + private void TraceEngine(string format, int arg1, BuildRequestEngineStatus arg2) { if (_debugDumpState) { - TraceEngine(format, [arg1, arg2]); + TraceEngine(format, [arg1, arg2.Box()]); } } - private void TraceEngine(string format, ulong arg1, ulong arg2) + private void TraceEngine(string format, int arg1, int arg2) { if (_debugDumpState) { diff --git a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs index 7b592d23fe9..ffbd8b80ec5 100644 --- a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs +++ b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs @@ -2,23 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; #if RUNTIME_TYPE_NETCORE using System.IO; #endif -using System.Runtime.InteropServices; using System.Runtime.Versioning; -using System.Text; using Microsoft.Build.Exceptions; using Microsoft.Build.Framework; using Microsoft.Build.Internal; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using BackendNativeMethods = Microsoft.Build.BackEnd.NativeMethods; +#if FEATURE_WINDOWSINTEROP +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Text; +using Windows.Win32; +using Windows.Win32.Foundation; +#endif #nullable disable @@ -62,9 +66,15 @@ private Process StartInternal(NodeLaunchData nodeLaunchData) CommunicationsUtilities.Trace($"Launching node from {nodeLaunchData.MSBuildLocation}"); - return NativeMethodsShared.IsWindows - ? StartProcessWindows(nodeLaunchData, exeName, creationFlags, redirectStreams, isNativeAppHost) - : StartProcessUnix(nodeLaunchData, exeName, creationFlags, redirectStreams, isNativeAppHost); +#if FEATURE_WINDOWSINTEROP + if (NativeMethodsShared.IsWindows) + { + return StartProcessWindows(nodeLaunchData, exeName, creationFlags, redirectStreams, isNativeAppHost); + } +#endif + return NativeMethodsShared.IsUnixLike + ? StartProcessUnix(nodeLaunchData, exeName, creationFlags, redirectStreams, isNativeAppHost) + : throw new PlatformNotSupportedException(); static void ValidateMSBuildLocation(string msbuildLocation) { @@ -153,7 +163,8 @@ private Process StartProcessUnix(NodeLaunchData nodeLaunchData, string exeName, } } - [SupportedOSPlatform("windows")] +#if FEATURE_WINDOWSINTEROP + [SupportedOSPlatform("windows6.1")] private static Process StartProcessWindows(NodeLaunchData nodeLaunchData, string exeName, uint creationFlags, bool redirectStreams, bool isNativeAppHost) { // Repeat the executable name as the first token of the command line because the command line @@ -220,19 +231,23 @@ private static Process StartProcessWindows(NodeLaunchData nodeLaunchData, string static void CloseProcessHandles(BackendNativeMethods.PROCESS_INFORMATION processInfo) { - if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != NativeMethods.InvalidHandle) +#pragma warning disable CA1416 // static local functions don't inherit [SupportedOSPlatform] (analyzer limitation) + if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != new IntPtr(-1)) { - NativeMethodsShared.CloseHandle(processInfo.hProcess); + PInvoke.CloseHandle((HANDLE)processInfo.hProcess); } - if (processInfo.hThread != IntPtr.Zero && processInfo.hThread != NativeMethods.InvalidHandle) + if (processInfo.hThread != IntPtr.Zero && processInfo.hThread != new IntPtr(-1)) { - NativeMethodsShared.CloseHandle(processInfo.hThread); + PInvoke.CloseHandle((HANDLE)processInfo.hThread); } +#pragma warning restore CA1416 } } +#endif - [SupportedOSPlatform("windows")] +#if FEATURE_WINDOWSINTEROP + [SupportedOSPlatform("windows6.1")] private static BackendNativeMethods.STARTUP_INFO CreateStartupInfo(bool redirectStreams) { var startInfo = new BackendNativeMethods.STARTUP_INFO @@ -290,6 +305,7 @@ private static IntPtr BuildEnvironmentBlock(IDictionary environm return Marshal.StringToHGlobalUni(sb.ToString()); } +#endif private static Process DisableMSBuildServer(Func func) { diff --git a/src/Build/Logging/InProcessConsoleConfiguration.cs b/src/Build/Logging/InProcessConsoleConfiguration.cs index 06bbc8f3e55..a92271afa6d 100644 --- a/src/Build/Logging/InProcessConsoleConfiguration.cs +++ b/src/Build/Logging/InProcessConsoleConfiguration.cs @@ -3,7 +3,13 @@ #nullable disable using System; +#if FEATURE_WINDOWSINTEROP using System.Diagnostics; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Storage.FileSystem; +using Windows.Win32.System.Console; +#endif namespace Microsoft.Build.BackEnd.Logging; @@ -24,14 +30,15 @@ public bool AcceptAnsiColorCodes get { bool acceptAnsiColorCodes = false; +#if FEATURE_WINDOWSINTEROP if (NativeMethodsShared.IsWindows && !Console.IsOutputRedirected) { try { - IntPtr stdOut = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); - if (NativeMethodsShared.GetConsoleMode(stdOut, out uint consoleMode)) + HANDLE stdOut = PInvoke.GetStdHandle(STD_HANDLE.STD_OUTPUT_HANDLE); + if (PInvoke.GetConsoleMode(stdOut, out CONSOLE_MODE consoleMode)) { - acceptAnsiColorCodes = (consoleMode & NativeMethodsShared.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; + acceptAnsiColorCodes = consoleMode.HasFlag(CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING); } } catch (Exception ex) @@ -40,6 +47,7 @@ public bool AcceptAnsiColorCodes } } else +#endif { // On posix OSes we expect console always supports VT100 coloring unless it is redirected acceptAnsiColorCodes = !Console.IsOutputRedirected; @@ -75,20 +83,19 @@ public bool OutputIsScreen { bool isScreen = false; +#if FEATURE_WINDOWSINTEROP if (NativeMethodsShared.IsWindows) { // Get the std out handle - IntPtr stdHandle = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); + HANDLE stdHandle = PInvoke.GetStdHandle(STD_HANDLE.STD_OUTPUT_HANDLE); - if (stdHandle != NativeMethods.InvalidHandle) + if (stdHandle != HANDLE.INVALID_HANDLE_VALUE) { - uint fileType = NativeMethodsShared.GetFileType(stdHandle); - - // The std out is a char type(LPT or Console) - isScreen = fileType == NativeMethodsShared.FILE_TYPE_CHAR; + isScreen = PInvoke.GetFileType(stdHandle) == FILE_TYPE.FILE_TYPE_CHAR; } } else +#endif { isScreen = !Console.IsOutputRedirected; } diff --git a/src/Build/Logging/SimpleErrorLogger.cs b/src/Build/Logging/SimpleErrorLogger.cs index fafc01e340d..ff6b1a0f736 100644 --- a/src/Build/Logging/SimpleErrorLogger.cs +++ b/src/Build/Logging/SimpleErrorLogger.cs @@ -24,7 +24,7 @@ public sealed class SimpleErrorLogger : INodeLogger private readonly uint? originalConsoleMode; public SimpleErrorLogger() { - (acceptAnsiColorCodes, _, originalConsoleMode) = NativeMethods.QueryIsScreenAndTryEnableAnsiColorCodes(NativeMethods.StreamHandleType.StdErr); + (acceptAnsiColorCodes, _, originalConsoleMode) = NativeMethods.QueryIsScreenAndTryEnableAnsiColorCodes(useStandardError: true); } public bool HasLoggedErrors { get; private set; } = false; @@ -85,7 +85,7 @@ public void Initialize(IEventSource eventSource) public void Shutdown() { - NativeMethods.RestoreConsoleMode(originalConsoleMode, NativeMethods.StreamHandleType.StdErr); + NativeMethods.RestoreConsoleMode(originalConsoleMode, useStandardError: true); } } } diff --git a/src/Directory.BeforeCommon.targets b/src/Directory.BeforeCommon.targets index 95e68ae8409..133fde5ff07 100644 --- a/src/Directory.BeforeCommon.targets +++ b/src/Directory.BeforeCommon.targets @@ -70,6 +70,12 @@ $(DefineConstants);FEATURE_MSCOREE + + + $(DefineConstants);FEATURE_WINDOWSINTEROP + true + + $(DefineConstants);TEST_ISWINDOWS diff --git a/src/Framework.UnitTests/BufferScopeTests.cs b/src/Framework.UnitTests/BufferScopeTests.cs deleted file mode 100644 index 1c31b1a5ca4..00000000000 --- a/src/Framework.UnitTests/BufferScopeTests.cs +++ /dev/null @@ -1,468 +0,0 @@ -// 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; -using Microsoft.Build.Utilities; -using Shouldly; -using Xunit; - -namespace Microsoft.Build.Framework.UnitTests; - -public unsafe class BufferScopeTests -{ - [Fact] - public void Construct_WithStackAlloc() - { - using BufferScope buffer = new(stackalloc char[10]); - buffer.Length.ShouldBe(10); - buffer[0] = 'Y'; - buffer[..1].ToString().ShouldBe("Y"); - } - - [Fact] - public void Construct_WithStackAlloc_GrowAndCopy() - { - using BufferScope buffer = new(stackalloc char[10]); - buffer.Length.ShouldBe(10); - buffer[0] = 'Y'; - buffer.EnsureCapacity(64, copy: true); - buffer.Length.ShouldBeGreaterThanOrEqualTo(64); - buffer[..1].ToString().ShouldBe("Y"); - } - - [Fact] - public void Construct_WithStackAlloc_Pin() - { - using BufferScope buffer = new(stackalloc char[10]); - buffer.Length.ShouldBe(10); - buffer[0] = 'Y'; - fixed (char* c = buffer) - { - (*c).ShouldBe('Y'); - *c = 'Z'; - } - - buffer[..1].ToString().ShouldBe("Z"); - } - - [Fact] - public void Construct_GrowAndCopy() - { - using BufferScope buffer = new(32); - buffer.Length.ShouldBeGreaterThanOrEqualTo(32); - buffer[0] = 'Y'; - buffer.EnsureCapacity(64, copy: true); - buffer.Length.ShouldBeGreaterThanOrEqualTo(64); - buffer[..1].ToString().ShouldBe("Y"); - } - - [Fact] - public void Construct_Pin() - { - using BufferScope buffer = new(64); - buffer.Length.ShouldBeGreaterThanOrEqualTo(64); - buffer[0] = 'Y'; - fixed (char* c = buffer) - { - (*c).ShouldBe('Y'); - *c = 'Z'; - } - - buffer[..1].ToString().ShouldBe("Z"); - } - - [Fact] - public void Construct_WithMinimumLength_ZeroLength() - { - using BufferScope buffer = new(0); - buffer.Length.ShouldBeGreaterThanOrEqualTo(0); - } - - [Fact] - public void Construct_WithMinimumLength_SmallLength() - { - using BufferScope buffer = new(5); - buffer.Length.ShouldBeGreaterThanOrEqualTo(5); - } - - [Fact] - public void Construct_WithSpan_EmptySpan() - { - using BufferScope buffer = new([]); - buffer.Length.ShouldBe(0); - } - - [Fact] - public void Construct_WithSpanAndMinimumLength_SpanLargerThanMinimum() - { - using BufferScope buffer = new(stackalloc char[20], 10); - buffer.Length.ShouldBe(20); - buffer[0] = 'A'; - buffer[19] = 'Z'; - buffer[0].ShouldBe('A'); - buffer[19].ShouldBe('Z'); - } - - [Fact] - public void Construct_WithSpanAndMinimumLength_SpanSmallerThanMinimum() - { - using BufferScope buffer = new(stackalloc char[5], 20); - buffer.Length.ShouldBeGreaterThanOrEqualTo(20); - } - - [Fact] - public void Construct_WithSpanAndMinimumLength_SpanEqualsMinimum() - { - using BufferScope buffer = new(stackalloc char[10], 10); - buffer.Length.ShouldBe(10); - } - - [Fact] - public void EnsureCapacity_AlreadySufficientCapacity() - { - using BufferScope buffer = new(20); - int originalLength = buffer.Length; - buffer[0] = 42; - - buffer.EnsureCapacity(10); - - buffer.Length.ShouldBe(originalLength); - buffer[0].ShouldBe(42); - } - - [Fact] - public void EnsureCapacity_GrowWithoutCopy() - { - using BufferScope buffer = new(10); - buffer[0] = 42; - buffer[5] = 100; - - buffer.EnsureCapacity(50, copy: false); - - buffer.Length.ShouldBeGreaterThanOrEqualTo(50); - // Data should not be copied when copy is false - } - - [Fact] - public void EnsureCapacity_GrowFromStackAllocWithCopy() - { - using BufferScope buffer = new(stackalloc char[5]); - buffer[0] = 'H'; - buffer[1] = 'e'; - buffer[2] = 'l'; - buffer[3] = 'l'; - buffer[4] = 'o'; - - buffer.EnsureCapacity(20, copy: true); - - buffer.Length.ShouldBeGreaterThanOrEqualTo(20); - buffer[0].ShouldBe('H'); - buffer[1].ShouldBe('e'); - buffer[2].ShouldBe('l'); - buffer[3].ShouldBe('l'); - buffer[4].ShouldBe('o'); - } - - [Fact] - public void EnsureCapacity_MultipleGrows() - { - using BufferScope buffer = new(5); - buffer[0] = 1; - buffer[1] = 2; - - buffer.EnsureCapacity(10, copy: true); - buffer[0].ShouldBe(1); - buffer[1].ShouldBe(2); - - buffer.EnsureCapacity(50, copy: true); - buffer.Length.ShouldBeGreaterThanOrEqualTo(50); - buffer[0].ShouldBe(1); - buffer[1].ShouldBe(2); - } - - [Fact] - public void Indexer_Range_FullRange() - { - using BufferScope buffer = new(stackalloc char[5]); - buffer[0] = 'A'; - buffer[1] = 'B'; - buffer[2] = 'C'; - buffer[3] = 'D'; - buffer[4] = 'E'; - - Span slice = buffer[..]; - slice.Length.ShouldBe(5); - slice[0].ShouldBe('A'); - slice[4].ShouldBe('E'); - } - - [Fact] - public void Indexer_Range_PartialRange() - { - using BufferScope buffer = new(stackalloc int[10]); - for (int i = 0; i < 10; i++) - { - buffer[i] = i; - } - - Span slice = buffer[2..8]; - slice.Length.ShouldBe(6); - slice[0].ShouldBe(2); - slice[5].ShouldBe(7); - } - - [Fact] - public void Indexer_Range_EmptyRange() - { - using BufferScope buffer = new(10); - Span slice = buffer[5..5]; - slice.Length.ShouldBe(0); - } - - [Fact] - public void Slice_ValidRange() - { - using BufferScope buffer = new(stackalloc char[10]); - for (int i = 0; i < 10; i++) - { - buffer[i] = (char)('A' + i); - } - - Span slice = buffer.Slice(3, 4); - slice.Length.ShouldBe(4); - slice[0].ShouldBe('D'); - slice[3].ShouldBe('G'); - } - - [Fact] - public void Slice_ZeroLength() - { - using BufferScope buffer = new(10); - Span slice = buffer.Slice(5, 0); - slice.Length.ShouldBe(0); - } - - [Fact] - public void AsSpan_ReturnsCorrectSpan() - { - using BufferScope buffer = new(5); - buffer[0] = 3.14; - buffer[1] = 2.71; - - Span span = buffer.AsSpan(); - span.Length.ShouldBe(buffer.Length); - span[0].ShouldBe(3.14); - span[1].ShouldBe(2.71); - } - - [Fact] - public void ImplicitOperator_ToSpan() - { - using BufferScope buffer = new(stackalloc int[3]); - buffer[0] = 10; - buffer[1] = 20; - buffer[2] = 30; - - Span span = buffer; - span.Length.ShouldBe(3); - span[0].ShouldBe(10); - span[1].ShouldBe(20); - span[2].ShouldBe(30); - } - - [Fact] - public void ImplicitOperator_ToReadOnlySpan() - { - using BufferScope buffer = new(stackalloc char[3]); - buffer[0] = 'X'; - buffer[1] = 'Y'; - buffer[2] = 'Z'; - - ReadOnlySpan readOnlySpan = buffer; - readOnlySpan.Length.ShouldBe(3); - readOnlySpan[0].ShouldBe('X'); - readOnlySpan[1].ShouldBe('Y'); - readOnlySpan[2].ShouldBe('Z'); - } - - [Fact] - public void GetEnumerator_IteratesCorrectly() - { - using BufferScope buffer = new(stackalloc int[5]); - for (int i = 0; i < 5; i++) - { - buffer[i] = i * 10; - } - - var values = new List(); - foreach (int value in buffer) - { - values.Add(value); - } - - values.ShouldBe([0, 10, 20, 30, 40]); - } - - [Fact] - public void GetEnumerator_EmptyBuffer() - { - using BufferScope buffer = new([]); - - var values = new List(); - foreach (string value in buffer) - { - values.Add(value); - } - - values.ShouldBeEmpty(); - } - - [Fact] - public void ToString_WithCharBuffer() - { - using BufferScope buffer = new(stackalloc char[5]); - buffer[0] = 'H'; - buffer[1] = 'e'; - buffer[2] = 'l'; - buffer[3] = 'l'; - buffer[4] = 'o'; - - string result = buffer.ToString(); - result.ShouldBe("Hello"); - } - - [Fact] - public void ToString_EmptyBuffer() - { - using BufferScope buffer = new([]); - string result = buffer.ToString(); - result.ShouldBe(""); - } - - [Fact] - public void GetPinnableReference_WithStackAlloc() - { - using BufferScope buffer = new(stackalloc byte[10]); - buffer[0] = 255; - buffer[9] = 128; - - ref byte reference = ref buffer.GetPinnableReference(); - reference.ShouldBe((byte)255); - - // Modify through reference - reference = 100; - buffer[0].ShouldBe((byte)100); - } - - [Fact] - public void GetPinnableReference_EmptyBuffer() - { - using BufferScope buffer = new([]); - ref int reference = ref buffer.GetPinnableReference(); - // Should not throw, but reference may be to null location - } - - [Fact] - public void Dispose_MultipleCallsSafe() - { - BufferScope buffer = new(100); - buffer[0] = 42; - - buffer.Dispose(); - buffer.Dispose(); // Should not throw - } - - [Fact] - public void Dispose_StackAllocBuffer() - { - BufferScope buffer = new(stackalloc int[10]); - buffer[0] = 42; - - buffer.Dispose(); // Should not throw for stack allocated buffer - } - - [Theory] - [InlineData(1)] - [InlineData(100)] - [InlineData(1000)] - [InlineData(10000)] - public void Construct_WithMinimumLength_VariousSizes(int minimumLength) - { - using BufferScope buffer = new(minimumLength); - buffer.Length.ShouldBeGreaterThanOrEqualTo(minimumLength); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(10)] - [InlineData(100)] - public void EnsureCapacity_BoundaryConditions(int capacity) - { - using BufferScope buffer = new(5); - buffer.EnsureCapacity(capacity); - buffer.Length.ShouldBeGreaterThanOrEqualTo(Math.Max(5, capacity)); - } - - [Fact] - public void WorksWithDifferentTypes_String() - { - using BufferScope buffer = new(5); - buffer[0] = "Hello"; - buffer[1] = "World"; - buffer[0].ShouldBe("Hello"); - buffer[1].ShouldBe("World"); - } - - [Fact] - public void WorksWithDifferentTypes_CustomStruct() - { - using BufferScope buffer = new(3); - var date1 = new DateTime(2025, 1, 1); - var date2 = new DateTime(2025, 12, 31); - - buffer[0] = date1; - buffer[1] = date2; - - buffer[0].ShouldBe(date1); - buffer[1].ShouldBe(date2); - } - - [Fact] - public void CombinedOperations_ComplexScenario() - { - using BufferScope buffer = new(stackalloc int[5], 10); - - // Initial setup - for (int i = 0; i < buffer.Length; i++) - { - buffer[i] = i + 1; - } - - // Grow the buffer - buffer.EnsureCapacity(20, copy: true); - - // Verify data was copied - for (int i = 0; i < Math.Min(5, buffer.Length); i++) - { - buffer[i].ShouldBe(i + 1); - } - - // Test slicing - Span slice = buffer[1..4]; - slice.Length.ShouldBe(3); - slice[0].ShouldBe(2); - slice[2].ShouldBe(4); - - // Test enumeration - var firstFive = buffer.AsSpan()[..5]; - var values = new List(); - foreach (int value in firstFive) - { - values.Add(value); - } - - values.ShouldBe([1, 2, 3, 4, 5]); - } -} diff --git a/src/Framework.UnitTests/BufferScope_Tests.cs b/src/Framework.UnitTests/BufferScope_Tests.cs new file mode 100644 index 00000000000..5d7730791e4 --- /dev/null +++ b/src/Framework.UnitTests/BufferScope_Tests.cs @@ -0,0 +1,416 @@ +// 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 Microsoft.Build.Utilities; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Tests for . + /// + public class BufferScope_Tests + { + [Fact] + public void MinimumLengthConstructor_RentsAtLeastRequestedSize() + { + using BufferScope buffer = new(16); + buffer.Length.ShouldBeGreaterThanOrEqualTo(16); + } + + [Fact] + public void InitialBufferConstructor_UsesProvidedSpan() + { + Span initial = stackalloc char[8]; + using BufferScope buffer = new(initial); + buffer.Length.ShouldBe(8); + } + + [Fact] + public void InitialBufferWithMinimum_UsesInitialWhenLargeEnough() + { + Span initial = stackalloc byte[32]; + using BufferScope buffer = new(initial, 16); + buffer.Length.ShouldBe(32); + } + + [Fact] + public void InitialBufferWithMinimum_RentsWhenInitialTooSmall() + { + Span initial = stackalloc byte[4]; + using BufferScope buffer = new(initial, 128); + buffer.Length.ShouldBeGreaterThanOrEqualTo(128); + } + + [Fact] + public void Indexer_GetsAndSetsValues() + { + using BufferScope buffer = new(4); + buffer[0] = 10; + buffer[1] = 20; + buffer[2] = 30; + buffer[0].ShouldBe(10); + buffer[1].ShouldBe(20); + buffer[2].ShouldBe(30); + } + + [Fact] + public void Slice_ReturnsRequestedRange() + { + using BufferScope buffer = new(10); + buffer[0] = 'a'; + buffer[1] = 'b'; + buffer[2] = 'c'; + buffer[3] = 'd'; + + Span slice = buffer.Slice(1, 2); + slice.Length.ShouldBe(2); + slice[0].ShouldBe('b'); + slice[1].ShouldBe('c'); + } + + [Fact] + public void ToString_ReturnsSpanContents() + { + Span initial = stackalloc char[5]; + using BufferScope buffer = new(initial); + buffer[0] = 'h'; + buffer[1] = 'e'; + buffer[2] = 'l'; + buffer[3] = 'l'; + buffer[4] = 'o'; + + buffer.ToString().ShouldBe("hello"); + } + + [Fact] + public void EnsureCapacity_NoOpWhenAlreadyLargeEnough() + { + using BufferScope buffer = new(64); + int originalLength = buffer.Length; + buffer.EnsureCapacity(32); + buffer.Length.ShouldBe(originalLength); + } + + [Fact] + public void EnsureCapacity_GrowsWhenNeeded() + { + Span initial = stackalloc byte[4]; + using BufferScope buffer = new(initial); + buffer.Length.ShouldBe(4); + + buffer.EnsureCapacity(128); + buffer.Length.ShouldBeGreaterThanOrEqualTo(128); + } + + [Fact] + public void EnsureCapacity_WithCopy_PreservesExistingContents() + { + Span initial = stackalloc int[4]; + using BufferScope buffer = new(initial); + buffer[0] = 1; + buffer[1] = 2; + buffer[2] = 3; + buffer[3] = 4; + + buffer.EnsureCapacity(64, copy: true); + + buffer[0].ShouldBe(1); + buffer[1].ShouldBe(2); + buffer[2].ShouldBe(3); + buffer[3].ShouldBe(4); + } + + [Fact] + public void AsSpan_ReturnsUnderlyingSpan() + { + using BufferScope buffer = new(8); + Span span = buffer.AsSpan(); + span.Length.ShouldBe(buffer.Length); + } + + [Fact] + public void ImplicitSpanConversion_Works() + { + using BufferScope buffer = new(8); + buffer[0] = 42; + Span span = buffer; + span[0].ShouldBe(42); + } + + [Fact] + public void ImplicitReadOnlySpanConversion_Works() + { + using BufferScope buffer = new(8); + buffer[0] = 42; + ReadOnlySpan span = buffer; + span[0].ShouldBe(42); + } + + [Fact] + public void GetEnumerator_IteratesOverElements() + { + using BufferScope buffer = new(stackalloc int[3]); + buffer[0] = 1; + buffer[1] = 2; + buffer[2] = 3; + + int sum = 0; + foreach (int value in buffer) + { + sum += value; + } + sum.ShouldBe(6); + } + + [Fact] + public void MinimumLengthConstructor_HandlesZeroLength() + { + using BufferScope buffer = new(0); + buffer.Length.ShouldBeGreaterThanOrEqualTo(0); + } + + [Fact] + public void InitialBufferConstructor_HandlesEmptySpan() + { + using BufferScope buffer = new([]); + buffer.Length.ShouldBe(0); + } + + [Fact] + public void InitialBufferWithMinimum_UsesInitialWhenEqualToMinimum() + { + using BufferScope buffer = new(stackalloc char[10], 10); + buffer.Length.ShouldBe(10); + } + + [Fact] + public void EnsureCapacity_GrowWithoutCopy_ExpandsBuffer() + { + using BufferScope buffer = new(10); + buffer[0] = 42; + buffer[5] = 100; + + buffer.EnsureCapacity(50, copy: false); + + buffer.Length.ShouldBeGreaterThanOrEqualTo(50); + } + + [Fact] + public void EnsureCapacity_MultipleGrows_PreservesCopiedData() + { + using BufferScope buffer = new(5); + buffer[0] = 1; + buffer[1] = 2; + + buffer.EnsureCapacity(10, copy: true); + buffer[0].ShouldBe(1); + buffer[1].ShouldBe(2); + + buffer.EnsureCapacity(50, copy: true); + buffer.Length.ShouldBeGreaterThanOrEqualTo(50); + buffer[0].ShouldBe(1); + buffer[1].ShouldBe(2); + } + + [Fact] + public void RangeSlicing_FullRange_ReturnsAllElements() + { + using BufferScope buffer = new(stackalloc char[5]); + buffer[0] = 'A'; + buffer[1] = 'B'; + buffer[2] = 'C'; + buffer[3] = 'D'; + buffer[4] = 'E'; + + Span slice = buffer[..]; + slice.Length.ShouldBe(5); + slice[0].ShouldBe('A'); + slice[4].ShouldBe('E'); + } + + [Fact] + public void RangeSlicing_PartialRange_ReturnsExpectedElements() + { + using BufferScope buffer = new(stackalloc int[10]); + for (int i = 0; i < 10; i++) + { + buffer[i] = i; + } + + Span slice = buffer[2..8]; + slice.Length.ShouldBe(6); + slice[0].ShouldBe(2); + slice[5].ShouldBe(7); + } + + [Fact] + public void RangeSlicing_EmptyRange_ReturnsEmptySpan() + { + using BufferScope buffer = new(10); + Span slice = buffer[5..5]; + slice.Length.ShouldBe(0); + } + + [Fact] + public void Slice_CanReturnZeroLengthSpan() + { + using BufferScope buffer = new(10); + Span slice = buffer.Slice(5, 0); + slice.Length.ShouldBe(0); + } + + [Fact] + public void GetEnumerator_EmptyBuffer_YieldsNoElements() + { + using BufferScope buffer = new([]); + + int count = 0; + foreach (string value in buffer) + { + _ = value; + count++; + } + + count.ShouldBe(0); + } + + [Fact] + public void ToString_EmptyBuffer_ReturnsEmptyString() + { + using BufferScope buffer = new([]); + buffer.ToString().ShouldBe(string.Empty); + } + + [Fact] + public void GetPinnableReference_CanModifyUnderlyingMemory() + { + using BufferScope buffer = new(stackalloc byte[10]); + buffer[0] = 255; + buffer[9] = 128; + + ref byte reference = ref buffer.GetPinnableReference(); + reference.ShouldBe((byte)255); + reference = 100; + + buffer[0].ShouldBe((byte)100); + } + + [Fact] + public void GetPinnableReference_EmptyBuffer_DoesNotThrow() + { + using BufferScope buffer = new([]); + buffer.GetPinnableReference(); + buffer.Length.ShouldBe(0); + } + + [Fact] + public void Fixed_PinsPooledBuffer() + { + using BufferScope buffer = new(64); + buffer[0] = 'Y'; + + unsafe + { + fixed (char* p = buffer) + { + (*p).ShouldBe('Y'); + *p = 'Z'; + } + } + + buffer[0].ShouldBe('Z'); + } + + [Fact] + public void WorksWithReferenceTypes() + { + using BufferScope buffer = new(5); + buffer[0] = "Hello"; + buffer[1] = "World"; + + buffer[0].ShouldBe("Hello"); + buffer[1].ShouldBe("World"); + } + + [Fact] + public void WorksWithValueTypeStructs() + { + using BufferScope buffer = new(3); + DateTime date1 = new(2025, 1, 1); + DateTime date2 = new(2025, 12, 31); + + buffer[0] = date1; + buffer[1] = date2; + + buffer[0].ShouldBe(date1); + buffer[1].ShouldBe(date2); + } + + [Fact] + public void CombinedOperations_GrowSliceAndEnumerate() + { + using BufferScope buffer = new(stackalloc int[5], 10); + + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = i + 1; + } + + buffer.EnsureCapacity(20, copy: true); + + for (int i = 0; i < 5; i++) + { + buffer[i].ShouldBe(i + 1); + } + + Span slice = buffer[1..4]; + slice.Length.ShouldBe(3); + slice[0].ShouldBe(2); + slice[2].ShouldBe(4); + + int sum = 0; + foreach (int value in buffer.AsSpan()[..5]) + { + sum += value; + } + + sum.ShouldBe(15); + } + + [Fact] + public void Dispose_ClearsSpan() + { + BufferScope buffer = new(16); + buffer.Length.ShouldBeGreaterThan(0); + buffer.Dispose(); + buffer.Length.ShouldBe(0); + } + + [Fact] + public void Dispose_SafeToCallMultipleTimes() + { + BufferScope buffer = new(8); + buffer.Dispose(); + // Calling Dispose a second time must not throw. ref structs cannot be + // captured by a lambda, so invoke directly. + buffer.Dispose(); + } + + [Fact] + public void Fixed_PinsUnderlyingMemory() + { + using BufferScope buffer = new(stackalloc char[8]); + buffer[0] = 'x'; + unsafe + { + fixed (char* p = buffer) + { + (*p).ShouldBe('x'); + } + } + } + } +} diff --git a/src/Framework.UnitTests/TypeInfoTests.cs b/src/Framework.UnitTests/TypeInfoTests.cs deleted file mode 100644 index 55c812b7208..00000000000 --- a/src/Framework.UnitTests/TypeInfoTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -// 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 Microsoft.Build.Utilities; -using Shouldly; -using Xunit; - -namespace Microsoft.Build.Framework.UnitTests; - -public class TypeInfoTests -{ - [Fact] - public void IsReferenceOrContainsReferences_ReferenceTypes_ReturnsTrue() - { - // Reference types should return true - TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); - TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); - TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); - } - - [Fact] - public void IsReferenceOrContainsReferences_ValueTypesWithoutReferences_ReturnsFalse() - { - // Value types without references should return false - TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); - TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); - TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); - TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); - TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); - } - - [Fact] - public void IsReferenceOrContainsReferences_ValueTypesWithReferences_ReturnsTrue() - { - // Value types containing references should return true - TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); - TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); - } - - [Fact] - public void IsReferenceOrContainsReferences_ResultIsCached() - { - // First call should compute the result - bool firstResult = TypeInfo.IsReferenceOrContainsReferences(); - - // Second call should use cached result - bool secondResult = TypeInfo.IsReferenceOrContainsReferences(); - - // Both calls should return the same result - secondResult.ShouldBe(firstResult); - } - -#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value - private enum TestEnum - { - Value1, - Value2 - } - - private struct SimpleStruct - { - public int X; - public int Y; - } - - private struct StructWithReference - { - public object Reference; - public int Value; - } - - private struct StructWithString - { - public string Text; - public double Number; - } -#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value -} diff --git a/src/Framework.UnitTests/TypeInfo_Tests.cs b/src/Framework.UnitTests/TypeInfo_Tests.cs new file mode 100644 index 00000000000..b16fe2923de --- /dev/null +++ b/src/Framework.UnitTests/TypeInfo_Tests.cs @@ -0,0 +1,145 @@ +// 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; +using Microsoft.Build.Utilities; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Tests for . + /// + public class TypeInfo_Tests + { + [Fact] + public void PrimitiveTypes_AreNotReferences() + { + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + } + + [Fact] + public void DateTime_IsNotReference() + { + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + } + + [Fact] + public void EnumTypes_AreNotReferences() + { + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + } + + [Fact] + public void StringType_IsReference() + { + TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); + } + + [Fact] + public void ObjectType_IsReference() + { + TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); + } + + [Fact] + public void ReferenceType_IsReference() + { + TypeInfo>.IsReferenceOrContainsReferences().ShouldBeTrue(); + } + + [Fact] + public void ContainingTestType_IsReference() + { + TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); + } + + [Fact] + public void StructContainingReference_ContainsReferences() + { + TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); + } + + [Fact] + public void StructContainingString_ContainsReferences() + { + TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); + } + + [Fact] + public void SimpleStruct_DoesNotContainReferences() + { + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + } + + [Fact] + public void PureValueStruct_DoesNotContainReferences() + { + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + } + + [Fact] + public void Result_IsCached_ReturnsConsistentValue() + { + // Call multiple times to exercise the cached code path. + bool first = TypeInfo.IsReferenceOrContainsReferences(); + bool second = TypeInfo.IsReferenceOrContainsReferences(); + bool third = TypeInfo.IsReferenceOrContainsReferences(); + + first.ShouldBe(second); + second.ShouldBe(third); + first.ShouldBeTrue(); + } + + [Fact] + public void Result_IsCached_ForValueType_ReturnsConsistentValue() + { + bool first = TypeInfo.IsReferenceOrContainsReferences(); + bool second = TypeInfo.IsReferenceOrContainsReferences(); + + second.ShouldBe(first); + first.ShouldBeFalse(); + } + + private struct StructWithReference + { +#pragma warning disable CS0649 // Field is never assigned — only the layout matters for the test + public object Reference; + public int Value; +#pragma warning restore CS0649 + } + + private struct StructWithString + { +#pragma warning disable CS0649 // Field is never assigned — only the layout matters for the test + public string Text; + public double Number; +#pragma warning restore CS0649 + } + + private struct SimpleStruct + { +#pragma warning disable CS0649 // Field is never assigned — only the layout matters for the test + public int X; + public int Y; +#pragma warning restore CS0649 + } + + private struct PureValueStruct + { +#pragma warning disable CS0649 // Field is never assigned — only the layout matters for the test + public int A; + public long B; + public double C; +#pragma warning restore CS0649 + } + } +} diff --git a/src/Framework/BackEnd/CommunicationsUtilities.cs b/src/Framework/BackEnd/CommunicationsUtilities.cs index 8d12a7b48ff..59ebe33bcfa 100644 --- a/src/Framework/BackEnd/CommunicationsUtilities.cs +++ b/src/Framework/BackEnd/CommunicationsUtilities.cs @@ -82,14 +82,14 @@ internal static int NodeConnectionTimeout /// Get environment block. /// [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [System.Runtime.Versioning.SupportedOSPlatform("windows")] + [System.Runtime.Versioning.SupportedOSPlatform("windows6.1")] private static extern unsafe char* GetEnvironmentStrings(); /// /// Free environment block. /// [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [System.Runtime.Versioning.SupportedOSPlatform("windows")] + [System.Runtime.Versioning.SupportedOSPlatform("windows6.1")] private static extern unsafe bool FreeEnvironmentStrings(char* pStrings); #if NETFRAMEWORK @@ -132,7 +132,7 @@ private sealed record class EnvironmentState( /// /// Copied from the BCL implementation to eliminate some expensive security asserts on .NET Framework. /// - [System.Runtime.Versioning.SupportedOSPlatform("windows")] + [System.Runtime.Versioning.SupportedOSPlatform("windows6.1")] private static FrozenDictionary GetEnvironmentVariablesWindows() { // The FrameworkDebugUtils static constructor can set the MSBUILDDEBUGPATH environment variable to propagate the debug path to out of proc nodes. diff --git a/src/Framework/EncodingUtilities.cs b/src/Framework/EncodingUtilities.cs index 342e07d3d67..afb8bf0a384 100644 --- a/src/Framework/EncodingUtilities.cs +++ b/src/Framework/EncodingUtilities.cs @@ -12,6 +12,9 @@ using Microsoft.Build.Framework; using Microsoft.Win32; +#if FEATURE_WINDOWSINTEROP +using Windows.Win32; +#endif #nullable disable @@ -55,14 +58,16 @@ internal static Encoding CurrentSystemOemEncoding try { +#if FEATURE_WINDOWSINTEROP if (NativeMethods.IsWindows) { #if RUNTIME_TYPE_NETCORE Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); #endif // get the current OEM code page - s_currentOemEncoding = Encoding.GetEncoding(NativeMethods.GetOEMCP()); + s_currentOemEncoding = Encoding.GetEncoding((int)PInvoke.GetOEMCP()); } +#endif } // theoretically, GetEncoding may throw an ArgumentException or a NotSupportedException. This should never // really happen, since the code page we pass in has just been returned from the "underlying platform", diff --git a/src/Framework/FileSystem/FileSystems.cs b/src/Framework/FileSystem/FileSystems.cs index 4ceaaa2085d..8c3f017647a 100644 --- a/src/Framework/FileSystem/FileSystems.cs +++ b/src/Framework/FileSystem/FileSystems.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if FEATURE_WINDOWSINTEROP using Microsoft.Build.Framework; +#endif namespace Microsoft.Build.Shared.FileSystem { @@ -14,11 +16,13 @@ internal static class FileSystems private static IFileSystem GetFileSystem() { +#if FEATURE_WINDOWSINTEROP if (NativeMethods.IsWindows) { return MSBuildOnWindowsFileSystem.Singleton(); } else +#endif { return ManagedFileSystem.Singleton(); } diff --git a/src/Framework/FileSystem/MSBuildOnWindowsFileSystem.cs b/src/Framework/FileSystem/MSBuildOnWindowsFileSystem.cs index d7386b41871..b6e8ae3a4e2 100644 --- a/src/Framework/FileSystem/MSBuildOnWindowsFileSystem.cs +++ b/src/Framework/FileSystem/MSBuildOnWindowsFileSystem.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -12,7 +12,7 @@ namespace Microsoft.Build.Shared.FileSystem /// Implementation of file system operations on windows. Combination of native and managed implementations. /// TODO Remove this class and replace with WindowsFileSystem. Test perf to ensure no regressions. /// - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] internal sealed class MSBuildOnWindowsFileSystem : IFileSystem { private static readonly MSBuildOnWindowsFileSystem Instance = new MSBuildOnWindowsFileSystem(); diff --git a/src/Framework/FileSystem/WindowsFileSystem.cs b/src/Framework/FileSystem/WindowsFileSystem.cs index c215adc4733..ad57fbc2fc7 100644 --- a/src/Framework/FileSystem/WindowsFileSystem.cs +++ b/src/Framework/FileSystem/WindowsFileSystem.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -8,7 +8,8 @@ using System.Runtime.InteropServices; using System.Runtime.Versioning; using Microsoft.Build.Framework; - +using Windows.Win32; +using Windows.Win32.Storage.FileSystem; namespace Microsoft.Build.Shared.FileSystem { @@ -29,7 +30,7 @@ internal enum FileArtifactType : byte /// Windows-specific implementation of file system operations using Windows native invocations. /// TODO For potential extra perf gains, provide native implementations for all IFileSystem methods and stop inheriting from ManagedFileSystem /// - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] internal sealed class WindowsFileSystem : ManagedFileSystem { private static readonly WindowsFileSystem Instance = new(); @@ -61,7 +62,9 @@ public override bool DirectoryExists(string path) throw new PathTooLongException(SR.FormatPathTooLong(path, NativeMethods.MaxPath)); } - return NativeMethods.DirectoryExistsWindows(path); + uint attrs = PInvoke.GetFileAttributes(path); + return attrs != PInvoke.INVALID_FILE_ATTRIBUTES + && (attrs & (uint)FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) != 0; } public override bool FileExists(string path) @@ -73,10 +76,7 @@ public override bool FileExists(string path) #endif } - public override bool FileOrDirectoryExists(string path) - { - return NativeMethods.FileOrDirectoryExistsWindows(path); - } + public override bool FileOrDirectoryExists(string path) => PInvoke.GetFileAttributes(path) != PInvoke.INVALID_FILE_ATTRIBUTES; public override DateTime GetLastWriteTimeUtc(string path) { diff --git a/src/Framework/Framework/Windows/Win32/IComIID.cs b/src/Framework/Framework/Windows/Win32/IComIID.cs new file mode 100644 index 00000000000..0e4cb48f077 --- /dev/null +++ b/src/Framework/Framework/Windows/Win32/IComIID.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// This file is only compiled for .NET Framework (net472) where static abstract +// interface members are not available. On .NET 7+, CsWin32 generates IComIID +// with static abstract members directly. + +using System; + +namespace Windows.Win32; + +/// +/// Common interface for COM interface wrapping structs. +/// On .NET 7+, this is provided by CsWin32 as a static abstract interface. +/// On .NET Framework, we provide this instance-based version. +/// +internal interface IComIID +{ + Guid Guid { get; } +} diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 39b437d4e8c..12c665e5592 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -16,12 +16,17 @@ true + + + $(DefaultItemExcludes);**/Framework/**/* + + - <_PackageFiles Include="$(ArtifactsBinDir)Microsoft.Build.Framework\$(Configuration)\net472\Microsoft.Build.Framework.tlb" - PackagePath="tools/net472" /> + <_PackageFiles Include="$(ArtifactsBinDir)Microsoft.Build.Framework\$(Configuration)\net472\Microsoft.Build.Framework.tlb" PackagePath="tools/net472" /> @@ -84,4 +89,49 @@ + + + + + + + + + + + + + + + + all + + + + + + + all + + + + + + + System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute; + System.Diagnostics.CodeAnalysis.UnscopedRefAttribute + + + + + + + + + + + + + diff --git a/src/Framework/NativeMethods.cs b/src/Framework/NativeMethods.cs index cf1c317cee4..f8a1f140e65 100644 --- a/src/Framework/NativeMethods.cs +++ b/src/Framework/NativeMethods.cs @@ -1,20 +1,30 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Versioning; using Microsoft.Build.Framework.Logging; using Microsoft.Build.Shared; using Microsoft.Win32; +#if FEATURE_WINDOWSINTEROP +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using Microsoft.Build.Utilities; using Microsoft.Win32.SafeHandles; using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Storage.FileSystem; +using Windows.Win32.System.Console; +using Windows.Win32.System.Diagnostics.Debug; +using Windows.Win32.System.SystemInformation; +using Windows.Win32.System.Threading; +using Wdk = Windows.Wdk; +using WdkThreading = Windows.Wdk.System.Threading; +#endif #nullable disable @@ -22,34 +32,11 @@ namespace Microsoft.Build.Framework; internal static class NativeMethods { - #region Constants - - internal const uint ERROR_INSUFFICIENT_BUFFER = 0x8007007A; - internal const uint STARTUP_LOADER_SAFEMODE = 0x10; - internal const uint S_OK = 0x0; - internal const uint S_FALSE = 0x1; - internal const uint ERROR_ACCESS_DENIED = 0x5; - internal const uint ERROR_FILE_NOT_FOUND = 0x80070002; - internal const uint FUSION_E_PRIVATE_ASM_DISALLOWED = 0x80131044; // Tried to find unsigned assembly in GAC - internal const uint RUNTIME_INFO_DONT_SHOW_ERROR_DIALOG = 0x40; - internal const uint FILE_TYPE_CHAR = 0x0002; - internal const Int32 STD_OUTPUT_HANDLE = -11; - internal const Int32 STD_ERROR_HANDLE = -12; - internal const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; - internal const uint RPC_S_CALLPENDING = 0x80010115; - internal const uint E_ABORT = (uint)0x80004004; - - internal const int FILE_ATTRIBUTE_READONLY = 0x00000001; - internal const int FILE_ATTRIBUTE_DIRECTORY = 0x00000010; - internal const int FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400; - /// /// Default buffer size to use when dealing with the Windows API. /// internal const int MAX_PATH = 260; - private const string kernel32Dll = "kernel32.dll"; - private const string WINDOWS_FILE_SYSTEM_REGISTRY_KEY = @"SYSTEM\CurrentControlSet\Control\FileSystem"; private const string WINDOWS_LONG_PATHS_ENABLED_VALUE_NAME = "LongPathsEnabled"; @@ -58,132 +45,6 @@ internal static class NativeMethods internal static DateTime MinFileDate { get; } = DateTime.FromFileTimeUtc(0); - internal static HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero); - - internal static IntPtr NullIntPtr = new IntPtr(0); - internal static IntPtr InvalidHandle = new IntPtr(-1); - - // As defined in winnt.h: - internal const ushort PROCESSOR_ARCHITECTURE_INTEL = 0; - internal const ushort PROCESSOR_ARCHITECTURE_ARM = 5; - internal const ushort PROCESSOR_ARCHITECTURE_IA64 = 6; - internal const ushort PROCESSOR_ARCHITECTURE_AMD64 = 9; - internal const ushort PROCESSOR_ARCHITECTURE_ARM64 = 12; - - internal const uint INFINITE = 0xFFFFFFFF; - internal const uint WAIT_ABANDONED_0 = 0x00000080; - internal const uint WAIT_OBJECT_0 = 0x00000000; - internal const uint WAIT_TIMEOUT = 0x00000102; - - #endregion - - #region Enums - - internal enum StreamHandleType - { - StdOut = STD_OUTPUT_HANDLE, - StdErr = STD_ERROR_HANDLE, - }; - - private enum PROCESSINFOCLASS : int - { - ProcessBasicInformation = 0, - ProcessQuotaLimits, - ProcessIoCounters, - ProcessVmCounters, - ProcessTimes, - ProcessBasePriority, - ProcessRaisePriority, - ProcessDebugPort, - ProcessExceptionPort, - ProcessAccessToken, - ProcessLdtInformation, - ProcessLdtSize, - ProcessDefaultHardErrorMode, - ProcessIoPortHandlers, // Note: this is kernel mode only - ProcessPooledUsageAndLimits, - ProcessWorkingSetWatch, - ProcessUserModeIOPL, - ProcessEnableAlignmentFaultFixup, - ProcessPriorityClass, - ProcessWx86Information, - ProcessHandleCount, - ProcessAffinityMask, - ProcessPriorityBoost, - MaxProcessInfoClass - }; - - private enum eDesiredAccess : int - { - DELETE = 0x00010000, - READ_CONTROL = 0x00020000, - WRITE_DAC = 0x00040000, - WRITE_OWNER = 0x00080000, - SYNCHRONIZE = 0x00100000, - STANDARD_RIGHTS_ALL = 0x001F0000, - - PROCESS_TERMINATE = 0x0001, - PROCESS_CREATE_THREAD = 0x0002, - PROCESS_SET_SESSIONID = 0x0004, - PROCESS_VM_OPERATION = 0x0008, - PROCESS_VM_READ = 0x0010, - PROCESS_VM_WRITE = 0x0020, - PROCESS_DUP_HANDLE = 0x0040, - PROCESS_CREATE_PROCESS = 0x0080, - PROCESS_SET_QUOTA = 0x0100, - PROCESS_SET_INFORMATION = 0x0200, - PROCESS_QUERY_INFORMATION = 0x0400, - PROCESS_ALL_ACCESS = SYNCHRONIZE | 0xFFF - } -#pragma warning disable 0649, 0169 - internal enum LOGICAL_PROCESSOR_RELATIONSHIP - { - RelationProcessorCore, - RelationNumaNode, - RelationCache, - RelationProcessorPackage, - RelationGroup, - RelationAll = 0xffff - } - internal struct SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX - { - public LOGICAL_PROCESSOR_RELATIONSHIP Relationship; - public uint Size; - public PROCESSOR_RELATIONSHIP Processor; - } - [StructLayout(LayoutKind.Sequential)] - internal unsafe struct PROCESSOR_RELATIONSHIP - { - public byte Flags; - private byte EfficiencyClass; - private fixed byte Reserved[20]; - public ushort GroupCount; - public IntPtr GroupInfo; - } -#pragma warning restore 0169, 0149 - - /// - /// Flags for CoWaitForMultipleHandles - /// - [Flags] - public enum COWAIT_FLAGS : int - { - /// - /// Exit when a handle is signaled. - /// - COWAIT_NONE = 0, - - /// - /// Exit when all handles are signaled AND a message is received. - /// - COWAIT_WAITALL = 0x00000001, - - /// - /// Exit when an RPC call is serviced. - /// - COWAIT_ALERTABLE = 0x00000002 - } - /// /// Processor architecture values /// @@ -223,180 +84,28 @@ internal enum ProcessorArchitectures Unknown } - internal enum SymbolicLink - { - File = 0, - Directory = 1, - AllowUnprivilegedCreate = 2, - } - - #endregion - - #region Structs - - /// - /// Structure that contain information about the system on which we are running - /// - [StructLayout(LayoutKind.Sequential)] - internal struct SYSTEM_INFO - { - // This is a union of a DWORD and a struct containing 2 WORDs. - internal ushort wProcessorArchitecture; - internal ushort wReserved; - - internal uint dwPageSize; - internal IntPtr lpMinimumApplicationAddress; - internal IntPtr lpMaximumApplicationAddress; - internal IntPtr dwActiveProcessorMask; - internal uint dwNumberOfProcessors; - internal uint dwProcessorType; - internal uint dwAllocationGranularity; - internal ushort wProcessorLevel; - internal ushort wProcessorRevision; - } - /// /// Wrap the intptr returned by OpenProcess in a safe handle. /// +#if FEATURE_WINDOWSINTEROP internal class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvalid { - // Create a SafeHandle, informing the base class - // that this SafeHandle instance "owns" the handle, - // and therefore SafeHandle should call - // our ReleaseHandle method when the SafeHandle - // is no longer in use - private SafeProcessHandle() : base(true) - { - } - - [SupportedOSPlatform("windows")] - protected override bool ReleaseHandle() - { - return CloseHandle(handle); - } - } - - /// - /// Contains information about the current state of both physical and virtual memory, including extended memory - /// - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] - internal class MemoryStatus - { - /// - /// Initializes a new instance of the class. - /// - public MemoryStatus() + internal SafeProcessHandle(IntPtr handle) : base(true) { - _length = (uint)Marshal.SizeOf(); + SetHandle(handle); } - /// - /// Size of the structure, in bytes. You must set this member before calling GlobalMemoryStatusEx. - /// - private uint _length; - - /// - /// Number between 0 and 100 that specifies the approximate percentage of physical - /// memory that is in use (0 indicates no memory use and 100 indicates full memory use). - /// - public uint MemoryLoad; - - /// - /// Total size of physical memory, in bytes. - /// - public ulong TotalPhysical; - - /// - /// Size of physical memory available, in bytes. - /// - public ulong AvailablePhysical; - - /// - /// Size of the committed memory limit, in bytes. This is physical memory plus the - /// size of the page file, minus a small overhead. - /// - public ulong TotalPageFile; - - /// - /// Size of available memory to commit, in bytes. The limit is ullTotalPageFile. - /// - public ulong AvailablePageFile; - - /// - /// Total size of the user mode portion of the virtual address space of the calling process, in bytes. - /// - public ulong TotalVirtual; - - /// - /// Size of unreserved and uncommitted memory in the user mode portion of the virtual - /// address space of the calling process, in bytes. - /// - public ulong AvailableVirtual; - - /// - /// Size of unreserved and uncommitted memory in the extended portion of the virtual - /// address space of the calling process, in bytes. - /// - public ulong AvailableExtendedVirtual; - } - - [StructLayout(LayoutKind.Sequential)] - private struct PROCESS_BASIC_INFORMATION - { - public uint ExitStatus; - public IntPtr PebBaseAddress; - public UIntPtr AffinityMask; - public int BasePriority; - public UIntPtr UniqueProcessId; - public UIntPtr InheritedFromUniqueProcessId; - - public readonly uint Size + private SafeProcessHandle() : base(true) { - get - { - unsafe - { - return (uint)sizeof(PROCESS_BASIC_INFORMATION); - } - } } - }; - - /// - /// Contains information about a file or directory; used by GetFileAttributesEx. - /// - [StructLayout(LayoutKind.Sequential)] - public struct WIN32_FILE_ATTRIBUTE_DATA - { - internal int fileAttributes; - internal uint ftCreationTimeLow; - internal uint ftCreationTimeHigh; - internal uint ftLastAccessTimeLow; - internal uint ftLastAccessTimeHigh; - internal uint ftLastWriteTimeLow; - internal uint ftLastWriteTimeHigh; - internal uint fileSizeHigh; - internal uint fileSizeLow; - } - /// - /// Contains the security descriptor for an object and specifies whether - /// the handle retrieved by specifying this structure is inheritable. - /// - [StructLayout(LayoutKind.Sequential)] - internal class SecurityAttributes - { - public SecurityAttributes() + [SupportedOSPlatform("windows6.1")] + protected override bool ReleaseHandle() { - _nLength = (uint)Marshal.SizeOf(); + return PInvoke.CloseHandle((HANDLE)handle); } - - private uint _nLength; - - public IntPtr lpSecurityDescriptor; - - public bool bInheritHandle; } +#endif private class SystemInformationData { @@ -412,23 +121,25 @@ private class SystemInformationData /// public readonly ProcessorArchitectures ProcessorArchitectureTypeNative; +#if FEATURE_WINDOWSINTEROP /// /// Convert SYSTEM_INFO architecture values to the internal enum /// /// /// - private static ProcessorArchitectures ConvertSystemArchitecture(ushort arch) + private static ProcessorArchitectures ConvertSystemArchitecture(PROCESSOR_ARCHITECTURE arch) { return arch switch { - PROCESSOR_ARCHITECTURE_INTEL => ProcessorArchitectures.X86, - PROCESSOR_ARCHITECTURE_AMD64 => ProcessorArchitectures.X64, - PROCESSOR_ARCHITECTURE_ARM => ProcessorArchitectures.ARM, - PROCESSOR_ARCHITECTURE_IA64 => ProcessorArchitectures.IA64, - PROCESSOR_ARCHITECTURE_ARM64 => ProcessorArchitectures.ARM64, + PROCESSOR_ARCHITECTURE.PROCESSOR_ARCHITECTURE_INTEL => ProcessorArchitectures.X86, + PROCESSOR_ARCHITECTURE.PROCESSOR_ARCHITECTURE_AMD64 => ProcessorArchitectures.X64, + PROCESSOR_ARCHITECTURE.PROCESSOR_ARCHITECTURE_ARM => ProcessorArchitectures.ARM, + PROCESSOR_ARCHITECTURE.PROCESSOR_ARCHITECTURE_IA64 => ProcessorArchitectures.IA64, + PROCESSOR_ARCHITECTURE.PROCESSOR_ARCHITECTURE_ARM64 => ProcessorArchitectures.ARM64, _ => ProcessorArchitectures.Unknown, }; } +#endif /// /// Read system info values @@ -438,17 +149,18 @@ public SystemInformationData() ProcessorArchitectureType = ProcessorArchitectures.Unknown; ProcessorArchitectureTypeNative = ProcessorArchitectures.Unknown; +#if FEATURE_WINDOWSINTEROP if (IsWindows) { - var systemInfo = new SYSTEM_INFO(); + SYSTEM_INFO systemInfo; + PInvoke.GetSystemInfo(out systemInfo); + ProcessorArchitectureType = ConvertSystemArchitecture(systemInfo.Anonymous.Anonymous.wProcessorArchitecture); - GetSystemInfo(ref systemInfo); - ProcessorArchitectureType = ConvertSystemArchitecture(systemInfo.wProcessorArchitecture); - - GetNativeSystemInfo(ref systemInfo); - ProcessorArchitectureTypeNative = ConvertSystemArchitecture(systemInfo.wProcessorArchitecture); + PInvoke.GetNativeSystemInfo(out systemInfo); + ProcessorArchitectureTypeNative = ConvertSystemArchitecture(systemInfo.Anonymous.Anonymous.wProcessorArchitecture); } else +#endif { ProcessorArchitectures processorArchitecture = ProcessorArchitectures.Unknown; @@ -483,6 +195,7 @@ public static int GetLogicalCoreCount() // .NET on Windows returns a core count limited to the current NUMA node // https://github.com/dotnet/runtime/issues/29686 // so always double-check it. +#if FEATURE_WINDOWSINTEROP if (IsWindows) { var result = GetLogicalCoreCountOnWindows(); @@ -491,34 +204,35 @@ public static int GetLogicalCoreCount() numberOfCpus = result; } } +#endif return numberOfCpus; } +#if FEATURE_WINDOWSINTEROP /// /// Get the exact physical core count on Windows /// Useful for getting the exact core count in 32 bits processes, /// as Environment.ProcessorCount has a 32-core limit in that case. /// https://github.com/dotnet/runtime/blob/221ad5b728f93489655df290c1ea52956ad8f51c/src/libraries/System.Runtime.Extensions/src/System/Environment.Windows.cs#L171-L210 /// - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] private static unsafe int GetLogicalCoreCountOnWindows() { uint len = 0; const int ERROR_INSUFFICIENT_BUFFER = 122; - if (!GetLogicalProcessorInformationEx(LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore, IntPtr.Zero, ref len) && + if (!PInvoke.GetLogicalProcessorInformationEx(LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore, null, ref len) && Marshal.GetLastWin32Error() == ERROR_INSUFFICIENT_BUFFER) { - // Allocate that much space - var buffer = new byte[len]; + using BufferScope buffer = new((int)len); fixed (byte* bufferPtr = buffer) { - // Call GetLogicalProcessorInformationEx with the allocated buffer - if (GetLogicalProcessorInformationEx(LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore, (IntPtr)bufferPtr, ref len)) + if (PInvoke.GetLogicalProcessorInformationEx( + LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore, + (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)bufferPtr, + ref len)) { - // Walk each SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX in the buffer, where the Size of each dictates how - // much space it's consuming. For each group relation, count the number of active processors in each of its group infos. int processorCount = 0; byte* ptr = bufferPtr; byte* endPtr = bufferPtr + len; @@ -527,9 +241,7 @@ private static unsafe int GetLogicalCoreCountOnWindows() var current = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)ptr; if (current->Relationship == LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore) { - // Flags is 0 if the core has a single logical proc, LTP_PC_SMT if more than one - // for now, assume "more than 1" == 2, as it has historically been for hyperthreading - processorCount += (current->Processor.Flags == 0) ? 1 : 2; + processorCount += (current->Anonymous.Processor.Flags == 0) ? 1 : 2; } ptr += current->Size; } @@ -540,10 +252,7 @@ private static unsafe int GetLogicalCoreCountOnWindows() return -1; } - - #endregion - - #region Member data +#endif internal static bool HasMaxPath => MaxPath == MAX_PATH; @@ -630,7 +339,7 @@ internal static bool IsMaxPathLegacyWindows() return longPathsStatus == LongPathsStatus.Disabled || longPathsStatus == LongPathsStatus.Missing; } - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] private static LongPathsStatus IsLongPathsEnabledRegistry() { using (RegistryKey fileSystemKey = Registry.LocalMachine.OpenSubKey(WINDOWS_FILE_SYSTEM_REGISTRY_KEY)) @@ -681,7 +390,7 @@ internal static SAC_State GetSACStateInternal() return SAC_State.NotApplicable; } - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] private static SAC_State GetSACStateRegistry() { SAC_State SACState = SAC_State.Missing; @@ -739,6 +448,7 @@ internal enum SAC_State /// /// Gets a flag indicating if we are running under a Unix-like system (Mac, Linux, etc.) /// + [UnsupportedOSPlatformGuard("windows")] internal static bool IsUnixLike { get { return s_isUnixLike; } @@ -784,7 +494,7 @@ internal static bool IsHaiku /// /// Gets a flag indicating if we are running under some version of Windows /// - [SupportedOSPlatformGuard("windows")] + [SupportedOSPlatformGuard("windows6.1")] internal static bool IsWindows { get @@ -890,7 +600,7 @@ internal static string FrameworkCurrentPath { if (s_frameworkCurrentPath == null) { - var baseTypeLocation = AssemblyUtilities.GetAssemblyLocation(typeof(string).GetTypeInfo().Assembly); + var baseTypeLocation = AssemblyUtilities.GetAssemblyLocation(typeof(string).Assembly); s_frameworkCurrentPath = Path.GetDirectoryName(baseTypeLocation) @@ -963,23 +673,6 @@ private static SystemInformationData SystemInformation /// internal static ProcessorArchitectures ProcessorArchitectureNative => SystemInformation.ProcessorArchitectureTypeNative; - #endregion - - #region Wrapper methods - - - [DllImport("kernel32.dll", SetLastError = true)] - [SupportedOSPlatform("windows")] - internal static extern void GetSystemInfo(ref SYSTEM_INFO lpSystemInfo); - - [DllImport("kernel32.dll", SetLastError = true)] - [SupportedOSPlatform("windows")] - internal static extern void GetNativeSystemInfo(ref SYSTEM_INFO lpSystemInfo); - - [DllImport("kernel32.dll", SetLastError = true)] - [SupportedOSPlatform("windows")] - internal static extern bool GetLogicalProcessorInformationEx(LOGICAL_PROCESSOR_RELATIONSHIP RelationshipType, IntPtr Buffer, ref uint ReturnedLength); - /// /// Get the last write time of the fullpath to a directory. If the pointed path is not a directory, or /// if the directory does not exist, then false is returned and fileModifiedTimeUtc is set DateTime.MinValue. @@ -988,30 +681,20 @@ private static SystemInformationData SystemInformation /// The UTC last write time for the directory internal static bool GetLastWriteDirectoryUtcTime(string fullPath, out DateTime fileModifiedTimeUtc) { - // This code was copied from the reference manager, if there is a bug fix in that code, see if the same fix should also be made - // there +#if FEATURE_WINDOWSINTEROP if (IsWindows) { - fileModifiedTimeUtc = DateTime.MinValue; - - WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA(); - bool success = GetFileAttributesEx(fullPath, 0, ref data); - if (success) + if (PInvoke.GetFileAttributesEx(fullPath, out WIN32_FILE_ATTRIBUTE_DATA data) + && ((FILE_FLAGS_AND_ATTRIBUTES)data.dwFileAttributes & FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) != 0) { - if ((data.fileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) - { - long dt = ((long)(data.ftLastWriteTimeHigh) << 32) | ((long)data.ftLastWriteTimeLow); - fileModifiedTimeUtc = DateTime.FromFileTimeUtc(dt); - } - else - { - // Path does not point to a directory - success = false; - } + fileModifiedTimeUtc = DateTime.FromFileTimeUtc(data.ftLastWriteTime.ToLong()); + return true; } - return success; + fileModifiedTimeUtc = DateTime.MinValue; + return false; } +#endif if (Directory.Exists(fullPath)) { @@ -1035,29 +718,31 @@ internal static string GetShortFilePath(string path) return path; } +#if FEATURE_WINDOWSINTEROP if (path != null) { - int length = GetShortPathName(path, null, 0); - int errorCode = Marshal.GetLastWin32Error(); + using BufferScope buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]); + int length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan()); + WIN32_ERROR errorCode = (WIN32_ERROR)Marshal.GetLastWin32Error(); - if (length > 0) + if (length > buffer.Length) { - char[] fullPathBuffer = new char[length]; - length = GetShortPathName(path, fullPathBuffer, length); - errorCode = Marshal.GetLastWin32Error(); + buffer.EnsureCapacity(length); + length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan()); + errorCode = (WIN32_ERROR)Marshal.GetLastWin32Error(); + } - if (length > 0) - { - string fullPath = new(fullPathBuffer, 0, length); - path = fullPath; - } + if (length > 0) + { + path = buffer.Slice(0, length).ToString(); } - if (length == 0 && errorCode != 0) + if (length == 0 && errorCode != WIN32_ERROR.ERROR_SUCCESS) { - ThrowExceptionForErrorCode(errorCode); + ((HRESULT)errorCode).ThrowOnFailure(); } } +#endif return path; } @@ -1067,7 +752,7 @@ internal static string GetShortFilePath(string path) /// /// /// - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] internal static string GetLongFilePath(string path) { if (IsUnixLike) @@ -1075,69 +760,71 @@ internal static string GetLongFilePath(string path) return path; } +#if FEATURE_WINDOWSINTEROP if (path != null) { - int length = GetLongPathName(path, null, 0); - int errorCode = Marshal.GetLastWin32Error(); + using BufferScope buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]); + int length = (int)PInvoke.GetLongPathName(path, buffer.AsSpan()); + WIN32_ERROR errorCode = (WIN32_ERROR)Marshal.GetLastWin32Error(); - if (length > 0) + if (length > buffer.Length) { - char[] fullPathBuffer = new char[length]; - length = GetLongPathName(path, fullPathBuffer, length); - errorCode = Marshal.GetLastWin32Error(); + buffer.EnsureCapacity(length); + length = (int)PInvoke.GetLongPathName(path, buffer.AsSpan()); + errorCode = (WIN32_ERROR)Marshal.GetLastWin32Error(); + } - if (length > 0) - { - string fullPath = new(fullPathBuffer, 0, length); - path = fullPath; - } + if (length > 0) + { + path = buffer.Slice(0, length).ToString(); } - if (length == 0 && errorCode != 0) + if (length == 0 && errorCode != WIN32_ERROR.ERROR_SUCCESS) { - ThrowExceptionForErrorCode(errorCode); + ((HRESULT)errorCode).ThrowOnFailure(); } } +#endif return path; } +#if FEATURE_WINDOWSINTEROP /// /// Retrieves the current global memory status. /// - internal static MemoryStatus GetMemoryStatus() + internal static unsafe bool TryGetMemoryStatus(out MEMORYSTATUSEX memoryStatus) { + memoryStatus = default; + if (IsWindows) { - MemoryStatus status = new MemoryStatus(); - bool returnValue = GlobalMemoryStatusEx(status); - if (!returnValue) - { - return null; - } - - return status; + memoryStatus.dwLength = (uint)sizeof(MEMORYSTATUSEX); + return PInvoke.GlobalMemoryStatusEx(ref memoryStatus); } - return null; + return false; } +#endif internal static bool MakeSymbolicLink(string newFileName, string existingFileName, ref string errorMessage) { bool symbolicLinkCreated; +#if FEATURE_WINDOWSINTEROP if (IsWindows) { Version osVersion = Environment.OSVersion.Version; - SymbolicLink flags = SymbolicLink.File; + SYMBOLIC_LINK_FLAGS flags = 0; // File = 0 (no named constant) if (osVersion.Major >= 11 || (osVersion.Major == 10 && osVersion.Build >= 14972)) { - flags |= SymbolicLink.AllowUnprivilegedCreate; + flags |= SYMBOLIC_LINK_FLAGS.SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; } - symbolicLinkCreated = CreateSymbolicLink(newFileName, existingFileName, flags); + symbolicLinkCreated = PInvoke.CreateSymbolicLink(newFileName, existingFileName, flags); errorMessage = symbolicLinkCreated ? null : Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()).Message; } else +#endif { symbolicLinkCreated = symlink(existingFileName, newFileName) == 0; errorMessage = symbolicLinkCreated ? null : Marshal.GetLastWin32Error().ToString(); @@ -1185,6 +872,7 @@ DateTime LastWriteFileUtcTime(string path) { DateTime fileModifiedTime = DateTime.MinValue; +#if FEATURE_WINDOWSINTEROP if (IsWindows) { if (Traits.Instance.EscapeHatches.AlwaysUseContentTimestamp) @@ -1192,16 +880,15 @@ DateTime LastWriteFileUtcTime(string path) return GetContentLastWriteFileUtcTime(path); } - WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA(); - bool success = NativeMethods.GetFileAttributesEx(path, 0, ref data); + bool success = PInvoke.GetFileAttributesEx(path, out WIN32_FILE_ATTRIBUTE_DATA data); - if (success && (data.fileAttributes & NativeMethods.FILE_ATTRIBUTE_DIRECTORY) == 0) + if (success && ((FILE_FLAGS_AND_ATTRIBUTES)data.dwFileAttributes & FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) == 0) { - long dt = ((long)(data.ftLastWriteTimeHigh) << 32) | ((long)data.ftLastWriteTimeLow); - fileModifiedTime = DateTime.FromFileTimeUtc(dt); + fileModifiedTime = DateTime.FromFileTimeUtc(data.ftLastWriteTime.ToLong()); // If file is a symlink _and_ we're not instructed to do the wrong thing, get a more accurate timestamp. - if ((data.fileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT && !Traits.Instance.EscapeHatches.UseSymlinkTimeInsteadOfTargetTime) + if (((FILE_FLAGS_AND_ATTRIBUTES)data.dwFileAttributes & FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_REPARSE_POINT) != 0 + && !Traits.Instance.EscapeHatches.UseSymlinkTimeInsteadOfTargetTime) { fileModifiedTime = GetContentLastWriteFileUtcTime(path); } @@ -1209,30 +896,32 @@ DateTime LastWriteFileUtcTime(string path) return fileModifiedTime; } - else - { - return File.Exists(path) - ? File.GetLastWriteTimeUtc(path) - : DateTime.MinValue; - } +#endif + + return File.Exists(path) + ? File.GetLastWriteTimeUtc(path) + : DateTime.MinValue; } } +#if FEATURE_WINDOWSINTEROP /// /// Get the SafeFileHandle for a file, while skipping reparse points (going directly to target file). /// /// Full path to the file in the filesystem /// the SafeFileHandle for a file (target file in case of symlinks) - [SupportedOSPlatform("windows")] - private static SafeFileHandle OpenFileThroughSymlinks(string fullPath) + [SupportedOSPlatform("windows6.1")] + private static unsafe SafeFileHandle OpenFileThroughSymlinks(string fullPath) { - return CreateFile(fullPath, - GENERIC_READ, - FILE_SHARE_READ, - IntPtr.Zero, - OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, /* No FILE_FLAG_OPEN_REPARSE_POINT; read through to content */ - IntPtr.Zero); + HANDLE h = PInvoke.CreateFile( + fullPath, + (uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ, + FILE_SHARE_MODE.FILE_SHARE_READ, + null, + FILE_CREATION_DISPOSITION.OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, /* No FILE_FLAG_OPEN_REPARSE_POINT; read through to content */ + HANDLE.Null); + return new SafeFileHandle((IntPtr)h.Value, ownsHandle: true); } /// @@ -1244,8 +933,8 @@ private static SafeFileHandle OpenFileThroughSymlinks(string fullPath) /// This is the most accurate timestamp-extraction mechanism, but it is too slow to use all the time. /// See https://github.com/dotnet/msbuild/issues/2052. /// - [SupportedOSPlatform("windows")] - private static DateTime GetContentLastWriteFileUtcTime(string fullPath) + [SupportedOSPlatform("windows6.1")] + private static unsafe DateTime GetContentLastWriteFileUtcTime(string fullPath) { DateTime fileModifiedTime = DateTime.MinValue; @@ -1254,7 +943,7 @@ private static DateTime GetContentLastWriteFileUtcTime(string fullPath) if (!handle.IsInvalid) { FILETIME ftCreationTime, ftLastAccessTime, ftLastWriteTime; - if (GetFileTime(handle, out ftCreationTime, out ftLastAccessTime, out ftLastWriteTime)) + if (PInvoke.GetFileTime((HANDLE)handle.DangerousGetHandle(), &ftCreationTime, &ftLastAccessTime, &ftLastWriteTime)) { long fileTime = ((long)(uint)ftLastWriteTime.dwHighDateTime) << 32 | (long)(uint)ftLastWriteTime.dwLowDateTime; @@ -1266,22 +955,7 @@ private static DateTime GetContentLastWriteFileUtcTime(string fullPath) return fileModifiedTime; } - - /// - /// Did the HRESULT succeed - /// - public static bool HResultSucceeded(int hr) - { - return hr >= 0; - } - - /// - /// Did the HRESULT Fail - /// - public static bool HResultFailed(int hr) - { - return hr < 0; - } +#endif /// /// Given an error code, converts it to an HRESULT and throws the appropriate exception. @@ -1302,10 +976,11 @@ public static void ThrowExceptionForErrorCode(int errorCode) Marshal.ThrowExceptionForHR(errorCode); } +#if FEATURE_WINDOWSINTEROP /// /// Kills the specified process by id and all of its children recursively. /// - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] internal static void KillTree(int processIdToKill) { // Note that GetProcessById does *NOT* internally hold on to the process handle. @@ -1330,7 +1005,7 @@ internal static void KillTree(int processIdToKill) // Grab the process handle. We want to keep this open for the duration of the function so that // it cannot be reused while we are running. - using (SafeProcessHandle hProcess = OpenProcess(eDesiredAccess.PROCESS_QUERY_INFORMATION, false, processIdToKill)) + using (SafeProcessHandle hProcess = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_INFORMATION, false, processIdToKill)) { if (hProcess.IsInvalid) { @@ -1342,7 +1017,7 @@ internal static void KillTree(int processIdToKill) // Kill this process, so that no further children can be created. thisProcess.Kill(); } - catch (Win32Exception e) when (e.NativeErrorCode == ERROR_ACCESS_DENIED) + catch (Win32Exception e) when (e.NativeErrorCode == (int)WIN32_ERROR.ERROR_ACCESS_DENIED) { // Access denied is potentially expected -- it happens when the process that // we're attempting to kill is already dead. So just ignore in that case. @@ -1378,7 +1053,7 @@ internal static void KillTree(int processIdToKill) /// Returns the parent process id for the specified process. /// Returns zero if it cannot be gotten for some reason. /// - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] internal static int GetParentProcessId(int processId) { int ParentID = 0; @@ -1394,7 +1069,7 @@ internal static int GetParentProcessId(int processId) // using (var r = FileUtilities.OpenRead("/proc/" + processId + "/stat")) // and could be again when FileUtilities moves to Framework - using var fileStream = new FileStream($"/proc/{processId}/stat", FileMode.Open, System.IO.FileAccess.Read); + using var fileStream = new FileStream($"/proc/{processId}/stat", FileMode.Open, FileAccess.Read); using StreamReader r = new(fileStream); line = r.ReadLine(); @@ -1417,16 +1092,16 @@ internal static int GetParentProcessId(int processId) } else { - using SafeProcessHandle hProcess = OpenProcess(eDesiredAccess.PROCESS_QUERY_INFORMATION, false, processId); + using SafeProcessHandle hProcess = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_INFORMATION, false, processId); { if (!hProcess.IsInvalid) { // UNDONE: NtQueryInformationProcess will fail if we are not elevated and other process is. Advice is to change to use ToolHelp32 API's // For now just return zero and worst case we will not kill some children. - PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION(); + var pbi = default(PROCESS_BASIC_INFORMATION); int pSize = 0; - if (0 == NtQueryInformationProcess(hProcess, PROCESSINFOCLASS.ProcessBasicInformation, ref pbi, pbi.Size, ref pSize)) + if (0 == NtQueryInformationProcess(hProcess, ref pbi, ref pSize)) { ParentID = (int)pbi.InheritedFromUniqueProcessId; } @@ -1441,7 +1116,7 @@ internal static int GetParentProcessId(int processId) /// Returns an array of all the immediate child processes by id. /// NOTE: The IntPtr in the tuple is the handle of the child process. CloseHandle MUST be called on this. /// - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] internal static List> GetChildProcessIds(int parentProcessId, DateTime parentStartTime) { List> myChildren = new List>(); @@ -1453,7 +1128,7 @@ internal static List> GetChildProcessIds(in // Hold the child process handle open so that children cannot die and restart with a different parent after we've started looking at it. // This way, any handle we pass back is guaranteed to be one of our actual children. #pragma warning disable CA2000 // Dispose objects before losing scope - caller must dispose returned handles - SafeProcessHandle childHandle = OpenProcess(eDesiredAccess.PROCESS_QUERY_INFORMATION, false, possibleChildProcess.Id); + SafeProcessHandle childHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_INFORMATION, false, possibleChildProcess.Id); #pragma warning restore CA2000 // Dispose objects before losing scope { if (childHandle.IsInvalid) @@ -1491,83 +1166,84 @@ internal static List> GetChildProcessIds(in return myChildren; } +#endif /// /// Internal, optimized GetCurrentDirectory implementation that simply delegates to the native method /// - /// - internal static unsafe string GetCurrentDirectory() + internal static string GetCurrentDirectory() { #if FEATURE_LEGACY_GETCURRENTDIRECTORY if (IsWindows) { - int bufferSize = GetCurrentDirectoryWin32(0, null); - char* buffer = stackalloc char[bufferSize]; - int pathLength = GetCurrentDirectoryWin32(bufferSize, buffer); - return new string(buffer, startIndex: 0, length: pathLength); + using BufferScope buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]); + int pathLength = (int)PInvoke.GetCurrentDirectory(buffer); + + if (pathLength > buffer.Length) + { + buffer.EnsureCapacity(pathLength); + pathLength = (int)PInvoke.GetCurrentDirectory(buffer); + } + + if (pathLength != 0) + { + return buffer.Slice(0, pathLength).ToString(); + } + + HRESULT.FromLastError().ThrowOnFailure(); } #endif return Directory.GetCurrentDirectory(); } - [SupportedOSPlatform("windows")] - private static unsafe int GetCurrentDirectoryWin32(int nBufferLength, char* lpBuffer) - { - int pathLength = GetCurrentDirectory(nBufferLength, lpBuffer); - VerifyThrowWin32Result(pathLength); - return pathLength; - } - - [SupportedOSPlatform("windows")] - internal static unsafe string GetFullPath(string path) + internal static bool SetCurrentDirectory(string path) { - char* buffer = stackalloc char[MAX_PATH]; - int fullPathLength = GetFullPathWin32(path, MAX_PATH, buffer, IntPtr.Zero); - - // if user is using long paths we could need to allocate a larger buffer - if (fullPathLength > MAX_PATH) +#if FEATURE_WINDOWSINTEROP + if (IsWindows) { - char* newBuffer = stackalloc char[fullPathLength]; - fullPathLength = GetFullPathWin32(path, fullPathLength, newBuffer, IntPtr.Zero); + return PInvoke.SetCurrentDirectory(path); + } +#endif - buffer = newBuffer; + // Make sure this does not throw + try + { + Directory.SetCurrentDirectory(path); + } + catch + { } - // Avoid creating new strings unnecessarily - return AreStringsEqual(buffer, fullPathLength, path) ? path : new string(buffer, startIndex: 0, length: fullPathLength); + return true; } - [SupportedOSPlatform("windows")] - private static unsafe int GetFullPathWin32(string target, int bufferLength, char* buffer, IntPtr mustBeZero) +#if FEATURE_WINDOWSINTEROP + [SupportedOSPlatform("windows6.1")] + internal static unsafe string GetFullPath(string path) { - int pathLength = GetFullPathName(target, bufferLength, buffer, mustBeZero); - VerifyThrowWin32Result(pathLength); - return pathLength; - } + using BufferScope buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]); + int fullPathLength = (int)PInvoke.GetFullPathName(path, buffer, null); - /// - /// Compare an unsafe char buffer with a to see if their contents are identical. - /// - /// The beginning of the char buffer. - /// The length of the buffer. - /// The string. - /// True only if the contents of and the first characters in are identical. - private static unsafe bool AreStringsEqual(char* buffer, int len, string s) - { - return s.AsSpan().SequenceEqual(new ReadOnlySpan(buffer, len)); - } + // If user is using long paths we could need to allocate a larger buffer + if (fullPathLength > buffer.Length) + { + buffer.EnsureCapacity(fullPathLength); + fullPathLength = (int)PInvoke.GetFullPathName(path, buffer, null); + } - internal static void VerifyThrowWin32Result(int result) - { - bool isError = result == 0; - if (isError) + if (fullPathLength == 0) { - int code = Marshal.GetLastWin32Error(); - ThrowExceptionForErrorCode(code); + HRESULT.FromLastError().ThrowOnFailure(); } + + // Avoid creating new strings unnecessarily + ReadOnlySpan result = buffer.AsSpan().Slice(0, fullPathLength); + return result.SequenceEqual(path.AsSpan()) ? path : result.ToString(); } - internal static (bool acceptAnsiColorCodes, bool outputIsScreen, uint? originalConsoleMode) QueryIsScreenAndTryEnableAnsiColorCodes(StreamHandleType handleType = StreamHandleType.StdOut) +#endif + + internal static (bool acceptAnsiColorCodes, bool outputIsScreen, uint? originalConsoleMode) QueryIsScreenAndTryEnableAnsiColorCodes(bool useStandardError = false) { if (Console.IsOutputRedirected) { @@ -1585,33 +1261,29 @@ internal static (bool acceptAnsiColorCodes, bool outputIsScreen, uint? originalC bool acceptAnsiColorCodes = false; bool outputIsScreen = false; uint? originalConsoleMode = null; +#if FEATURE_WINDOWSINTEROP if (IsWindows) { try { - IntPtr outputStream = GetStdHandle((int)handleType); - if (GetConsoleMode(outputStream, out uint consoleMode)) + HANDLE outputStream = PInvoke.GetStdHandle(useStandardError ? STD_HANDLE.STD_ERROR_HANDLE : STD_HANDLE.STD_OUTPUT_HANDLE); + if (PInvoke.GetConsoleMode(outputStream, out CONSOLE_MODE consoleMode)) { - if ((consoleMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == ENABLE_VIRTUAL_TERMINAL_PROCESSING) + if (consoleMode.HasFlag(CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING)) { - // Console is already in required state. acceptAnsiColorCodes = true; } else { - originalConsoleMode = consoleMode; - consoleMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; - if (SetConsoleMode(outputStream, consoleMode) && GetConsoleMode(outputStream, out consoleMode)) + originalConsoleMode = (uint)consoleMode; + consoleMode |= CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING; + if (PInvoke.SetConsoleMode(outputStream, consoleMode) && PInvoke.GetConsoleMode(outputStream, out consoleMode)) { - // We only know if vt100 is supported if the previous call actually set the new flag, older - // systems ignore the setting. - acceptAnsiColorCodes = (consoleMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == ENABLE_VIRTUAL_TERMINAL_PROCESSING; + acceptAnsiColorCodes = consoleMode.HasFlag(CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING); } } - uint fileType = GetFileType(outputStream); - // The std out is a char type (LPT or Console). - outputIsScreen = fileType == FILE_TYPE_CHAR; + outputIsScreen = PInvoke.GetFileType(outputStream) == FILE_TYPE.FILE_TYPE_CHAR; acceptAnsiColorCodes &= outputIsScreen; } } @@ -1621,6 +1293,7 @@ internal static (bool acceptAnsiColorCodes, bool outputIsScreen, uint? originalC } } else +#endif { // On posix OSes detect whether the terminal supports VT100 from the value of the TERM environment variable. acceptAnsiColorCodes = AnsiDetector.IsAnsiSupported(Environment.GetEnvironmentVariable("TERM")); @@ -1630,18 +1303,17 @@ internal static (bool acceptAnsiColorCodes, bool outputIsScreen, uint? originalC return (acceptAnsiColorCodes, outputIsScreen, originalConsoleMode); } - internal static void RestoreConsoleMode(uint? originalConsoleMode, StreamHandleType handleType = StreamHandleType.StdOut) + internal static void RestoreConsoleMode(uint? originalConsoleMode, bool useStandardError = false) { +#if FEATURE_WINDOWSINTEROP if (IsWindows && originalConsoleMode is not null) { - IntPtr stdOut = GetStdHandle((int)handleType); - _ = SetConsoleMode(stdOut, originalConsoleMode.Value); + HANDLE stdOut = PInvoke.GetStdHandle(useStandardError ? STD_HANDLE.STD_ERROR_HANDLE : STD_HANDLE.STD_OUTPUT_HANDLE); + _ = PInvoke.SetConsoleMode(stdOut, (CONSOLE_MODE)originalConsoleMode.Value); } +#endif } - #endregion - - #region PInvoke [SupportedOSPlatform("linux")] [DllImport("libc", SetLastError = true)] internal static extern int chmod(string pathname, int mode); @@ -1650,219 +1322,45 @@ internal static void RestoreConsoleMode(uint? originalConsoleMode, StreamHandleT [DllImport("libc", SetLastError = true)] internal static extern int mkdir(string path, int mode); - /// - /// Gets the current OEM code page which is used by console apps - /// (as opposed to the Windows/ANSI code page) - /// Basically for each ANSI code page (set in Regional settings) there's a corresponding OEM code page - /// that needs to be used for instance when writing to batch files - /// - [DllImport(kernel32Dll)] - [SupportedOSPlatform("windows")] - internal static extern int GetOEMCP(); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - [SupportedOSPlatform("windows")] - internal static extern bool GetFileAttributesEx(String name, int fileInfoLevel, ref WIN32_FILE_ATTRIBUTE_DATA lpFileInformation); - - [DllImport("kernel32.dll", PreserveSig = true, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - [SupportedOSPlatform("windows")] - internal static extern bool FreeLibrary([In] IntPtr module); - - [DllImport("kernel32.dll", PreserveSig = true, BestFitMapping = false, ThrowOnUnmappableChar = true, CharSet = CharSet.Ansi, SetLastError = true)] - [SupportedOSPlatform("windows")] - internal static extern IntPtr GetProcAddress(IntPtr module, string procName); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true)] - [SupportedOSPlatform("windows")] - internal static extern IntPtr LoadLibrary(string fileName); - - /// - /// Gets the fully qualified filename of the currently executing .exe. - /// - /// of the module for which we are finding the file name. - /// The character buffer used to return the file name. - /// The length of the buffer. - [DllImport(kernel32Dll, SetLastError = true, CharSet = CharSet.Unicode)] - [SupportedOSPlatform("windows")] - internal static extern int GetModuleFileName(HandleRef hModule, [Out] char[] buffer, int length); - - [DllImport("kernel32.dll")] - [SupportedOSPlatform("windows")] - internal static extern IntPtr GetStdHandle(int nStdHandle); - - [DllImport("kernel32.dll")] - [SupportedOSPlatform("windows")] - internal static extern uint GetFileType(IntPtr hFile); - - [DllImport("kernel32.dll")] - internal static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); - - [DllImport("kernel32.dll")] - internal static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); - - [SuppressMessage("Microsoft.Usage", "CA2205:UseManagedEquivalentsOfWin32Api", Justification = "Using unmanaged equivalent for performance reasons")] - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [SupportedOSPlatform("windows")] - internal static extern unsafe int GetCurrentDirectory(int nBufferLength, char* lpBuffer); - - [SuppressMessage("Microsoft.Usage", "CA2205:UseManagedEquivalentsOfWin32Api", Justification = "Using unmanaged equivalent for performance reasons")] - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SetCurrentDirectory")] - [return: MarshalAs(UnmanagedType.Bool)] - [SupportedOSPlatform("windows")] - internal static extern bool SetCurrentDirectoryWindows(string path); - - internal static bool SetCurrentDirectory(string path) - { - if (IsWindows) - { - return SetCurrentDirectoryWindows(path); - } - - // Make sure this does not throw - try - { - Directory.SetCurrentDirectory(path); - } - catch - { - } - return true; - } - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [SupportedOSPlatform("windows")] - internal static extern unsafe int GetFullPathName(string target, int bufferLength, char* buffer, IntPtr mustBeZero); - - [DllImport("KERNEL32.DLL")] - [SupportedOSPlatform("windows")] - private static extern SafeProcessHandle OpenProcess(eDesiredAccess dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwProcessId); - - [DllImport("NTDLL.DLL")] - [SupportedOSPlatform("windows")] - private static extern int NtQueryInformationProcess(SafeProcessHandle hProcess, PROCESSINFOCLASS pic, ref PROCESS_BASIC_INFORMATION pbi, uint cb, ref int pSize); - - [return: MarshalAs(UnmanagedType.Bool)] - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - [SupportedOSPlatform("windows")] - private static extern bool GlobalMemoryStatusEx([In, Out] MemoryStatus lpBuffer); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, BestFitMapping = false)] - [SupportedOSPlatform("windows")] - internal static extern int GetShortPathName(string path, [Out] char[] fullpath, [In] int length); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, BestFitMapping = false)] - [SupportedOSPlatform("windows")] - internal static extern int GetLongPathName([In] string path, [Out] char[] fullpath, [In] int length); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - [SupportedOSPlatform("windows")] - internal static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, SecurityAttributes lpPipeAttributes, int nSize); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - [SupportedOSPlatform("windows")] - internal static extern bool ReadFile(SafeFileHandle hFile, byte[] lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, IntPtr lpOverlapped); - - /// - /// CoWaitForMultipleHandles allows us to wait in an STA apartment and still service RPC requests from other threads. - /// VS needs this in order to allow the in-proc compilers to properly initialize, since they will make calls from the - /// build thread which the main thread (blocked on BuildSubmission.Execute) must service. - /// - [DllImport("ole32.dll")] - [SupportedOSPlatform("windows")] - public static extern int CoWaitForMultipleHandles(COWAIT_FLAGS dwFlags, int dwTimeout, int cHandles, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] pHandles, out int pdwIndex); - - internal const uint GENERIC_READ = 0x80000000; - internal const uint FILE_SHARE_READ = 0x1; - internal const uint FILE_ATTRIBUTE_NORMAL = 0x80; - internal const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000; - internal const uint OPEN_EXISTING = 3; - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, - SetLastError = true)] - [SupportedOSPlatform("windows")] - internal static extern SafeFileHandle CreateFile( - string lpFileName, - uint dwDesiredAccess, - uint dwShareMode, - IntPtr lpSecurityAttributes, - uint dwCreationDisposition, - uint dwFlagsAndAttributes, - IntPtr hTemplateFile); - - [DllImport("kernel32.dll", SetLastError = true)] - [SupportedOSPlatform("windows")] - internal static extern bool GetFileTime( - SafeFileHandle hFile, - out FILETIME lpCreationTime, - out FILETIME lpLastAccessTime, - out FILETIME lpLastWriteTime); - - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - [SupportedOSPlatform("windows")] - internal static extern bool CloseHandle(IntPtr hObject); - - [DllImport("kernel32.dll", SetLastError = true)] - [SupportedOSPlatform("windows")] - internal static extern bool SetThreadErrorMode(int newMode, out int oldMode); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.I1)] - [SupportedOSPlatform("windows")] - internal static extern bool CreateSymbolicLink(string symLinkFileName, string targetFileName, SymbolicLink dwFlags); - [DllImport("libc", SetLastError = true)] internal static extern int symlink(string oldpath, string newpath); - #endregion - - #region helper methods - - internal static bool DirectoryExists(string fullPath) - { - return IsWindows - ? DirectoryExistsWindows(fullPath) - : Directory.Exists(fullPath); - } - - [SupportedOSPlatform("windows")] - internal static bool DirectoryExistsWindows(string fullPath) - { - WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA(); - bool success = GetFileAttributesEx(fullPath, 0, ref data); - return success && (data.fileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; - } - - internal static bool FileExists(string fullPath) - { - return IsWindows - ? FileExistsWindows(fullPath) - : File.Exists(fullPath); - } - - [SupportedOSPlatform("windows")] - internal static bool FileExistsWindows(string fullPath) +#if FEATURE_WINDOWSINTEROP + [SupportedOSPlatform("windows6.1")] + internal static unsafe bool SetThreadErrorMode(int newMode, out int oldMode) { - WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA(); - bool success = GetFileAttributesEx(fullPath, 0, ref data); - return success && (data.fileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0; + THREAD_ERROR_MODE oldModeU; + bool result = PInvoke.SetThreadErrorMode((THREAD_ERROR_MODE)newMode, &oldModeU); + oldMode = (int)oldModeU; + return result; } - internal static bool FileOrDirectoryExists(string path) + [SupportedOSPlatform("windows6.1")] + private static unsafe SafeProcessHandle OpenProcess(PROCESS_ACCESS_RIGHTS dwDesiredAccess, bool bInheritHandle, int dwProcessId) { - return IsWindows - ? FileOrDirectoryExistsWindows(path) - : File.Exists(path) || Directory.Exists(path); + HANDLE h = PInvoke.OpenProcess(dwDesiredAccess, bInheritHandle, (uint)dwProcessId); + return new SafeProcessHandle((IntPtr)h.Value); } - [SupportedOSPlatform("windows")] - internal static bool FileOrDirectoryExistsWindows(string path) + [SupportedOSPlatform("windows6.1")] + private static unsafe int NtQueryInformationProcess( + SafeProcessHandle hProcess, + ref PROCESS_BASIC_INFORMATION pbi, + ref int pSize) { - WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA(); - return GetFileAttributesEx(path, 0, ref data); + fixed (PROCESS_BASIC_INFORMATION* pbiPtr = &pbi) + { + uint returnLength = 0; + NTSTATUS status = Wdk.PInvoke.NtQueryInformationProcess( + (HANDLE)hProcess.DangerousGetHandle(), + WdkThreading.PROCESSINFOCLASS.ProcessBasicInformation, + pbiPtr, + (uint)sizeof(PROCESS_BASIC_INFORMATION), + ref returnLength); + pSize = (int)returnLength; + return status.Value; + } } - #endregion +#endif } diff --git a/src/Framework/NativeMethods.json b/src/Framework/NativeMethods.json new file mode 100644 index 00000000000..a856b62ef49 --- /dev/null +++ b/src/Framework/NativeMethods.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://aka.ms/CsWin32.schema.json", + "allowMarshaling": false, + "useSafeHandles": false, + "className": "PInvoke", + "comInterop": { + "preserveSigMethods": [ + "*" + ] + } +} diff --git a/src/Framework/NativeMethods.txt b/src/Framework/NativeMethods.txt new file mode 100644 index 00000000000..dea8021e1f2 --- /dev/null +++ b/src/Framework/NativeMethods.txt @@ -0,0 +1,81 @@ +BSTR +CLSCTX +CloseHandle +CoCreateInstance +CoGetClassObject +CoInitializeSecurity +CoSetProxyBlanket +CoWaitForMultipleHandles +COWAIT_FLAGS +CreateFile +CreatePipe +CreateSymbolicLink +DebugCreate +IDebugClient4 +DEBUG_PROC_DESC_NO_PATHS +DEBUG_PROC_DESC_NO_SERVICES +DEBUG_PROC_DESC_NO_MTS_PACKAGES +DEBUG_PROC_DESC_NO_SESSION_ID +DEBUG_PROC_DESC_NO_USER_NAME +EOLE_AUTHENTICATION_CAPABILITIES +FACILITY_CODE +FILE_ACCESS_RIGHTS +FILE_CREATION_DISPOSITION +FILE_FLAGS_AND_ATTRIBUTES +FILE_SHARE_MODE +FILETIME +GetConsoleMode +GetCurrentDirectory +GetCurrentProcessId +GetFileAttributes +GetFileAttributesEx +GET_FILEEX_INFO_LEVELS +WIN32_FILE_ATTRIBUTE_DATA +GetFileTime +GetFileType +GetFullPathName +GetLogicalProcessorInformationEx +GetLongPathName +GetModuleFileName +GetNativeSystemInfo +GetOEMCP +GetShortPathName +GetStdHandle +GetSystemInfo +GlobalMemoryStatusEx +HANDLE +HRESULT +IClassFactory +INVALID_FILE_ATTRIBUTES +INVALID_HANDLE_VALUE +MEMORYSTATUSEX +NtQueryInformationProcess +OpenProcess +PROCESS_ACCESS_RIGHTS +PROCESS_BASIC_INFORMATION +PropVariantClear +PWSTR +ReadFile +S_OK +S_FALSE +SECURITY_ATTRIBUTES +SetConsoleMode +SetCurrentDirectory +SetThreadErrorMode +SYMBOLIC_LINK_FLAGS +SYSTEM_INFO +SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX +VARENUM +VARIANT +VariantClear +WIN32_ERROR +WIN32_FIND_DATAW +FreeLibrary +GetProcAddress +LoadLibrary +MAX_PATH +RPC_C_AUTHN_LEVEL +RPC_C_AUTHN_WINNT +RPC_C_AUTHZ_NONE +RPC_C_IMP_LEVEL +RPC_E_TOO_LATE diff --git a/src/Framework/Telemetry/CrashTelemetry.cs b/src/Framework/Telemetry/CrashTelemetry.cs index cc4d4948346..3cea5b9344d 100644 --- a/src/Framework/Telemetry/CrashTelemetry.cs +++ b/src/Framework/Telemetry/CrashTelemetry.cs @@ -7,6 +7,9 @@ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; +#if NETFRAMEWORK +using Windows.Win32.System.SystemInformation; +#endif namespace Microsoft.Build.Framework.Telemetry; @@ -460,10 +463,9 @@ private void PopulateMemoryStats() try { #if NETFRAMEWORK - NativeMethods.MemoryStatus? memoryStatus = NativeMethods.GetMemoryStatus(); - if (memoryStatus != null) + if (NativeMethods.TryGetMemoryStatus(out MEMORYSTATUSEX memoryStatus)) { - MemoryLoadPercent = (int)memoryStatus.MemoryLoad; + MemoryLoadPercent = (int)memoryStatus.dwMemoryLoad; } #else // On .NET Core, GC.GetGCMemoryInfo() provides the total available memory diff --git a/src/Framework/Utilities/ProcessExtensions.cs b/src/Framework/Utilities/ProcessExtensions.cs index 104666a57f4..4dca1ab5fd0 100644 --- a/src/Framework/Utilities/ProcessExtensions.cs +++ b/src/Framework/Utilities/ProcessExtensions.cs @@ -6,11 +6,22 @@ using Microsoft.Build.Framework; #if NET -using System.Buffers; using System.IO; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; +using Microsoft.Build.Utilities; +#endif +#if FEATURE_WINDOWSINTEROP && NET +using Microsoft.Build.Shared.Win32.Wmi; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.System.Diagnostics.Debug.Extensions; +using Windows.Win32.System.Variant; +using IWbemClassObject = Microsoft.Build.Shared.Win32.Wmi.IWbemClassObject; +using IWbemLocator = Microsoft.Build.Shared.Win32.Wmi.IWbemLocator; +using IWbemServices = Microsoft.Build.Shared.Win32.Wmi.IWbemServices; #endif namespace Microsoft.Build.Shared @@ -22,6 +33,7 @@ public static void KillTree(this Process process, int timeoutMilliseconds) #if NET process.Kill(entireProcessTree: true); #else +#if FEATURE_WINDOWSINTEROP if (NativeMethods.IsWindows) { try @@ -35,6 +47,7 @@ public static void KillTree(this Process process, int timeoutMilliseconds) } } else +#endif { throw new NotSupportedException(); } @@ -47,11 +60,35 @@ public static void KillTree(this Process process, int timeoutMilliseconds) /// /// Retrieves the full command line for a process in a cross-platform manner. + /// On Windows, command-line retrieval is opt-in via the MSBUILDPROCESSCOMMANDLINESOURCE + /// environment variable (values: Wmi or DebugEngine); when unset, the command line + /// is not retrieved. /// /// The process to get the command line for. /// The command line string, or null if it cannot be retrieved. /// True if the command line was successfully retrieved, false if there was an error or the platform doesn't support command line retrieval. public static bool TryGetCommandLine(this Process? process, out string? commandLine) + => TryGetCommandLine(process, GetConfiguredCommandLineSource(), out commandLine); + + private static CommandLineSource GetConfiguredCommandLineSource() + { + string? value = Environment.GetEnvironmentVariable("MSBUILDPROCESSCOMMANDLINESOURCE"); + if (string.IsNullOrEmpty(value)) + { + return CommandLineSource.None; + } + + return Enum.TryParse(value, ignoreCase: true, out CommandLineSource parsed) + ? parsed + : CommandLineSource.None; + } + + /// + /// Retrieves the full command line for a process, allowing the caller to choose the + /// underlying Windows API via . + /// On non-Windows platforms is ignored. + /// + public static bool TryGetCommandLine(this Process? process, CommandLineSource source, out string? commandLine) { commandLine = null; @@ -62,8 +99,27 @@ public static bool TryGetCommandLine(this Process? process, out string? commandL try { +#if FEATURE_WINDOWSINTEROP && NET + if (NativeMethods.IsWindows) + { + if (source == CommandLineSource.None) + { + commandLine = null; + return true; + } + + commandLine = Windows.GetCommandLine(process.Id, source); + return true; + } +#endif #if NET - if (NativeMethods.IsOSX || NativeMethods.IsBSD) + if (NativeMethods.IsWindows) + { + // Windows without CsWin32 (source builds) - cannot query WMI/DebugEngine + commandLine = null; + return true; + } + else if (NativeMethods.IsOSX || NativeMethods.IsBSD) { commandLine = BSD.GetCommandLine(process.Id); return true; @@ -106,58 +162,310 @@ private static string ParseNullSeparatedArguments(ReadOnlySpan data, int m } // Rent a char buffer for UTF-8 decoding (max char count equals byte count for ASCII-like content) - char[] charBuffer = ArrayPool.Shared.Rent(data.Length); - try + using BufferScope charBuffer = new(data.Length); + + int totalChars = 0; + int argsFound = 0; + + while (!data.IsEmpty && argsFound < maxArgs) { - int totalChars = 0; - int argsFound = 0; + int nullIndex = data.IndexOf((byte)0); + ReadOnlySpan segment = nullIndex >= 0 ? data.Slice(0, nullIndex) : data; - while (!data.IsEmpty && argsFound < maxArgs) + if (!segment.IsEmpty) { - int nullIndex = data.IndexOf((byte)0); - ReadOnlySpan segment = nullIndex >= 0 ? data.Slice(0, nullIndex) : data; + // Add space separator between arguments + if (totalChars > 0) + { + charBuffer[totalChars++] = ' '; + } + + // Decode UTF-8 directly into the char buffer + int charsWritten = Encoding.UTF8.GetChars(segment, charBuffer.AsSpan().Slice(totalChars)); - if (!segment.IsEmpty) + // UTF-8 decoder converts null bytes to null chars - replace them with spaces for safety + Span decodedChars = charBuffer.Slice(totalChars, charsWritten); + for (int i = 0; i < decodedChars.Length; i++) { - // Add space separator between arguments - if (totalChars > 0) + if (decodedChars[i] == '\0') { - charBuffer[totalChars++] = ' '; + decodedChars[i] = ' '; } + } - // Decode UTF-8 directly into the char buffer - int charsWritten = Encoding.UTF8.GetChars(segment, charBuffer.AsSpan(totalChars)); + totalChars += charsWritten; + argsFound++; + } - // UTF-8 decoder converts null bytes to null chars - replace them with spaces for safety - Span decodedChars = charBuffer.AsSpan(totalChars, charsWritten); - for (int i = 0; i < decodedChars.Length; i++) - { - if (decodedChars[i] == '\0') - { - decodedChars[i] = ' '; - } - } + if (nullIndex < 0) + { + break; + } - totalChars += charsWritten; - argsFound++; - } + data = data.Slice(nullIndex + 1); + } - if (nullIndex < 0) - { - break; - } + return charBuffer.Slice(0, totalChars).ToString(); + } +#endif + + /// + /// Selects the underlying Windows API used to retrieve another process's command line. + /// On non-Windows platforms the value is accepted but ignored. + /// + public enum CommandLineSource + { + /// + /// Do not attempt to retrieve the command line. Default behavior; + /// returns with a command line on Windows. + /// + None = 0, + + /// + /// Query WMI's Win32_Process.CommandLine via IWbemLocator/IWbemServices. + /// + Wmi, - data = data.Slice(nullIndex + 1); + /// + /// Call dbgeng!IDebugClient4::GetRunningProcessDescriptionWide. Avoids the WMI service + /// and returns UTF-16 text directly (no ANSI-to-Unicode conversion). + /// + DebugEngine, + } + +#if FEATURE_WINDOWSINTEROP && NET + /// + /// Windows-specific command line retrieval. + /// + [SupportedOSPlatform("windows6.1")] + private static class Windows + { + // WBEM status codes + private static readonly HRESULT WBEM_S_FALSE = (HRESULT)1; // No more objects in enumeration + private const int WBEM_FLAG_FORWARD_ONLY = 0x00000020; + private const int WBEM_FLAG_RETURN_IMMEDIATELY = 0x00000010; + private const int WBEM_INFINITE = -1; + + // Flags for IDebugClient4::GetRunningProcessDescriptionWide. By default the Description output + // concatenates service names, MTS package names, command line, session id, and user name; we + // exclude everything except the command line. + private const uint DebugProcessDescriptionFlags = + PInvoke.DEBUG_PROC_DESC_NO_PATHS + | PInvoke.DEBUG_PROC_DESC_NO_SERVICES + | PInvoke.DEBUG_PROC_DESC_NO_MTS_PACKAGES + | PInvoke.DEBUG_PROC_DESC_NO_SESSION_ID + | PInvoke.DEBUG_PROC_DESC_NO_USER_NAME; + + /// + /// Retrieves the command line for a process using the requested . + /// + internal static string? GetCommandLine(int processId, CommandLineSource source) => source switch + { + CommandLineSource.Wmi => GetCommandLineViaWmi(processId), + CommandLineSource.DebugEngine => GetCommandLineViaDebugEngine(processId), + _ => null, + }; + + /// + /// Retrieves the command line for a process by querying WMI Win32_Process via COM. + /// Runs: SELECT CommandLine FROM Win32_Process WHERE ProcessId='' + /// Uses CsWin32-generated P/Invoke for ole32.dll functions and manually defined COM structs + /// for WMI interfaces (which are not in Win32 metadata). + /// + internal static unsafe string? GetCommandLineViaWmi(int processId) + { + HRESULT hr = PInvoke.CoInitializeSecurity( + pSecDesc: default, + cAuthSvc: -1, + asAuthSvc: null, + dwAuthnLevel: RPC_C_AUTHN_LEVEL.RPC_C_AUTHN_LEVEL_DEFAULT, + dwImpLevel: RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IMPERSONATE, + pAuthList: null, + dwCapabilities: EOLE_AUTHENTICATION_CAPABILITIES.EOAC_NONE); + + // RPC_E_TOO_LATE (0x80010119) means another call already set security — not fatal. + if (hr.Failed && hr != HRESULT.RPC_E_TOO_LATE) + { + throw new InvalidOperationException( + $"WMI CoInitializeSecurity failed for PID {processId}. HRESULT: 0x{hr.Value:X8}"); + } + + Guid clsid = IWbemLocator.CLSID; + hr = PInvoke.CoCreateInstance(in clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.Get(), out void* locatorPtr); + using ComScope locator = new(locatorPtr); + if (hr.Failed) + { + throw new InvalidOperationException( + $"WMI CoCreateInstance failed for PID {processId}. HRESULT: 0x{hr.Value:X8}"); + } + + using ComScope services = new(); + + fixed (char* networkResource = @"ROOT\CIMV2") + { + hr = locator.Pointer->ConnectServer( + networkResource, + strUser: null, strPassword: null, strLocale: null, + lSecurityFlags: 0, strAuthority: null, + pCtx: null, + services); + } + + if (hr.Failed) + { + throw new InvalidOperationException( + $"WMI ConnectServer failed for PID {processId}. HRESULT: 0x{hr.Value:X8}"); + } + + hr = PInvoke.CoSetProxyBlanket( + pProxy: (IUnknown*)services.Pointer, + dwAuthnSvc: PInvoke.RPC_C_AUTHN_WINNT, + dwAuthzSvc: PInvoke.RPC_C_AUTHZ_NONE, + pServerPrincName: default, + dwAuthnLevel: RPC_C_AUTHN_LEVEL.RPC_C_AUTHN_LEVEL_CALL, + dwImpLevel: RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IMPERSONATE, + pAuthInfo: null, + dwCapabilities: EOLE_AUTHENTICATION_CAPABILITIES.EOAC_NONE); + + if (hr.Failed) + { + throw new InvalidOperationException( + $"WMI CoSetProxyBlanket failed for PID {processId}. HRESULT: 0x{hr.Value:X8}"); } - return new string(charBuffer, 0, totalChars); + string query = $"SELECT CommandLine FROM Win32_Process WHERE ProcessId='{processId}'"; + using ComScope enumerator = new(); + +#pragma warning disable SA1519 // Braces should not be omitted from multi-line child statement + fixed (char* queryLanguage = "WQL") + fixed (char* queryStr = query) +#pragma warning restore SA1519 + { + hr = services.Pointer->ExecQuery( + queryLanguage, + queryStr, + WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, + pCtx: null, + enumerator); + } + + if (hr.Failed) + { + throw new InvalidOperationException( + $"WMI ExecQuery failed for PID {processId}. HRESULT: 0x{hr.Value:X8}"); + } + + + using ComScope obj = new(); + uint returned; + hr = enumerator.Pointer->Next(WBEM_INFINITE, 1, obj, &returned); + if (hr == WBEM_S_FALSE || returned == 0) + { + // No matching process found. + return null; + } + + if (hr.Failed) + { + throw new InvalidOperationException( + $"WMI IEnumWbemClassObject.Next failed for PID {processId}. HRESULT: 0x{hr.Value:X8}"); + } + + using VARIANT val = default; + fixed (char* propName = "CommandLine") + { + hr = obj.Pointer->Get(propName, 0, &val, pType: null, plFlavor: null); + } + + if (hr.Failed) + { + throw new InvalidOperationException( + $"WMI IWbemClassObject.Get(\"CommandLine\") failed for PID {processId}. HRESULT: 0x{hr.Value:X8}"); + } + + if (val.Type == VARENUM.VT_BSTR) + { + return ((BSTR)val).ToString(); + } + + return null; } - finally + + /// + /// Retrieves the command line for a process via dbgeng!IDebugClient4::GetRunningProcessDescriptionWide. + /// IDebugClient4 is the oldest interface version that exposes the Wide variant, so the returned + /// text is already UTF-16 and does not need to be converted from ANSI. Returns null if the target + /// cannot be inspected (for example, access denied, protected process, or the debug engine is unavailable). + /// + internal static unsafe string? GetCommandLineViaDebugEngine(int processId) { - ArrayPool.Shared.Return(charBuffer); + HRESULT hr = PInvoke.DebugCreate(IID.Get(), out void* clientPtr); + using ComScope client = new(clientPtr); + if (hr.Failed || client.Pointer is null) + { + return null; + } + + // First call with null buffers to discover required sizes (in characters, including the + // trailing null terminator). + uint exeSize; + uint descSize; + hr = client.Pointer->GetRunningProcessDescriptionWide( + Server: 0, + SystemId: (uint)processId, + Flags: DebugProcessDescriptionFlags, + ExeName: null, + ExeNameSize: 0, + ActualExeNameSize: &exeSize, + Description: null, + DescriptionSize: 0, + ActualDescriptionSize: &descSize); + + // A hard failure with no sizes reported means the PID can't be inspected. + if (hr.Failed && exeSize == 0 && descSize == 0) + { + return null; + } + + using BufferScope exeBuffer = new((int)exeSize); + using BufferScope descBuffer = new((int)descSize); + +#pragma warning disable SA1519 // Braces should not be omitted from multi-line child statement + fixed (char* pExe = exeBuffer) + fixed (char* pDesc = descBuffer) +#pragma warning restore SA1519 + { + hr = client.Pointer->GetRunningProcessDescriptionWide( + Server: 0, + SystemId: (uint)processId, + Flags: DebugProcessDescriptionFlags, + ExeName: pExe, + ExeNameSize: exeSize, + ActualExeNameSize: &exeSize, + Description: pDesc, + DescriptionSize: descSize, + ActualDescriptionSize: &descSize); + } + + if (hr.Failed) + { + return null; + } + + // Sizes include the trailing null terminator. + string desc = descSize > 1 ? descBuffer.Slice(0, (int)descSize - 1).ToString() : string.Empty; + if (!string.IsNullOrEmpty(desc)) + { + return desc; + } + + // With our exclusion flags the Description contains just the command line; fall back to + // the executable name for protected/system processes where the command line is not returned. + string exe = exeSize > 1 ? exeBuffer.Slice(0, (int)exeSize - 1).ToString() : string.Empty; + return string.IsNullOrEmpty(exe) ? null : exe; } } -#endif +#endif // FEATURE_WINDOWSINTEROP && NET #if NET /// @@ -231,57 +539,51 @@ private static int Sysctl(ReadOnlySpan name, Span oldp, ref nuint old return null; } - // Rent a buffer from ArrayPool and pin it for sysctl - byte[] buffer = ArrayPool.Shared.Rent((int)size); - try - { - if (Sysctl(mib, buffer.AsSpan(0, (int)size), ref size) != 0) - { - return null; - } + // Rent a buffer for sysctl + using BufferScope buffer = new((int)size); - // Buffer format (KERN_PROCARGS2): - // int argc (number of arguments including executable) - // fully-qualified executable path (null-terminated) - // padding null bytes - // argv[0] .. argv[argc-1] (each null-terminated) - // environment variables (not needed) - ReadOnlySpan data = buffer.AsSpan(0, (int)size); + if (Sysctl(mib, buffer.AsSpan().Slice(0, (int)size), ref size) != 0) + { + return null; + } - if (data.Length < sizeof(int)) - { - return null; - } + // Buffer format (KERN_PROCARGS2): + // int argc (number of arguments including executable) + // fully-qualified executable path (null-terminated) + // padding null bytes + // argv[0] .. argv[argc-1] (each null-terminated) + // environment variables (not needed) + ReadOnlySpan data = buffer.AsSpan().Slice(0, (int)size); - int argc = MemoryMarshal.Read(data); - if (argc <= 0) - { - return null; - } + if (data.Length < sizeof(int)) + { + return null; + } - data = data.Slice(sizeof(int)); + int argc = MemoryMarshal.Read(data); + if (argc <= 0) + { + return null; + } - // Skip past the executable path (first null terminator) - int execPathEnd = data.IndexOf((byte)0); - if (execPathEnd < 0) - { - return null; - } + data = data.Slice(sizeof(int)); - data = data.Slice(execPathEnd + 1); + // Skip past the executable path (first null terminator) + int execPathEnd = data.IndexOf((byte)0); + if (execPathEnd < 0) + { + return null; + } - // Skip padding null bytes between executable path and argv[0] - while (!data.IsEmpty && data[0] == 0) - { - data = data.Slice(1); - } + data = data.Slice(execPathEnd + 1); - return ParseNullSeparatedArguments(data, argc); - } - finally + // Skip padding null bytes between executable path and argv[0] + while (!data.IsEmpty && data[0] == 0) { - ArrayPool.Shared.Return(buffer); + data = data.Slice(1); } + + return ParseNullSeparatedArguments(data, argc); } } #endif diff --git a/src/Framework/Utilities/TypeInfo.cs b/src/Framework/Utilities/TypeInfo.cs index 1a071b48798..ffec80daf3b 100644 --- a/src/Framework/Utilities/TypeInfo.cs +++ b/src/Framework/Utilities/TypeInfo.cs @@ -7,6 +7,7 @@ using System; using System.Runtime.InteropServices; #endif +using System.Threading; namespace Microsoft.Build.Utilities; @@ -15,17 +16,24 @@ namespace Microsoft.Build.Utilities; /// internal static partial class TypeInfo { - private static bool? s_hasReferences; + // Tri-state: 0 = not computed, 1 = false (no references), 2 = true (has references) + private static int s_hasReferences; /// /// Returns if the type is a reference type or contains references. /// public static bool IsReferenceOrContainsReferences() { + int value = Volatile.Read(ref s_hasReferences); + if (value != 0) + { + return value == 2; + } + #if NET - return s_hasReferences ??= RuntimeHelpers.IsReferenceOrContainsReferences(); + bool result = RuntimeHelpers.IsReferenceOrContainsReferences(); #else - return s_hasReferences ??= HasReferences(); + bool result = HasReferences(); static bool HasReferences() { @@ -54,5 +62,8 @@ static bool HasReferences() } } #endif + + Interlocked.CompareExchange(ref s_hasReferences, result ? 2 : 1, 0); + return result; } } diff --git a/src/Framework/Utilities/Wmi/IEnumWbemClassObject.cs b/src/Framework/Utilities/Wmi/IEnumWbemClassObject.cs new file mode 100644 index 00000000000..7ee3eb3bce1 --- /dev/null +++ b/src/Framework/Utilities/Wmi/IEnumWbemClassObject.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Manually defined WMI COM struct following CsWin32 struct-based COM patterns. +// WMI interfaces are not in Win32 metadata, so they cannot be generated by CsWin32. +// Pattern follows dotnet/sdk ISetupConfiguration.cs (src/Cli/dotnet/Microsoft/VisualStudio/Setup/Configuration/). + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Shared.Win32.Wmi; + +/// +/// Used to enumerate CIM objects returned by . +/// +/// +/// +/// +[SupportedOSPlatform("windows6.0")] +internal unsafe struct IEnumWbemClassObject : IComIID +{ + public static Guid Guid { get; } = new(0x027947E1, 0xD731, 0x11CE, 0xA3, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01); + + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ReadOnlySpan data = + [ + 0xE1, 0x47, 0x79, 0x02, + 0x31, 0xD7, + 0xCE, 0x11, + 0xA3, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 + ]; + + return ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + } + } + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (IEnumWbemClassObject* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (IEnumWbemClassObject* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (IEnumWbemClassObject* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } + + // IEnumWbemClassObject vtable layout (indices 3-7): + // 3 = Reset + // 4 = Next <-- Used + // 5 = NextAsync + // 6 = Clone + // 7 = Skip + + /// + /// Retrieves the next object(s) in the enumeration, waiting up to a specified timeout. + /// + /// + /// + /// + public HRESULT Next(int lTimeout, uint uCount, IWbemClassObject** apObjects, uint* puReturned) + { + fixed (IEnumWbemClassObject* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[4])( + pThis, lTimeout, uCount, apObjects, puReturned); + } + } +} diff --git a/src/Framework/Utilities/Wmi/IWbemClassObject.cs b/src/Framework/Utilities/Wmi/IWbemClassObject.cs new file mode 100644 index 00000000000..adca2ca76c6 --- /dev/null +++ b/src/Framework/Utilities/Wmi/IWbemClassObject.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Manually defined WMI COM struct following CsWin32 struct-based COM patterns. +// WMI interfaces are not in Win32 metadata, so they cannot be generated by CsWin32. +// Pattern follows dotnet/sdk ISetupConfiguration.cs (src/Cli/dotnet/Microsoft/VisualStudio/Setup/Configuration/). +// +// Only the Get method (vtable index 4) is used. Other methods are documented as +// vtable placeholders for correct indexing. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Variant; + +namespace Microsoft.Build.Shared.Win32.Wmi; + +/// +/// Represents a single CIM object returned from WMI queries. +/// +/// +/// +/// +[SupportedOSPlatform("windows6.0")] +internal unsafe struct IWbemClassObject : IComIID +{ + public static Guid Guid { get; } = new(0xDC12A681, 0x737F, 0x11CF, 0x88, 0x4D, 0x00, 0xAA, 0x00, 0x4B, 0x2E, 0x24); + + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ReadOnlySpan data = + [ + 0x81, 0xA6, 0x12, 0xDC, + 0x7F, 0x73, + 0xCF, 0x11, + 0x88, 0x4D, 0x00, 0xAA, 0x00, 0x4B, 0x2E, 0x24 + ]; + + return ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + } + } + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (IWbemClassObject* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (IWbemClassObject* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (IWbemClassObject* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } + + // IWbemClassObject vtable layout (indices 3-26): + // 3 = GetQualifierSet + // 4 = Get <-- Used + // 5 = Put + // 6 = Delete + // 7 = GetNames + // 8 = BeginEnumeration + // 9 = Next + // 10 = EndEnumeration + // 11 = GetPropertyQualifierSet + // 12 = Clone + // 13 = GetObjectText + // 14 = SpawnDerivedClass + // 15 = SpawnInstance + // 16 = CompareTo + // 17 = GetPropertyOrigin + // 18 = InheritsFrom + // 19 = GetMethod + // 20 = PutMethod + // 21 = DeleteMethod + // 22 = BeginMethodEnumeration + // 23 = NextMethod + // 24 = EndMethodEnumeration + // 25 = GetMethodQualifierSet + // 26 = GetMethodOrigin + + /// + /// Gets the value of a named property. Returns a VARIANT containing the value. + /// + /// + /// + /// + public HRESULT Get(char* wszName, int lFlags, VARIANT* pVal, int* pType, int* plFlavor) + { + fixed (IWbemClassObject* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[4])( + pThis, wszName, lFlags, pVal, pType, plFlavor); + } + } +} diff --git a/src/Framework/Utilities/Wmi/IWbemLocator.cs b/src/Framework/Utilities/Wmi/IWbemLocator.cs new file mode 100644 index 00000000000..981dcbb9a7f --- /dev/null +++ b/src/Framework/Utilities/Wmi/IWbemLocator.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Manually defined WMI COM struct following CsWin32 struct-based COM patterns. +// WMI interfaces are not in Win32 metadata, so they cannot be generated by CsWin32. +// Pattern follows dotnet/sdk ISetupConfiguration.cs (src/Cli/dotnet/Microsoft/VisualStudio/Setup/Configuration/). + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Shared.Win32.Wmi; + +/// +/// Used to obtain the initial namespace pointer to the IWbemServices interface for WMI +/// on a particular host computer. Wraps the native IWbemLocator COM interface. +/// +/// +/// +/// +[SupportedOSPlatform("windows6.0")] +internal unsafe struct IWbemLocator : IComIID +{ + public static Guid Guid { get; } = new(0xDC12A687, 0x737F, 0x11CF, 0x88, 0x4D, 0x00, 0xAA, 0x00, 0x4B, 0x2E, 0x24); + + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ReadOnlySpan data = + [ + 0x87, 0xA6, 0x12, 0xDC, + 0x7F, 0x73, + 0xCF, 0x11, + 0x88, 0x4D, 0x00, 0xAA, 0x00, 0x4B, 0x2E, 0x24 + ]; + + return ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + } + } + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (IWbemLocator* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (IWbemLocator* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (IWbemLocator* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } + + // IWbemLocator methods (vtable index 3) + + /// + /// Connects to the WMI service on the specified host. + /// + /// + /// + /// + public HRESULT ConnectServer( + char* strNetworkResource, + char* strUser, + char* strPassword, + char* strLocale, + int lSecurityFlags, + char* strAuthority, + void* pCtx, + IWbemServices** ppNamespace) + { + fixed (IWbemLocator* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[3])( + pThis, strNetworkResource, strUser, strPassword, strLocale, lSecurityFlags, strAuthority, pCtx, ppNamespace); + } + } + + /// + /// CLSID for WbemLocator COM class. + /// + public static Guid CLSID { get; } = new(0x4590F811, 0x1D3A, 0x11D0, 0x89, 0x1F, 0x00, 0xAA, 0x00, 0x4B, 0x2E, 0x24); +} diff --git a/src/Framework/Utilities/Wmi/IWbemServices.cs b/src/Framework/Utilities/Wmi/IWbemServices.cs new file mode 100644 index 00000000000..1a097158629 --- /dev/null +++ b/src/Framework/Utilities/Wmi/IWbemServices.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Manually defined WMI COM struct following CsWin32 struct-based COM patterns. +// WMI interfaces are not in Win32 metadata, so they cannot be generated by CsWin32. +// Pattern follows dotnet/sdk ISetupConfiguration.cs (src/Cli/dotnet/Microsoft/VisualStudio/Setup/Configuration/). +// +// Only the methods actually used (ExecQuery) are defined with typed signatures. +// Unused vtable slots are kept as placeholders to maintain correct indices. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Microsoft.Build.Shared.Win32.Wmi; + +/// +/// Used to make calls to WMI. This is the primary WMI interface. +/// +/// +/// +/// +[SupportedOSPlatform("windows6.0")] +internal unsafe struct IWbemServices : IComIID +{ + public static Guid Guid { get; } = new(0x9556DC99, 0x828C, 0x11CF, 0xA3, 0x7E, 0x00, 0xAA, 0x00, 0x32, 0x40, 0xC7); + + static ref readonly Guid IComIID.Guid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ReadOnlySpan data = + [ + 0x99, 0xDC, 0x56, 0x95, + 0x8C, 0x82, + 0xCF, 0x11, + 0xA3, 0x7E, 0x00, 0xAA, 0x00, 0x32, 0x40, 0xC7 + ]; + + return ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + } + } + + private readonly void** _lpVtbl; + + // IUnknown methods (vtable indices 0-2) + + public HRESULT QueryInterface(Guid* riid, void** ppvObject) + { + fixed (IWbemServices* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[0])(pThis, riid, ppvObject); + } + } + + public uint AddRef() + { + fixed (IWbemServices* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[1])(pThis); + } + } + + public uint Release() + { + fixed (IWbemServices* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[2])(pThis); + } + } + + // IWbemServices vtable layout (indices 3-25): + // 3 = OpenNamespace + // 4 = CancelAsyncCall + // 5 = QueryObjectSink + // 6 = GetObject + // 7 = GetObjectAsync + // 8 = PutClass + // 9 = PutClassAsync + // 10 = DeleteClass + // 11 = DeleteClassAsync + // 12 = CreateClassEnum + // 13 = CreateClassEnumAsync + // 14 = PutInstance + // 15 = PutInstanceAsync + // 16 = DeleteInstance + // 17 = DeleteInstanceAsync + // 18 = CreateInstanceEnum + // 19 = CreateInstanceEnumAsync + // 20 = ExecQuery <-- Used + // 21 = ExecQueryAsync + // 22 = ExecNotificationQuery + // 23 = ExecNotificationQueryAsync + // 24 = ExecMethod + // 25 = ExecMethodAsync + + /// + /// Executes a WQL query to retrieve objects. + /// + /// + /// + /// + public HRESULT ExecQuery( + char* strQueryLanguage, + char* strQuery, + int lFlags, + void* pCtx, + IEnumWbemClassObject** ppEnum) + { + fixed (IWbemServices* pThis = &this) + { + return ((delegate* unmanaged[Stdcall])_lpVtbl[20])( + pThis, strQueryLanguage, strQuery, lFlags, pCtx, ppEnum); + } + } +} diff --git a/src/Framework/Windows/Win32/Foundation/BSTR.cs b/src/Framework/Windows/Win32/Foundation/BSTR.cs new file mode 100644 index 00000000000..78751d68487 --- /dev/null +++ b/src/Framework/Windows/Win32/Foundation/BSTR.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Partial extension for CsWin32-generated BSTR struct to add IDisposable +// and safe construction from managed strings. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Windows.Win32.Foundation; + +internal readonly unsafe partial struct BSTR : IDisposable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The managed string to convert to a . + public BSTR(string value) + : this((char*)Marshal.StringToBSTR(value)) { } + + /// + /// Gets a value indicating whether the is null. + /// + public bool IsNull => Value is null; + + /// + /// Frees the underlying BSTR and clears this instance to default. + /// + /// + /// + /// Disposing is only safe when the caller disposes the in place + /// (e.g. via using on a local or stackalloc'd field). Do not dispose a by-value copy. + /// + /// + public void Dispose() + { + Marshal.FreeBSTR((nint)Value); + Unsafe.AsRef(in this) = default; + } +} diff --git a/src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs b/src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs new file mode 100644 index 00000000000..e28e487c2de --- /dev/null +++ b/src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; + +namespace System; + +/// +/// Extension members for . +/// +internal static class FileTimeExtensions +{ + extension(FILETIME fileTime) + { + /// + /// Converts the and + /// fields into a single 64-bit value representing the number of 100-nanosecond intervals + /// since January 1, 1601 (UTC). + /// + internal long ToLong() => ((long)(uint)fileTime.dwHighDateTime << 32) | (long)(uint)fileTime.dwLowDateTime; + } +} diff --git a/src/Framework/Windows/Win32/Foundation/HRESULT.cs b/src/Framework/Windows/Win32/Foundation/HRESULT.cs new file mode 100644 index 00000000000..8da39ae522f --- /dev/null +++ b/src/Framework/Windows/Win32/Foundation/HRESULT.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Windows.Win32.System.Diagnostics.Debug; + +namespace Windows.Win32.Foundation; + +internal partial struct HRESULT +{ + /// + /// Convert a Windows error to an . [HRESULT_FROM_WIN32] + /// + public static explicit operator HRESULT(WIN32_ERROR error) => + // https://learn.microsoft.com/windows/win32/api/winerror/nf-winerror-hresult_from_win32 + // return (HRESULT)(x) <= 0 ? (HRESULT)(x) : (HRESULT) (((x) & 0x0000FFFF) | (FACILITY_WIN32 << 16) | 0x80000000); + (HRESULT)(int)((int)error <= 0 ? (int)error : (((int)error & 0x0000FFFF) | ((int)FACILITY_CODE.FACILITY_WIN32 << 16) | 0x80000000)); + + /// + /// Create an from the last Windows error. + /// + public static HRESULT FromLastError() => (HRESULT)(WIN32_ERROR)Marshal.GetLastWin32Error(); +} diff --git a/src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs b/src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs new file mode 100644 index 00000000000..deb63a126be --- /dev/null +++ b/src/Framework/Windows/Win32/GeneratedInteropClsCompliance.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// CsWin32 generates these internal COM interface structs with +// [MarshalAs(UnmanagedType.SafeArray, SafeArraySubTypes = new[] { ... })] attributes +// whose array argument is not CLS-compliant, producing CS3016 warnings under +// [assembly: CLSCompliant(true)]. Mark each generated type [CLSCompliant(false)] +// here via a partial declaration so the warning is expressed semantically rather +// than suppressed wholesale. +// +// A [SuppressMessage] in a global-suppressions file is not an option: the C# +// compiler does not honor SuppressMessageAttribute for CSxxxx warnings, only +// for analyzer diagnostics. See https://github.com/dotnet/roslyn/issues/68526. +// +// While this approach works, having the attribute on the generated types produces CS3019 +// warnings (as the attribute doesn't make sense on internals). We disable this in the +// .editorconfig, for anything in the CsWin32 subfolders. + +using System; + +namespace Windows.Win32.System.Com +{ + [CLSCompliant(false)] + internal partial struct IClassFactory + { + } + + [CLSCompliant(false)] + internal partial struct ISequentialStream + { + } + + [CLSCompliant(false)] + internal partial struct IStream + { + } + + [CLSCompliant(false)] + internal partial struct ITypeComp + { + } + + [CLSCompliant(false)] + internal partial struct ITypeInfo + { + } + + [CLSCompliant(false)] + internal partial struct ITypeLib + { + } +} + +namespace Windows.Win32.System.Com.StructuredStorage +{ + [CLSCompliant(false)] + internal partial struct IEnumSTATSTG + { + } + + [CLSCompliant(false)] + internal partial struct IStorage + { + } +} + +namespace Windows.Win32.System.Ole +{ + [CLSCompliant(false)] + internal partial struct IRecordInfo + { + } +} + +namespace Windows.Win32.System.Diagnostics.Debug.Extensions +{ + [CLSCompliant(false)] + internal partial struct IDebugClient + { + } + + [CLSCompliant(false)] + internal partial struct IDebugClient4 + { + } + + [CLSCompliant(false)] + internal partial struct IDebugBreakpoint + { + } + + [CLSCompliant(false)] + internal partial struct IDebugOutputCallbacks + { + } + + [CLSCompliant(false)] + internal partial struct IDebugInputCallbacks + { + } + + [CLSCompliant(false)] + internal partial struct IDebugEventCallbacks + { + } +} diff --git a/src/Framework/Windows/Win32/IID.cs b/src/Framework/Windows/Win32/IID.cs new file mode 100644 index 00000000000..cd4dfc0d3c6 --- /dev/null +++ b/src/Framework/Windows/Win32/IID.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Copied from dotnet/sdk to provide IID lookup for CsWin32 struct-based COM interfaces. + +using System; +using System.Runtime.CompilerServices; + +namespace Windows.Win32; + +internal static unsafe class IID +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid Get() where T : unmanaged, IComIID + { +#if NETFRAMEWORK || NETSTANDARD + // On .NET Framework and netstandard2.0, IComIID is instance-based. + return default(T).Guid; +#else + // On .NET, CsWin32 generates IComIID with static abstract Guid property. + return T.Guid; +#endif + } +} diff --git a/src/Framework/Windows/Win32/PInvoke.GetFileAttributesEx.cs b/src/Framework/Windows/Win32/PInvoke.GetFileAttributesEx.cs new file mode 100644 index 00000000000..23cb0a71da6 --- /dev/null +++ b/src/Framework/Windows/Win32/PInvoke.GetFileAttributesEx.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.Versioning; +using Windows.Win32.Foundation; +using Windows.Win32.Storage.FileSystem; + +namespace Windows.Win32; + +internal partial class PInvoke +{ + /// + [SupportedOSPlatform("windows6.1")] + internal static unsafe bool GetFileAttributesEx(string name, out WIN32_FILE_ATTRIBUTE_DATA lpFileInformation) + { + fixed (WIN32_FILE_ATTRIBUTE_DATA* fileInfoPtr = &lpFileInformation) + { + return GetFileAttributesEx(name, GET_FILEEX_INFO_LEVELS.GetFileExInfoStandard, fileInfoPtr); + } + } +} diff --git a/src/Framework/Windows/Win32/System/Com/ComClassFactory.cs b/src/Framework/Windows/Win32/System/Com/ComClassFactory.cs new file mode 100644 index 00000000000..277c4d0bf43 --- /dev/null +++ b/src/Framework/Windows/Win32/System/Com/ComClassFactory.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Copied from dotnet/sdk to provide COM object activation without +// Activator.CreateInstance (AOT-compatible). + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Versioning; +using Windows.Win32.Foundation; + +namespace Windows.Win32.System.Com; + +/// +/// Wraps a native pointer to create COM objects +/// without going through or +/// . +/// +[SupportedOSPlatform("windows6.1")] +internal sealed unsafe class ComClassFactory : IDisposable +{ + private IClassFactory* _classFactory; + + private ComClassFactory(IClassFactory* classFactory) + { + _classFactory = classFactory; + } + + /// + /// Attempts to get a class factory for the given COM class ID. + /// + public static bool TryCreate( + Guid classId, + [NotNullWhen(true)] out ComClassFactory? factory, + out HRESULT result) + { + IClassFactory* classFactory; + Guid iid = typeof(IClassFactory).GUID; + result = PInvoke.CoGetClassObject( + &classId, + CLSCTX.CLSCTX_INPROC_SERVER, + null, + &iid, + (void**)&classFactory); + + if (result.Failed || classFactory is null) + { + factory = null; + return false; + } + + factory = new ComClassFactory(classFactory); + return true; + } + + /// + /// Creates an instance of the COM class via the class factory. + /// + public ComScope TryCreateInstance(out HRESULT result) + where TInterface : unmanaged, IComIID + { + Guid iid = IID.Get(); + ComScope scope = default; + result = _classFactory->CreateInstance(null, &iid, scope); + return scope; + } + + public void Dispose() + { + if (_classFactory is not null) + { + _classFactory->Release(); + _classFactory = null; + } + } +} diff --git a/src/Framework/Windows/Win32/System/Com/ComScope.cs b/src/Framework/Windows/Win32/System/Com/ComScope.cs new file mode 100644 index 00000000000..047544fceb2 --- /dev/null +++ b/src/Framework/Windows/Win32/System/Com/ComScope.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Copied from dotnet/sdk to provide COM pointer lifetime management +// using the CsWin32 struct-based COM pattern. + +using System; +using System.Runtime.CompilerServices; + +namespace Windows.Win32.System.Com; + +/// +/// Lifetime management struct for a native COM pointer. Meant to be utilized in a statement +/// to ensure is called when going out of scope with the using. +/// +/// +/// This should be one of the struct COM definitions as generated by CsWin32, +/// or a manually defined COM struct implementing . +/// +internal readonly unsafe ref struct ComScope where T : unmanaged, IComIID +{ + // Keeping internal as nint allows us to use Unsafe methods to get + // significantly better generated code. + private readonly nint _value; + + /// + /// Gets the pointer to the COM interface. + /// + public T* Pointer => (T*)_value; + + /// + /// Initializes a new instance of the struct. + /// + public ComScope(T* value) => _value = (nint)value; + + /// + /// Initializes a new instance of the struct with a void pointer. + /// + public ComScope(void* value) => _value = (nint)value; + + public static implicit operator T*(in ComScope scope) => (T*)scope._value; + + public static implicit operator void*(in ComScope scope) => (void*)scope._value; + + public static implicit operator nint(in ComScope scope) => scope._value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator T**(in ComScope scope) => + (T**)Unsafe.AsPointer(ref Unsafe.AsRef(in scope._value)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator void**(in ComScope scope) => + (void**)Unsafe.AsPointer(ref Unsafe.AsRef(in scope._value)); + + /// + /// if the pointer is null. + /// + public bool IsNull => _value == 0; + + /// + public void Dispose() + { + IUnknown* unknown = (IUnknown*)_value; + + // Really want this to be null after disposal to avoid double releases, but we also want + // to maintain the readonly state of the struct to allow passing as `in` without creating implicit + // copies (which would break the T** and void** operators). Because + // is a , it cannot be captured or copied to the heap, so all disposals + // observe the original storage. + *(void**)this = null; + if (unknown is not null) + { + unknown->Release(); + } + } +} diff --git a/src/Framework/Windows/Win32/System/Variant/VARIANT.cs b/src/Framework/Windows/Win32/System/Variant/VARIANT.cs new file mode 100644 index 00000000000..d41df1c97fa --- /dev/null +++ b/src/Framework/Windows/Win32/System/Variant/VARIANT.cs @@ -0,0 +1,100 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com.StructuredStorage; +using static Windows.Win32.System.Variant.VARENUM; + +namespace Windows.Win32.System.Variant; + +internal unsafe partial struct VARIANT : IDisposable +{ + // See WinForms sources for additional VARIANT functionality when needed. + + /// + /// Gets a value indicating whether this is empty. + /// + public bool IsEmpty => vt == VT_EMPTY && data.llVal == 0; + + /// + /// Gets the type of this . + /// + public VARENUM Type => vt & VT_TYPEMASK; + + /// + /// Gets a value indicating whether this is a by-reference value. + /// + public bool Byref => vt.HasFlag(VT_BYREF); + + /// + /// Gets a reference to the value type field. + /// + /// + /// + /// Use to read the type as some of the bits overlap with data. + /// + /// + [UnscopedRef] + public ref VARENUM vt => ref Anonymous.Anonymous.vt; + + /// + /// Gets a reference to the data union of this . + /// + [UnscopedRef] + public ref _Anonymous_e__Union._Anonymous_e__Struct._Anonymous_e__Union data => ref Anonymous.Anonymous.Anonymous; + + /// + /// Releases resources used by this . + /// + [SupportedOSPlatform("windows6.1")] + public void Dispose() => Clear(); + + /// + /// Clears the value of this , releasing any associated resources. + /// + [SupportedOSPlatform("windows6.1")] + public void Clear() + { + // PropVariantClear is essentially a superset of VariantClear it calls CoTaskMemFree on the following types: + // + // - VT_LPWSTR, VT_LPSTR, VT_CLSID (psvVal) + // - VT_BSTR_BLOB (bstrblobVal.pData) + // - VT_CF (pclipdata->pClipData, pclipdata) + // - VT_BLOB, VT_BLOB_OBJECT (blob.pData) + // - VT_STREAM, VT_STREAMED_OBJECT (pStream) + // - VT_VERSIONED_STREAM (pVersionedStream->pStream, pVersionedStream) + // - VT_STORAGE, VT_STORED_OBJECT (pStorage) + // + // If the VARTYPE is a VT_VECTOR, the contents are cleared as above and CoTaskMemFree is also called on + // cabstr.pElems. + // + // https://learn.microsoft.com/windows/win32/api/oleauto/nf-oleauto-variantclear#remarks + // + // - VT_BSTR (SysFreeString) + // - VT_DISPATCH / VT_UNKOWN (->Release(), if not VT_BYREF) + + if (IsEmpty) + { + return; + } + + fixed (void* t = &this) + { + PInvoke.PropVariantClear((PROPVARIANT*)t); + } + + vt = VT_EMPTY; + data = default; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator BSTR(VARIANT value) => + value.vt == VT_BSTR ? value.data.bstrVal : ThrowInvalidCast(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static T ThrowInvalidCast() => throw new InvalidCastException(); +} diff --git a/src/Shared/UnitTests/NativeMethodsShared_Tests.cs b/src/Shared/UnitTests/NativeMethodsShared_Tests.cs index 579229649ce..a32e2148ce2 100644 --- a/src/Shared/UnitTests/NativeMethodsShared_Tests.cs +++ b/src/Shared/UnitTests/NativeMethodsShared_Tests.cs @@ -9,8 +9,10 @@ using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Xunit; - - +#if FEATURE_WINDOWSINTEROP +using Windows.Win32; +using Windows.Win32.Foundation; +#endif #nullable disable @@ -32,16 +34,16 @@ public sealed class NativeMethodsShared_Tests /// when that bug was in play this test would fail. /// [WindowsOnlyFact("No Kernel32.dll except on Windows.")] - [SupportedOSPlatform("windows")] // bypass CA1416: Validate platform compatibility + [SupportedOSPlatform("windows6.1")] public void TestGetProcAddress() { - IntPtr kernel32Dll = NativeMethodsShared.LoadLibrary("kernel32.dll"); + HMODULE kernel32Dll = PInvoke.LoadLibrary("kernel32.dll"); try { - IntPtr processHandle = NativeMethodsShared.NullIntPtr; - if (kernel32Dll != NativeMethodsShared.NullIntPtr) + IntPtr processHandle = IntPtr.Zero; + if (!kernel32Dll.IsNull) { - processHandle = NativeMethodsShared.GetProcAddress(kernel32Dll, "GetCurrentProcessId"); + processHandle = (IntPtr)PInvoke.GetProcAddress(kernel32Dll, "GetCurrentProcessId").Value; } else { @@ -49,7 +51,7 @@ public void TestGetProcAddress() } // Make sure the pointer passed back for the method is not null - Assert.NotEqual(processHandle, NativeMethodsShared.NullIntPtr); + Assert.NotEqual(processHandle, IntPtr.Zero); // Actually call the method GetProcessIdDelegate processIdDelegate = Marshal.GetDelegateForFunctionPointer(processHandle); @@ -60,9 +62,9 @@ public void TestGetProcAddress() } finally { - if (kernel32Dll != NativeMethodsShared.NullIntPtr) + if (!kernel32Dll.IsNull) { - NativeMethodsShared.FreeLibrary(kernel32Dll); + PInvoke.FreeLibrary(kernel32Dll); } } } diff --git a/src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs b/src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs index 2216ea79388..c724deb8221 100644 --- a/src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs +++ b/src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs @@ -11,6 +11,10 @@ using Microsoft.Build.Utilities; using Shouldly; using Xunit; +#if FEATURE_WINDOWSINTEROP +using Windows.Win32; +using Windows.Win32.Foundation; +#endif namespace Microsoft.Build.Tasks.UnitTests { @@ -76,7 +80,7 @@ public void ManifestPopulationCheck(string? manifestName, bool expectedResult) } } - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] [WindowsOnlyTheory] [InlineData(null, true)] [InlineData("buildIn.manifest", true)] @@ -150,7 +154,7 @@ public void E2EScenarioTests(string? manifestName, bool expectedResult) static string NormalizeLineEndings(string input) => input.Replace("\r\n", "\n").Replace("\r", "\n"); } - [SupportedOSPlatform("windows")] + [SupportedOSPlatform("windows6.1")] internal sealed class AssemblyNativeResourceManager { public enum LoadLibraryFlags : uint { LOAD_LIBRARY_AS_DATAFILE = 2 }; @@ -195,7 +199,7 @@ public enum LoadLibraryFlags : uint { LOAD_LIBRARY_AS_DATAFILE = 2 }; } finally { - NativeMethodsShared.FreeLibrary(hModule); + PInvoke.FreeLibrary((HMODULE)hModule); } return null; diff --git a/src/Tasks/AssemblyDependency/AssemblyInformation.cs b/src/Tasks/AssemblyDependency/AssemblyInformation.cs index 5ecfd2e4705..5bd89d06f76 100644 --- a/src/Tasks/AssemblyDependency/AssemblyInformation.cs +++ b/src/Tasks/AssemblyDependency/AssemblyInformation.cs @@ -7,14 +7,18 @@ using System.Globalization; using System.IO; #if !FEATURE_ASSEMBLYLOADCONTEXT -using System.Linq; using System.Runtime.InteropServices; +using Microsoft.Build.Utilities; #endif using System.Reflection; using System.Runtime.Versioning; using System.Text; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; +#if !FEATURE_ASSEMBLYLOADCONTEXT +using Windows.Win32.Foundation; +#endif + #if FEATURE_ASSEMBLYLOADCONTEXT using System.Reflection.Metadata; using System.Reflection.PortableExecutable; @@ -39,7 +43,6 @@ internal class AssemblyInformation : DisposableBase private readonly IMetaDataDispenser _metadataDispenser; private readonly IMetaDataAssemblyImport _assemblyImport; private static Guid s_importerGuid = new Guid(((GuidAttribute)Attribute.GetCustomAttribute(typeof(IMetaDataImport), typeof(GuidAttribute), false)).Value); - private readonly Assembly _assembly; #endif private readonly string _sourceFile; private FrameworkName _frameworkName; @@ -74,16 +77,10 @@ internal AssemblyInformation(string sourceFile) _sourceFile = sourceFile; #if !FEATURE_ASSEMBLYLOADCONTEXT - if (NativeMethodsShared.IsWindows) - { - // Create the metadata dispenser and open scope on the source file. - _metadataDispenser = (IMetaDataDispenser)new CorMetaDataDispenser(); - _assemblyImport = (IMetaDataAssemblyImport)_metadataDispenser.OpenScope(sourceFile, 0, ref s_importerGuid); - } - else - { - _assembly = Assembly.ReflectionOnlyLoadFrom(sourceFile); - } + // net472-only = inherently Windows. CsWin32 types used directly. + // Create the metadata dispenser and open scope on the source file. + _metadataDispenser = (IMetaDataDispenser)new CorMetaDataDispenser(); + _assemblyImport = (IMetaDataAssemblyImport)_metadataDispenser.OpenScope(sourceFile, 0, ref s_importerGuid); #endif } @@ -361,7 +358,7 @@ private string GetStringCustomAttribute(IMetaDataImport2 import2, uint assemblyS { int hr = import2.GetCustomAttributeByName(assemblyScope, attributeName, out IntPtr data, out uint valueLen); - if (hr == NativeMethodsShared.S_OK) + if (hr == HRESULT.S_OK) { // if an custom attribute exists, parse the contents of the blob if (NativeMethods.TryReadMetadataString(_sourceFile, data, valueLen, out string propertyValue)) @@ -380,33 +377,7 @@ private string GetStringCustomAttribute(IMetaDataImport2 import2, uint assemblyS private FrameworkName GetFrameworkName() { #if !FEATURE_ASSEMBLYLOADCONTEXT - if (!NativeMethodsShared.IsWindows) - { - CustomAttributeData attr = null; - - foreach (CustomAttributeData a in _assembly.GetCustomAttributesData()) - { - try - { - if (a.AttributeType == typeof(TargetFrameworkAttribute)) - { - attr = a; - break; - } - } - catch - { - } - } - - string name = null; - if (attr != null) - { - name = (string)attr.ConstructorArguments[0].Value; - } - return name == null ? null : new FrameworkName(name); - } - + // net472-only = inherently Windows. FrameworkName frameworkAttribute = null; try { @@ -653,10 +624,10 @@ protected override void DisposeUnmanagedResources() /// /// path to the file /// The CLR runtime version or empty if the path does not exist. - internal static string GetRuntimeVersion(string path) + internal static unsafe string GetRuntimeVersion(string path) { #if FEATURE_MSCOREE - if (NativeMethodsShared.IsWindows) + // net472-only = inherently Windows. CsWin32 types used directly. { #if DEBUG // Just to make sure and exercise the code that uses dwLength to allocate the buffer @@ -665,34 +636,28 @@ internal static string GetRuntimeVersion(string path) #else int bufferLength = 11; // 11 is the length of a runtime version and null terminator v2.0.50727/0 #endif + using BufferScope buffer = new(stackalloc char[bufferLength]); - unsafe + fixed (char* bufferPtr = buffer) { - // Allocate an initial buffer - char* runtimeVersion = stackalloc char[bufferLength]; - // Run GetFileVersion, this should succeed using the initial buffer. // It also returns the dwLength which is used if there is insufficient buffer. - uint hresult = NativeMethods.GetFileVersion(path, runtimeVersion, bufferLength, out int dwLength); + HRESULT hresult = NativeMethods.GetFileVersion(path, bufferPtr, bufferLength, out int dwLength); - if (hresult == NativeMethodsShared.ERROR_INSUFFICIENT_BUFFER) + if (hresult == (HRESULT)WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER) { // Allocate new buffer based on the returned length. - char* runtimeVersion2 = stackalloc char[dwLength]; - runtimeVersion = runtimeVersion2; - - // Get the RuntimeVersion in this second call. - bufferLength = dwLength; - hresult = NativeMethods.GetFileVersion(path, runtimeVersion, bufferLength, out dwLength); + buffer.EnsureCapacity(dwLength); + fixed (char* newBufferPtr = buffer) + { + // Run GetFileVersion again, this should succeed using the new buffer. + hresult = NativeMethods.GetFileVersion(path, newBufferPtr, dwLength, out dwLength); + } } - return hresult == NativeMethodsShared.S_OK ? new string(runtimeVersion, 0, dwLength - 1) : string.Empty; + return hresult == HRESULT.S_OK ? buffer.Slice(0, dwLength - 1).ToString() : string.Empty; } } - else - { - return ManagedRuntimeVersionReader.GetRuntimeVersion(path); - } #else return ManagedRuntimeVersionReader.GetRuntimeVersion(path); #endif @@ -705,13 +670,9 @@ internal static string GetRuntimeVersion(string path) private AssemblyNameExtension[] ImportAssemblyDependencies() { #if !FEATURE_ASSEMBLYLOADCONTEXT + // net472-only = inherently Windows. var asmRefs = new List(); - if (!NativeMethodsShared.IsWindows) - { - return _assembly.GetReferencedAssemblies().Select(a => new AssemblyNameExtension(a)).ToArray(); - } - IntPtr asmRefEnum = IntPtr.Zero; var asmRefTokens = new UInt32[GENMAN_ENUM_TOKEN_BUF_SIZE]; // Ensure the enum handle is closed. diff --git a/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs b/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs index 1a6656c607d..f124dfec341 100644 --- a/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs +++ b/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs @@ -11,6 +11,7 @@ using System.Runtime.Versioning; using Microsoft.Build.Framework; using Microsoft.Build.Shared; +using Windows.Win32.Foundation; #nullable disable @@ -182,31 +183,25 @@ internal static string RetrievePathFromFusionName(string strongName) string value; - if (NativeMethodsShared.IsWindows) - { - uint hr = NativeMethods.CreateAssemblyCache(out IAssemblyCache assemblyCache, 0); + // net472-only = inherently Windows. CsWin32 types used directly. + uint hr = NativeMethods.CreateAssemblyCache(out IAssemblyCache assemblyCache, 0); - ErrorUtilities.VerifyThrow(hr == NativeMethodsShared.S_OK, "CreateAssemblyCache failed, hr {0}", hr); + ErrorUtilities.VerifyThrow(hr == HRESULT.S_OK, "CreateAssemblyCache failed, hr {0}", hr); - var assemblyInfo = new ASSEMBLY_INFO { cbAssemblyInfo = (uint)Marshal.SizeOf() }; + var assemblyInfo = new ASSEMBLY_INFO { cbAssemblyInfo = (uint)Marshal.SizeOf() }; - assemblyCache.QueryAssemblyInfo(0, strongName, ref assemblyInfo); + assemblyCache.QueryAssemblyInfo(0, strongName, ref assemblyInfo); - if (assemblyInfo.cbAssemblyInfo == 0) - { - return null; - } + if (assemblyInfo.cbAssemblyInfo == 0) + { + return null; + } - assemblyInfo.pszCurrentAssemblyPathBuf = new string(new char[assemblyInfo.cchBuf]); + assemblyInfo.pszCurrentAssemblyPathBuf = new string(new char[assemblyInfo.cchBuf]); - assemblyCache.QueryAssemblyInfo(0, strongName, ref assemblyInfo); + assemblyCache.QueryAssemblyInfo(0, strongName, ref assemblyInfo); - value = assemblyInfo.pszCurrentAssemblyPathBuf; - } - else - { - value = NativeMethods.AssemblyCacheEnum.AssemblyPathFromStrongName(strongName); - } + value = assemblyInfo.pszCurrentAssemblyPathBuf; return value; } diff --git a/src/Tasks/ComReference.cs b/src/Tasks/ComReference.cs index 67e50ba29d6..77fb2352d4f 100644 --- a/src/Tasks/ComReference.cs +++ b/src/Tasks/ComReference.cs @@ -7,6 +7,8 @@ using Microsoft.Build.Utilities; using COMException = System.Runtime.InteropServices.COMException; using Marshal = System.Runtime.InteropServices.Marshal; +using Windows.Win32; +using Windows.Win32.Foundation; #nullable disable @@ -380,8 +382,8 @@ internal static string StripTypeLibNumberFromPath(string typeLibPath, FileExists // so the old code would fail to find them on disk using the simplistic checks above. if (lastChance) { - IntPtr libraryHandle = NativeMethodsShared.LoadLibrary(typeLibPath); - if (IntPtr.Zero != libraryHandle) + HMODULE libraryHandle = PInvoke.LoadLibrary(typeLibPath); + if (!libraryHandle.IsNull) { try { @@ -389,7 +391,7 @@ internal static string StripTypeLibNumberFromPath(string typeLibPath, FileExists } finally { - NativeMethodsShared.FreeLibrary(libraryHandle); + PInvoke.FreeLibrary(libraryHandle); } } else @@ -401,28 +403,21 @@ internal static string StripTypeLibNumberFromPath(string typeLibPath, FileExists return typeLibPath; } - private static string GetModuleFileName(IntPtr handle) + private static string GetModuleFileName(HMODULE handle) { - char[] buffer = null; + using BufferScope buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]); // Try increased buffer sizes if on longpath-enabled Windows - for (int bufferSize = NativeMethodsShared.MAX_PATH; bufferSize <= NativeMethodsShared.MaxPath; bufferSize *= 2) + for (int bufferSize = (int)PInvoke.MAX_PATH; bufferSize <= NativeMethodsShared.MaxPath; bufferSize *= 2) { - buffer = System.Buffers.ArrayPool.Shared.Rent(bufferSize); - try - { - var handleRef = new System.Runtime.InteropServices.HandleRef(buffer, handle); - int pathLength = NativeMethodsShared.GetModuleFileName(handleRef, buffer, bufferSize); + buffer.EnsureCapacity(bufferSize); - bool isBufferTooSmall = (uint)Marshal.GetLastWin32Error() == NativeMethodsShared.ERROR_INSUFFICIENT_BUFFER; - if (pathLength != 0 && !isBufferTooSmall) - { - return new string(buffer, 0, pathLength); - } - } - finally + int pathLength = (int)PInvoke.GetModuleFileName(handle, buffer.AsSpan()); + + // GetModuleFileName returns bufferSize when truncated; treat that as "buffer too small". + if (pathLength != 0 && pathLength < bufferSize) { - System.Buffers.ArrayPool.Shared.Return(buffer); + return buffer.Slice(0, pathLength).ToString(); } // Double check that the buffer is not insanely big diff --git a/src/Tasks/FileState.cs b/src/Tasks/FileState.cs index 806dda2d893..b66269af271 100644 --- a/src/Tasks/FileState.cs +++ b/src/Tasks/FileState.cs @@ -3,9 +3,15 @@ using System; using System.IO; +#if FEATURE_WINDOWSINTEROP using System.Runtime.InteropServices; +#endif using Microsoft.Build.Framework; using Microsoft.Build.Shared; +#if FEATURE_WINDOWSINTEROP +using Windows.Win32; +using Windows.Win32.Storage.FileSystem; +#endif #nullable disable @@ -86,6 +92,7 @@ public FileDirInfo(string filename) _filename = FileUtilities.AttemptToShortenPath(filename); // This is no-op unless the path actually is too long +#if FEATURE_WINDOWSINTEROP int oldMode = 0; if (NativeMethodsShared.IsWindows) @@ -98,13 +105,14 @@ public FileDirInfo(string filename) // mode back, since this may have wide-ranging effects. NativeMethodsShared.SetThreadErrorMode(1 /* ErrorModes.SEM_FAILCRITICALERRORS */, out oldMode); } +#endif try { +#if FEATURE_WINDOWSINTEROP if (NativeMethodsShared.IsWindows) { - var data = new NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA(); - bool success = NativeMethodsShared.GetFileAttributesEx(_filename, 0, ref data); + bool success = PInvoke.GetFileAttributesEx(_filename, out WIN32_FILE_ATTRIBUTE_DATA data); if (!success) { @@ -132,14 +140,14 @@ public FileDirInfo(string filename) } Exists = true; - IsDirectory = (data.fileAttributes & NativeMethodsShared.FILE_ATTRIBUTE_DIRECTORY) != 0; + IsDirectory = ((FILE_FLAGS_AND_ATTRIBUTES)data.dwFileAttributes & FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) != 0; IsReadOnly = !IsDirectory - && (data.fileAttributes & NativeMethodsShared.FILE_ATTRIBUTE_READONLY) != 0; - LastWriteTimeUtc = - DateTime.FromFileTimeUtc(((long)data.ftLastWriteTimeHigh << 0x20) | data.ftLastWriteTimeLow); - Length = IsDirectory ? 0 : (((long)data.fileSizeHigh << 0x20) | data.fileSizeLow); + && ((FILE_FLAGS_AND_ATTRIBUTES)data.dwFileAttributes & FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_READONLY) != 0; + LastWriteTimeUtc = DateTime.FromFileTimeUtc(data.ftLastWriteTime.ToLong()); + Length = IsDirectory ? 0 : ((long)((ulong)data.nFileSizeHigh << 32) | data.nFileSizeLow); } else +#endif { var fileInfo = new FileInfo(_filename); @@ -174,11 +182,13 @@ public FileDirInfo(string filename) } finally { +#if FEATURE_WINDOWSINTEROP // Reset the error mode on Windows if (NativeMethodsShared.IsWindows) { NativeMethodsShared.SetThreadErrorMode(oldMode, out _); } +#endif } } diff --git a/src/Tasks/IFixedTypeInfo.cs b/src/Tasks/IFixedTypeInfo.cs new file mode 100644 index 00000000000..dc04c959958 --- /dev/null +++ b/src/Tasks/IFixedTypeInfo.cs @@ -0,0 +1,44 @@ +// 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.Runtime.InteropServices; + +#nullable disable + +namespace Microsoft.Build.Tasks +{ + /// + /// The original ITypeInfo interface in the CLR has incorrect definitions for GetRefTypeOfImplType and GetRefTypeInfo. + /// It uses ints for marshalling handles which will result in a crash on 64 bit systems. This is a temporary interface + /// for use until the one in the CLR is fixed. When it is we can go back to using ITypeInfo. + /// + [Guid("00020401-0000-0000-C000-000000000046")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComImport] + public interface IFixedTypeInfo + { + void GetTypeAttr(out IntPtr ppTypeAttr); + void GetTypeComp(out System.Runtime.InteropServices.ComTypes.ITypeComp ppTComp); + void GetFuncDesc(int index, out IntPtr ppFuncDesc); + void GetVarDesc(int index, out IntPtr ppVarDesc); + void GetNames(int memid, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2), Out] String[] rgBstrNames, int cMaxNames, out int pcNames); + void GetRefTypeOfImplType(int index, out IntPtr href); + void GetImplTypeFlags(int index, out System.Runtime.InteropServices.ComTypes.IMPLTYPEFLAGS pImplTypeFlags); + void GetIDsOfNames([MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex = 1), In] String[] rgszNames, int cNames, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] int[] pMemId); + void Invoke([MarshalAs(UnmanagedType.IUnknown)] Object pvInstance, int memid, Int16 wFlags, ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams, IntPtr pVarResult, IntPtr pExcepInfo, out int puArgErr); + void GetDocumentation(int index, out String strName, out String strDocString, out int dwHelpContext, out String strHelpFile); + void GetDllEntry(int memid, System.Runtime.InteropServices.ComTypes.INVOKEKIND invKind, IntPtr pBstrDllName, IntPtr pBstrName, IntPtr pwOrdinal); + void GetRefTypeInfo(IntPtr hRef, out IFixedTypeInfo ppTI); + void AddressOfMember(int memid, System.Runtime.InteropServices.ComTypes.INVOKEKIND invKind, out IntPtr ppv); + void CreateInstance([MarshalAs(UnmanagedType.IUnknown)] Object pUnkOuter, [In] ref Guid riid, [MarshalAs(UnmanagedType.IUnknown), Out] out Object ppvObj); + void GetMops(int memid, out String pBstrMops); + void GetContainingTypeLib(out System.Runtime.InteropServices.ComTypes.ITypeLib ppTLB, out int pIndex); + [PreserveSig] + void ReleaseTypeAttr(IntPtr pTypeAttr); + [PreserveSig] + void ReleaseFuncDesc(IntPtr pFuncDesc); + [PreserveSig] + void ReleaseVarDesc(IntPtr pVarDesc); + } +} diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj index 0c5667703f2..64ca733bc1c 100644 --- a/src/Tasks/Microsoft.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.Build.Tasks.csproj @@ -163,6 +163,7 @@ + @@ -613,9 +614,7 @@ - + diff --git a/src/Tasks/NativeMethods.cs b/src/Tasks/NativeMethods.cs index b301947ff09..d187ed70a9b 100644 --- a/src/Tasks/NativeMethods.cs +++ b/src/Tasks/NativeMethods.cs @@ -23,44 +23,14 @@ using System.Runtime.Versioning; using Microsoft.Build.Utilities; +#if FEATURE_MSCOREE +using Windows.Win32.Foundation; +#endif + #nullable disable namespace Microsoft.Build.Tasks { - /// - /// The original ITypeInfo interface in the CLR has incorrect definitions for GetRefTypeOfImplType and GetRefTypeInfo. - /// It uses ints for marshalling handles which will result in a crash on 64 bit systems. This is a temporary interface - /// for use until the one in the CLR is fixed. When it is we can go back to using ITypeInfo. - /// - [Guid("00020401-0000-0000-C000-000000000046")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [ComImport] - public interface IFixedTypeInfo - { - void GetTypeAttr(out IntPtr ppTypeAttr); - void GetTypeComp(out System.Runtime.InteropServices.ComTypes.ITypeComp ppTComp); - void GetFuncDesc(int index, out IntPtr ppFuncDesc); - void GetVarDesc(int index, out IntPtr ppVarDesc); - void GetNames(int memid, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2), Out] String[] rgBstrNames, int cMaxNames, out int pcNames); - void GetRefTypeOfImplType(int index, out IntPtr href); - void GetImplTypeFlags(int index, out System.Runtime.InteropServices.ComTypes.IMPLTYPEFLAGS pImplTypeFlags); - void GetIDsOfNames([MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex = 1), In] String[] rgszNames, int cNames, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] int[] pMemId); - void Invoke([MarshalAs(UnmanagedType.IUnknown)] Object pvInstance, int memid, Int16 wFlags, ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams, IntPtr pVarResult, IntPtr pExcepInfo, out int puArgErr); - void GetDocumentation(int index, out String strName, out String strDocString, out int dwHelpContext, out String strHelpFile); - void GetDllEntry(int memid, System.Runtime.InteropServices.ComTypes.INVOKEKIND invKind, IntPtr pBstrDllName, IntPtr pBstrName, IntPtr pwOrdinal); - void GetRefTypeInfo(IntPtr hRef, out IFixedTypeInfo ppTI); - void AddressOfMember(int memid, System.Runtime.InteropServices.ComTypes.INVOKEKIND invKind, out IntPtr ppv); - void CreateInstance([MarshalAs(UnmanagedType.IUnknown)] Object pUnkOuter, [In] ref Guid riid, [MarshalAs(UnmanagedType.IUnknown), Out] out Object ppvObj); - void GetMops(int memid, out String pBstrMops); - void GetContainingTypeLib(out System.Runtime.InteropServices.ComTypes.ITypeLib ppTLB, out int pIndex); - [PreserveSig] - void ReleaseTypeAttr(IntPtr pTypeAttr); - [PreserveSig] - void ReleaseFuncDesc(IntPtr pFuncDesc); - [PreserveSig] - void ReleaseVarDesc(IntPtr pVarDesc); - } - [GuidAttribute("00020406-0000-0000-C000-000000000046")] [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] [ComImport] @@ -1107,7 +1077,7 @@ internal static extern int CreateAssemblyNameObject( /// The size, in bytes, of the returned szBuffer. /// HResult. [DllImport(MscoreeDLL, SetLastError = true, CharSet = CharSet.Unicode)] - internal static extern unsafe uint GetFileVersion([MarshalAs(UnmanagedType.LPWStr)] string szFileName, [Out] char* szBuffer, int cchBuffer, out int dwLength); + internal static extern unsafe HRESULT GetFileVersion([MarshalAs(UnmanagedType.LPWStr)] string szFileName, [Out] char* szBuffer, int cchBuffer, out int dwLength); #endif #endregion diff --git a/src/Tasks/Unzip.cs b/src/Tasks/Unzip.cs index 581a1c82885..4c31a0b4862 100644 --- a/src/Tasks/Unzip.cs +++ b/src/Tasks/Unzip.cs @@ -266,7 +266,7 @@ private void Extract(ZipArchive sourceArchive, DirectoryInfo destinationDirector // We don't apply UnixFileMode.None because .zip files created on Windows and .zip files created // with previous versions of .NET don't include permissions. UnixFileMode mode = (UnixFileMode)(zipArchiveEntry.ExternalAttributes >> 16) & OwnershipPermissions; - if (mode != UnixFileMode.None && !NativeMethodsShared.IsWindows) + if (mode != UnixFileMode.None && NativeMethodsShared.IsUnixLike) { fileStreamOptions.UnixCreateMode = mode; } diff --git a/src/Utilities.UnitTests/ProcessExtensions_Tests.cs b/src/Utilities.UnitTests/ProcessExtensions_Tests.cs index d9491c7ef54..a72a90c87fb 100644 --- a/src/Utilities.UnitTests/ProcessExtensions_Tests.cs +++ b/src/Utilities.UnitTests/ProcessExtensions_Tests.cs @@ -110,5 +110,221 @@ public async Task TryGetCommandLine_RunningProcess_ContainsArguments() } } } + + [Fact] + public void TryGetCommandLine_NullProcess_ReturnsFalse() + { + Process process = null; + process.TryGetCommandLine(out string commandLine).ShouldBeFalse(); + commandLine.ShouldBeNull(); + } + + [Fact] + public void TryGetCommandLine_ExitedProcess_ReturnsFalse() + { + var psi = NativeMethodsShared.IsWindows + ? new ProcessStartInfo("cmd.exe", "/c echo hello") + : new ProcessStartInfo("echo", "hello"); + psi.UseShellExecute = false; + psi.CreateNoWindow = true; + using Process p = Process.Start(psi); + p.WaitForExit(10000); + p.HasExited.ShouldBeTrue(); + + p.TryGetCommandLine(out string commandLine).ShouldBeFalse(); + commandLine.ShouldBeNull(); + } + +#if FEATURE_WINDOWSINTEROP && NET + [WindowsOnlyFact] + public async Task GetCommandLine_ViaWmi_ContainsExpectedExecutable() + { + using Process p = StartLongRunningProcess(); + try + { + await Task.Delay(300); + var sw = Stopwatch.StartNew(); + p.TryGetCommandLine(ProcessExtensions.CommandLineSource.Wmi, out string commandLine).ShouldBeTrue(); + sw.Stop(); + _output.WriteLine($"WMI GetCommandLine elapsed: {sw.Elapsed.TotalMilliseconds:F2} ms"); + + commandLine.ShouldNotBeNull(); + commandLine.ShouldContain("ping", Case.Insensitive); + } + finally + { + if (!p.HasExited) + { + p.KillTree(5000); + } + } + } + + [WindowsOnlyFact] + public async Task GetCommandLine_ViaWmi_ContainsArguments() + { + using Process p = StartLongRunningProcess(); + try + { + await Task.Delay(300); + p.TryGetCommandLine(ProcessExtensions.CommandLineSource.Wmi, out string commandLine).ShouldBeTrue(); + + commandLine.ShouldNotBeNull(); + commandLine.ShouldMatch(@"(127\.0\.0\.1|31)"); + } + finally + { + if (!p.HasExited) + { + p.KillTree(5000); + } + } + } + + [WindowsOnlyFact] + public async Task GetCommandLine_ViaDebugEngine_ContainsExpectedExecutable() + { + using Process p = StartLongRunningProcess(); + try + { + await Task.Delay(300); + var sw = Stopwatch.StartNew(); + p.TryGetCommandLine(ProcessExtensions.CommandLineSource.DebugEngine, out string commandLine).ShouldBeTrue(); + sw.Stop(); + _output.WriteLine($"DebugEngine GetCommandLine elapsed: {sw.Elapsed.TotalMilliseconds:F2} ms"); + + // DebugEngine may return a non-null result that contains the executable name. + // For protected or system processes it may fall back to the exe path. + commandLine.ShouldNotBeNull(); + commandLine.ShouldContain("ping", Case.Insensitive); + } + finally + { + if (!p.HasExited) + { + p.KillTree(5000); + } + } + } + + [WindowsOnlyFact] + public async Task GetCommandLine_ViaDebugEngine_ContainsArguments() + { + using Process p = StartLongRunningProcess(); + try + { + await Task.Delay(300); + p.TryGetCommandLine(ProcessExtensions.CommandLineSource.DebugEngine, out string commandLine).ShouldBeTrue(); + + commandLine.ShouldNotBeNull(); + // DebugEngine description should include command line arguments + commandLine.ShouldMatch(@"(127\.0\.0\.1|31)"); + } + finally + { + if (!p.HasExited) + { + p.KillTree(5000); + } + } + } + + [WindowsOnlyFact] + public async Task GetCommandLine_BothSources_ReturnEquivalentResults() + { + using Process p = StartLongRunningProcess(); + try + { + await Task.Delay(300); + p.TryGetCommandLine(ProcessExtensions.CommandLineSource.Wmi, out string wmiResult).ShouldBeTrue(); + p.TryGetCommandLine(ProcessExtensions.CommandLineSource.DebugEngine, out string debugResult).ShouldBeTrue(); + + _output.WriteLine($"WMI result: {wmiResult}"); + _output.WriteLine($"DebugEngine result: {debugResult}"); + + // Both should be non-null and contain "ping" + wmiResult.ShouldNotBeNull(); + debugResult.ShouldNotBeNull(); + wmiResult.ShouldContain("ping", Case.Insensitive); + debugResult.ShouldContain("ping", Case.Insensitive); + + // Both should contain the same target address or timeout argument + wmiResult.ShouldMatch(@"(127\.0\.0\.1|31)"); + debugResult.ShouldMatch(@"(127\.0\.0\.1|31)"); + } + finally + { + if (!p.HasExited) + { + p.KillTree(5000); + } + } + } + + [WindowsOnlyFact] + public void GetCommandLine_ViaWmi_RunningProcess_ReturnsCommandLine() + { + using Process dummy = StartLongRunningProcess(); + try + { + dummy.TryGetCommandLine(ProcessExtensions.CommandLineSource.Wmi, out string commandLine).ShouldBeTrue(); + commandLine.ShouldNotBeNull(); + commandLine.ShouldContain("ping", Case.Insensitive); + } + finally + { + if (!dummy.HasExited) + { + dummy.KillTree(5000); + } + } + } + + [WindowsOnlyFact] + public void GetCommandLine_ViaDebugEngine_RunningProcess_ReturnsCommandLine() + { + using Process dummy = StartLongRunningProcess(); + try + { + dummy.TryGetCommandLine(ProcessExtensions.CommandLineSource.DebugEngine, out string commandLine).ShouldBeTrue(); + commandLine.ShouldNotBeNull(); + commandLine.ShouldContain("ping", Case.Insensitive); + } + finally + { + if (!dummy.HasExited) + { + dummy.KillTree(5000); + } + } + } + + [WindowsOnlyFact] + public void TryGetCommandLine_WithSource_NullProcess_ReturnsFalse() + { + Process process = null; + process.TryGetCommandLine(ProcessExtensions.CommandLineSource.Wmi, out string commandLine).ShouldBeFalse(); + commandLine.ShouldBeNull(); + } + + [WindowsOnlyFact] + public void TryGetCommandLine_WithSource_ExitedProcess_ReturnsFalse() + { + var psi = new ProcessStartInfo("cmd.exe", "/c echo hello") + { + UseShellExecute = false, + CreateNoWindow = true, + }; + using Process p = Process.Start(psi); + p.WaitForExit(10000); + p.HasExited.ShouldBeTrue(); + + p.TryGetCommandLine(ProcessExtensions.CommandLineSource.Wmi, out string commandLine).ShouldBeFalse(); + commandLine.ShouldBeNull(); + + p.TryGetCommandLine(ProcessExtensions.CommandLineSource.DebugEngine, out commandLine).ShouldBeFalse(); + commandLine.ShouldBeNull(); + } +#endif } }