Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public MonitoringDescriptionAttribute(string description) { }
public partial class Process : System.ComponentModel.Component, System.IDisposable
{
public Process() { }
public Process(Microsoft.Win32.SafeHandles.SafeProcessHandle processHandle, Microsoft.Win32.SafeHandles.SafeFileHandle? standardInput = null, Microsoft.Win32.SafeHandles.SafeFileHandle? standardOutput = null, Microsoft.Win32.SafeHandles.SafeFileHandle? standardError = null, System.Diagnostics.ProcessStartInfo? startInfo = null) { }
public int BasePriority { get { throw null; } }
public bool EnableRaisingEvents { get { throw null; } set { } }
public int ExitCode { get { throw null; } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,52 @@ public Process()
_errorStreamReadMode = StreamReadMode.Undefined;
}

/// <summary>
/// Initializes a new instance of the <see cref="Process"/> class from an existing process handle,
/// with optional standard I/O stream handles and start info.
/// </summary>
/// <param name="processHandle">A <see cref="SafeProcessHandle"/> representing the process.</param>
/// <param name="standardInput">An optional <see cref="SafeFileHandle"/> for the standard input stream of the process. The handle must support write access.</param>
/// <param name="standardOutput">An optional <see cref="SafeFileHandle"/> for the standard output stream of the process. The handle must support read access.</param>
/// <param name="standardError">An optional <see cref="SafeFileHandle"/> for the standard error stream of the process. The handle must support read access.</param>
/// <param name="startInfo">An optional <see cref="ProcessStartInfo"/> containing encoding information for the streams.</param>
Comment thread
adamsitnik marked this conversation as resolved.
Outdated
public Process(SafeProcessHandle processHandle, SafeFileHandle? standardInput = null, SafeFileHandle? standardOutput = null, SafeFileHandle? standardError = null, ProcessStartInfo? startInfo = null)
Comment thread
adamsitnik marked this conversation as resolved.
Outdated
{
ArgumentNullException.ThrowIfNull(processHandle);
Comment thread
adamsitnik marked this conversation as resolved.

GC.SuppressFinalize(this);
_machineName = ".";
_outputStreamReadMode = StreamReadMode.Undefined;
_errorStreamReadMode = StreamReadMode.Undefined;

SetProcessHandle(processHandle);
SetProcessId(processHandle.ProcessId);

if (startInfo is not null)
{
_startInfo = startInfo;
}
Comment thread
adamsitnik marked this conversation as resolved.
Outdated

if (standardInput is not null)
{
_standardInput = new StreamWriter(OpenStream(standardInput, FileAccess.Write),
startInfo?.StandardInputEncoding ?? GetStandardInputEncoding(), StreamBufferSize)
{
AutoFlush = true
};
}
if (standardOutput is not null)
{
Comment thread
adamsitnik marked this conversation as resolved.
_standardOutput = new StreamReader(OpenStream(standardOutput, FileAccess.Read),
startInfo?.StandardOutputEncoding ?? GetStandardOutputEncoding(), true, StreamBufferSize);
}
if (standardError is not null)
{
Comment thread
adamsitnik marked this conversation as resolved.
_standardError = new StreamReader(OpenStream(standardError, FileAccess.Read),
startInfo?.StandardErrorEncoding ?? GetStandardOutputEncoding(), true, StreamBufferSize);
}
}

private Process(string machineName, bool isRemoteMachine, int processId, ProcessInfo? processInfo)
{
GC.SuppressFinalize(this);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
// 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.ComponentModel;
using System.IO;
using System.IO.Pipes;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using Microsoft.DotNet.RemoteExecutor;
using Microsoft.DotNet.XUnitExtensions;
using Microsoft.Win32.SafeHandles;
using Xunit;

Expand All @@ -27,7 +23,7 @@ public void ProcessStartedWithInvalidHandles_ConsoleReportsInvalidHandles()
return RemoteExecutor.SuccessExitCode;
});

Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo));
Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, (nint)(-1), (nint)(-1), (nint)(-1)));
Comment thread
adamsitnik marked this conversation as resolved.
Outdated
}

[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
Expand Down Expand Up @@ -64,7 +60,7 @@ public void ProcessStartedWithInvalidHandles_CanStartChildProcessWithDerivedInva
}
}, restrictHandles.ToString(), killOnParentExit.ToString());

Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo));
Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, (nint)(-1), (nint)(-1), (nint)(-1)));
}

[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
Expand Down Expand Up @@ -102,13 +98,110 @@ public void ProcessStartedWithInvalidHandles_CanRedirectOutput(bool restrictHand
}
}, restrictHandles.ToString());

Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo));
Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, (nint)(-1), (nint)(-1), (nint)(-1)));
}

