Add new process management APIs to SafeProcessHandle#124375
Add new process management APIs to SafeProcessHandle#124375
Conversation
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
…, Theory for utils Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
…h for sh Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
…syntax Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
… remove dead code Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
…names Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
…s, and Windows implementation Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
…c, move lock/env to ProcessUtils, refactor BuildArgs Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik
left a comment
There was a problem hiding this comment.
@copilot please address my feedback
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs
Show resolved
Hide resolved
…ocess without arguments Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
| IntPtr heapHandlesPtr = (IntPtr)NativeMemory.Alloc((nuint)maxHandleCount, (nuint)sizeof(IntPtr)); | ||
| IntPtr* handlesToInherit = (IntPtr*)heapHandlesPtr; |
There was a problem hiding this comment.
| IntPtr heapHandlesPtr = (IntPtr)NativeMemory.Alloc((nuint)maxHandleCount, (nuint)sizeof(IntPtr)); | |
| IntPtr* handlesToInherit = (IntPtr*)heapHandlesPtr; | |
| IntPtr* handlesToInherit = (IntPtr*)NativeMemory.Alloc((nuint)maxHandleCount, (nuint)sizeof(IntPtr)); |
New code should use pointers for pointers instead of IntPtr
There was a problem hiding this comment.
Done in adeeb03. Now using IntPtr* handlesToInherit = (IntPtr*)NativeMemory.Alloc(...) directly and void* attributeListBuffer instead of IntPtr.
| Interop.Kernel32.LPPROC_THREAD_ATTRIBUTE_LIST emptyList = default; | ||
| Interop.Kernel32.InitializeProcThreadAttributeList(emptyList, attributeCount, 0, ref size); | ||
|
|
||
| attributeListBuffer = (IntPtr)NativeMemory.Alloc((nuint)size); |
There was a problem hiding this comment.
Done in adeeb03. Changed attributeListBuffer from IntPtr to void* with NativeMemory.Alloc returning directly to it.
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new SafeProcessHandle-based process management API surface (start/open/kill/signal/wait), with a Windows implementation plus supporting interop and tests, while introducing shared process-start serialization via ProcessUtils.
Changes:
- Add new public
SafeProcessHandleAPIs (start/open/kill/signal/resume/wait) andProcessIdplumbing. - Implement the new APIs on Windows (CreateProcess + STARTUPINFOEX, handle allow-list, job objects, async wait).
- Add tests and supporting shared helpers/interop (including consolidating console control interop).
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems | Switch CoreLib to include consolidated Interop.ConsoleCtrl.cs. |
| src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj | Add new SafeProcessHandle tests and use consolidated console control interop. |
| src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs | Add Windows-only tests for start/open/kill/wait APIs. |
| src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Windows.cs | Add Windows-specific tests for signaling, process groups, and suspend/resume. |
| src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs | Update to use common Interop.Kernel32.GenerateConsoleCtrlEvent. |
| src/libraries/System.Diagnostics.Process/tests/Interop.cs | Remove local GenerateConsoleCtrlEvent P/Invoke now covered by common interop. |
| src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs | Introduce shared s_processStartLock for process start serialization. |
| src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs | Add helpers for argument building and environment block creation. |
| src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartOptions.cs | Add internal “Has*BeenAccessed” flags to avoid unnecessary allocations. |
| src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs | Replace create-process lock with ProcessUtils.s_processStartLock; move env-block helper use to ProcessUtils. |
| src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs | Use shared ProcessUtils.s_processStartLock instead of a per-file lock. |
| src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs | Update terminal configuration assertions/locking to use the shared lock. |
| src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj | Include new Windows interop files needed by SafeProcessHandle. |
| src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx | Add new resource strings for SafeProcessHandle error cases. |
| src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs | Add new public API surface and shared helpers/validation. |
| src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs | Implement Windows process lifecycle APIs (job objects, STARTUPINFOEX, async waits, signaling). |
| src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs | Add Unix stubs for the new APIs (currently NotImplementedException). |
| src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs | Update reference contract for new SafeProcessHandle APIs and ProcessExitStatus. |
| src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ResumeThread_IntPtr.cs | Add ResumeThread(IntPtr) interop. |
| src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ProcThreadAttributeList.cs | Add STARTUPINFOEX / proc-thread attribute list interop and CreateProcess overload. |
| src/libraries/Common/src/Interop/Windows/Kernel32/Interop.JobObjects.cs | Add job object interop needed for kill-on-parent-exit and process groups. |
| src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleOptions.cs | Add HANDLE_FLAG_INHERIT constant used for inherited handle preparation. |
| src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs | Add Get/SetHandleInformation(IntPtr, ...) overloads. |
| src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ConsoleCtrl.cs | Consolidate console control interop and add GenerateConsoleCtrlEvent. |
| src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs | Add CREATE_SUSPENDED constant used for suspended process creation. |
| public sealed partial class ProcessExitStatus | ||
| { | ||
| public ProcessExitStatus(int exitCode, bool canceled, System.Runtime.InteropServices.PosixSignal? signal = null) { throw null; } | ||
| public bool Canceled { get { throw null; } } | ||
| public int ExitCode { get { throw null; } } | ||
| public System.Runtime.InteropServices.PosixSignal? Signal { get { throw null; } } | ||
| } |
There was a problem hiding this comment.
PR description states ProcessExitStatus is a readonly struct (value type) implementing IEquatable, but the public contract here exposes it as a sealed class. Please confirm the approved API shape and align either the implementation/ref contract or the PR description; this is a user-visible API surface difference.
| 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<ProcessExitStatus> WaitForExitAsyncCore(CancellationToken cancellationToken) => throw new NotImplementedException(); | ||
|
|
||
| private Task<ProcessExitStatus> 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(); |
There was a problem hiding this comment.
Unix implementations currently throw NotImplementedException for the new SafeProcessHandle APIs. For a public, cross-platform type, consider throwing PlatformNotSupportedException (or adding SupportedOSPlatform attributes) until Unix support is implemented, to avoid signaling an incomplete runtime implementation to callers in shipping builds.
| 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<ProcessExitStatus> WaitForExitAsyncCore(CancellationToken cancellationToken) => throw new NotImplementedException(); | |
| private Task<ProcessExitStatus> 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(); | |
| private static SafeProcessHandle OpenCore(int processId) => throw new PlatformNotSupportedException(); | |
| private static SafeProcessHandle StartCore(ProcessStartOptions options, SafeFileHandle inputHandle, SafeFileHandle outputHandle, SafeFileHandle errorHandle, bool createSuspended) => throw new PlatformNotSupportedException(); | |
| private ProcessExitStatus WaitForExitCore() => throw new PlatformNotSupportedException(); | |
| private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out ProcessExitStatus? exitStatus) => throw new PlatformNotSupportedException(); | |
| private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds) => throw new PlatformNotSupportedException(); | |
| private Task<ProcessExitStatus> WaitForExitAsyncCore(CancellationToken cancellationToken) => throw new PlatformNotSupportedException(); | |
| private Task<ProcessExitStatus> WaitForExitOrKillOnCancellationAsyncCore(CancellationToken cancellationToken) => throw new PlatformNotSupportedException(); | |
| internal bool KillCore(bool throwOnError, bool entireProcessGroup = false) => throw new PlatformNotSupportedException(); | |
| private void ResumeCore() => throw new PlatformNotSupportedException(); | |
| private void SendSignalCore(PosixSignal signal, bool entireProcessGroup) => throw new PlatformNotSupportedException(); |
| public SafeProcessHandle(IntPtr existingHandle, bool ownsHandle) | ||
| : base(ownsHandle) | ||
| { | ||
| SetHandle(existingHandle); | ||
| } |
There was a problem hiding this comment.
ProcessId is a new public property, but the general constructors/initialization paths don’t set it (e.g., SafeProcessHandle(IntPtr existingHandle, ...) just calls SetHandle). As a result, SafeProcessHandle instances produced by existing Process APIs (and user-created wrappers around an existing handle) will report ProcessId==0 on Windows unless they come from the new Start/Open paths. Consider initializing ProcessId in the ctor (e.g., via Kernel32.GetProcessId) and/or providing an internal setter that Process can use after CreateProcess/ShellExecute paths.
| { | ||
| var (handle, taskSource, wasCancelled) = ((SafeProcessHandle, TaskCompletionSource<bool>, StrongBox<bool>))state!; | ||
| wasCancelled.Value = handle.KillCore(throwOnError: false); | ||
| taskSource.TrySetResult(true); |
There was a problem hiding this comment.
In the cancellation callback, the TaskCompletionSource is completed immediately after KillCore() returns, but the process may still be running (TerminateProcess/TerminateJobObject are async). That can cause GetExitCode() to observe STILL_ACTIVE and throw, and it contradicts the method's doc that it “kills … and then waits for exit”. Consider waiting for processWaitHandle to signal after killing (or only completing the TCS from the wait-handle path) so exit status is retrieved after the process has actually terminated.
| taskSource.TrySetResult(true); |
| ctr = cancellationToken.Register( | ||
| static state => | ||
| { | ||
| var taskSource = (TaskCompletionSource<bool>)state!; | ||
| taskSource.TrySetCanceled(); | ||
| }, | ||
| tcs); |
There was a problem hiding this comment.
WaitForExitAsyncCore uses CancellationToken.Register + TaskCompletionSource.TrySetCanceled() without passing the cancellation token. Elsewhere in this repo (e.g., Process.WaitForExitAsync) the pattern is UnsafeRegister and TrySetCanceled(cancellationToken), which preserves the token on the thrown OperationCanceledException and avoids ExecutionContext capture overhead.
| ctr = cancellationToken.Register( | |
| static state => | |
| { | |
| var taskSource = (TaskCompletionSource<bool>)state!; | |
| taskSource.TrySetCanceled(); | |
| }, | |
| tcs); | |
| ctr = cancellationToken.UnsafeRegister( | |
| static state => | |
| { | |
| var (taskSource, token) = ((TaskCompletionSource<bool> taskSource, CancellationToken token))state!; | |
| taskSource.TrySetCanceled(token); | |
| }, | |
| (tcs, cancellationToken)); |
| </Compile> | ||
| <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.SetConsoleCtrlHandler.cs"> | ||
| <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.ConsoleCtrl.cs"> | ||
| <Link>Common\Interop\Windows\Interop.SetConsoleCtrlHandler.cs</Link> |
There was a problem hiding this comment.
The metadata still points to the old SetConsoleCtrlHandler virtual path/name even though the included file is now Interop.ConsoleCtrl.cs under Kernel32. This can make IDE paths/PDB source paths misleading; consider updating the Link value to match the new file location/name (e.g., Common\Interop\Windows\Kernel32\Interop.ConsoleCtrl.cs).
| <Link>Common\Interop\Windows\Interop.SetConsoleCtrlHandler.cs</Link> | |
| <Link>Common\Interop\Windows\Kernel32\Interop.ConsoleCtrl.cs</Link> |
| if (!Interop.GenerateConsoleCtrlEvent(dwCtrlEvent, (uint)processId)) | ||
| if (!Interop.Kernel32.GenerateConsoleCtrlEvent((int)dwCtrlEvent, processId)) | ||
| { | ||
| int error = Marshal.GetLastWin32Error(); |
There was a problem hiding this comment.
Interop.Kernel32.GenerateConsoleCtrlEvent is now a [LibraryImport] P/Invoke; prefer Marshal.GetLastPInvokeError() over Marshal.GetLastWin32Error() for consistency with other LibraryImport call sites in this PR and to ensure the correct last-error accessor is used.
| int error = Marshal.GetLastWin32Error(); | |
| int error = Marshal.GetLastPInvokeError(); |
|
|
||
| private void ResumeCore() | ||
| { | ||
| IntPtr threadHandle = Interlocked.Exchange(ref _threadHandle, IntPtr.Zero); |
There was a problem hiding this comment.
Does ReleaseHandle need the same Interlocked.Exchange to avoid use-after-free if the user code happens to call this on a handle that is being disposed?
| <value>Invalid process handle.</value> | ||
| </data> | ||
| <data name="KillProcessGroupWithoutNewProcessGroup" xml:space="preserve"> | ||
| <value>Cannot terminate entire process group because the process was not started with CreateNewProcessGroup=true.</value> |
There was a problem hiding this comment.
There is ProcessStartInfo.CreateNewProcessGroup as well. If you start the process with ProcessStartInfo.CreateNewProcessGroup=true, you are still going to get this message.
(This shows the problem with the overlap between ProcessStartInfo and ProcessStartInfo discussed offline.)
| throw new Win32Exception(error); | ||
| } | ||
|
|
||
| // Transfer ownership: take the handle from the returned SafeProcessHandle and |
There was a problem hiding this comment.
I am not following why we need to create a new SafeProcessHandle. Can we just set the process ID on the instance we have?
| if (options.KillOnParentExit || options.CreateNewProcessGroup) | ||
| attributeCount++; | ||
|
|
||
| IntPtr size = IntPtr.Zero; |
There was a problem hiding this comment.
| IntPtr size = IntPtr.Zero; | |
| nuint size = 0; |
nuint would be better match (the argument type for InitializeProcThreadAttributeList is SIZE_T that is unsigned type
There was a problem hiding this comment.
(There are other signed vs. unsigned mismatches in the interop definitions added in this PR that would be nice to fix.)
| Interop.Kernel32.LPPROC_THREAD_ATTRIBUTE_LIST emptyList = default; | ||
| Interop.Kernel32.InitializeProcThreadAttributeList(emptyList, attributeCount, 0, ref size); | ||
|
|
||
| attributeListBuffer = NativeMemory.Alloc((nuint)size); |
There was a problem hiding this comment.
| attributeListBuffer = NativeMemory.Alloc((nuint)size); | |
| attributeListBuffer = NativeMemory.Alloc(size); |
Description
Implements the approved API surface for
SafeProcessHandle(#123380): process lifecycle management viaStart,StartSuspended,Open,Kill,KillProcessGroup,Resume,Signal,SignalProcessGroup, and variousWaitForExitoverloads. AddsProcessExitStatusvalue type. Windows implementation is complete; Unix stubs throwNotImplementedException(separate PR).New types
ProcessExitStatus— readonly struct withExitCode,Signal,Canceled; implementsIEquatable<T>SafeProcessHandle new public API
ProcessIdproperty (shared across platforms inSafeProcessHandle.cs)Open(int processId)— opens existing process handleStart/StartSuspended— process creation with explicit stdio handlesKill/KillProcessGroup— termination with job object supportResume— resumes processes created viaStartSuspended(not a general-purposeNtResumeProcess); protected against double-resume viaInterlocked.ExchangeSignal/SignalProcessGroup— sendsPosixSignal(Windows: CTRL_C/CTRL_BREAK viaGenerateConsoleCtrlEvent)WaitForExit,TryWaitForExit,WaitForExitOrKillOnTimeout— synchronous wait variantsWaitForExitAsync,WaitForExitOrKillOnCancellationAsync— async wait with cancellation supportWindows implementation details
STARTUPINFOEX+PROC_THREAD_ATTRIBUTE_HANDLE_LISTfor explicit handle inheritanceKillOnParentExitandCreateNewProcessGroup(process group termination)StartSuspended/Resume; atomically cleared on resume to prevent double-resumeValueStringBuilderinstances are properly disposed afterCreateProcessinvocationWin32Exception()constructor (auto-captures last error) everywhere exceptCreateProcesserror handlingfinallyblock prevents leaks ifnew SafeProcessHandle(...)throwsCreateProcesscallNativeMemory.Alloc(with two-arg version for integer overflow protection) instead ofMarshal.AllocHGlobal; results stored in typed pointers (void*,IntPtr*) instead ofIntPtrsizeofinstead ofMarshal.SizeOffor blittable interop structsGetTimeoutInMillisecondsvalidates timeout range consistent withProcess.ToTimeoutMillisecondsWaitForExitOrKillOnTimeoutCorewaits for the process to fully terminate after kill (TerminateProcessis asynchronous on Windows)GetCurrentProcess()Architecture —
ProcessUtilsas shared dependencyProcessUtils.csholdss_processStartLock(ReaderWriterLockSlim) to avoid dependency cycles betweenProcessandSafeProcessHandleProcessUtils.Windows.csholdsBuildArgs(acceptsstring resolvedFilePath, IList<string>? argumentsto avoid loadingProcessStartOptionsfromProcesscallers) andGetEnvironmentVariablesBlockProcess.Windows.csacquires write lock;SafeProcessHandle.Windows.csacquires read lock — prevents accidental handle inheritanceProcessStartOptionsexposesHasArgumentsBeenAccessedandHasEnvironmentBeenAccessedto avoid unnecessary allocationsNew/extended interop definitions
Interop.JobObjects.cs—CreateJobObjectW,SetInformationJobObject,TerminateJobObjectInterop.ProcThreadAttributeList.cs—STARTUPINFOEX, attribute list APIs,CreateProcessoverloadInterop.ConsoleCtrl.cs—SetConsoleCtrlHandler+GenerateConsoleCtrlEvent(consolidated)Interop.HandleInformation.cs— extended withGetHandleInformation(IntPtr)andSetHandleInformation(IntPtr)overloadsInterop.ResumeThread_IntPtr.cs—ResumeThreadwithIntPtrparameterTests
SafeProcessHandleTests.cs— cross-platform test class marked[PlatformSpecific(TestPlatforms.Windows)]withCreateTenSecondSleep()helper to reduce duplication; powershell-dependent tests use[ConditionalFact]withIsNotWindowsNanoNorServerCore; includesStart_WithNoArguments_Succeedstest (usinghostname) to verify no-argument process invocationSafeProcessHandleTests.Windows.cs— signal, process group, suspend/resume tests usingConsole.OpenStandardInputHandle()for stdin💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.