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(); }