Skip to content

Add Process constructor accepting SafeProcessHandle and optional stream handles#128491

Draft
Copilot wants to merge 7 commits into
mainfrom
copilot/add-safe-handle-support-to-process
Draft

Add Process constructor accepting SafeProcessHandle and optional stream handles#128491
Copilot wants to merge 7 commits into
mainfrom
copilot/add-safe-handle-support-to-process

Conversation

Copilot AI commented May 22, 2026

Copy link
Copy Markdown
Contributor

Adds a new public Process constructor that creates instances from user-provided safe handles, enabling advanced interop scenarios where users call CreateProcess (or equivalent) directly but still want Process class features like async stream reading and WaitForExit.

Description

API Addition

public Process(
    SafeProcessHandle processHandle,
    ProcessStartInfo startInfo,
    SafeFileHandle? standardInput = null,
    SafeFileHandle? standardOutput = null,
    SafeFileHandle? standardError = null)

On Windows, standardOutput and standardError handles must be opened for asynchronous I/O (IsAsync == true); an ArgumentException is thrown otherwise. The processHandle must be valid (IsInvalid == false); an ArgumentException is thrown otherwise. The caller transfers ownership of the standard stream handles to the Process instance — they will be disposed when the Process is disposed.

Changes

  • Ref assembly (ref/System.Diagnostics.Process.cs): New constructor signature
  • Implementation (Process.cs): Constructor wires up handle, process ID, and wraps optional file handles into StreamWriter/StreamReader using existing OpenStream + encoding logic. Validates that processHandle is valid, that standardOutput/standardError handles are async on Windows, and that processHandle/startInfo are non-null. ProcessStartInfo is mandatory (with ArgumentNullException) to prevent StartInfo.get from throwing on an associated process.
  • Tests (ProcessHandlesTests.Windows.cs):
    • Extracted CreateProcessWithHandles static helper reused by both RunWithHandles and the new test
    • Added INVALID_HANDLE_VALUE constant to replace magic numbers
    • Added ProcessStartedWithAnonymousPipeHandles_CanCaptureOutput test: creates async anonymous pipes, enables inheritance, spawns child process, wraps read handles in new Process ctor, uses ReadAllText() to assert captured stdout/stderr
    • Added ProcessConstructor_NonAsyncHandle_ThrowsArgumentException [Theory] test for IsAsync validation of both stdout and stderr
  • Resources (Strings.resx): Added Argument_HandleNotAsync error message

Usage Example

SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: true);
// ... CreateProcess with writeHandle as hStdOutput ...

using Process process = new(safeProcessHandle, new ProcessStartInfo(), standardOutput: readHandle);
(string stdout, string stderr) = process.ReadAllText();

Copilot AI and others added 2 commits May 22, 2026 15:22
…ream handles

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/3a2f55b9-2cc6-46ba-87af-1dc53029c488

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
…unused usings

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/3a2f55b9-2cc6-46ba-87af-1dc53029c488

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 22, 2026 15:49
Copilot AI review requested due to automatic review settings May 22, 2026 15:49

@adamsitnik adamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address my feedback

Comment thread src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs Outdated
Comment thread src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs Outdated
Comment thread src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

…ANDLE_VALUE, extract CreateProcessWithHandles helper

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/12cdb475-8f8c-4309-b5d7-d0b02c675048

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 22, 2026 16:19

Copilot AI commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

@copilot address my feedback

Done in a53a1fc. All three items addressed:

  • Simplified _startInfo assignment (removed unnecessary null check)
  • Used INVALID_HANDLE_VALUE constant instead of (nint)(-1) magic numbers
  • Extracted CreateProcessWithHandles helper that's reused by both RunWithHandles and the new test

Copilot AI requested a review from adamsitnik May 22, 2026 16:20

@adamsitnik adamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address my feedback

Comment thread src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs Outdated
Comment thread src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs Outdated
… Windows

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/b6feaf9e-a3a5-4056-a01a-9aa9761be8be

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 22, 2026 17:18

Copilot AI commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

@copilot address my feedback

Done in a6488fc. Changes:

  • Added asyncRead: true to CreateAnonymousPipe calls
  • Replaced ReadToEnd with process.ReadAllText()
  • Added IsAsync validation for standardOutput/standardError on Windows (throws ArgumentException if not async)

Copilot AI requested review from Copilot and removed request for Copilot May 25, 2026 10:06

@adamsitnik adamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address my feedback

Comment thread src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs Outdated
Comment thread src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs Outdated
…dd IsAsync test

- Make ProcessStartInfo a mandatory parameter (moved to second position)
  with ArgumentNullException validation to prevent StartInfo.get from
  throwing when the process is associated.
- Restore GetSafeFileHandleId helper that was incorrectly removed (it is
  used in ProcessHandlesTests.cs).
- Document handle ownership transfer in XML docs for the constructor.
- Add tests verifying ArgumentException is thrown when non-async handles
  are passed for standardOutput/standardError on Windows.
- Update ref assembly to match new signature.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/3b168351-c6d4-4437-8a79-e949d29ff057

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 25, 2026 10:32

Copilot AI commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot address my feedback

