From 88868b7a781f4e5b9037b8721f30440207a7aa42 Mon Sep 17 00:00:00 2001 From: hangy Date: Fri, 23 Dec 2022 22:40:21 +0100 Subject: [PATCH] Extend ProcessStartInfo to allow setting LOGON_NETCREDENTIALS_ONLY (#77637) Co-authored-by: Adam Sitnik --- .../System/WindowsIdentityFixture.cs | 2 +- .../System/WindowsTestFileShare.cs | 118 +++++++++++ .../ref/System.Diagnostics.Process.cs | 2 + .../src/Resources/Strings.resx | 3 + .../src/System/Diagnostics/Process.Windows.cs | 10 +- .../Diagnostics/ProcessStartInfo.Unix.cs | 7 + .../Diagnostics/ProcessStartInfo.Windows.cs | 18 ++ .../tests/ProcessStartInfoTests.Windows.cs | 72 +++++++ .../tests/ProcessStartInfoTests.cs | 193 +++++++++++++----- .../tests/ProcessTestBase.cs | 2 +- .../System.Diagnostics.Process.Tests.csproj | 4 + .../FileStreamConformanceTests.Windows.cs | 82 +------- .../tests/System.IO.FileSystem.Tests.csproj | 3 + 13 files changed, 381 insertions(+), 135 deletions(-) create mode 100644 src/libraries/Common/tests/TestUtilities/System/WindowsTestFileShare.cs create mode 100644 src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.Windows.cs diff --git a/src/libraries/Common/tests/TestUtilities/System/WindowsIdentityFixture.cs b/src/libraries/Common/tests/TestUtilities/System/WindowsIdentityFixture.cs index 708c22c078173..ae5a86b1e7420 100644 --- a/src/libraries/Common/tests/TestUtilities/System/WindowsIdentityFixture.cs +++ b/src/libraries/Common/tests/TestUtilities/System/WindowsIdentityFixture.cs @@ -34,7 +34,7 @@ public sealed partial class WindowsTestAccount : IDisposable private readonly string _userName; private SafeAccessTokenHandle _accountTokenHandle; public SafeAccessTokenHandle AccountTokenHandle => _accountTokenHandle; - public string AccountName { get; set; } + public string AccountName { get; private set; } public string Password { get; } public WindowsTestAccount(string userName) diff --git a/src/libraries/Common/tests/TestUtilities/System/WindowsTestFileShare.cs b/src/libraries/Common/tests/TestUtilities/System/WindowsTestFileShare.cs new file mode 100644 index 0000000000000..b59ff9dfd1172 --- /dev/null +++ b/src/libraries/Common/tests/TestUtilities/System/WindowsTestFileShare.cs @@ -0,0 +1,118 @@ +// 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 System.ServiceProcess; + +namespace System +{ + public sealed partial class WindowsTestFileShare : IDisposable + { + private static readonly Lazy _canShareFiles = new Lazy(() => + { + if (!PlatformDetection.IsWindows || !PlatformDetection.IsPrivilegedProcess) + { + return false; + } + + try + { + // the "Server Service" allows for file sharing. It can be disabled on some machines. + using (ServiceController sharingService = new ServiceController("Server")) + { + return sharingService.Status == ServiceControllerStatus.Running; + } + } + catch (InvalidOperationException) + { + // The service is not installed. + return false; + } + }); + + private readonly string _shareName; + + private readonly string _path; + + private bool _disposedValue; + + public WindowsTestFileShare(string shareName, string path) + { + _shareName = shareName; + _path = path; + Initialize(); + } + + public static bool CanShareFiles => _canShareFiles.Value; + + private void Initialize() + { + SHARE_INFO_502 shareInfo = default; + shareInfo.shi502_netname = _shareName; + shareInfo.shi502_path = _path; + shareInfo.shi502_remark = "folder created to test UNC file paths"; + shareInfo.shi502_max_uses = -1; + + int infoSize = Marshal.SizeOf(shareInfo); + IntPtr infoBuffer = Marshal.AllocCoTaskMem(infoSize); + + try + { + Marshal.StructureToPtr(shareInfo, infoBuffer, false); + + const int NERR_DuplicateShare = 2118; + int shareResult = NetShareAdd(string.Empty, 502, infoBuffer, IntPtr.Zero); + if (shareResult == NERR_DuplicateShare) + { + NetShareDel(string.Empty, _shareName, 0); + shareResult = NetShareAdd(string.Empty, 502, infoBuffer, IntPtr.Zero); + } + + if (shareResult != 0 && shareResult != NERR_DuplicateShare) + { + throw new Exception($"Failed to create a file share, NetShareAdd returned {shareResult}"); + } + } + finally + { + Marshal.FreeCoTaskMem(infoBuffer); + } + } + + [LibraryImport(Interop.Libraries.Netapi32)] + private static partial int NetShareAdd([MarshalAs(UnmanagedType.LPWStr)] string servername, int level, IntPtr buf, IntPtr parm_err); + + [LibraryImport(Interop.Libraries.Netapi32)] + private static partial int NetShareDel([MarshalAs(UnmanagedType.LPWStr)] string servername, [MarshalAs(UnmanagedType.LPWStr)] string netname, int reserved); + + public void Dispose() + { + if (_disposedValue) + { + return; + } + + NetShareDel(string.Empty, _shareName, 0); + _disposedValue = true; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SHARE_INFO_502 + { + [MarshalAs(UnmanagedType.LPWStr)] + public string shi502_netname; + public uint shi502_type; + [MarshalAs(UnmanagedType.LPWStr)] + public string shi502_remark; + public int shi502_permissions; + public int shi502_max_uses; + public int shi502_current_uses; + [MarshalAs(UnmanagedType.LPWStr)] + public string shi502_path; + public IntPtr shi502_passwd; + public int shi502_reserved; + public IntPtr shi502_security_descriptor; + } + } +} + diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index a80bfe4f6ac29..ed06fe1dc1ca8 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -232,6 +232,8 @@ public ProcessStartInfo(string fileName, string arguments) { } public string FileName { get { throw null; } set { } } [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")] public bool LoadUserProfile { get { throw null; } set { } } + [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")] + public bool UseCredentialsForNetworkingOnly { get { throw null; } set { } } [System.CLSCompliantAttribute(false)] [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")] public System.Security.SecureString? Password { get { throw null; } set { } } diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index 08a17e891e065..d6ac4b8688cca 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -269,6 +269,9 @@ ProcessStartInfo.Password and ProcessStartInfo.PasswordInClearText cannot both be set. Use only one of them. + + ProcessStartInfo.LoadUserProfile and ProcessStartInfo.UseCredentialsForNetworkingOnly cannot both be set. Use only one of them. + Index and count must refer to a location within the buffer. diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index 5631e6d2f1752..4df3bc7f14dec 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -524,10 +524,18 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) } Interop.Advapi32.LogonFlags logonFlags = (Interop.Advapi32.LogonFlags)0; - if (startInfo.LoadUserProfile) + if (startInfo.LoadUserProfile && startInfo.UseCredentialsForNetworkingOnly) + { + throw new ArgumentException(SR.CantEnableConflictingLogonFlags, nameof(startInfo)); + } + else if (startInfo.LoadUserProfile) { logonFlags = Interop.Advapi32.LogonFlags.LOGON_WITH_PROFILE; } + else if (startInfo.UseCredentialsForNetworkingOnly) + { + logonFlags = Interop.Advapi32.LogonFlags.LOGON_NETCREDENTIALS_ONLY; + } fixed (char* passwordInClearTextPtr = startInfo.PasswordInClearText ?? string.Empty) fixed (char* environmentBlockPtr = environmentBlock) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs index bcfa52548a99e..9664815dcde79 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs @@ -34,6 +34,13 @@ public bool LoadUserProfile set { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(LoadUserProfile))); } } + [SupportedOSPlatform("windows")] + public bool UseCredentialsForNetworkingOnly + { + get { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(UseCredentialsForNetworkingOnly))); } + set { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(UseCredentialsForNetworkingOnly))); } + } + public bool UseShellExecute { get; set; } public string[] Verbs => Array.Empty(); diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Windows.cs index b80b61326983e..8869c1809341f 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Windows.cs @@ -25,6 +25,24 @@ public string Domain [SupportedOSPlatform("windows")] public bool LoadUserProfile { get; set; } + /// + /// Gets or sets a value that indicates whether the user credentials + /// are only to be used for network resources. + /// + /// true if the user credentials are only to be used for + /// network resources. + /// + /// This property is referenced if the process is being started + /// by using the user name, password, and domain. + /// If the value is true, the process is started with the + /// caller's identity. The system creates a new logon session with + /// the given credentials, which is used on the network only. + /// The system does not validate the specified credentials. Therefore, + /// the process can start, but it may not have access to network resources. + /// + [SupportedOSPlatform("windows")] + public bool UseCredentialsForNetworkingOnly { get; set; } + [CLSCompliant(false)] [SupportedOSPlatform("windows")] public SecureString? Password { get; set; } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.Windows.cs new file mode 100644 index 0000000000000..57dff3dce3b02 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.Windows.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Security; +using Microsoft.DotNet.RemoteExecutor; +using Xunit; + +namespace System.Diagnostics.Tests +{ + partial class ProcessStartInfoTests : ProcessTestBase + { + private static bool IsAdmin_IsNotNano_RemoteExecutorIsSupported_CanShareFiles + => IsAdmin_IsNotNano_RemoteExecutorIsSupported && WindowsTestFileShare.CanShareFiles; + + [ConditionalFact(nameof(IsAdmin_IsNotNano_RemoteExecutorIsSupported_CanShareFiles))] // Nano has no "netapi32.dll", Admin rights are required + [PlatformSpecific(TestPlatforms.Windows)] + [OuterLoop("Requires admin privileges")] + public void TestUserNetworkCredentialsPropertiesOnWindows() + { + const string ShareName = "testForDotNet"; + const string TestFileContent = "42"; + const string UncPathEnvVar = nameof(UncPathEnvVar); + + string testFilePath = GetTestFilePath(); + File.WriteAllText(testFilePath, TestFileContent); + + using WindowsTestFileShare fileShare = new WindowsTestFileShare(ShareName, Path.GetDirectoryName(testFilePath)); + string testFileUncPath = $"\\\\{Environment.MachineName}\\{ShareName}\\{Path.GetFileName(testFilePath)}"; + + using Process process = CreateProcess(() => + { + try + { + Assert.Equal(TestFileContent, File.ReadAllText(Environment.GetEnvironmentVariable(UncPathEnvVar))); + + return RemoteExecutor.SuccessExitCode; + } + catch (Exception ex) when (ex is SecurityException or UnauthorizedAccessException) + { + return -1; + } + }); + process.StartInfo.Environment[UncPathEnvVar] = testFileUncPath; + process.StartInfo.UseCredentialsForNetworkingOnly = true; + + using TestProcessState processInfo = CreateUserAndExecute(process, Setup, Cleanup); + + Assert.Equal(Environment.UserName, Helpers.GetProcessUserName(process)); + + Assert.True(process.WaitForExit(WaitInMS)); + Assert.Equal(RemoteExecutor.SuccessExitCode, process.ExitCode); + + void Setup(string username, string _) + { + if (PlatformDetection.IsNotWindowsServerCore) // for this particular Windows version it fails with Attempted to perform an unauthorized operation (#46619) + { + SetAccessControl(username, testFilePath, Path.GetDirectoryName(testFilePath), add: true); + } + } + + void Cleanup(string username, string _) + { + if (PlatformDetection.IsNotWindowsServerCore) + { + // remove the access + SetAccessControl(username, testFilePath, Path.GetDirectoryName(testFilePath), add: false); + } + } + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs index 66fe0a2dabf4a..686780fd018d7 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs @@ -4,25 +4,24 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.ComponentModel; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Security.Cryptography; +using System.Security; +using System.Security.AccessControl; using System.Security.Principal; using System.Text; -using System.ComponentModel; -using System.Security; using System.Threading; using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.XUnitExtensions; using Microsoft.Win32; -using Microsoft.Win32.SafeHandles; using Xunit; -using System.Security.AccessControl; namespace System.Diagnostics.Tests { - public class ProcessStartInfoTests : ProcessTestBase + public partial class ProcessStartInfoTests : ProcessTestBase { private const string ItemSeparator = "CAFF9451396B4EEF8A5155A15BDC2080"; // random string that shouldn't be in any env vars; used instead of newline to separate env var strings @@ -453,7 +452,7 @@ public void TestWorkingDirectoryPropertyDefaultCase() [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public void TestWorkingDirectoryPropertyInChildProcess() { - string workingDirectory = string.IsNullOrEmpty(Environment.SystemDirectory) ? TestDirectory : Environment.SystemDirectory ; + string workingDirectory = string.IsNullOrEmpty(Environment.SystemDirectory) ? TestDirectory : Environment.SystemDirectory; Assert.NotEqual(workingDirectory, Directory.GetCurrentDirectory()); var psi = new ProcessStartInfo { WorkingDirectory = workingDirectory }; RemoteExecutor.Invoke(wd => @@ -468,61 +467,31 @@ public void TestWorkingDirectoryPropertyInChildProcess() [OuterLoop("Requires admin privileges")] public void TestUserCredentialsPropertiesOnWindows() { - const string username = "testForDotnetRuntime"; - using WindowsTestAccount testAccount = new WindowsTestAccount(username); + using Process longRunning = CreateProcessLong(); + longRunning.StartInfo.LoadUserProfile = true; - bool hasStarted = false; - SafeProcessHandle handle = null; - Process p = null; - string workingDirectory = null; + using TestProcessState testAccountCleanup = CreateUserAndExecute(longRunning, Setup, Cleanup); - try - { - p = CreateProcessLong(); - - workingDirectory = string.IsNullOrEmpty(p.StartInfo.WorkingDirectory) - ? Directory.GetCurrentDirectory() - : p.StartInfo.WorkingDirectory; + string username = testAccountCleanup.ProcessAccountName.Split('\\').Last(); + Assert.Equal(username, Helpers.GetProcessUserName(longRunning)); + bool isProfileLoaded = GetNamesOfUserProfiles().Any(profile => profile.Equals(username)); + Assert.True(isProfileLoaded); + void Setup(string username, string workingDirectory) + { if (PlatformDetection.IsNotWindowsServerCore) // for this particular Windows version it fails with Attempted to perform an unauthorized operation (#46619) { // ensure the new user can access the .exe (otherwise you get Access is denied exception) - SetAccessControl(username, p.StartInfo.FileName, workingDirectory, add: true); + SetAccessControl(username, longRunning.StartInfo.FileName, workingDirectory, add: true); } - - p.StartInfo.LoadUserProfile = true; - p.StartInfo.UserName = username; - p.StartInfo.PasswordInClearText = testAccount.Password; - - try - { - hasStarted = p.Start(); - } - catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_SHARING_VIOLATION) - { - throw new SkipTestException($"{p.StartInfo.FileName} has been locked by some other process"); - } - - - Assert.Equal(username, Helpers.GetProcessUserName(p)); - bool isProfileLoaded = GetNamesOfUserProfiles().Any(profile => profile.Equals(username)); - Assert.True(isProfileLoaded); } - finally - { - if (handle != null) - handle.Dispose(); - - if (hasStarted) - { - p.Kill(); - - Assert.True(p.WaitForExit(WaitInMS)); - } + void Cleanup(string username, string workingDirectory) + { if (PlatformDetection.IsNotWindowsServerCore) { - SetAccessControl(username, p.StartInfo.FileName, workingDirectory, add: false); // remove the access + // remove the access + SetAccessControl(username, longRunning.StartInfo.FileName, workingDirectory, add: false); } } } @@ -536,7 +505,7 @@ private static void SetAccessControl(string userName, string filePath, string di DirectoryInfo directoryInfo = new DirectoryInfo(directoryPath); DirectorySecurity directorySecurity = directoryInfo.GetAccessControl(); - Apply(userName, directorySecurity, FileSystemRights.Read , add); + Apply(userName, directorySecurity, FileSystemRights.Read, add); directoryInfo.SetAccessControl(directorySecurity); static void Apply(string userName, FileSystemSecurity accessControl, FileSystemRights rights, bool add) @@ -957,6 +926,25 @@ public void LoadUserProfile_GetSetUnix_ThrowsPlatformNotSupportedException() Assert.Throws(() => info.LoadUserProfile = false); } + [Theory] + [InlineData(true)] + [InlineData(false)] + [PlatformSpecific(TestPlatforms.Windows)] + public void UseCredentialsForNetworkingOnly_SetWindows_GetReturnsExpected(bool useCredentialsForNetworkingOnly) + { + var info = new ProcessStartInfo { UseCredentialsForNetworkingOnly = useCredentialsForNetworkingOnly }; + Assert.Equal(useCredentialsForNetworkingOnly, info.UseCredentialsForNetworkingOnly); + } + + [Fact] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public void UseCredentialsForNetworkingOnly_GetSetUnix_ThrowsPlatformNotSupportedException() + { + var info = new ProcessStartInfo(); + Assert.Throws(() => info.UseCredentialsForNetworkingOnly); + Assert.Throws(() => info.UseCredentialsForNetworkingOnly = false); + } + [Theory] [InlineData(null)] [InlineData("")] @@ -1155,7 +1143,7 @@ private static unsafe string GetAssociationString(int flags, int str, string psz return $"Didn't get expected HRESULT (1) when getting char count. HRESULT was 0x{result:x8}"; string value = new string((char)0, (int)count - 1); - fixed(char* s = value) + fixed (char* s = value) { result = AssocQueryStringW(flags, str, pszAssoc, pszExtra, s, ref count); } @@ -1205,7 +1193,7 @@ public static TheoryData UseShellExecute { TheoryData data = new TheoryData { false }; - if ( !PlatformDetection.IsInAppContainer // https://github.com/dotnet/runtime/issues/21919 + if (!PlatformDetection.IsInAppContainer // https://github.com/dotnet/runtime/issues/21919 && !PlatformDetection.IsWindowsNanoServer // By design && !PlatformDetection.IsWindowsIoTCore) data.Add(true); @@ -1348,5 +1336,102 @@ private void VerifyNotepadMainWindowTitle(Process process, string filename) Assert.StartsWith(expected, title); } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] // No Notepad on Nano + [OuterLoop("Launches notepad")] + [PlatformSpecific(TestPlatforms.Windows)] + public void StartInfo_LoadUserProfile_And_UseCredentialsForNetworkingOnly_AreIncompatible() + { + ProcessStartInfo info = new ProcessStartInfo + { + LoadUserProfile = true, + UseCredentialsForNetworkingOnly = true, + UserName = "dummy", + PasswordInClearText = "not used, because ArgumentException should be thrown before", + FileName = "notepad.exe", + Arguments = null, + WindowStyle = ProcessWindowStyle.Minimized + }; + + Assert.Throws("startInfo", () => + { + using (var process = Process.Start(info)) + { + Assert.False(process != null, $"Process started despite incompatible options {nameof(info.LoadUserProfile)} and {nameof(info.UseCredentialsForNetworkingOnly)} were enabled"); + } + }); + } + + private static TestProcessState CreateUserAndExecute( + Process process, + Action additionalSetup = null, + Action additionalCleanup = null, + [CallerMemberName] string memberName = "") + { + string callerIntials = new string(memberName.Where(c => char.IsUpper(c)).Take(18).ToArray()); + + WindowsTestAccount processAccount = new WindowsTestAccount(string.Concat("d", callerIntials)); + string workingDirectory = string.IsNullOrEmpty(process.StartInfo.WorkingDirectory) + ? Directory.GetCurrentDirectory() + : process.StartInfo.WorkingDirectory; + + additionalSetup?.Invoke(processAccount.AccountName, workingDirectory); + + process.StartInfo.UserName = processAccount.AccountName.Split('\\').Last(); + process.StartInfo.Domain = processAccount.AccountName.Split('\\').First(); + process.StartInfo.PasswordInClearText = processAccount.Password; + + try + { + bool hasStarted = process.Start(); + return new TestProcessState(process, hasStarted, processAccount, workingDirectory, additionalCleanup); + } + catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_SHARING_VIOLATION) + { + throw new SkipTestException($"{process.StartInfo.FileName} has been locked by some other process"); + } + } + + private class TestProcessState : IDisposable + { + private readonly Process _process; + + private readonly bool _hasStarted; + + private readonly WindowsTestAccount _processAccount; + + private readonly string _workingDirectory; + + private readonly Action _additionalCleanup; + + public TestProcessState( + Process process, + bool hasStarted, + WindowsTestAccount processAccount, + string workingDirectory, + Action additionalCleanup) + { + _process = process; + _hasStarted = hasStarted; + _processAccount = processAccount; + _workingDirectory = workingDirectory; + _additionalCleanup = additionalCleanup; + } + + public string ProcessAccountName => _processAccount?.AccountName; + + public void Dispose() + { + if (_hasStarted) + { + _process.Kill(); + + Assert.True(_process.WaitForExit(WaitInMS)); + } + + _additionalCleanup?.Invoke(_processAccount?.AccountName, _workingDirectory); + _processAccount?.Dispose(); + } + } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTestBase.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTestBase.cs index 52a480595f0f7..ab4abf0015afc 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTestBase.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTestBase.cs @@ -13,7 +13,7 @@ namespace System.Diagnostics.Tests { public partial class ProcessTestBase : FileCleanupTestBase { - protected readonly int WaitInMS = 30 * 1000 * PlatformDetection.SlowRuntimeTimeoutModifier; + protected static readonly int WaitInMS = 30 * 1000 * PlatformDetection.SlowRuntimeTimeoutModifier; protected Process _process; protected readonly List _processes = new List(); diff --git a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj index e23bb4a94a71c..78a4b99602aa8 100644 --- a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj +++ b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj @@ -43,6 +43,7 @@ + + + @@ -64,6 +67,7 @@ + Content PreserveNewest diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.Windows.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.Windows.cs index 5f78df2e58c41..e612c16e78019 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.Windows.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.Windows.cs @@ -7,7 +7,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; -using System.ServiceProcess; using System.Threading.Tasks; using Xunit; using System.Threading; @@ -78,63 +77,17 @@ protected override string GetTestFilePath(int? index = null, [CallerMemberName] [PlatformSpecific(TestPlatforms.Windows)] // the test setup is Windows-specific [Collection(nameof(DisableParallelization))] // don't run in parallel, as file sharing logic is not thread-safe [OuterLoop("Requires admin privileges to create a file share")] - [ConditionalClass(typeof(UncFilePathFileStreamStandaloneConformanceTests), nameof(CanShareFiles))] + [ConditionalClass(typeof(WindowsTestFileShare), nameof(WindowsTestFileShare.CanShareFiles))] public class UncFilePathFileStreamStandaloneConformanceTests : UnbufferedAsyncFileStreamStandaloneConformanceTests { - public static bool CanShareFiles => _canShareFiles.Value; - - private static Lazy _canShareFiles = new Lazy(() => - { - if (!PlatformDetection.IsPrivilegedProcess) - { - return false; - } - - try - { - // the "Server Service" allows for file sharing. It can be disabled on some machines. - using (ServiceController sharingService = new ServiceController("Server")) - { - return sharingService.Status == ServiceControllerStatus.Running; - } - } - catch (InvalidOperationException) - { - // The service is not installed. - return false; - } - }); + private WindowsTestFileShare _testShare; protected override string GetTestFilePath(int? index = null, [CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0) { string testDirectoryPath = Path.GetFullPath(TestDirectory); string shareName = new DirectoryInfo(testDirectoryPath).Name; string fileName = GetTestFileName(index, memberName, lineNumber); - - SHARE_INFO_502 shareInfo = default; - shareInfo.shi502_netname = shareName; - shareInfo.shi502_path = testDirectoryPath; - shareInfo.shi502_remark = "folder created to test UNC file paths"; - shareInfo.shi502_max_uses = -1; - - int infoSize = Marshal.SizeOf(shareInfo); - IntPtr infoBuffer = Marshal.AllocCoTaskMem(infoSize); - - try - { - Marshal.StructureToPtr(shareInfo, infoBuffer, false); - - int shareResult = NetShareAdd(string.Empty, 502, infoBuffer, IntPtr.Zero); - - if (shareResult != 0 && shareResult != 2118) // is a failure that is not a NERR_DuplicateShare - { - throw new Exception($"Failed to create a file share, NetShareAdd returned {shareResult}"); - } - } - finally - { - Marshal.FreeCoTaskMem(infoBuffer); - } + _testShare = new WindowsTestFileShare(shareName, testDirectoryPath); // now once the folder has been shared we can use "localhost" to access it: // both type of slashes are valid, so let's test one for Debug and another for other configs @@ -147,42 +100,15 @@ protected override string GetTestFilePath(int? index = null, [CallerMemberName] protected override void Dispose(bool disposing) { - string testDirectoryPath = Path.GetFullPath(TestDirectory); - string shareName = new DirectoryInfo(testDirectoryPath).Name; - try { - NetShareDel(string.Empty, shareName, 0); + _testShare?.Dispose(); } finally { base.Dispose(disposing); } } - - [StructLayout(LayoutKind.Sequential)] - public struct SHARE_INFO_502 - { - [MarshalAs(UnmanagedType.LPWStr)] - public string shi502_netname; - public uint shi502_type; - [MarshalAs(UnmanagedType.LPWStr)] - public string shi502_remark; - public int shi502_permissions; - public int shi502_max_uses; - public int shi502_current_uses; - [MarshalAs(UnmanagedType.LPWStr)] - public string shi502_path; - public IntPtr shi502_passwd; - public int shi502_reserved; - public IntPtr shi502_security_descriptor; - } - - [DllImport(Interop.Libraries.Netapi32)] - public static extern int NetShareAdd([MarshalAs(UnmanagedType.LPWStr)]string servername, int level, IntPtr buf, IntPtr parm_err); - - [DllImport(Interop.Libraries.Netapi32)] - public static extern int NetShareDel([MarshalAs(UnmanagedType.LPWStr)] string servername, [MarshalAs(UnmanagedType.LPWStr)] string netname, int reserved); } [PlatformSpecific(TestPlatforms.Windows)] // the test setup is Windows-specifc diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index a6bd197f71c8f..0cc7805d4fd4b 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -235,6 +235,9 @@ + + +