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 @@ -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 @@ -218,6 +218,140 @@ 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.ResolvePath(startInfo.FileName);
if (string.IsNullOrEmpty(resolvedFileName))
{
Interop.ErrorInfo error = Interop.Error.ENOENT.Info();
throw ProcessUtils.CreateExceptionForErrorStartingProcess(error.GetErrorMessage(), error.RawErrno, startInfo.FileName, startInfo.WorkingDirectory);
}

string[] argv = ProcessUtils.ParseArgv(startInfo);
bool configuredTerminal = false, usesTerminal = UsesTerminal(stdinFd, stdoutHandle, stderrHandle);
byte** argvPtr = null, envpPtr = null;
byte* resolvedPathBufferPtr = null;
bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false;

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

int resolvedPathByteCount = Encoding.UTF8.GetByteCount(resolvedFileName);
// Keep small paths on the stack, while avoiding excessive stack usage for long executable paths.
const int ResolvedPathStackBufferSize =
#if DEBUG
2; // make sure we test both code paths
#else
256;
#endif
Span<byte> resolvedPathBuffer = resolvedPathByteCount + 1 <= ResolvedPathStackBufferSize
? stackalloc byte[ResolvedPathStackBufferSize]
: new Span<byte>(resolvedPathBufferPtr = (byte*)NativeMemory.Alloc((nuint)(resolvedPathByteCount + 1)), resolvedPathByteCount + 1);
resolvedPathBuffer = resolvedPathBuffer[..(resolvedPathByteCount + 1)];

int resolvedPathBytesWritten = Encoding.UTF8.GetBytes(resolvedFileName, resolvedPathBuffer);
Debug.Assert(resolvedPathBytesWritten == resolvedPathByteCount);
resolvedPathBuffer[resolvedPathBytesWritten] = (byte)0;

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

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

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

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

fixed (byte* resolvedPathPtr = resolvedPathBuffer)
{
ProcessStartArguments args = new()
{
ResolvedPath = resolvedPathPtr,
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);
NativeMemory.Free(resolvedPathBufferPtr);
}
}

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