All feedback addressed in b7cc712:

  • ProcessStartInfo is now a mandatory second parameter with ArgumentNullException validation
  • Restored GetSafeFileHandleId that was incorrectly removed
  • Added XML docs documenting handle ownership transfer
  • Added tests for IsAsync validation (ProcessConstructor_NonAsyncOutputHandle_ThrowsArgumentException and ProcessConstructor_NonAsyncErrorHandle_ThrowsArgumentException)

Copilot AI requested a review from adamsitnik May 25, 2026 10:33

@adamsitnik adamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address my feedback

Comment thread src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs Outdated
Comment thread src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs Outdated
…dle.IsInvalid

- Merged two similar IsAsync validation tests into a single [Theory]
- Removed trailing empty line before closing brace
- Added processHandle.IsInvalid validation in constructor

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ad6e82f0-2e52-4b04-b10f-b3add859ad18

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 25, 2026 10:46

Copilot AI commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot address my feedback

All addressed in 41ae4c8:

  • Merged the two IsAsync tests into a [Theory] with [InlineData(true/false)]
  • Removed trailing empty line
  • Added processHandle.IsInvalid validation (throws ArgumentException)

using (errorReadHandle)
using (errorWriteHandle)
{
// Enable inheritance on the write ends so the child process can use them.

@jkotas jkotas May 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is robust only if you control all other process starts that may be done by the current process.

If I am reading this correctly, this will create inheritable handles in the main test process without synchronizing with other process starts and so the handles may get accidently inherited by other processes. Is this test going to be flaky because of that?

Also, do the docs need to mention all gotchas about inherited handles and how the user code is expected to deal with them? I am not sure whether this API is the best way to address the end-to-end scenario once you take all the gotchas into account.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is robust only if you control all other process starts that may be done by the current process.

I agree, but this is true for all ways of starting processes on Windows (the standard handles always need to be inheritable).

If I am reading this correctly, this will create inheritable handles in the main test process without synchronizing with other process starts and so the handles may get accidently inherited by other processes.

Correct.

Is this test going to be flaky because of that?

All process tests are executed sequentially, so not for now.

// Process tests can conflict with each other, as they modify ambient state
// like the console code page and environment variables
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]

Also, do the docs need to mention all gotchas about inherited handles and how the user code is expected to deal with them?

They don't for now (but this can be ofc fixed).

I am not sure whether this API is the best way to address the end-to-end scenario once you take all the gotchas into account.

The alternative is to let the users pass the windows process attributes (and creation flags?) via ProcessStartInfo, something that we have discussed offline a while ago that would be passed to UpdateProcThreadAttribute:

public List<(int attribute, GCHandle value, nint byteSize)> ProcessAttributes { get; set; }

But it has one major limitation: it would support only the CreateProcess sys-call. So for example if the user wanted to use CreateProcessAsUser to implement #128378, this would not work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternative

I think there are more alternatives. For example, we can allow user to provide a callback that receives some of the arguments prepared for the Win32 API invocation, the user provided callback combines these arguments with their custom options and invokes the Win32 APIs of their choice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, we can allow user to provide a callback that receives some of the arguments prepared for the Win32 API invocation

There are few simple arguments that such callback would need to accept that would be easy to implement:

  • pointer to the command line string (char*)
  • pointer to new environment block (char*)
  • pointer to current directory name (char*)
  • handle inheritance flag (bool)
  • creation flags (int)

But advanced usages like the use case described by the customer in #71595 (comment)

I'm currently using JobObjects to limit CPU time and RAM. But I'd also like to restrict file and network access.
So, I need to use CreateRestrictedToken (or simular) API with Process capturing capabilities.

would also require us to expose STARTUPINFO and STARTUPINFOEX.

Moreover, really advanced scenarios like specifying Jobs would be still very tricky to get right together with all our existing APIs. For example, to provide Jobs, we need to specify the count up-front to allocate the right size of a buffer:

jobHandles = (IntPtr*)NativeMemory.Alloc(1, (nuint)sizeof(IntPtr));

The same is true for attribute count:

// Determine the number of attributes we need to set in the proc thread attribute list.
int attributeCount = 0;

And some of the Windows sys-calls used for spawning processes don't support STARTUPINFOEX with lpAttributeList (CreateProcessWithTokenW docs mention STARTUPINFOEX, but they don't mention that EXTENDED_STARTUPINFO_PRESENT is not supported anyway.). So in certain situations users would need to ignore some of the input we provide (like the attribute/job list) and start a suspended process, then add the jobs, resume the process and all of that like we do here

@jkotas from my perspective asking the users to deal with handle inheritance is lesser evil compared to exposing all the Win32 contracts and trying to provide a callback that would support all scenarios.

Also the proposed ctor could work on Unix as well. As users could just somehow spawn the process, use SafeProcessHandle.Open and give us the SPH and the pipe handles.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would also require us to expose STARTUPINFO and STARTUPINFOEX

I do not think we need to expose these structures. We can prepare the stdin/stdout/stderr handles, command line, environment, take the process create lock, and leave everything else to be provided by the user. They can supply their own definition of the PInvoke and all structures that the PInvoke needs. For example, if they want to start a process with custom job settings, they can do it all on their own.

Also the proposed ctor could work on Unix as well.

I would like to see what a "done" state looks like for the long tail of process APIs features. I do not think we want to be on the feature creep path (adding an API for every niche feature somebody asks for) that we are on currently. I do not have a fundamental problem with a few more APIs if we think that the use case is common enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants