Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f1c70ae
Add Process.Start callback overload with ProcessStartArguments and tests
Copilot Jun 1, 2026
59234fd
Fix CreateProcess environment parameter cast in Windows test
Copilot Jun 1, 2026
3163021
address my own feedback:
adamsitnik Jun 2, 2026
ba27166
Address @adamsitnik test feedback: use ReadAllText, Redirect helper, …
Copilot Jun 2, 2026
9c12e8e
Preserve size rationale comment alongside local const
Copilot Jun 2, 2026
8b593b3
Apply suggestions from code review
adamsitnik Jun 2, 2026
d01ae8e
Apply suggestions from code review
adamsitnik Jun 2, 2026
b29a721
address feedback:
adamsitnik Jun 2, 2026
03cda31
Address Process callback test feedback
Copilot Jun 2, 2026
090d716
Use NativeMemory.Alloc in Unix process callback test
Copilot Jun 2, 2026
cf52098
Apply suggestions from code review
adamsitnik Jun 2, 2026
2245788
Update ProcessStartArguments docs for public setters
Copilot Jun 2, 2026
d46d4bc
Revert standard handle semantics wording in ProcessStartArguments docs
Copilot Jun 2, 2026
3079bdd
Update callback path to expose Unix resolved executable pointer
Copilot Jun 2, 2026
26a3800
Adjust Unix resolved path buffer strategy
Copilot Jun 3, 2026
d30a6f6
Make ProcessStartArguments a ref struct
Copilot Jun 3, 2026
39ef8d1
Fix Windows callback stderr assertion spacing
Copilot Jun 3, 2026
f93911a
Remove unnecessary ToInt32 conversions in Unix callback start path
Copilot Jun 8, 2026
4d460ec
Apply suggestions from code review
adamsitnik Jun 8, 2026
7c65489
Address Unix callback path and docs feedback
Copilot Jun 8, 2026
47c08c4
Fix Unix callback marshaller lifetime compile error
Copilot Jun 8, 2026
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 @@ -111,7 +111,7 @@ private static unsafe partial int ForkAndExecProcess(
/// Allocates a single native memory block containing both a null-terminated pointer array
/// and the UTF-8 encoded string data for the given array of strings.
/// </summary>
private static unsafe void AllocArgvArray(string[] arr, ref byte** arrPtr)
internal static unsafe void AllocArgvArray(string[] arr, ref byte** arrPtr)
{
int count = arr.Length;

Expand Down Expand Up @@ -150,7 +150,7 @@ private static unsafe void AllocArgvArray(string[] arr, ref byte** arrPtr)
/// Allocates a single native memory block containing both a null-terminated pointer array
/// and the UTF-8 encoded "key=value\0" data for all non-null entries in the environment dictionary.
/// </summary>
private static unsafe void AllocEnvpArray(IDictionary<string, string?> env, ref byte** arrPtr)
internal static unsafe void AllocEnvpArray(IDictionary<string, string?> env, ref byte** arrPtr)
{
// First pass: count entries with non-null values and compute total buffer size.
int count = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ public void Refresh() { }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] // this needs to come after the ios attribute due to limitations in the platform analyzer
public static System.Diagnostics.Process? Start(System.Diagnostics.ProcessStartInfo startInfo) { throw null; }
[System.CLSCompliantAttribute(false)]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")]
public static System.Diagnostics.Process Start(System.Diagnostics.ProcessStartInfo startInfo, System.Func<System.Diagnostics.ProcessStartArguments, Microsoft.Win32.SafeHandles.SafeProcessHandle> callback) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
Comment thread
adamsitnik marked this conversation as resolved.
Comment thread
adamsitnik marked this conversation as resolved.
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] // this needs to come after the ios attribute due to limitations in the platform analyzer
Expand Down Expand Up @@ -287,6 +292,21 @@ public readonly partial struct ProcessOutputLine
public bool StandardError { get { throw null; } }
public override string ToString() { throw null; }
}
public ref partial struct ProcessStartArguments
{
public ProcessStartArguments() { }
Comment thread
adamsitnik marked this conversation as resolved.
[System.CLSCompliantAttribute(false)]
public unsafe void* Arguments { get { throw null; } set { } }
[System.CLSCompliantAttribute(false)]
public unsafe void* EnvironmentVariables { get { throw null; } set { } }
[System.CLSCompliantAttribute(false)]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")]
public unsafe byte* ResolvedPath { get { throw null; } set { } }
public System.Diagnostics.ProcessStartInfo ProcessStartInfo { get { throw null; } set { } }
public System.IntPtr StandardError { get { throw null; } set { } }
public System.IntPtr StandardInput { get { throw null; } set { } }
public System.IntPtr StandardOutput { get { throw null; } set { } }
}
public enum ProcessPriorityClass
{
Normal = 32,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Compile Include="System.Diagnostics.Process.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
using System.IO;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using System.Runtime.Versioning;
using System.Security;
using System.Text;
using System.Threading;
using Microsoft.Win32.SafeHandles;

Expand Down Expand Up @@ -177,16 +177,14 @@ private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileH
internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle,
SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles, out ProcessWaitState.Holder? waitStateHolder)
{
waitStateHolder = null;

ProcessUtils.EnsureInitialized();

if (startInfo.UseShellExecute)
{
return s_startWithShellExecute!(startInfo, stdinHandle, stdoutHandle, stderrHandle, out waitStateHolder);
}

string? filename;
string filename;
string[] argv;

IDictionary<string, string?> env = startInfo.Environment;
Expand All @@ -203,12 +201,8 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile

bool usesTerminal = UsesTerminal(stdinHandle, stdoutHandle, stderrHandle);

filename = ProcessUtils.ResolvePath(startInfo.FileName);
filename = ProcessUtils.ResolveValidPath(startInfo.FileName, cwd);
argv = ProcessUtils.ParseArgv(startInfo);
if (Directory.Exists(filename))
{
throw new Win32Exception(SR.DirectoryNotValidAsInput);
}

return ForkAndExecProcess(
startInfo, filename, argv, env, cwd,
Expand All @@ -218,6 +212,118 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile
out waitStateHolder);
}

internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo startInfo, SafeFileHandle? stdinFd, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle,
Func<ProcessStartArguments, SafeProcessHandle> callback, out ProcessWaitState.Holder? waitStateHolder)
{
waitStateHolder = null;
ProcessUtils.EnsureInitialized();

string resolvedFileName = ProcessUtils.ResolveValidPath(startInfo.FileName, startInfo.WorkingDirectory);

string[] argv = ProcessUtils.ParseArgv(startInfo);
bool configuredTerminal = false, usesTerminal = UsesTerminal(stdinFd, stdoutHandle, stderrHandle);
byte** argvPtr = null, envpPtr = null;
bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false;
scoped Utf8StringMarshaller.ManagedToUnmanagedIn resolvedPathMarshaller = default;
Span<byte> resolvedPathBuffer = stackalloc byte[Utf8StringMarshaller.ManagedToUnmanagedIn.BufferSize];

try
{
Interop.Sys.AllocArgvArray(argv, ref argvPtr);
Interop.Sys.AllocEnvpArray(startInfo.Environment, ref envpPtr);

resolvedPathMarshaller.FromManaged(resolvedFileName, resolvedPathBuffer);

nint stdinRawFd = -1, stdoutRawFd = -1, stderrRawFd = -1;

if (stdinFd is not null)
{
stdinFd.DangerousAddRef(ref stdinRefAdded);
stdinRawFd = stdinFd.DangerousGetHandle();
}

if (stdoutHandle is not null)
{
stdoutHandle.DangerousAddRef(ref stdoutRefAdded);
stdoutRawFd = stdoutHandle.DangerousGetHandle();
}

if (stderrHandle is not null)
{
stderrHandle.DangerousAddRef(ref stderrRefAdded);
stderrRawFd = stderrHandle.DangerousGetHandle();
}

ProcessStartArguments args = new()
{
ResolvedPath = resolvedPathMarshaller.ToUnmanaged(),
Arguments = argvPtr,
EnvironmentVariables = envpPtr,
StandardInput = stdinRawFd,
StandardOutput = stdoutRawFd,
StandardError = stderrRawFd,
ProcessStartInfo = startInfo,
};

// Lock to avoid races with OnSigChild
// By using a ReaderWriterLock we allow multiple processes to start concurrently.
ProcessUtils.s_processStartLock.EnterReadLock();

try
{
if (usesTerminal)
{
ProcessUtils.ConfigureTerminalForChildProcesses(1);
configuredTerminal = true;
}

SafeProcessHandle processHandle = callback(args);
if (processHandle is null || processHandle.IsInvalid)
{
throw new ArgumentException(SR.Argument_InvalidHandle, nameof(callback));
}

waitStateHolder = new ProcessWaitState.Holder(processHandle.ProcessId, isNewChild: true, usesTerminal);
return processHandle;
}
finally
{
ProcessUtils.s_processStartLock.ExitReadLock();
}
}
catch
{
if (configuredTerminal)
{
// We failed to launch a child that could use the terminal.
ProcessUtils.s_processStartLock.EnterWriteLock();
ProcessUtils.ConfigureTerminalForChildProcesses(-1);
ProcessUtils.s_processStartLock.ExitWriteLock();
}

throw;
}
finally
{
if (stdinRefAdded)
{
stdinFd!.DangerousRelease();
}
if (stdoutRefAdded)
{
stdoutHandle!.DangerousRelease();
}
if (stderrRefAdded)
{
stderrHandle!.DangerousRelease();
}

NativeMemory.Free(envpPtr);
NativeMemory.Free(argvPtr);
resolvedPathMarshaller.Free();
}
}