private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo)
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public unsafe void ProcessStartedWithAnonymousPipeHandles_CanCaptureOutput()
{
const nint INVALID_HANDLE_VALUE = -1;
SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle outputReadHandle, out SafeFileHandle outputWriteHandle);
SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle errorReadHandle, out SafeFileHandle errorWriteHandle);
Comment thread
adamsitnik marked this conversation as resolved.
Outdated

using (outputReadHandle)
using (outputWriteHandle)
using (errorReadHandle)
using (errorWriteHandle)
{
// Enable inheritance on the write ends so the child process can use them.
Copy link
Copy Markdown
Member

@jkotas jkotas May 25, 2026

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.

if (!Interop.Kernel32.SetHandleInformation(
outputWriteHandle,
Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT,
Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}

if (!Interop.Kernel32.SetHandleInformation(
errorWriteHandle,
Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT,
Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}

using Process remoteProcess = CreateProcess(() =>
{
Console.Write("stdout_hello");
Console.Error.Write("stderr_hello");

return RemoteExecutor.SuccessExitCode;
});

ProcessStartInfo startInfo = remoteProcess.StartInfo;
string arguments = $"\"{startInfo.FileName}\" {startInfo.Arguments}\0";

Interop.Kernel32.STARTUPINFOEX startupInfoEx = default;
Interop.Kernel32.PROCESS_INFORMATION processInfo = default;
Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default;

startupInfoEx.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFOEX);
startupInfoEx.StartupInfo.hStdInput = (nint)(-1);
startupInfoEx.StartupInfo.hStdOutput = outputWriteHandle.DangerousGetHandle();
startupInfoEx.StartupInfo.hStdError = errorWriteHandle.DangerousGetHandle();
startupInfoEx.StartupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES;

fixed (char* commandLinePtr = arguments)
{
bool retVal = Interop.Kernel32.CreateProcess(
null,
commandLinePtr,
ref unused_SecAttrs,
ref unused_SecAttrs,
bInheritHandles: true,
Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT,
null,
null,
&startupInfoEx,
&processInfo
);

if (!retVal)
{
throw new Win32Exception();
}
}

try
{
SafeProcessHandle safeProcessHandle = new(processInfo.hProcess, ownsHandle: true);
Comment thread
adamsitnik marked this conversation as resolved.
Outdated

using Process process = new(
safeProcessHandle,
standardOutput: outputReadHandle,
standardError: errorReadHandle);

// Close the write ends so reads don't block once the child exits.
outputWriteHandle.Close();
errorWriteHandle.Close();

string stdout = process.StandardOutput.ReadToEnd();
string stderr = process.StandardError.ReadToEnd();
Comment thread
adamsitnik marked this conversation as resolved.
Outdated

process.WaitForExit(WaitInMS);

Assert.Equal("stdout_hello", stdout);
Assert.Equal("stderr_hello", stderr);
Assert.Equal(RemoteExecutor.SuccessExitCode, process.ExitCode);
}
finally
{
Interop.Kernel32.CloseHandle(processInfo.hThread);
}
}
}

private unsafe int RunWithHandles(ProcessStartInfo startInfo, nint hStdInput, nint hStdOutput, nint hStdError, bool inheritHandles = false)
{
// RemoteExector has provided us with the right path and arguments,
// we just need to add the terminating null character.
string arguments = $"\"{startInfo.FileName}\" {startInfo.Arguments}\0";
Expand All @@ -118,9 +211,9 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo)
Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default;

startupInfoEx.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFOEX);
startupInfoEx.StartupInfo.hStdInput = INVALID_HANDLE_VALUE;
startupInfoEx.StartupInfo.hStdOutput = INVALID_HANDLE_VALUE;
startupInfoEx.StartupInfo.hStdError = INVALID_HANDLE_VALUE;
startupInfoEx.StartupInfo.hStdInput = hStdInput;
startupInfoEx.StartupInfo.hStdOutput = hStdOutput;
startupInfoEx.StartupInfo.hStdError = hStdError;

// If STARTF_USESTDHANDLES is not set, the new process will inherit the standard handles.
startupInfoEx.StartupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES;
Expand All @@ -133,7 +226,7 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo)
commandLinePtr,
ref unused_SecAttrs,
ref unused_SecAttrs,
bInheritHandles: false,
bInheritHandles: inheritHandles,
Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT,
null,
null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
Link="Common\Interop\Windows\Kernel32\Interop.SetConsoleCtrlHandler.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetFinalPathNameByHandle.cs"
Link="Common\Interop\Windows\Kernel32\Interop.GetFinalPathNameByHandle.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.HandleInformation.cs"
Link="Common\Interop\Windows\Kernel32\Interop.HandleInformation.cs" />
<!-- Helpers -->
<Compile Include="$(CommonTestPath)TestUtilities\System\WindowsTestFileShare.cs" Link="Common\TestUtilities\System\WindowsTestFileShare.cs" />
</ItemGroup>
Expand Down
Loading