diff --git a/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs b/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs index 893a0aaed9073c..95daea39e5fdf2 100644 --- a/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs +++ b/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs @@ -44,6 +44,7 @@ internal static partial class StartupInfoOptions internal const int CREATE_UNICODE_ENVIRONMENT = 0x00000400; internal const int CREATE_NO_WINDOW = 0x08000000; internal const int CREATE_NEW_PROCESS_GROUP = 0x00000200; + internal const int CREATE_SUSPENDED = 0x00000004; } } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SetConsoleCtrlHandler.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ConsoleCtrl.cs similarity index 78% rename from src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SetConsoleCtrlHandler.cs rename to src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ConsoleCtrl.cs index 63d2db5a2f0744..88c78c214f4b48 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SetConsoleCtrlHandler.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ConsoleCtrl.cs @@ -16,5 +16,9 @@ internal static partial class Kernel32 [LibraryImport(Libraries.Kernel32, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static unsafe partial bool SetConsoleCtrlHandler(delegate* unmanaged handler, [MarshalAs(UnmanagedType.Bool)] bool Add); + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool GenerateConsoleCtrlEvent(int dwCtrlEvent, int dwProcessGroupId); } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs index 57ae67529f36bb..4352f0aaa91f53 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs @@ -20,5 +20,13 @@ internal enum HandleFlags : uint [LibraryImport(Libraries.Kernel32, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool SetHandleInformation(SafeHandle hObject, HandleFlags dwMask, HandleFlags dwFlags); + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool GetHandleInformation(IntPtr hObject, out int lpdwFlags); + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetHandleInformation(IntPtr hObject, int dwMask, int dwFlags); } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleOptions.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleOptions.cs index 4a2b32f7c3c301..182ba49df5ca4c 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleOptions.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleOptions.cs @@ -10,6 +10,7 @@ internal static partial class HandleOptions internal const int DUPLICATE_SAME_ACCESS = 2; internal const int STILL_ACTIVE = 0x00000103; internal const int TOKEN_ADJUST_PRIVILEGES = 0x20; + internal const int HANDLE_FLAG_INHERIT = 0x00000001; } } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.JobObjects.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.JobObjects.cs new file mode 100644 index 00000000000000..d88ebb9707cf25 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.JobObjects.cs @@ -0,0 +1,66 @@ +// 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; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + internal static partial IntPtr CreateJobObjectW(IntPtr lpJobAttributes, IntPtr lpName); + + internal const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; + + internal enum JOBOBJECTINFOCLASS + { + JobObjectBasicLimitInformation = 2, + JobObjectExtendedLimitInformation = 9 + } + + [StructLayout(LayoutKind.Sequential)] + internal struct IO_COUNTERS + { + internal ulong ReadOperationCount; + internal ulong WriteOperationCount; + internal ulong OtherOperationCount; + internal ulong ReadTransferCount; + internal ulong WriteTransferCount; + internal ulong OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct JOBOBJECT_BASIC_LIMIT_INFORMATION + { + internal long PerProcessUserTimeLimit; + internal long PerJobUserTimeLimit; + internal uint LimitFlags; + internal UIntPtr MinimumWorkingSetSize; + internal UIntPtr MaximumWorkingSetSize; + internal uint ActiveProcessLimit; + internal UIntPtr Affinity; + internal uint PriorityClass; + internal uint SchedulingClass; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + internal JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + internal IO_COUNTERS IoInfo; + internal UIntPtr ProcessMemoryLimit; + internal UIntPtr JobMemoryLimit; + internal UIntPtr PeakProcessMemoryUsed; + internal UIntPtr PeakJobMemoryUsed; + } + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetInformationJobObject(IntPtr hJob, JOBOBJECTINFOCLASS JobObjectInfoClass, ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION lpJobObjectInfo, uint cbJobObjectInfoLength); + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool TerminateJobObject(IntPtr hJob, uint uExitCode); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ProcThreadAttributeList.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ProcThreadAttributeList.cs new file mode 100644 index 00000000000000..7887a5de1a64f1 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ProcThreadAttributeList.cs @@ -0,0 +1,64 @@ +// 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; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + internal const int PROC_THREAD_ATTRIBUTE_HANDLE_LIST = 0x00020002; + internal const int PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x0002000D; + internal const int EXTENDED_STARTUPINFO_PRESENT = 0x00080000; + + [StructLayout(LayoutKind.Sequential)] + internal struct STARTUPINFOEX + { + internal STARTUPINFO StartupInfo; + internal LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList; + } + + internal struct LPPROC_THREAD_ATTRIBUTE_LIST + { + internal IntPtr AttributeList; + } + + [LibraryImport(Libraries.Kernel32, EntryPoint = "CreateProcessW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static unsafe partial bool CreateProcess( + char* lpApplicationName, + char* lpCommandLine, + ref SECURITY_ATTRIBUTES procSecAttrs, + ref SECURITY_ATTRIBUTES threadSecAttrs, + [MarshalAs(UnmanagedType.Bool)] bool bInheritHandles, + int dwCreationFlags, + char* lpEnvironment, + string? lpCurrentDirectory, + ref STARTUPINFOEX lpStartupInfo, + ref PROCESS_INFORMATION lpProcessInformation + ); + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static unsafe partial bool InitializeProcThreadAttributeList( + LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, + int dwAttributeCount, + int dwFlags, + ref IntPtr lpSize); + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static unsafe partial bool UpdateProcThreadAttribute( + LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, + int dwFlags, + IntPtr attribute, + void* lpValue, + IntPtr cbSize, + void* lpPreviousValue, + IntPtr lpReturnSize); + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + internal static unsafe partial void DeleteProcThreadAttributeList(LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ResumeThread_IntPtr.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ResumeThread_IntPtr.cs new file mode 100644 index 00000000000000..0495f60a564f86 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ResumeThread_IntPtr.cs @@ -0,0 +1,14 @@ +// 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; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + internal static partial int ResumeThread(IntPtr hThread); + } +} 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 e05e8c2867db5f..e4d357db7b69ca 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -10,7 +10,21 @@ public sealed partial class SafeProcessHandle : Microsoft.Win32.SafeHandles.Safe { public SafeProcessHandle() : base (default(bool)) { } public SafeProcessHandle(System.IntPtr existingHandle, bool ownsHandle) : base (default(bool)) { } + public int ProcessId { get { throw null; } } + public bool Kill() { throw null; } + public bool KillProcessGroup() { throw null; } + public static Microsoft.Win32.SafeHandles.SafeProcessHandle Open(int processId) { throw null; } protected override bool ReleaseHandle() { throw null; } + public void Resume() { } + public void Signal(System.Runtime.InteropServices.PosixSignal signal) { } + public void SignalProcessGroup(System.Runtime.InteropServices.PosixSignal signal) { } + public static Microsoft.Win32.SafeHandles.SafeProcessHandle Start(System.Diagnostics.ProcessStartOptions options, Microsoft.Win32.SafeHandles.SafeFileHandle? input, Microsoft.Win32.SafeHandles.SafeFileHandle? output, Microsoft.Win32.SafeHandles.SafeFileHandle? error) { throw null; } + public static Microsoft.Win32.SafeHandles.SafeProcessHandle StartSuspended(System.Diagnostics.ProcessStartOptions options, Microsoft.Win32.SafeHandles.SafeFileHandle? input, Microsoft.Win32.SafeHandles.SafeFileHandle? output, Microsoft.Win32.SafeHandles.SafeFileHandle? error) { throw null; } + public bool TryWaitForExit(System.TimeSpan timeout, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Diagnostics.ProcessExitStatus? exitStatus) { throw null; } + public System.Diagnostics.ProcessExitStatus WaitForExit() { throw null; } + public System.Threading.Tasks.Task WaitForExitAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Threading.Tasks.Task WaitForExitOrKillOnCancellationAsync(System.Threading.CancellationToken cancellationToken) { throw null; } + public System.Diagnostics.ProcessExitStatus WaitForExitOrKillOnTimeout(System.TimeSpan timeout) { throw null; } } } namespace System.Diagnostics diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs index f3cac1f1af898b..1fe9f5434d25a5 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs @@ -1,17 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -/*============================================================ -** -** Class: SafeProcessHandle -** -** A wrapper for a process handle -** -** -===========================================================*/ - using System; +using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.Win32.SafeHandles { @@ -35,16 +32,34 @@ internal SafeProcessHandle(int processId, SafeWaitHandle handle) : handle.DangerousAddRef(ref _releaseRef); } - internal int ProcessId { get; } - protected override bool ReleaseHandle() { if (_releaseRef) { - Debug.Assert(_handle != null); + Debug.Assert(_handle is not null); _handle.DangerousRelease(); } return true; } + + private static SafeProcessHandle OpenCore(int processId) => throw new NotImplementedException(); + + private static SafeProcessHandle StartCore(ProcessStartOptions options, SafeFileHandle inputHandle, SafeFileHandle outputHandle, SafeFileHandle errorHandle, bool createSuspended) => throw new NotImplementedException(); + + private ProcessExitStatus WaitForExitCore() => throw new NotImplementedException(); + + private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out ProcessExitStatus? exitStatus) => throw new NotImplementedException(); + + private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds) => throw new NotImplementedException(); + + private Task WaitForExitAsyncCore(CancellationToken cancellationToken) => throw new NotImplementedException(); + + private Task WaitForExitOrKillOnCancellationAsyncCore(CancellationToken cancellationToken) => throw new NotImplementedException(); + + internal bool KillCore(bool throwOnError, bool entireProcessGroup = false) => throw new NotImplementedException(); + + private void ResumeCore() => throw new NotImplementedException(); + + private void SendSignalCore(PosixSignal signal, bool entireProcessGroup) => throw new NotImplementedException(); } } diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs index 1fc7a409713278..6ee7468682ef11 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs @@ -1,26 +1,582 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -/*============================================================ -** -** Class: SafeProcessHandle -** -** A wrapper for a process handle -** -** -===========================================================*/ - using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Security; +using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.Win32.SafeHandles { public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvalid { + // Static job object used for KillOnParentExit functionality + // All child processes with KillOnParentExit=true are assigned to this job + // Note: The job handle is intentionally never closed - it should live for the + // lifetime of the process. When this process exits, the job object is destroyed + // by the OS, which terminates all child processes in the job. + private static readonly Lazy s_killOnParentExitJob = new(CreateKillOnParentExitJob); + + // Thread handle for suspended processes (only used on Windows) + private IntPtr _threadHandle; + + // Job handle for CreateNewProcessGroup functionality (only used on Windows) + // This is specific to each process and is used to terminate the entire process group + private IntPtr _processGroupJobHandle; + + private SafeProcessHandle(IntPtr processHandle, IntPtr threadHandle, IntPtr processGroupJobHandle, int processId) + : base(ownsHandle: true) + { + SetHandle(processHandle); + _threadHandle = threadHandle; + _processGroupJobHandle = processGroupJobHandle; + ProcessId = processId; + } + + private static unsafe IntPtr CreateKillOnParentExitJob() + { + IntPtr jobHandle = Interop.Kernel32.CreateJobObjectW(IntPtr.Zero, IntPtr.Zero); + if (jobHandle == IntPtr.Zero) + { + throw new Win32Exception(); + } + + Interop.Kernel32.JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; + limitInfo.BasicLimitInformation.LimitFlags = Interop.Kernel32.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + if (!Interop.Kernel32.SetInformationJobObject( + jobHandle, + Interop.Kernel32.JOBOBJECTINFOCLASS.JobObjectExtendedLimitInformation, + ref limitInfo, + (uint)sizeof(Interop.Kernel32.JOBOBJECT_EXTENDED_LIMIT_INFORMATION))) + { + Interop.Kernel32.CloseHandle(jobHandle); + throw new Win32Exception(); + } + + return jobHandle; + } + protected override bool ReleaseHandle() { + if (_threadHandle != IntPtr.Zero) + { + Interop.Kernel32.CloseHandle(_threadHandle); + } + + if (_processGroupJobHandle != IntPtr.Zero) + { + Interop.Kernel32.CloseHandle(_processGroupJobHandle); + } + return Interop.Kernel32.CloseHandle(handle); } + + internal int GetExitCode() + { + if (!Interop.Kernel32.GetExitCodeProcess(this, out int exitCode)) + { + throw new Win32Exception(); + } + else if (exitCode == Interop.Kernel32.HandleOptions.STILL_ACTIVE) + { + throw new InvalidOperationException(); + } + + return exitCode; + } + + private static unsafe SafeProcessHandle StartCore(ProcessStartOptions options, SafeFileHandle inputHandle, SafeFileHandle outputHandle, SafeFileHandle errorHandle, bool createSuspended) + { + ProcessUtils.s_processStartLock.EnterReadLock(); + try + { + return StartCoreSerialized(options, inputHandle, outputHandle, errorHandle, createSuspended); + } + finally + { + ProcessUtils.s_processStartLock.ExitReadLock(); + } + } + + private static unsafe SafeProcessHandle StartCoreSerialized(ProcessStartOptions options, SafeFileHandle inputHandle, SafeFileHandle outputHandle, SafeFileHandle errorHandle, bool createSuspended) + { + Interop.Kernel32.STARTUPINFOEX startupInfoEx = default; + Interop.Kernel32.PROCESS_INFORMATION processInfo = default; + Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default; + SafeProcessHandle? procSH = null; + IntPtr currentProcHandle = Interop.Kernel32.GetCurrentProcess(); + void* attributeListBuffer = null; + Interop.Kernel32.LPPROC_THREAD_ATTRIBUTE_LIST attributeList = default; + + // In certain scenarios, the same handle may be passed for multiple stdio streams: + // - NUL file for all three + // - A single pipe/socket for both stdout and stderr (for combined output) + // - A single pipe/socket for stdin and stdout (for terminal emulation) + // - A single handle for all three streams + using SafeFileHandle duplicatedInput = Duplicate(inputHandle, currentProcHandle); + using SafeFileHandle duplicatedOutput = inputHandle.DangerousGetHandle() == outputHandle.DangerousGetHandle() + ? duplicatedInput + : Duplicate(outputHandle, currentProcHandle); + using SafeFileHandle duplicatedError = outputHandle.DangerousGetHandle() == errorHandle.DangerousGetHandle() + ? duplicatedOutput + : (inputHandle.DangerousGetHandle() == errorHandle.DangerousGetHandle() + ? duplicatedInput + : Duplicate(errorHandle, currentProcHandle)); + + int maxHandleCount = 3 + (options.HasInheritedHandlesBeenAccessed ? options.InheritedHandles.Count : 0); + + IntPtr* handlesToInherit = (IntPtr*)NativeMemory.Alloc((nuint)maxHandleCount, (nuint)sizeof(IntPtr)); + IntPtr processGroupJobHandle = IntPtr.Zero; + + try + { + int handleCount = 0; + + IntPtr inputPtr = duplicatedInput.DangerousGetHandle(); + IntPtr outputPtr = duplicatedOutput.DangerousGetHandle(); + IntPtr errorPtr = duplicatedError.DangerousGetHandle(); + + PrepareHandleAllowList(options, handlesToInherit, ref handleCount, inputPtr, outputPtr, errorPtr); + + if (options.CreateNewProcessGroup) + { + // This must happen before starting the process to ensure atomicity. + processGroupJobHandle = Interop.Kernel32.CreateJobObjectW(IntPtr.Zero, IntPtr.Zero); + if (processGroupJobHandle == IntPtr.Zero) + { + throw new Win32Exception(); + } + } + + int attributeCount = 1; // Always need handle list + if (options.KillOnParentExit || options.CreateNewProcessGroup) + attributeCount++; + + IntPtr size = IntPtr.Zero; + Interop.Kernel32.LPPROC_THREAD_ATTRIBUTE_LIST emptyList = default; + Interop.Kernel32.InitializeProcThreadAttributeList(emptyList, attributeCount, 0, ref size); + + attributeListBuffer = NativeMemory.Alloc((nuint)size); + attributeList.AttributeList = (IntPtr)attributeListBuffer; + + if (!Interop.Kernel32.InitializeProcThreadAttributeList(attributeList, attributeCount, 0, ref size)) + { + throw new Win32Exception(); + } + + if (!Interop.Kernel32.UpdateProcThreadAttribute( + attributeList, + 0, + (IntPtr)Interop.Kernel32.PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + handlesToInherit, + (IntPtr)(handleCount * sizeof(IntPtr)), + null, + IntPtr.Zero)) + { + throw new Win32Exception(); + } + + if (options.KillOnParentExit || options.CreateNewProcessGroup) + { + IntPtr* pJobHandle = stackalloc IntPtr[2]; + int jobsCount = 0; + + if (options.KillOnParentExit) + pJobHandle[jobsCount++] = s_killOnParentExitJob.Value; + if (options.CreateNewProcessGroup) + pJobHandle[jobsCount++] = processGroupJobHandle; + + if (!Interop.Kernel32.UpdateProcThreadAttribute( + attributeList, + 0, + Interop.Kernel32.PROC_THREAD_ATTRIBUTE_JOB_LIST, + pJobHandle, + jobsCount * sizeof(IntPtr), + null, + IntPtr.Zero)) + { + throw new Win32Exception(); + } + } + + startupInfoEx.lpAttributeList = attributeList; + startupInfoEx.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFOEX); + startupInfoEx.StartupInfo.hStdInput = inputPtr; + startupInfoEx.StartupInfo.hStdOutput = outputPtr; + startupInfoEx.StartupInfo.hStdError = errorPtr; + startupInfoEx.StartupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES; + + int creationFlags = Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT; + if (createSuspended) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_SUSPENDED; + if (options.CreateNewProcessGroup) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NEW_PROCESS_GROUP; + + string? environmentBlock = null; + if (options.HasEnvironmentBeenAccessed) + { + creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_UNICODE_ENVIRONMENT; + environmentBlock = ProcessUtils.GetEnvironmentVariablesBlock(options.Environment); + } + + string? workingDirectory = options.WorkingDirectory; + int errorCode = 0; + + ValueStringBuilder applicationName = new(stackalloc char[256]); + ValueStringBuilder commandLine = new(stackalloc char[256]); + try + { + ProcessUtils.BuildArgs(options.FileName, options.HasArgumentsBeenAccessed ? options.Arguments : null, ref applicationName, ref commandLine); + + fixed (char* environmentBlockPtr = environmentBlock) + fixed (char* applicationNamePtr = &applicationName.GetPinnableReference()) + fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) + { + bool retVal = Interop.Kernel32.CreateProcess( + applicationNamePtr, + commandLinePtr, + ref unused_SecAttrs, + ref unused_SecAttrs, + true, + creationFlags, + environmentBlockPtr, + workingDirectory, + ref startupInfoEx, + ref processInfo + ); + if (!retVal) + errorCode = Marshal.GetLastPInvokeError(); + } + } + finally + { + applicationName.Dispose(); + commandLine.Dispose(); + } + + if (errorCode != 0) + { + throw new Win32Exception(errorCode); + } + + procSH = new SafeProcessHandle( + processInfo.hProcess, + createSuspended ? processInfo.hThread : IntPtr.Zero, + processGroupJobHandle, + processInfo.dwProcessId); + + if (!createSuspended) + { + Interop.Kernel32.CloseHandle(processInfo.hThread); + } + } + finally + { + NativeMemory.Free(handlesToInherit); + + if (attributeListBuffer != null) + { + Interop.Kernel32.DeleteProcThreadAttributeList(attributeList); + NativeMemory.Free(attributeListBuffer); + } + + if (procSH is null) + { + if (processInfo.hProcess != IntPtr.Zero) + { + Interop.Kernel32.CloseHandle(processInfo.hProcess); + } + + if (processInfo.hThread != IntPtr.Zero) + { + Interop.Kernel32.CloseHandle(processInfo.hThread); + } + + if (processGroupJobHandle != IntPtr.Zero) + { + Interop.Kernel32.CloseHandle(processGroupJobHandle); + } + } + } + + return procSH; + + static SafeFileHandle Duplicate(SafeFileHandle sourceHandle, nint currentProcHandle) + { + // From https://learn.microsoft.com/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute: + // PROC_THREAD_ATTRIBUTE_HANDLE_LIST: "These handles must be created as inheritable handles and must not include pseudo handles". + // To ensure the handles we pass are inheritable, they are duplicated here. + + if (!Interop.Kernel32.DuplicateHandle( + currentProcHandle, + sourceHandle, + currentProcHandle, + out SafeFileHandle duplicated, + 0, + true, + Interop.Kernel32.HandleOptions.DUPLICATE_SAME_ACCESS)) + { + throw new Win32Exception(); + } + + return duplicated; + } + } + + private static unsafe void PrepareHandleAllowList(ProcessStartOptions options, IntPtr* handlesToInherit, ref int handleCount, IntPtr inputPtr, IntPtr outputPtr, IntPtr errorPtr) + { + handlesToInherit[handleCount++] = inputPtr; + if (outputPtr != inputPtr) + handlesToInherit[handleCount++] = outputPtr; + if (errorPtr != inputPtr && errorPtr != outputPtr) + handlesToInherit[handleCount++] = errorPtr; + + if (options.HasInheritedHandlesBeenAccessed) + { + foreach (SafeHandle handle in options.InheritedHandles) + { + IntPtr handlePtr = handle.DangerousGetHandle(); + + bool isDuplicate = false; + for (int i = 0; i < handleCount; i++) + { + if (handlesToInherit[i] == handlePtr) + { + isDuplicate = true; + break; + } + } + + if (!isDuplicate) + { + if (!Interop.Kernel32.GetHandleInformation(handlePtr, out int flags)) + { + throw new Win32Exception(); + } + + if ((flags & Interop.Kernel32.HandleOptions.HANDLE_FLAG_INHERIT) == 0) + { + if (!Interop.Kernel32.SetHandleInformation( + handlePtr, + Interop.Kernel32.HandleOptions.HANDLE_FLAG_INHERIT, + Interop.Kernel32.HandleOptions.HANDLE_FLAG_INHERIT)) + { + throw new Win32Exception(); + } + } + + handlesToInherit[handleCount++] = handlePtr; + } + } + } + } + + private ProcessExitStatus WaitForExitCore() + { + using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this); + processWaitHandle.WaitOne(Timeout.Infinite); + + return new ProcessExitStatus(GetExitCode(), false); + } + + private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out ProcessExitStatus? exitStatus) + { + using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this); + if (!processWaitHandle.WaitOne(milliseconds)) + { + exitStatus = null; + return false; + } + + exitStatus = new ProcessExitStatus(GetExitCode(), false); + return true; + } + + private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds) + { + bool wasKilledOnTimeout = false; + using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this); + if (!processWaitHandle.WaitOne(milliseconds)) + { + wasKilledOnTimeout = KillCore(throwOnError: false); + processWaitHandle.WaitOne(Timeout.Infinite); + } + + return new ProcessExitStatus(GetExitCode(), wasKilledOnTimeout); + } + + private async Task WaitForExitAsyncCore(CancellationToken cancellationToken) + { + using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this); + + TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + RegisteredWaitHandle? registeredWaitHandle = null; + CancellationTokenRegistration ctr = default; + + try + { + registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject( + processWaitHandle, + (state, timedOut) => ((TaskCompletionSource)state!).TrySetResult(true), + tcs, + Timeout.Infinite, + executeOnlyOnce: true); + + if (cancellationToken.CanBeCanceled) + { + ctr = cancellationToken.Register( + static state => + { + var taskSource = (TaskCompletionSource)state!; + taskSource.TrySetCanceled(); + }, + tcs); + } + + await tcs.Task.ConfigureAwait(false); + } + finally + { + ctr.Dispose(); + registeredWaitHandle?.Unregister(null); + } + + return new ProcessExitStatus(GetExitCode(), false); + } + + private async Task WaitForExitOrKillOnCancellationAsyncCore(CancellationToken cancellationToken) + { + using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this); + + TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + RegisteredWaitHandle? registeredWaitHandle = null; + CancellationTokenRegistration ctr = default; + StrongBox wasKilledBox = new(false); + + try + { + registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject( + processWaitHandle, + (state, timedOut) => ((TaskCompletionSource)state!).TrySetResult(true), + tcs, + Timeout.Infinite, + executeOnlyOnce: true); + + if (cancellationToken.CanBeCanceled) + { + ctr = cancellationToken.Register( + static state => + { + var (handle, taskSource, wasCancelled) = ((SafeProcessHandle, TaskCompletionSource, StrongBox))state!; + wasCancelled.Value = handle.KillCore(throwOnError: false); + taskSource.TrySetResult(true); + }, + (this, tcs, wasKilledBox)); + } + + await tcs.Task.ConfigureAwait(false); + } + finally + { + ctr.Dispose(); + registeredWaitHandle?.Unregister(null); + } + + return new ProcessExitStatus(GetExitCode(), wasKilledBox.Value); + } + + internal bool KillCore(bool throwOnError, bool entireProcessGroup = false) + { + if (entireProcessGroup && _processGroupJobHandle == IntPtr.Zero) + { + throw new InvalidOperationException(SR.KillProcessGroupWithoutNewProcessGroup); + } + + if (entireProcessGroup + ? Interop.Kernel32.TerminateJobObject(_processGroupJobHandle, unchecked((uint)-1)) + : Interop.Kernel32.TerminateProcess(this, exitCode: -1)) + { + return true; + } + + int error = Marshal.GetLastPInvokeError(); + return error switch + { + Interop.Errors.ERROR_SUCCESS => true, + Interop.Errors.ERROR_ACCESS_DENIED => false, + _ when !throwOnError => false, + _ => throw new Win32Exception(error), + }; + } + + private void ResumeCore() + { + IntPtr threadHandle = Interlocked.Exchange(ref _threadHandle, IntPtr.Zero); + if (threadHandle == IntPtr.Zero) + { + throw new InvalidOperationException(SR.CannotResumeNonSuspendedProcess); + } + + try + { + int result = Interop.Kernel32.ResumeThread(threadHandle); + if (result == -1) + { + throw new Win32Exception(); + } + } + finally + { + Interop.Kernel32.CloseHandle(threadHandle); + } + } + + private void SendSignalCore(PosixSignal signal, bool entireProcessGroup) + { + if (signal == PosixSignal.SIGKILL) + { + KillCore(throwOnError: true, entireProcessGroup); + return; + } + + int ctrlEvent = signal switch + { + PosixSignal.SIGINT => Interop.Kernel32.CTRL_C_EVENT, + PosixSignal.SIGQUIT => Interop.Kernel32.CTRL_BREAK_EVENT, + _ => throw new ArgumentException(SR.Format(SR.SignalNotSupportedOnWindows, signal), nameof(signal)) + }; + + if (!Interop.Kernel32.GenerateConsoleCtrlEvent(ctrlEvent, ProcessId)) + { + throw new Win32Exception(); + } + } + + private static SafeProcessHandle OpenCore(int processId) + { + const int desiredAccess = Interop.Advapi32.ProcessOptions.PROCESS_QUERY_LIMITED_INFORMATION + | Interop.Advapi32.ProcessOptions.SYNCHRONIZE + | Interop.Advapi32.ProcessOptions.PROCESS_TERMINATE; + + SafeProcessHandle safeHandle = Interop.Kernel32.OpenProcess(desiredAccess, inherit: false, processId); + + if (safeHandle.IsInvalid) + { + int error = Marshal.GetLastPInvokeError(); + safeHandle.Dispose(); + throw new Win32Exception(error); + } + + // Transfer ownership: take the handle from the returned SafeProcessHandle and + // create a new one with the ProcessId set properly. + IntPtr rawHandle = safeHandle.DangerousGetHandle(); + safeHandle.SetHandleAsInvalid(); // Prevent the original from closing it + return new SafeProcessHandle(rawHandle, IntPtr.Zero, IntPtr.Zero, processId); + } } } diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs index c7d52e23e0b0ce..7308a8f5b87eec 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs @@ -1,26 +1,31 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -/*============================================================ -** -** Class: SafeProcessHandle -** -** A wrapper for a process handle -** -** -===========================================================*/ - using System; +using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.Win32.SafeHandles { + /// + /// A wrapper for a process handle. + /// public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvalid { internal static readonly SafeProcessHandle InvalidHandle = new SafeProcessHandle(); /// - /// Creates a . + /// Gets the process ID. + /// + public int ProcessId { get; private set; } + + /// + /// Creates a . /// public SafeProcessHandle() : this(IntPtr.Zero) @@ -42,5 +47,278 @@ public SafeProcessHandle(IntPtr existingHandle, bool ownsHandle) { SetHandle(existingHandle); } + + /// + /// Opens an existing child process by its process ID. + /// + /// The process ID of the process to open. + /// A that represents the opened process. + /// Thrown when is negative or zero. + /// Thrown when the process could not be opened. + /// + /// On Windows, this method uses OpenProcess with PROCESS_QUERY_LIMITED_INFORMATION, SYNCHRONIZE, and PROCESS_TERMINATE permissions. + /// On Linux with pidfd support, this method uses the pidfd_open syscall. + /// On other Unix systems, this method uses kill(pid, 0) to verify the process exists and the caller has permission to signal it. + /// + public static SafeProcessHandle Open(int processId) + { + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(processId, 0); + + return OpenCore(processId); + } + + /// + /// Starts a new process. + /// + /// The process start options. + /// The handle to use for standard input, or to provide no input. + /// The handle to use for standard output, or to discard output. + /// The handle to use for standard error, or to discard error. + /// A handle to the started process. + /// Thrown when is null. + public static SafeProcessHandle Start(ProcessStartOptions options, SafeFileHandle? input, SafeFileHandle? output, SafeFileHandle? error) + { + return StartInternal(options, input, output, error, createSuspended: false); + } + + /// + /// Starts a new process in a suspended state. + /// + /// Process start options. + /// Standard input handle. + /// Standard output handle. + /// Standard error handle. + /// A handle to the suspended process. Call to start execution. + /// Thrown when is null. + public static SafeProcessHandle StartSuspended(ProcessStartOptions options, SafeFileHandle? input, SafeFileHandle? output, SafeFileHandle? error) + { + return StartInternal(options, input, output, error, createSuspended: true); + } + + private static SafeProcessHandle StartInternal(ProcessStartOptions options, SafeFileHandle? input, SafeFileHandle? output, SafeFileHandle? error, bool createSuspended) + { + ArgumentNullException.ThrowIfNull(options); + + SafeFileHandle? nullHandle = null; + + if (input is null || output is null || error is null) + { + nullHandle = File.OpenNullHandle(); + + input ??= nullHandle; + output ??= nullHandle; + error ??= nullHandle; + } + + try + { + return StartCore(options, input, output, error, createSuspended); + } + finally + { + nullHandle?.Dispose(); + } + } + + /// + /// Waits for the process to exit without a timeout. + /// + /// The exit status of the process. + /// Thrown when the handle is invalid. + public ProcessExitStatus WaitForExit() + { + Validate(); + + return WaitForExitCore(); + } + + /// + /// Waits for the process to exit within the specified timeout. + /// + /// The maximum time to wait for the process to exit. + /// When this method returns true, contains the exit status of the process. + /// true if the process exited before the timeout; otherwise, false. + /// Thrown when the handle is invalid. + public bool TryWaitForExit(TimeSpan timeout, [NotNullWhen(true)] out ProcessExitStatus? exitStatus) + { + Validate(); + + return TryWaitForExitCore(GetTimeoutInMilliseconds(timeout), out exitStatus); + } + + /// + /// Waits for the process to exit within the specified timeout. + /// If the process does not exit before the timeout, it is killed and then waited for exit. + /// + /// The maximum time to wait for the process to exit before killing it. + /// The exit status of the process. If the process was killed due to timeout, Canceled will be true. + /// Thrown when the handle is invalid. + public ProcessExitStatus WaitForExitOrKillOnTimeout(TimeSpan timeout) + { + Validate(); + + return WaitForExitOrKillOnTimeoutCore(GetTimeoutInMilliseconds(timeout)); + } + + /// + /// Waits asynchronously for the process to exit and reports the exit status. + /// + /// A cancellation token that can be used to cancel the wait operation. + /// A task that represents the asynchronous wait operation. The task result contains the exit status of the process. + /// Thrown when the handle is invalid. + /// Thrown when the cancellation token is canceled. + /// + /// When the cancellation token is canceled, this method stops waiting and throws . + /// The process is NOT killed and continues running. If you want to kill the process on cancellation, + /// use instead. + /// + public Task WaitForExitAsync(CancellationToken cancellationToken = default) + { + Validate(); + + return WaitForExitAsyncCore(cancellationToken); + } + + /// + /// Waits asynchronously for the process to exit and reports the exit status. + /// When cancelled, kills the process and then waits for exit without timeout. + /// + /// A cancellation token that can be used to cancel the wait operation and kill the process. + /// A task that represents the asynchronous wait operation. The task result contains the exit status of the process. + /// If the process was killed due to cancellation, the Canceled property will be true. + /// Thrown when the handle is invalid. + /// + /// When the cancellation token is canceled, this method kills the process and waits for it to exit. + /// The returned exit status will have the property set to true if the process was killed. + /// If the cancellation token cannot be canceled (e.g., ), this method behaves identically + /// to and will wait indefinitely for the process to exit. + /// + public Task WaitForExitOrKillOnCancellationAsync(CancellationToken cancellationToken) + { + Validate(); + + return WaitForExitOrKillOnCancellationAsyncCore(cancellationToken); + } + + /// + /// Terminates the process. + /// + /// + /// true if the process was terminated; false if the process had already exited. + /// + /// Thrown when the handle is invalid. + /// Thrown when the kill operation fails for reasons other than the process having already exited. + public bool Kill() + { + Validate(); + + return KillCore(throwOnError: true, entireProcessGroup: false); + } + + /// + /// Terminates the entire process group. + /// + /// + /// true if the process group was terminated; false if the process had already exited. + /// + /// Thrown when the handle is invalid. + /// Thrown when the kill operation fails for reasons other than the process having already exited. + /// + /// On Unix, sends SIGKILL to all processes in the process group. + /// On Windows, requires the process to have been started with =true. + /// Terminates all processes in the job object. If the process was not started with =true, + /// throws an . + /// + public bool KillProcessGroup() + { + Validate(); + + return KillCore(throwOnError: true, entireProcessGroup: true); + } + + /// + /// Resumes a process that was created via . + /// + /// Thrown when the process was not started in a suspended state, or has already been resumed. + /// Thrown when the resume operation fails. + /// + /// This method can only be called once. After the process has been resumed, calling this method again will throw an . + /// This is not a general purpose process resume (like NtResumeProcess). It can only resume processes created via . + /// + public void Resume() + { + Validate(); + + ResumeCore(); + } + + /// + /// Sends a signal to the process. + /// + /// The signal to send. + /// Thrown when the handle is invalid. + /// Thrown when the signal value is not supported. + /// Thrown when the signal is not supported on the current platform. + /// Thrown when the signal operation fails. + /// + /// On Windows, only SIGINT (mapped to CTRL_C_EVENT), SIGQUIT (mapped to CTRL_BREAK_EVENT), and SIGKILL are supported. + /// The process must have been started with set to true for signals to work properly. + /// On Windows, signals are always sent to the entire process group, not just the single process. + /// On Unix/Linux, all signals defined in PosixSignal are supported, and the signal is sent only to the specific process. + /// + public void Signal(PosixSignal signal) + { + if (!Enum.IsDefined(signal)) + { + throw new ArgumentOutOfRangeException(nameof(signal)); + } + + Validate(); + + SendSignalCore(signal, entireProcessGroup: false); + } + + /// + /// Sends a signal to the entire process group. + /// + /// The signal to send. + /// Thrown when the handle is invalid. + /// Thrown when the signal value is not supported. + /// Thrown when the signal operation fails. + /// + /// On Windows, only SIGINT (mapped to CTRL_C_EVENT), SIGQUIT (mapped to CTRL_BREAK_EVENT), and SIGKILL are supported. + /// The process must have been started with set to true for signals to work properly. + /// On Windows, signals are always sent to the entire process group. + /// On Unix/Linux, all signals defined in PosixSignal are supported, and the signal is sent to all processes in the process group. + /// + public void SignalProcessGroup(PosixSignal signal) + { + if (!Enum.IsDefined(signal)) + { + throw new ArgumentOutOfRangeException(nameof(signal)); + } + + Validate(); + + SendSignalCore(signal, entireProcessGroup: true); + } + + private void Validate() + { + if (IsInvalid) + { + throw new InvalidOperationException(SR.InvalidProcessHandle); + } + } + + internal static int GetTimeoutInMilliseconds(TimeSpan timeout) + { + long totalMilliseconds = (long)timeout.TotalMilliseconds; + + ArgumentOutOfRangeException.ThrowIfLessThan(totalMilliseconds, -1, nameof(timeout)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(totalMilliseconds, int.MaxValue, nameof(timeout)); + + return (int)totalMilliseconds; + } } } diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index 67d05840e9796b..413f8f8e5f133c 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -339,4 +339,16 @@ Could not resolve the file. + + Invalid process handle. + + + Cannot terminate entire process group because the process was not started with CreateNewProcessGroup=true. + + + Cannot resume a process that was not started with StartSuspended. + + + Signal {0} is not supported on Windows. Only SIGINT, SIGQUIT, and SIGKILL are supported. + \ No newline at end of file diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index 0342c73e632f25..1ad9beb82fb3f6 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -224,6 +224,16 @@ + + + + + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs index f6746fad4efd4d..da7c8948cd0432 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs @@ -17,7 +17,7 @@ internal static void ConfigureTerminalForChildProcesses(int increment, bool conf int childrenUsingTerminalRemaining = Interlocked.Add(ref s_childrenUsingTerminalCount, increment); if (increment > 0) { - Debug.Assert(s_processStartLock.IsReadLockHeld); + Debug.Assert(ProcessUtils.s_processStartLock.IsReadLockHeld); Debug.Assert(configureConsole); // At least one child is using the terminal. @@ -25,7 +25,7 @@ internal static void ConfigureTerminalForChildProcesses(int increment, bool conf } else { - Debug.Assert(s_processStartLock.IsWriteLockHeld); + Debug.Assert(ProcessUtils.s_processStartLock.IsWriteLockHeld); if (childrenUsingTerminalRemaining == 0 && configureConsole) { @@ -44,7 +44,7 @@ private static unsafe void SetDelayedSigChildConsoleConfigurationHandler() private static void DelayedSigChildConsoleConfiguration() { // Lock to avoid races with Process.Start - s_processStartLock.EnterWriteLock(); + ProcessUtils.s_processStartLock.EnterWriteLock(); try { if (s_childrenUsingTerminalCount == 0) @@ -55,7 +55,7 @@ private static void DelayedSigChildConsoleConfiguration() } finally { - s_processStartLock.ExitWriteLock(); + ProcessUtils.s_processStartLock.ExitWriteLock(); } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs index f26a8f70ffba74..943d88b53f51c3 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs @@ -18,7 +18,6 @@ public partial class Process : IDisposable { private static volatile bool s_initialized; private static readonly object s_initializedGate = new object(); - private static readonly ReaderWriterLockSlim s_processStartLock = new ReaderWriterLockSlim(); /// /// Puts a Process component in state to interact with operating system processes that run in a @@ -501,7 +500,7 @@ private bool ForkAndExecProcess( // Lock to avoid races with OnSigChild // By using a ReaderWriterLock we allow multiple processes to start concurrently. - s_processStartLock.EnterReadLock(); + ProcessUtils.s_processStartLock.EnterReadLock(); try { if (usesTerminal) @@ -548,14 +547,14 @@ private bool ForkAndExecProcess( } finally { - s_processStartLock.ExitReadLock(); + ProcessUtils.s_processStartLock.ExitReadLock(); if (_waitStateHolder == null && usesTerminal) { // We failed to launch a child that could use the terminal. - s_processStartLock.EnterWriteLock(); + ProcessUtils.s_processStartLock.EnterWriteLock(); ConfigureTerminalForChildProcesses(-1); - s_processStartLock.ExitWriteLock(); + ProcessUtils.s_processStartLock.ExitWriteLock(); } } } @@ -1017,7 +1016,7 @@ private static int OnSigChild(int reapAll, int configureConsole) // DelayedSigChildConsoleConfiguration will be called. // Lock to avoid races with Process.Start - s_processStartLock.EnterWriteLock(); + ProcessUtils.s_processStartLock.EnterWriteLock(); try { bool childrenUsingTerminalPre = AreChildrenUsingTerminal; @@ -1029,7 +1028,7 @@ private static int OnSigChild(int reapAll, int configureConsole) } finally { - s_processStartLock.ExitWriteLock(); + ProcessUtils.s_processStartLock.ExitWriteLock(); } } 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 cebd7469d43667..f63d3e6d30e790 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 @@ -16,8 +16,6 @@ namespace System.Diagnostics { public partial class Process : IDisposable { - private static readonly object s_createProcessLock = new object(); - private string? _processName; /// @@ -453,7 +451,8 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) // calls. We do not want one process to inherit the handles created concurrently for another // process, as that will impact the ownership and lifetimes of those handles now inherited // into multiple child processes. - lock (s_createProcessLock) + ProcessUtils.s_processStartLock.EnterWriteLock(); + try { try { @@ -512,7 +511,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) if (startInfo._environmentVariables != null) { creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_UNICODE_ENVIRONMENT; - environmentBlock = GetEnvironmentVariablesBlock(startInfo._environmentVariables!); + environmentBlock = ProcessUtils.GetEnvironmentVariablesBlock(startInfo._environmentVariables!); } string? workingDirectory = startInfo.WorkingDirectory; @@ -630,6 +629,10 @@ ref processInfo // pointer to PROCESS_INFORMATION childErrorPipeHandle?.Dispose(); } } + finally + { + ProcessUtils.s_processStartLock.ExitWriteLock(); + } if (startInfo.RedirectStandardInput) { @@ -861,33 +864,6 @@ private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHand } } - private static string GetEnvironmentVariablesBlock(DictionaryWrapper sd) - { - // https://learn.microsoft.com/windows/win32/procthread/changing-environment-variables - // "All strings in the environment block must be sorted alphabetically by name. The sort is - // case-insensitive, Unicode order, without regard to locale. Because the equal sign is a - // separator, it must not be used in the name of an environment variable." - - var keys = new string[sd.Count]; - sd.Keys.CopyTo(keys, 0); - Array.Sort(keys, StringComparer.OrdinalIgnoreCase); - - // Join the null-terminated "key=val\0" strings - var result = new StringBuilder(8 * keys.Length); - foreach (string key in keys) - { - string? value = sd[key]; - - // Ignore null values for consistency with Environment.SetEnvironmentVariable - if (value != null) - { - result.Append(key).Append('=').Append(value).Append('\0'); - } - } - - return result.ToString(); - } - private static string GetErrorMessage(int error) => Interop.Kernel32.GetMessage(error); /// Gets the friendly name of the process. diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartOptions.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartOptions.cs index acac5b0c546375..6b58234b5132f8 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartOptions.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartOptions.cs @@ -134,6 +134,12 @@ public IList InheritedHandles /// public bool CreateNewProcessGroup { get; set; } + internal bool HasEnvironmentBeenAccessed => _environment is not null; + + internal bool HasInheritedHandlesBeenAccessed => _inheritedHandles is not null; + + internal bool HasArgumentsBeenAccessed => _arguments is not null; + /// /// Initializes a new instance of the class. /// diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs index 6208deff5d094d..a28f4296ec6854 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs @@ -1,12 +1,61 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.IO; +using System.Text; namespace System.Diagnostics { internal static partial class ProcessUtils { + internal static void BuildArgs(string resolvedFilePath, IList? arguments, ref ValueStringBuilder applicationName, ref ValueStringBuilder commandLine) + { + applicationName.Append(resolvedFilePath); + applicationName.NullTerminate(); + + // From: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw + // "Because argv[0] is the module name, C programmers generally repeat the module name as the first token in the command line." + // The truth is that some programs REQUIRE it (example: findstr). That is why we repeat it. + PasteArguments.AppendArgument(ref commandLine, resolvedFilePath); + + if (arguments is not null) + { + foreach (string argument in arguments) + { + PasteArguments.AppendArgument(ref commandLine, argument); + } + } + commandLine.NullTerminate(); + } + + internal static string GetEnvironmentVariablesBlock(IDictionary sd) + { + // https://learn.microsoft.com/windows/win32/procthread/changing-environment-variables + // "All strings in the environment block must be sorted alphabetically by name. The sort is + // case-insensitive, Unicode order, without regard to locale. Because the equal sign is a + // separator, it must not be used in the name of an environment variable." + + var keys = new string[sd.Count]; + sd.Keys.CopyTo(keys, 0); + Array.Sort(keys, StringComparer.OrdinalIgnoreCase); + + // Join the null-terminated "key=val\0" strings + var result = new StringBuilder(8 * keys.Length); + foreach (string key in keys) + { + string? value = sd[key]; + + // Ignore null values for consistency with Environment.SetEnvironmentVariable + if (value != null) + { + result.Append(key).Append('=').Append(value).Append('\0'); + } + } + + return result.ToString(); + } + private static bool IsExecutable(string fullPath) { return File.Exists(fullPath); diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs index c3ead1de0030bf..9753f0b7d6370e 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs @@ -2,11 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO; +using System.Threading; namespace System.Diagnostics { internal static partial class ProcessUtils { + internal static readonly ReaderWriterLockSlim s_processStartLock = new ReaderWriterLockSlim(); + internal static string? FindProgramInPath(string program) { string? pathEnvVar = System.Environment.GetEnvironmentVariable("PATH"); diff --git a/src/libraries/System.Diagnostics.Process/tests/Interop.cs b/src/libraries/System.Diagnostics.Process/tests/Interop.cs index 6bd2ddebbc8f6a..ce3f72ce74a693 100644 --- a/src/libraries/System.Diagnostics.Process/tests/Interop.cs +++ b/src/libraries/System.Diagnostics.Process/tests/Interop.cs @@ -73,10 +73,6 @@ public struct SID_AND_ATTRIBUTES } - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId); - [DllImport("kernel32.dll")] public static extern bool GetProcessWorkingSetSizeEx(SafeProcessHandle hProcess, out IntPtr lpMinimumWorkingSetSize, out IntPtr lpMaximumWorkingSetSize, out uint flags); diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs index 11992f29453dbe..76173add1f305a 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs @@ -31,7 +31,7 @@ private static void SendSignal(PosixSignal signal, int processId) _ => throw new ArgumentOutOfRangeException(nameof(signal)) }; - if (!Interop.GenerateConsoleCtrlEvent(dwCtrlEvent, (uint)processId)) + if (!Interop.Kernel32.GenerateConsoleCtrlEvent((int)dwCtrlEvent, processId)) { int error = Marshal.GetLastWin32Error(); if (error == Interop.Errors.ERROR_INVALID_FUNCTION && PlatformDetection.IsInContainer) diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Windows.cs new file mode 100644 index 00000000000000..37922549fc5e2b --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Windows.cs @@ -0,0 +1,168 @@ +// 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.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.Diagnostics.Tests +{ + public partial class SafeProcessHandleTests + { + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] + public void SendSignal_SIGINT_TerminatesProcessInNewProcessGroup() + { + if (Console.IsInputRedirected) + { + return; + } + + ProcessStartOptions options = new("timeout") + { + Arguments = { "/t", "3", "/nobreak" }, + CreateNewProcessGroup = true + }; + + using SafeFileHandle stdin = Console.OpenStandardInputHandle(); + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: stdin, output: null, error: null); + + bool hasExited = processHandle.TryWaitForExit(TimeSpan.Zero, out _); + Assert.False(hasExited, "Process should still be running before signal is sent"); + + processHandle.Signal(PosixSignal.SIGINT); + + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(3000)); + + Assert.NotEqual(0, exitStatus.ExitCode); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] + public void Signal_SIGQUIT_TerminatesProcessInNewProcessGroup() + { + if (Console.IsInputRedirected) + { + return; + } + + ProcessStartOptions options = new("timeout") + { + Arguments = { "/t", "3", "/nobreak" }, + CreateNewProcessGroup = true + }; + + using SafeFileHandle stdin = Console.OpenStandardInputHandle(); + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: stdin, output: null, error: null); + + bool hasExited = processHandle.TryWaitForExit(TimeSpan.Zero, out _); + Assert.False(hasExited, "Process should still be running before signal is sent"); + + processHandle.Signal(PosixSignal.SIGQUIT); + + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(3000)); + + Assert.NotEqual(0, exitStatus.ExitCode); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] + public void Signal_UnsupportedSignal_ThrowsArgumentException() + { + if (Console.IsInputRedirected) + { + return; + } + + ProcessStartOptions options = new("timeout") + { + Arguments = { "/t", "3", "/nobreak" }, + CreateNewProcessGroup = true + }; + + using SafeFileHandle stdin = Console.OpenStandardInputHandle(); + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: stdin, output: null, error: null); + + try + { + Assert.Throws(() => processHandle.Signal(PosixSignal.SIGTERM)); + } + finally + { + processHandle.Kill(); + processHandle.WaitForExit(); + } + } + + [Fact] + public void CreateNewProcessGroup_CanBeSetToTrue() + { + ProcessStartOptions options = new("cmd.exe") + { + Arguments = { "/c", "echo test" }, + CreateNewProcessGroup = true + }; + + Assert.True(options.CreateNewProcessGroup); + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); + Assert.Equal(0, exitStatus.ExitCode); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] + public void Kill_EntireProcessGroup_WithoutCreateNewProcessGroup_Throws() + { + if (Console.IsInputRedirected) + { + return; + } + + ProcessStartOptions options = new("timeout") + { + Arguments = { "/t", "3", "/nobreak" }, + CreateNewProcessGroup = false + }; + + using SafeFileHandle stdin = Console.OpenStandardInputHandle(); + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: stdin, output: null, error: null); + + Assert.Throws(() => processHandle.KillProcessGroup()); + + Assert.True(processHandle.Kill()); + } + + [Fact] + public void StartSuspended_ResumeCompletes() + { + ProcessStartOptions options = new("cmd.exe") + { + Arguments = { "/c", "echo test" } + }; + + using SafeProcessHandle processHandle = SafeProcessHandle.StartSuspended(options, input: null, output: null, error: null); + + bool hasExited = processHandle.TryWaitForExit(TimeSpan.FromMilliseconds(200), out _); + Assert.False(hasExited, "Suspended process should not have exited yet"); + + processHandle.Resume(); + + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); + Assert.Equal(0, exitStatus.ExitCode); + } + + [Fact] + public void Resume_OnNonSuspendedProcess_ThrowsInvalidOperationException() + { + ProcessStartOptions options = new("cmd.exe") + { + Arguments = { "/c", "echo test" } + }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + Assert.Throws(() => processHandle.Resume()); + + processHandle.WaitForExit(); + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs new file mode 100644 index 00000000000000..083671a2b091ba --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs @@ -0,0 +1,328 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.Diagnostics.Tests +{ + [PlatformSpecific(TestPlatforms.Windows)] + public partial class SafeProcessHandleTests + { + private static ProcessStartOptions CreateTenSecondSleep() => new("powershell") { Arguments = { "-InputFormat", "None", "-Command", "Start-Sleep 10" } }; + + [Fact] + public static void Start_WithNoArguments_Succeeds() + { + ProcessStartOptions options = new("hostname"); + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + ProcessExitStatus exitStatus = processHandle.WaitForExit(); + Assert.Equal(0, exitStatus.ExitCode); + Assert.False(exitStatus.Canceled); + Assert.Null(exitStatus.Signal); + } + + [Fact] + public static void GetProcessId_ReturnsValidPid() + { + ProcessStartOptions info = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(info, input: null, output: null, error: null); + int pid = processHandle.ProcessId; + + Assert.NotEqual(0, pid); + Assert.NotEqual(-1, pid); + Assert.True(pid > 0, "Process ID should be a positive integer"); + + nint handleValue = processHandle.DangerousGetHandle(); + Assert.NotEqual(handleValue, (nint)pid); + + ProcessExitStatus exitStatus = processHandle.WaitForExit(); + Assert.Equal(0, exitStatus.ExitCode); + Assert.Null(exitStatus.Signal); + Assert.False(exitStatus.Canceled); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] + public static void Kill_KillsRunningProcess() + { + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); + + bool wasKilled = processHandle.Kill(); + Assert.True(wasKilled); + + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); + Assert.False(exitStatus.Canceled); + Assert.Equal(-1, exitStatus.ExitCode); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] + public static void Kill_CanBeCalledMultipleTimes() + { + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); + + bool firstKill = processHandle.Kill(); + Assert.True(firstKill); + + _ = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); + + bool secondKill = processHandle.Kill(); + Assert.False(secondKill); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] + public static void WaitForExit_Called_After_Kill_ReturnsExitCodeImmediately() + { + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); + + bool wasKilled = processHandle.Kill(); + Assert.True(wasKilled); + + Stopwatch stopwatch = Stopwatch.StartNew(); + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(3)); + + Assert.InRange(stopwatch.Elapsed, TimeSpan.Zero, TimeSpan.FromSeconds(1)); + Assert.False(exitStatus.Canceled); + Assert.NotEqual(0, exitStatus.ExitCode); + } + + [Fact] + public static void Kill_OnAlreadyExitedProcess_ReturnsFalse() + { + ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + ProcessExitStatus exitStatus = processHandle.WaitForExit(); + Assert.Equal(0, exitStatus.ExitCode); + + bool wasKilled = processHandle.Kill(); + Assert.False(wasKilled); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] + public static void WaitForExitOrKillOnTimeout_KillsOnTimeout() + { + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); + + Stopwatch stopwatch = Stopwatch.StartNew(); + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(300)); + + Assert.InRange(stopwatch.Elapsed, TimeSpan.FromMilliseconds(290), TimeSpan.FromMilliseconds(2000)); + Assert.True(exitStatus.Canceled); + Assert.NotEqual(0, exitStatus.ExitCode); + } + + [Fact] + public static void WaitForExit_WaitsIndefinitelyForProcessToComplete() + { + ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + Stopwatch stopwatch = Stopwatch.StartNew(); + ProcessExitStatus exitStatus = processHandle.WaitForExit(); + stopwatch.Stop(); + + Assert.Equal(0, exitStatus.ExitCode); + Assert.False(exitStatus.Canceled); + Assert.Null(exitStatus.Signal); + } + + [Fact] + public static void TryWaitForExit_ReturnsTrueWhenProcessExitsBeforeTimeout() + { + ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + bool exited = processHandle.TryWaitForExit(TimeSpan.FromSeconds(5), out ProcessExitStatus? exitStatus); + + Assert.True(exited); + Assert.NotNull(exitStatus); + Assert.Equal(0, exitStatus.ExitCode); + Assert.False(exitStatus.Canceled); + Assert.Null(exitStatus.Signal); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] + public static void TryWaitForExit_ReturnsFalseWhenProcessDoesNotExitBeforeTimeout() + { + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); + + try + { + Stopwatch stopwatch = Stopwatch.StartNew(); + bool exited = processHandle.TryWaitForExit(TimeSpan.FromMilliseconds(300), out ProcessExitStatus? exitStatus); + stopwatch.Stop(); + + Assert.False(exited); + Assert.Null(exitStatus); + Assert.InRange(stopwatch.Elapsed, TimeSpan.FromMilliseconds(290), TimeSpan.FromMilliseconds(2000)); + } + finally + { + processHandle.Kill(); + processHandle.WaitForExit(); + } + } + + [Fact] + public static void WaitForExitOrKillOnTimeout_DoesNotKillWhenProcessExitsBeforeTimeout() + { + ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); + + Assert.Equal(0, exitStatus.ExitCode); + Assert.False(exitStatus.Canceled, "Process should not be marked as canceled when it exits normally before timeout"); + Assert.Null(exitStatus.Signal); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] + public static void WaitForExitOrKillOnTimeout_KillsAndWaitsWhenTimeoutOccurs() + { + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); + + Stopwatch stopwatch = Stopwatch.StartNew(); + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(300)); + stopwatch.Stop(); + + Assert.InRange(stopwatch.Elapsed, TimeSpan.FromMilliseconds(290), TimeSpan.FromSeconds(2)); + Assert.True(exitStatus.Canceled, "Process should be marked as canceled when killed due to timeout"); + Assert.NotEqual(0, exitStatus.ExitCode); + Assert.Equal(-1, exitStatus.ExitCode); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] + public static async Task WaitForExitOrKillOnCancellationAsync_KillsOnCancellation() + { + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); + + Stopwatch stopwatch = Stopwatch.StartNew(); + using CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(300)); + + ProcessExitStatus exitStatus = await processHandle.WaitForExitOrKillOnCancellationAsync(cts.Token); + + Assert.InRange(stopwatch.Elapsed, TimeSpan.FromMilliseconds(270), TimeSpan.FromSeconds(2)); + Assert.True(exitStatus.Canceled); + Assert.NotEqual(0, exitStatus.ExitCode); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] + public static async Task WaitForExitAsync_ThrowsOnCancellation() + { + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); + + try + { + Stopwatch stopwatch = Stopwatch.StartNew(); + using CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(300)); + + await Assert.ThrowsAnyAsync(async () => + await processHandle.WaitForExitAsync(cts.Token)); + + stopwatch.Stop(); + + Assert.InRange(stopwatch.Elapsed, TimeSpan.FromMilliseconds(250), TimeSpan.FromMilliseconds(2000)); + + bool hasExited = processHandle.TryWaitForExit(TimeSpan.Zero, out _); + Assert.False(hasExited, "Process should still be running after cancellation"); + } + finally + { + processHandle.Kill(); + } + } + + [Fact] + public static async Task WaitForExitAsync_CompletesNormallyWhenProcessExits() + { + ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(5)); + ProcessExitStatus exitStatus = await processHandle.WaitForExitAsync(cts.Token); + + Assert.Equal(0, exitStatus.ExitCode); + Assert.False(exitStatus.Canceled); + Assert.Null(exitStatus.Signal); + } + + [Fact] + public static async Task WaitForExitAsync_WithoutCancellationToken_CompletesNormally() + { + ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + ProcessExitStatus exitStatus = await processHandle.WaitForExitAsync(); + + Assert.Equal(0, exitStatus.ExitCode); + Assert.False(exitStatus.Canceled); + Assert.Null(exitStatus.Signal); + } + + [Fact] + public static async Task WaitForExitOrKillOnCancellationAsync_CompletesNormallyWhenProcessExits() + { + ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(1)); + ProcessExitStatus exitStatus = await processHandle.WaitForExitOrKillOnCancellationAsync(cts.Token); + + Assert.Equal(0, exitStatus.ExitCode); + Assert.False(exitStatus.Canceled); + Assert.Null(exitStatus.Signal); + } + + [Fact] + public static void KillOnParentExit_CanBeSetToTrue() + { + ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" }, KillOnParentExit = true }; + + Assert.True(options.KillOnParentExit); + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); + Assert.Equal(0, exitStatus.ExitCode); + } + + [Fact] + public static void KillOnParentExit_DefaultsToFalse() + { + ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + Assert.False(options.KillOnParentExit); + } + + [Fact] + public static void Open_InvalidProcessId_Throws() + { + Assert.Throws(() => SafeProcessHandle.Open(0)); + Assert.Throws(() => SafeProcessHandle.Open(-1)); + } + + [Fact] + public static void Open_CurrentProcess_Succeeds() + { + int currentPid = Environment.ProcessId; + using SafeProcessHandle handle = SafeProcessHandle.Open(currentPid); + + Assert.False(handle.IsInvalid); + Assert.Equal(currentPid, handle.ProcessId); + } + } +} 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 3084d4a59bbfcc..c64461f79c39ea 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 @@ -36,6 +36,7 @@ + @@ -45,6 +46,7 @@ + - + 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 c744f85d3e05ce..620bf266f9bf51 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 @@ -2094,7 +2094,7 @@ Common\Interop\Windows\Interop.OBJECT_ATTRIBUTES.cs - + Common\Interop\Windows\Interop.SetConsoleCtrlHandler.cs