Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -306,6 +306,9 @@
<data name="Argument_EncodingConversionOverflowBytes" xml:space="preserve">
<value>The output byte buffer is too small to contain the encoded data, encoding '{0}' fallback '{1}'.</value>
</data>
<data name="Argument_HandleNotAsync" xml:space="preserve">
<value>The handle must be opened for asynchronous I/O on Windows.</value>
</data>
<data name="Argument_EncodingConversionOverflowChars" xml:space="preserve">
<value>The output char buffer is too small to contain the decoded characters, encoding '{0}' fallback '{1}'.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,58 @@ 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>
public Process(SafeProcessHandle processHandle, SafeFileHandle? standardInput = null, SafeFileHandle? standardOutput = null, SafeFileHandle? standardError = null, ProcessStartInfo? startInfo = null)
{
ArgumentNullException.ThrowIfNull(processHandle);

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

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

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.
if (OperatingSystem.IsWindows() && !standardOutput.IsAsync)
{
throw new ArgumentException(SR.Argument_HandleNotAsync, nameof(standardOutput));
}

_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.
if (OperatingSystem.IsWindows() && !standardError.IsAsync)
{
throw new ArgumentException(SR.Argument_HandleNotAsync, nameof(standardError));
}

_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,20 +1,18 @@
// 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;

namespace System.Diagnostics.Tests
{
public partial class ProcessHandlesTests
{
private const nint INVALID_HANDLE_VALUE = -1;

[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public void ProcessStartedWithInvalidHandles_ConsoleReportsInvalidHandles()
{
Expand All @@ -27,7 +25,7 @@ public void ProcessStartedWithInvalidHandles_ConsoleReportsInvalidHandles()
return RemoteExecutor.SuccessExitCode;
});

Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo));
Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE));
}

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

Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo));
Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE));
}

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

Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo));
Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE));
}

[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public void ProcessStartedWithAnonymousPipeHandles_CanCaptureOutput()
{
SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle outputReadHandle, out SafeFileHandle outputWriteHandle, asyncRead: true);
SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle errorReadHandle, out SafeFileHandle errorWriteHandle, asyncRead: true);

using (outputReadHandle)
using (outputWriteHandle)
using (errorReadHandle)
using (errorWriteHandle)
{
// Enable inheritance on the write ends so the child process can use them.
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;
});

Interop.Kernel32.PROCESS_INFORMATION processInfo = CreateProcessWithHandles(
remoteProcess.StartInfo,
INVALID_HANDLE_VALUE,
outputWriteHandle.DangerousGetHandle(),
errorWriteHandle.DangerousGetHandle(),
inheritHandles: true);

try
{
SafeProcessHandle safeProcessHandle = new(processInfo.hProcess, ownsHandle: true);

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, string stderr) = process.ReadAllText();

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 RunWithInvalidHandles(ProcessStartInfo startInfo)
private int RunWithHandles(ProcessStartInfo startInfo, nint hStdInput, nint hStdOutput, nint hStdError, bool inheritHandles = false)
{
const nint INVALID_HANDLE_VALUE = -1;
Interop.Kernel32.PROCESS_INFORMATION processInfo = CreateProcessWithHandles(startInfo, hStdInput, hStdOutput, hStdError, inheritHandles);

try
{
using SafeProcessHandle safeProcessHandle = new(processInfo.hProcess, ownsHandle: true);

try
{
ProcessExitStatus exitStatus = safeProcessHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(WaitInMS));
return exitStatus.ExitCode;
}
finally
{
safeProcessHandle.Kill();
}
}
finally
{
Interop.Kernel32.CloseHandle(processInfo.hThread);
}
}

private static unsafe Interop.Kernel32.PROCESS_INFORMATION CreateProcessWithHandles(ProcessStartInfo startInfo, nint hStdInput, nint hStdOutput, nint hStdError, bool inheritHandles)
{
// 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,22 +207,21 @@ 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;

bool retVal = false;
fixed (char* commandLinePtr = arguments)
{
retVal = Interop.Kernel32.CreateProcess(
bool retVal = Interop.Kernel32.CreateProcess(
null,
commandLinePtr,
ref unused_SecAttrs,
ref unused_SecAttrs,
bInheritHandles: false,
bInheritHandles: inheritHandles,
Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT,
null,
null,
Expand All @@ -147,24 +235,7 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo)
}
}

try
{
using SafeProcessHandle safeProcessHandle = new(processInfo.hProcess, ownsHandle: true);

try
{
ProcessExitStatus exitStatus = safeProcessHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(WaitInMS));
return exitStatus.ExitCode;
}
finally
{
safeProcessHandle.Kill();
}
}
finally
{
Interop.Kernel32.CloseHandle(processInfo.hThread);
}
return processInfo;
}

private static unsafe string GetSafeFileHandleId(SafeFileHandle handle)
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