diff --git a/src/coreclr/src/System.Private.CoreLib/PinvokeAnalyzerExceptionList.analyzerdata b/src/coreclr/src/System.Private.CoreLib/PinvokeAnalyzerExceptionList.analyzerdata
index 4bbdccae19d803..5d45345491c369 100644
--- a/src/coreclr/src/System.Private.CoreLib/PinvokeAnalyzerExceptionList.analyzerdata
+++ b/src/coreclr/src/System.Private.CoreLib/PinvokeAnalyzerExceptionList.analyzerdata
@@ -1,3 +1,7 @@
normaliz.dll!IsNormalizedString
normaliz.dll!NormalizeString
+
+
+user32.dll!GetProcessWindowStation
+user32.dll!GetUserObjectInformationW
diff --git a/src/libraries/Common/src/Interop/Windows/User32/Interop.Constants.cs b/src/libraries/Common/src/Interop/Windows/User32/Interop.Constants.cs
index 45fce95f67a569..5566ba2b2802fd 100644
--- a/src/libraries/Common/src/Interop/Windows/User32/Interop.Constants.cs
+++ b/src/libraries/Common/src/Interop/Windows/User32/Interop.Constants.cs
@@ -225,5 +225,7 @@ internal partial class User32
public const int UOI_FLAGS = 1;
+ public const int HWND_BROADCAST = 0xffff;
+
}
}
diff --git a/src/libraries/Common/src/Interop/Windows/User32/Interop.GetUserObjectInformation.cs b/src/libraries/Common/src/Interop/Windows/User32/Interop.GetUserObjectInformation.cs
index 85205d3570a1ee..3144de671e3042 100644
--- a/src/libraries/Common/src/Interop/Windows/User32/Interop.GetUserObjectInformation.cs
+++ b/src/libraries/Common/src/Interop/Windows/User32/Interop.GetUserObjectInformation.cs
@@ -10,6 +10,6 @@ internal partial class Interop
internal partial class User32
{
[DllImport(Libraries.User32, SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)]
- public static extern bool GetUserObjectInformationW(IntPtr hObj, int nIndex, ref USEROBJECTFLAGS pvBuffer, int nLength, ref int lpnLengthNeeded);
+ public static extern unsafe bool GetUserObjectInformationW(IntPtr hObj, int nIndex, void* pvBuffer, uint nLength, ref uint lpnLengthNeeded);
}
}
diff --git a/src/libraries/Microsoft.Win32.SystemEvents/src/Microsoft/Win32/SystemEvents.cs b/src/libraries/Microsoft.Win32.SystemEvents/src/Microsoft/Win32/SystemEvents.cs
index 6e24b137a806d6..ca6c3b5c38bd6e 100644
--- a/src/libraries/Microsoft.Win32.SystemEvents/src/Microsoft/Win32/SystemEvents.cs
+++ b/src/libraries/Microsoft.Win32.SystemEvents/src/Microsoft/Win32/SystemEvents.cs
@@ -77,32 +77,24 @@ private static unsafe bool UserInteractive
{
get
{
- if (Environment.OSVersion.Platform == System.PlatformID.Win32NT)
+ IntPtr hwinsta = Interop.User32.GetProcessWindowStation();
+ if (hwinsta != IntPtr.Zero && s_processWinStation != hwinsta)
{
- IntPtr hwinsta = IntPtr.Zero;
-
- hwinsta = Interop.User32.GetProcessWindowStation();
- if (hwinsta != IntPtr.Zero && s_processWinStation != hwinsta)
- {
- s_isUserInteractive = true;
+ s_isUserInteractive = true;
- int lengthNeeded = 0;
- Interop.User32.USEROBJECTFLAGS flags = default;
+ uint dummy = 0;
+ Interop.User32.USEROBJECTFLAGS flags = default;
- if (Interop.User32.GetUserObjectInformationW(hwinsta, Interop.User32.UOI_FLAGS, ref flags, sizeof(Interop.User32.USEROBJECTFLAGS), ref lengthNeeded))
+ if (Interop.User32.GetUserObjectInformationW(hwinsta, Interop.User32.UOI_FLAGS, &flags, (uint)sizeof(Interop.User32.USEROBJECTFLAGS), ref dummy))
+ {
+ if ((flags.dwFlags & Interop.User32.WSF_VISIBLE) == 0)
{
- if ((flags.dwFlags & Interop.User32.WSF_VISIBLE) == 0)
- {
- s_isUserInteractive = false;
- }
+ s_isUserInteractive = false;
}
- s_processWinStation = hwinsta;
}
+ s_processWinStation = hwinsta;
}
- else
- {
- s_isUserInteractive = true;
- }
+
return s_isUserInteractive;
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/Interop/Windows/User32/Interop.Constants.cs b/src/libraries/System.Private.CoreLib/src/Interop/Windows/User32/Interop.Constants.cs
deleted file mode 100644
index a48329fb6b191a..00000000000000
--- a/src/libraries/System.Private.CoreLib/src/Interop/Windows/User32/Interop.Constants.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-internal static partial class Interop
-{
- internal static partial class User32
- {
- internal const int HWND_BROADCAST = 0xffff;
- internal const int WM_SETTINGCHANGE = 0x001A;
- }
-}
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 41360b2000db4f..cf8edf1de1e00a 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -1325,11 +1325,22 @@
Common\Interop\Windows\Shell32\Interop.SHGetKnownFolderPath.cs
-
+
+ Common\Interop\Windows\User32\Interop.Constants.cs
+
Common\Interop\Windows\User32\Interop.LoadString.cs
+
+ Common\Interop\Windows\User32\Interop.GetProcessWindowStation.cs
+
+
+ Common\Interop\Windows\User32\Interop.GetUserObjectInformation.cs
+
+
+ Common\Interop\Windows\User32\Interop.USEROBJECTFLAGS.cs
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.cs
index d733feb5316221..3a51a58fc91a3a 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.cs
@@ -17,6 +17,8 @@ public static partial class Environment
{
private static Func? s_directoryCreateDirectory;
+ public static bool UserInteractive => true;
+
private static string CurrentDirectoryCore
{
get => Interop.Sys.GetCwd();
diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs
index 17a49080d661a3..98c913a09804e1 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs
@@ -122,6 +122,28 @@ public static string SystemDirectory
}
}
+ public static unsafe bool UserInteractive
+ {
+ get
+ {
+ // Per documentation of GetProcessWindowStation, this handle should not be closed
+ IntPtr handle = Interop.User32.GetProcessWindowStation();
+ if (handle != IntPtr.Zero)
+ {
+ Interop.User32.USEROBJECTFLAGS flags = default;
+ uint dummy = 0;
+ if (Interop.User32.GetUserObjectInformationW(handle, Interop.User32.UOI_FLAGS, &flags, (uint)sizeof(Interop.User32.USEROBJECTFLAGS), ref dummy))
+ {
+ return ((flags.dwFlags & Interop.User32.WSF_VISIBLE) == 0);
+ }
+ }
+
+ // If we can't determine, return true optimistically
+ // This will include cases like Windows Nano which do not expose WindowStations
+ return true;
+ }
+ }
+
public static unsafe long WorkingSet
{
get
diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.cs
index 1454f1d56e4040..194ae6099bb513 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Environment.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Environment.cs
@@ -138,8 +138,6 @@ public static OperatingSystem OSVersion
}
}
- public static bool UserInteractive => true;
-
public static Version Version
{
get
diff --git a/src/libraries/System.Runtime.Extensions/tests/System/EnvironmentTests.cs b/src/libraries/System.Runtime.Extensions/tests/System/EnvironmentTests.cs
index ea33c8e9eda47e..26571f9697a5a8 100644
--- a/src/libraries/System.Runtime.Extensions/tests/System/EnvironmentTests.cs
+++ b/src/libraries/System.Runtime.Extensions/tests/System/EnvironmentTests.cs
@@ -165,8 +165,23 @@ public void SystemPageSize_Valid()
}
[Fact]
- public void UserInteractive_True()
+ [PlatformSpecific(TestPlatforms.AnyUnix)]
+ public void UserInteractive_Unix_True()
+ {
+ Assert.True(Environment.UserInteractive);
+ }
+
+ [Fact]
+ [PlatformSpecific(TestPlatforms.Windows)]
+ public void UserInteractive_Windows_DoesNotThrow()
+ {
+ var dummy = Environment.UserInteractive; // Does not throw
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindowsNanoServer))]
+ public void UserInteractive_WindowsNano()
{
+ // Defaults to true on Nano, because it doesn't expose WindowStations
Assert.True(Environment.UserInteractive);
}
diff --git a/src/libraries/System.ServiceProcess.ServiceController/tests/ServiceBaseTests.cs b/src/libraries/System.ServiceProcess.ServiceController/tests/ServiceBaseTests.cs
index c6c43dce26b5dd..8b62f638a9aff3 100644
--- a/src/libraries/System.ServiceProcess.ServiceController/tests/ServiceBaseTests.cs
+++ b/src/libraries/System.ServiceProcess.ServiceController/tests/ServiceBaseTests.cs
@@ -142,7 +142,16 @@ public void TestOnExecuteCustomCommand()
ServiceController controller = ConnectToServer();
controller.ExecuteCommand(128);
- Assert.Equal(128, _testService.GetByte());
+ // Response from test service:
+ // 128 => Environment.UserInteractive == false
+ // 129 => Environment.UserInteractive == true
+ //
+ // On Windows Nano and other SKU that do not expose Window Stations, Environment.UserInteractive
+ // will always return true, even within a service process.
+ // Otherwise, we expect it to be false.
+ // (This is the only place we verify Environment.UserInteractive can return false)
+ byte expected = PlatformDetection.HasWindowsShell ? (byte)128 : (byte)129;
+ Assert.Equal(expected, _testService.GetByte());
controller.Stop();
Assert.Equal((int)PipeMessageByteCode.Stop, _testService.GetByte());
diff --git a/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/TestService.cs b/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/TestService.cs
index c50f129e8ea0bf..604289322f9ce8 100644
--- a/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/TestService.cs
+++ b/src/libraries/System.ServiceProcess.ServiceController/tests/System.ServiceProcess.ServiceController.TestService/TestService.cs
@@ -43,6 +43,10 @@ protected override void OnContinue()
protected override void OnCustomCommand(int command)
{
base.OnCustomCommand(command);
+
+ if (Environment.UserInteractive) // see ServiceBaseTests.TestOnExecuteCustomCommand()
+ command++;
+
WriteStreamAsync(PipeMessageByteCode.OnCustomCommand, command).Wait();
}