private static SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle,
SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,80 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S
return procSH;
}

internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo startInfo, SafeFileHandle stdinHandle, SafeFileHandle stdoutHandle, SafeFileHandle stderrHandle,
Func<ProcessStartArguments, SafeProcessHandle> callback)
{
ValueStringBuilder commandLine = new(stackalloc char[256]);
ProcessUtils.BuildCommandLine(startInfo, ref commandLine);
commandLine.NullTerminate();

string? environmentBlock = null;
if (startInfo._environmentVariables != null)
{
environmentBlock = ProcessUtils.GetEnvironmentVariablesBlock(startInfo._environmentVariables!);
}

nint stdin = -1, stdout = -1, stderr = -1;
bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false;

// Acquire the process lock to avoid accidental handle inheritance issues.
ProcessUtils.s_processStartLock.EnterWriteLock();

try
{
ProcessUtils.DuplicateAsInheritableIfNeeded(stdinHandle, ref stdin, ref stdinRefAdded);
ProcessUtils.DuplicateAsInheritableIfNeeded(stdoutHandle, ref stdout, ref stdoutRefAdded);
ProcessUtils.DuplicateAsInheritableIfNeeded(stderrHandle, ref stderr, ref stderrRefAdded);

ProcessStartArguments args = new()
{
StandardInput = stdin,
StandardOutput = stdout,
StandardError = stderr,
ProcessStartInfo = startInfo,
};

Comment thread
adamsitnik marked this conversation as resolved.
fixed (char* commandLinePtr = &commandLine.GetPinnableReference())
fixed (char* environmentBlockPtr = environmentBlock)
{
args.Arguments = commandLinePtr;
args.EnvironmentVariables = environmentBlockPtr;

SafeProcessHandle startedProcess = callback(args);
if (startedProcess is null || startedProcess.IsInvalid)
{
throw new ArgumentException(SR.Argument_InvalidHandle, nameof(callback));
}

Comment thread
adamsitnik marked this conversation as resolved.
return startedProcess;
}
Comment thread
adamsitnik marked this conversation as resolved.
}
finally
{
// If the provided handle was inheritable, just release the reference we added.
// Otherwise if we created a valid duplicate, close it.

if (stdinRefAdded)
stdinHandle.DangerousRelease();
else if (!IsInvalidHandle(stdin))
Interop.Kernel32.CloseHandle(stdin);

if (stdoutRefAdded)
stdoutHandle.DangerousRelease();
else if (!IsInvalidHandle(stdout))
Interop.Kernel32.CloseHandle(stdout);

if (stderrRefAdded)
stderrHandle.DangerousRelease();
else if (!IsInvalidHandle(stderr))
Interop.Kernel32.CloseHandle(stderr);

ProcessUtils.s_processStartLock.ExitWriteLock();

commandLine.Dispose();
}
}

