diff --git a/src/PowerShell.Core.Instrumentation/PowerShell.Core.Instrumentation.man b/src/PowerShell.Core.Instrumentation/PowerShell.Core.Instrumentation.man
index fb221cfe964..bb4e15351e5 100644
--- a/src/PowerShell.Core.Instrumentation/PowerShell.Core.Instrumentation.man
+++ b/src/PowerShell.Core.Instrumentation/PowerShell.Core.Instrumentation.man
@@ -121,6 +121,18 @@
value="0x3002"
version="1"
/>
+
+
-
+
+
@@ -2409,6 +2445,12 @@
symbol="T_EXPERIMENTALFEATURE"
value="107"
/>
+
-
+
+
@@ -2593,11 +2647,23 @@
name="PSWorkflow"
symbol="K_PSWORKFLOW"
/>
-
+
+
@@ -4004,6 +4070,20 @@
name="StackTrace"
/>
+
+
+
+
+
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -5535,6 +5647,14 @@
id="PS_PROVIDER.task.T_ExperimentalFeature.message"
value="PowerShell Experimental Features"
/>
+
+
+
+
+
+
+
+
diff --git a/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs b/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs
index 530493f320a..36a6be4074b 100644
--- a/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs
+++ b/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs
@@ -179,9 +179,12 @@ public static bool IsStaSupported
{
int result = Interop.Windows.CoInitializeEx(IntPtr.Zero, Interop.Windows.COINIT_APARTMENTTHREADED);
- // If 0 is returned the thread has been initialized for the first time
- // as an STA and thus supported and needs to be uninitialized.
- if (result > 0)
+ // Per COM documentation: Each successful call to CoInitializeEx (including S_FALSE)
+ // must be balanced by a corresponding call to CoUninitialize.
+ // - S_OK (0) means we initialized for the first time.
+ // - S_FALSE (1) means already initialized, but still increments the reference count.
+ // Both require CoUninitialize to decrement the reference count.
+ if (result >= 0)
{
Interop.Windows.CoUninitialize();
}
diff --git a/src/System.Management.Automation/engine/remoting/common/PSETWTracer.cs b/src/System.Management.Automation/engine/remoting/common/PSETWTracer.cs
index 2fd2dc0a913..989ad33e987 100644
--- a/src/System.Management.Automation/engine/remoting/common/PSETWTracer.cs
+++ b/src/System.Management.Automation/engine/remoting/common/PSETWTracer.cs
@@ -166,6 +166,9 @@ internal enum PSEventId : int
ExperimentalFeature_InvalidName = 0x3001,
ExperimentalFeature_ReadConfig_Error = 0x3002,
+ // Windows Diagnostics And Usage Data Settings
+ Telemetry_Setting_Error = 0x3011,
+
// Scheduled Jobs
ScheduledJob_Start = 0xD001,
ScheduledJob_Complete = 0xD002,
@@ -240,6 +243,7 @@ internal enum PSTask : int
ProviderStop = 0x69,
ExecutePipeline = 0x6A,
ExperimentalFeature = 0x6B,
+ Telemetry = 0x6C,
ScheduledJob = 0x6E,
NamedPipe = 0x6F,
ISEOperation = 0x78,
diff --git a/src/System.Management.Automation/utils/Telemetry.cs b/src/System.Management.Automation/utils/Telemetry.cs
index abb92d4bc43..ed28c0baa21 100644
--- a/src/System.Management.Automation/utils/Telemetry.cs
+++ b/src/System.Management.Automation/utils/Telemetry.cs
@@ -168,12 +168,20 @@ public static class ApplicationInsightsTelemetry
///
static ApplicationInsightsTelemetry()
{
- // If we can't send telemetry, there's no reason to do any of this
CanSendTelemetry = !GetEnvironmentVariableAsBool(name: _telemetryOptoutEnvVar, defaultValue: false)
&& Platform.TryDeriveFromCache("telemetry.uuid", out s_uuidPath);
+#if !UNIX
+ if (CanSendTelemetry)
+ {
+ // Respect the diagnostics and feedback setting in Windows.
+ CanSendTelemetry = WindowsDataCollectionSetting.CanCollectDiagnostics(PlatformDataCollectionLevel.Enhanced);
+ }
+#endif
+
if (!CanSendTelemetry)
{
+ // Avoid the initialization work if we can't send telemetry.
return;
}
diff --git a/src/System.Management.Automation/utils/WindowsDataCollectionSetting.cs b/src/System.Management.Automation/utils/WindowsDataCollectionSetting.cs
new file mode 100644
index 00000000000..5f8b607550a
--- /dev/null
+++ b/src/System.Management.Automation/utils/WindowsDataCollectionSetting.cs
@@ -0,0 +1,185 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#if !UNIX
+
+using System;
+using System.Management.Automation.Internal;
+using System.Management.Automation.Tracing;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+
+namespace Microsoft.PowerShell.Telemetry;
+
+internal enum PlatformDataCollectionLevel : int
+{
+ ///
+ /// Minimum — only security-related data. Enterprise/education editions only.
+ ///
+ Security = 0,
+
+ ///
+ /// Device info, capabilities, and basic reliability data.
+ ///
+ Basic = 1,
+
+ ///
+ /// More detailed usage and reliability data, including app/feature usage patterns.
+ /// Removed as a user-facing option in Windows 11 (collapsed into Full).
+ ///
+ Enhanced = 2,
+
+ ///
+ /// All of the above plus advanced diagnostics data that can help Microsoft fix problems.
+ ///
+ Full = 3,
+}
+
+///
+/// Minimal projection of IInspectable, the base interface for all WinRT objects.
+/// Slots 3–5 in every WinRT interface vtable (after IUnknown's QueryInterface/AddRef/Release).
+///
+[GeneratedComInterface]
+[Guid("AF86E2E0-B12D-4C6A-9C5A-D7AA65101E90")]
+internal partial interface IInspectable
+{
+ void GetIids(out uint iidCount, out nint iids);
+
+ nint GetRuntimeClassName();
+
+ int GetTrustLevel();
+}
+
+///
+/// Projection of the WinRT interface Windows.System.Profile.IPlatformDiagnosticsAndUsageDataSettingsStatics
+/// (IID B6E24C1B-7B1C-4B32-8C62-A66597CE723A).
+/// Vtable slots 6–9, following the three IInspectable slots.
+///
+[GeneratedComInterface]
+[Guid("B6E24C1B-7B1C-4B32-8C62-A66597CE723A")]
+internal partial interface IPlatformDiagnosticsAndUsageDataSettingsStatics : IInspectable
+{
+ PlatformDataCollectionLevel GetCollectionLevel();
+
+ long AddCollectionLevelChanged(nint handler);
+
+ void RemoveCollectionLevelChanged(long token);
+
+ // WinRT marshals bool as a byte; use byte to avoid any MarshalAs ambiguity with the source generator.
+ byte CanCollectDiagnostics(PlatformDataCollectionLevel level);
+}
+
+///
+/// Wraps Windows.System.Profile.PlatformDiagnosticsAndUsageDataSettings using compile-time COM interop
+/// and source-generated P/Invoke. No extra runtime DLLs are required.
+///
+internal static partial class WindowsDataCollectionSetting
+{
+ ///
+ /// Returns if the device's diagnostic data collection policy permits collecting at or above .
+ ///
+ /// The minimum to test against.
+ internal static bool CanCollectDiagnostics(PlatformDataCollectionLevel level)
+ {
+ const string ClassName = "Windows.System.Profile.PlatformDiagnosticsAndUsageDataSettings";
+
+ // When initializing WinRT on the calling thread, use the multi-threaded apartment (MTA).
+ // This is to cover the case where PowerShell gets used in a thread-pool thread.
+ // See the doc at https://learn.microsoft.com/windows/win32/api/roapi/ne-roapi-ro_init_type
+ const int RO_INIT_MULTITHREADED = 1;
+
+ // Return values for 'RoInitialize':
+ // - S_OK (0) - we successfully initialized; must call 'RoUninitialize'.
+ // - S_FALSE (1) - already initialized with the same apartment type; must still call 'RoUninitialize'.
+ // - RPC_E_CHANGED_MODE - already initialized with a different apartment type; WinRT still works, do NOT call 'RoUninitialize'.
+ const int RPC_E_CHANGED_MODE = unchecked((int)0x80010106);
+
+ int initHr = -1;
+ nint hstring = default;
+ nint factoryPtr = default;
+
+ try
+ {
+ // Initialize WinRT on the calling thread. 'RoGetActivationFactory' requires it.
+ initHr = RoInitialize(RO_INIT_MULTITHREADED);
+ if (initHr < 0 && initHr != RPC_E_CHANGED_MODE)
+ {
+ // The call to initialize the Windows Runtime failed.
+ // Throw an exception with the HRESULT error code to provide more context on the failure.
+ Marshal.ThrowExceptionForHR(initHr);
+ }
+
+ Marshal.ThrowExceptionForHR(
+ WindowsCreateString(ClassName, (uint)ClassName.Length, out hstring));
+
+ Guid iid = new("B6E24C1B-7B1C-4B32-8C62-A66597CE723A");
+ Marshal.ThrowExceptionForHR(
+ RoGetActivationFactory(hstring, ref iid, out factoryPtr));
+
+ var comWrappers = new StrategyBasedComWrappers();
+ var comObject = comWrappers.GetOrCreateObjectForComInstance(factoryPtr, CreateObjectFlags.None);
+ var platformSetting = (IPlatformDiagnosticsAndUsageDataSettingsStatics)comObject;
+
+ return platformSetting.CanCollectDiagnostics(level) != 0;
+ }
+ catch (Exception ex)
+ {
+ // Log any exceptions that occur during this process, but swallow them and return false to disable telemetry rather than crashing the product.
+ // This API is only used to gate telemetry collection, so failure should be non-fatal.
+ PSEtwLog.LogOperationalError(
+ PSEventId.Telemetry_Setting_Error,
+ PSOpcode.Exception,
+ PSTask.Telemetry,
+ PSKeyword.UseAlwaysOperational,
+ ex.GetType().FullName,
+ ex.Message,
+ ex.StackTrace);
+
+ return false;
+ }
+ finally
+ {
+ if (factoryPtr != default)
+ {
+ Marshal.Release(factoryPtr);
+ }
+
+ if (hstring != default)
+ {
+ _ = WindowsDeleteString(hstring);
+ }
+
+ // Per COM documentation: Each successful call to 'RoInitialize' (including S_FALSE)
+ // must be balanced by a corresponding call to 'RoUninitialize'.
+ if (initHr >= 0)
+ {
+ RoUninitialize();
+ }
+ }
+ }
+
+ [LibraryImport("api-ms-win-core-winrt-string-l1-1-0.dll", StringMarshalling = StringMarshalling.Utf16)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ private static partial int WindowsCreateString(
+ string sourceString,
+ uint length,
+ out nint hstring);
+
+ [LibraryImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ private static partial int WindowsDeleteString(nint hstring);
+
+ [LibraryImport("api-ms-win-core-winrt-l1-1-0.dll")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ private static partial int RoGetActivationFactory(nint activatableClassId, ref Guid iid, out nint factory);
+
+ [LibraryImport("api-ms-win-core-winrt-l1-1-0.dll")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ private static partial int RoInitialize(int initType);
+
+ [LibraryImport("api-ms-win-core-winrt-l1-1-0.dll")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ private static partial void RoUninitialize();
+}
+
+#endif
diff --git a/test/powershell/engine/Basic/Telemetry.Tests.ps1 b/test/powershell/engine/Basic/Telemetry.Tests.ps1
index 2378b9e5a66..0da08316a56 100644
--- a/test/powershell/engine/Basic/Telemetry.Tests.ps1
+++ b/test/powershell/engine/Basic/Telemetry.Tests.ps1
@@ -5,8 +5,53 @@
# these tests aren't going to check that telemetry is being sent
# only that we're not treating the telemetry.uuid file correctly
+function Get-OSTelemetryLevel {
+ <#
+ .SYNOPSIS
+ Returns the effective Windows Telemetry level (0-3).
+ Logic: Checks GPO overrides, then System preferences, then defaults to 1.
+ #>
+
+ $gpoPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection"
+ $sysPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection"
+ $valueName = "AllowTelemetry"
+
+ # 1. Check the "Managed" Policy (Group Policy)
+ if (Test-Path $gpoPath) {
+ $gpoValue = Get-ItemProperty -Path $gpoPath -Name $valueName -ErrorAction SilentlyContinue
+ if ($gpoValue -and $gpoValue.$valueName) {
+ return [int]$gpoValue.$valueName
+ }
+ }
+
+ # 2. Check the "User/System" Preference (Settings App)
+ if (Test-Path $sysPath) {
+ $sysValue = Get-ItemProperty -Path $sysPath -Name $valueName -ErrorAction SilentlyContinue
+ if ($sysValue -and $sysValue.$valueName) {
+ return [int]$sysValue.$valueName
+ }
+ }
+
+ # 3. Fallback to OS Default (Basic/Required)
+ return 1
+}
+
Describe "Telemetry for shell startup" -Tag CI {
BeforeAll {
+ $skipTelemetryTests = $false
+
+ if ($IsWindows) {
+ ## Skip telemetry tests if the OS telemetry level is less than 2 (Enhanced) -- PS telemetry is disabled in this case.
+ $osTelemetryLevel = Get-OSTelemetryLevel
+ $skipTelemetryTests = $osTelemetryLevel -lt 2
+ }
+
+ if ($skipTelemetryTests) {
+ $originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
+ $PSDefaultParameterValues["it:skip"] = $true
+ return
+ }
+
# if the telemetry file exists, move it out of the way
# the member is internal, but we can retrieve it via reflection
$cacheDir = [System.Management.Automation.Platform].GetField("CacheDirectory","NonPublic,Static").GetValue($null)
@@ -23,6 +68,11 @@ Describe "Telemetry for shell startup" -Tag CI {
}
AfterAll {
+ if ($skipTelemetryTests) {
+ $global:PSDefaultParameterValues = $originalDefaultParameterValues
+ return
+ }
+
# check and reset the telemetry.uuid file
if ( $uuidFileExists ) {
if ( Test-Path -Path "${uuidPath}.original" ) {