private static unsafe SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo)
{
if (!string.IsNullOrEmpty(startInfo.UserName) || startInfo.Password != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,4 +372,7 @@
<data name="NotSupportedForNonChildProcess" xml:space="preserve">
<value>On Unix, it's impossible to obtain the exit status of a non-child process.</value>
</data>
<data name="Argument_InvalidHandle" xml:space="preserve">
<value>The callback must return a valid SafeProcessHandle for the newly created process.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<Compile Include="System\Diagnostics\Process.Scenarios.cs" />
<Compile Include="System\Diagnostics\ProcessExitStatus.cs" />
<Compile Include="System\Diagnostics\ProcessOutputLine.cs" />
<Compile Include="System\Diagnostics\ProcessStartArguments.cs" />
<Compile Include="System\Diagnostics\ProcessTextOutput.cs" />
<Compile Include="System\Diagnostics\ProcessInfo.cs" />
<Compile Include="System\Diagnostics\ProcessModule.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,13 +358,20 @@ private SafeProcessHandle GetProcessHandle()
return new SafeProcessHandle(_waitStateHolder!.IncrementRefCount());
}

private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles)
private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles, Func<ProcessStartArguments, SafeProcessHandle>? callback)
{
SafeProcessHandle startedProcess = SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles, out ProcessWaitState.Holder? waitStateHolder);
ProcessWaitState.Holder? waitStateHolder = null;

SafeProcessHandle startedProcess = callback is null
? SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles, out waitStateHolder)
: SafeProcessHandle.StartWithCallback(startInfo, stdinHandle!, stdoutHandle!, stderrHandle!, callback, out waitStateHolder);

Debug.Assert(!startedProcess.IsInvalid);

// SafeProcessHandle has its own copy of the wait state holder, so we need to increment the ref count for our copy.
_waitStateHolder = waitStateHolder!.IncrementRefCount();
_waitStateHolder = callback is null
? waitStateHolder!.IncrementRefCount() // SafeProcessHandle has its own copy of the wait state holder, so we need to increment the ref count for our copy.
: waitStateHolder; // we created a dedicated holder

SetProcessHandle(startedProcess);
SetProcessId(startedProcess.ProcessId);
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,9 +508,11 @@ private SafeProcessHandle GetProcessHandle(int access, bool throwIfExited = true

private static ConsoleEncoding GetStandardOutputEncoding() => GetEncoding((int)Interop.Kernel32.GetConsoleOutputCP());

private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles)
private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles, Func<ProcessStartArguments, SafeProcessHandle>? callback)
{
SafeProcessHandle startedProcess = SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles);
SafeProcessHandle startedProcess = callback is null
? SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles)
: SafeProcessHandle.StartWithCallback(startInfo, stdinHandle!, stdoutHandle!, stderrHandle!, callback);

if (startedProcess.IsInvalid)
{
Expand Down
Loading
Loading