Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
Expand All @@ -17,7 +16,7 @@ internal static unsafe int ForkAndExecProcess(
string filename, string[] argv, IDictionary<string, string?> env, string? cwd,
bool setUser, uint userId, uint groupId, uint[]? groups,
out int lpChildPid, SafeFileHandle? stdinFd, SafeFileHandle? stdoutFd, SafeFileHandle? stderrFd,
bool startDetached, bool killOnParentExit, SafeHandle[]? inheritedHandles = null)
ProcessStartInfo startInfo, SafeHandle[]? inheritedHandles = null)
{
byte** argvPtr = null, envpPtr = null;
int result = -1;
Expand Down Expand Up @@ -76,7 +75,10 @@ internal static unsafe int ForkAndExecProcess(
filename, argvPtr, envpPtr, cwd,
setUser ? 1 : 0, userId, groupId, pGroups, groups?.Length ?? 0,
out lpChildPid, stdinRawFd, stdoutRawFd, stderrRawFd,
pInheritedFds, inheritedFdCount, startDetached ? 1 : 0, killOnParentExit ? 1 : 0);
pInheritedFds, inheritedFdCount, startInfo.StartDetached ? 1 : 0,
#pragma warning disable CA1416 // these getters work on all platforms
startInfo.KillOnParentExit ? 1 : 0, startInfo.StartSuspended ? 1 : 0);
#pragma warning restore CA1416
}
return result == 0 ? 0 : Marshal.GetLastPInvokeError();
}
Expand Down Expand Up @@ -105,7 +107,7 @@ private static unsafe partial int ForkAndExecProcess(
string filename, byte** argv, byte** envp, string? cwd,
int setUser, uint userId, uint groupId, uint* groups, int groupsLength,
out int lpChildPid, int stdinFd, int stdoutFd, int stderrFd,
int* inheritedFds, int inheritedFdCount, int startDetached, int killOnParentExit);
int* inheritedFds, int inheritedFdCount, int startDetached, int killOnParentExit, int startSuspended);

/// <summary>
/// Allocates a single native memory block containing both a null-terminated pointer array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public void Kill() { }
public int ProcessId { get { throw null; } }
protected override bool ReleaseHandle() { throw null; }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("macos")]
public void Resume() { }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
Expand Down Expand Up @@ -356,6 +357,7 @@ public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable<
public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardOutputHandle { get { throw null; } set { } }
public bool StartDetached { get { throw null; } set { } }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("macos")]
public bool StartSuspended { get { throw null; } set { } }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public bool UseCredentialsForNetworkingOnly { get { throw null; } set { } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Text;
using System.Threading;
using Microsoft.Win32.SafeHandles;

Expand All @@ -29,7 +25,6 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali
private readonly SafeWaitHandle? _handle;
private readonly bool _releaseRef;
private readonly ProcessWaitState.Holder? _waitStateHolder;

internal SafeProcessHandle(ProcessWaitState.Holder waitStateHolder) : base(ownsHandle: true)
{
_waitStateHolder = waitStateHolder;
Expand Down Expand Up @@ -136,10 +131,7 @@ private bool SignalCore(PosixSignal signal)
return true;
}

private static void ResumeCore()
{
throw new PlatformNotSupportedException();
}
private void ResumeCore() => SignalCore(PosixSignal.SIGCONT);
Comment thread
adamsitnik marked this conversation as resolved.

private ProcessExitStatus WaitForExitCore()
{
Expand Down Expand Up @@ -362,9 +354,7 @@ private static SafeProcessHandle ForkAndExecProcess(
resolvedFilename, argv, env, cwd,
setCredentials, userId, groupId, groups,
out childPid, stdinHandle, stdoutHandle, stderrHandle,
#pragma warning disable CA1416 // KillOnParentExit getter works on all platforms; the native shim is a no-op where unsupported
startInfo.StartDetached, startInfo.KillOnParentExit, inheritedHandles);
#pragma warning restore CA1416
startInfo, inheritedHandles);

if (errno == 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali
private static readonly Lazy<Interop.Kernel32.SafeJobHandle> s_killOnParentExitJob = new(CreateKillOnParentExitJob);

// When the process was started with StartSuspended, this holds the main thread handle
// so that Resume() can call ResumeThread on it. The handle is closed after Resume() is called
// or when the SafeProcessHandle is disposed.
// so that Resume() can call ResumeThread on it. The handle is closed when the SafeProcessHandle is disposed.
private IntPtr _mainThreadHandle;

/// <summary>
Expand Down Expand Up @@ -705,24 +704,14 @@ private static void AssignJobAndResumeThread(IntPtr hThread, SafeProcessHandle p

private void ResumeCore()
{
Validate();

IntPtr threadHandle = Interlocked.Exchange(ref _mainThreadHandle, IntPtr.Zero);
if (threadHandle == IntPtr.Zero)
if (_mainThreadHandle == IntPtr.Zero)
{
throw new InvalidOperationException(SR.ProcessNotStartedSuspended);
}

try
if (Interop.Kernel32.ResumeThread(_mainThreadHandle) == 0xFFFFFFFF)
{
if (Interop.Kernel32.ResumeThread(threadHandle) == 0xFFFFFFFF)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
finally
{
Interop.Kernel32.CloseHandle(threadHandle);
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,24 @@ internal static SafeProcessHandle Start(ProcessStartInfo startInfo, bool fallbac
/// Resumes the process that was started with <see cref="ProcessStartInfo.StartSuspended" /> set to <see langword="true" />.
/// </summary>
/// <remarks>
/// This method can only be called once. After the process has been resumed, calling this method again
/// throws <see cref="InvalidOperationException" />.
/// On Windows, this calls <c>ResumeThread</c> on the main thread of the process.
/// On macOS, this sends <c>SIGCONT</c> to the process.
/// </remarks>
/// <exception cref="InvalidOperationException">The process was not started with <see cref="ProcessStartInfo.StartSuspended" /> set to <see langword="true" />, or has already been resumed.</exception>
/// <exception cref="PlatformNotSupportedException">The current operating system is not Windows.</exception>
/// <exception cref="Win32Exception">The thread could not be resumed.</exception>
/// <exception cref="InvalidOperationException">The handle is invalid.</exception>
/// <exception cref="PlatformNotSupportedException">The current operating system is not supported.</exception>
/// <exception cref="Win32Exception">The OS call to resume the process failed.</exception>
[SupportedOSPlatform("windows")]
public void Resume() => ResumeCore();
[SupportedOSPlatform("macos")]
public void Resume()
{
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{
throw new PlatformNotSupportedException();
}

Validate();
ResumeCore();
Comment thread
adamsitnik marked this conversation as resolved.
}

/// <summary>
/// Sends a request to the OS to terminate the process.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@
<value>The StartSuspended property cannot be used with UseShellExecute set to true.</value>
</data>
<data name="ProcessNotStartedSuspended" xml:space="preserve">
<value>Resume can only be called on a process that was started with StartSuspended set to true and has not been resumed yet.</value>
<value>Resume can only be called on a process that was started with StartSuspended set to true.</value>
</data>
<data name="DirectoryNotValidAsInput" xml:space="preserve">
<value>The FileName property should not be a directory unless UseShellExecute is set.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,14 @@ public string Arguments
/// <see href="https://learn.microsoft.com/windows/win32/procthread/process-creation-flags">CREATE_SUSPENDED</see> flag.
/// </para>
/// <para>
/// On macOS, the process is started with the <c>POSIX_SPAWN_START_SUSPENDED</c> flag.
/// </para>
/// <para>
/// This property cannot be used together with <see cref="UseShellExecute" /> set to <see langword="true" />.
/// </para>
/// </remarks>
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("macos")]
public bool StartSuspended { get; set; }

/// <summary>
Expand Down Expand Up @@ -469,10 +473,17 @@ internal void ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inherite
throw new InvalidOperationException(SR.StartDetachedNotCompatible);
}

if (OperatingSystem.IsWindows() && StartSuspended && UseShellExecute)
#pragma warning disable CA1416 // StartSuspended getter works on all platforms; the attribute guards the actual effect
if (StartSuspended && !OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{
throw new PlatformNotSupportedException();
}

if (StartSuspended && UseShellExecute)
{
throw new InvalidOperationException(SR.StartSuspendedNotCompatible);
}
#pragma warning restore CA1416

if (InheritedHandles is not null && (UseShellExecute || !string.IsNullOrEmpty(UserName)))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
namespace System.Diagnostics.Tests
{
[ConditionalClass(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[PlatformSpecific(TestPlatforms.Windows)]
[PlatformSpecific(TestPlatforms.Windows | TestPlatforms.OSX)]
public class StartSuspendedTests : ProcessTestBase
{
[ConditionalFact]
Expand Down Expand Up @@ -53,21 +53,7 @@ public void StartSuspended_ProcessIdIsValid()
}

[ConditionalFact]
public void Resume_CalledTwice_ThrowsInvalidOperationException()
{
Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode);
process.StartInfo.StartSuspended = true;

using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo);

processHandle.Resume();

Assert.Throws<InvalidOperationException>(() => processHandle.Resume());

processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(WaitInMS));
}

[ConditionalFact]
[PlatformSpecific(TestPlatforms.Windows)]
public void Resume_OnNonSuspendedProcess_ThrowsInvalidOperationException()
{
Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode);
Expand Down Expand Up @@ -191,13 +177,13 @@ public async Task StartSuspended_WithPipeRedirection_Works()
}
}

public class StartSuspendedTests_NonWindows : ProcessTestBase
public class StartSuspendedTests_NonWindowsNonMacOS : ProcessTestBase
{
[Fact]
[SkipOnPlatform(TestPlatforms.Windows, "Resume throws PlatformNotSupportedException on non-Windows")]
public void Resume_OnNonWindows_ThrowsPlatformNotSupportedException()
[SkipOnPlatform(TestPlatforms.Windows | TestPlatforms.OSX, "Resume is supported on Windows and macOS")]
public void Resume_OnNonSupportedOS_ThrowsPlatformNotSupportedException()
{
using SafeProcessHandle handle = new();
using SafeProcessHandle handle = SafeProcessHandle.Open(Environment.ProcessId);
Assert.Throws<PlatformNotSupportedException>(() => handle.Resume());
}
}
Expand Down
25 changes: 20 additions & 5 deletions src/native/libs/System.Native/pal_process.c
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ static int32_t ForkAndExecProcessInternal(
const char* filename, char* const argv[], char* const envp[], const char* cwd,
int32_t setCredentials, uint32_t userId, uint32_t groupId, uint32_t* groups, int32_t groupsLength,
int32_t* childPid, int32_t stdinFd, int32_t stdoutFd, int32_t stderrFd,
int32_t* inheritedFds, int32_t inheritedFdCount, int32_t startDetached, int32_t applyPDeathSig);
int32_t* inheritedFds, int32_t inheritedFdCount, int32_t startDetached, int32_t applyPDeathSig, int32_t startSuspended);

#if HAVE_PR_SET_PDEATHSIG
// Dedicated thread infrastructure for PR_SET_PDEATHSIG.
Expand Down Expand Up @@ -401,7 +401,7 @@ static void* PDeathSigThreadFunc(void* arg)
req->filename, req->argv, req->envp, req->cwd,
req->setCredentials, req->userId, req->groupId, req->groups, req->groupsLength,
&childPid, req->stdinFd, req->stdoutFd, req->stderrFd,
req->inheritedFds, req->inheritedFdCount, req->startDetached, 1);
req->inheritedFds, req->inheritedFdCount, req->startDetached, 1, 0);
req->childPid = childPid;
req->errnoValue = errno;

Expand Down Expand Up @@ -535,7 +535,8 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename,
int32_t* inheritedFds,
int32_t inheritedFdCount,
int32_t startDetached,
int32_t killOnParentExit)
int32_t killOnParentExit,
int32_t startSuspended)
{
#if HAVE_PR_SET_PDEATHSIG
if (killOnParentExit)
Expand All @@ -554,14 +555,14 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename,
filename, argv, envp, cwd,
setCredentials, userId, groupId, groups, groupsLength,
childPid, stdinFd, stdoutFd, stderrFd,
inheritedFds, inheritedFdCount, startDetached, 0);
inheritedFds, inheritedFdCount, startDetached, 0, startSuspended);
}

static int32_t ForkAndExecProcessInternal(
const char* filename, char* const argv[], char* const envp[], const char* cwd,
int32_t setCredentials, uint32_t userId, uint32_t groupId, uint32_t* groups, int32_t groupsLength,
int32_t* childPid, int32_t stdinFd, int32_t stdoutFd, int32_t stderrFd,
int32_t* inheritedFds, int32_t inheritedFdCount, int32_t startDetached, int32_t applyPDeathSig)
int32_t* inheritedFds, int32_t inheritedFdCount, int32_t startDetached, int32_t applyPDeathSig, int32_t startSuspended)
{
#if HAVE_FORK || defined(TARGET_OSX) || defined(TARGET_MACCATALYST)
assert(NULL != filename && NULL != argv && NULL != envp && NULL != childPid &&
Expand Down Expand Up @@ -667,6 +668,12 @@ static int32_t ForkAndExecProcessInternal(
flags |= POSIX_SPAWN_SETSID;
}

// When startSuspended is set, start the process in a suspended state.
if (startSuspended)
{
flags |= POSIX_SPAWN_START_SUSPENDED;
}

if ((result = posix_spawnattr_setflags(&attr, flags)) != 0
|| (result = posix_spawnattr_setsigdefault(&attr, &sigdefault_set)) != 0
|| (result = posix_spawnattr_setsigmask(&attr, &current_mask)) != 0 // Set the child's signal mask to match the parent's current mask
Expand Down Expand Up @@ -732,6 +739,13 @@ static int32_t ForkAndExecProcessInternal(
#endif

#if HAVE_FORK
if (startSuspended)
{
// POSIX_SPAWN_START_SUSPENDED is only available in the posix_spawn() path (macOS, !setCredentials).
// The fork() path does not support startSuspended.
errno = ENOTSUP;
return -1;
}
bool success = true;
int waitForChildToExecPipe[2] = {-1, -1};
pid_t processId = -1;
Expand Down Expand Up @@ -1012,6 +1026,7 @@ done:;
(void)inheritedFdCount;
(void)startDetached;
(void)applyPDeathSig;
(void)startSuspended;
return -1;
#endif
}
Expand Down
3 changes: 2 additions & 1 deletion src/native/libs/System.Native/pal_process.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ PALEXPORT int32_t SystemNative_ForkAndExecProcess(
int32_t* inheritedFds, // array of fds to explicitly inherit (-1 to disable restriction)
int32_t inheritedFdCount, // count of fds in inheritedFds; -1 means no restriction
int32_t startDetached, // whether to start the process as a leader of a new session
int32_t killOnParentExit); // whether to kill the child when the parent exits
int32_t killOnParentExit, // whether to kill the child when the parent exits
int32_t startSuspended); // whether to start the process in a suspended state (macOS only)
Comment thread
adamsitnik marked this conversation as resolved.

/************
* The values below in the header are fixed and correct for managed callers to use forever.
Expand Down
3 changes: 2 additions & 1 deletion src/native/libs/System.Native/pal_process_wasi.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename,
int32_t* inheritedFds,
int32_t inheritedFdCount,
int32_t startDetached,
int32_t killOnParentExit)
int32_t killOnParentExit,
int32_t startSuspended)
{
return -1;
}
Comment thread
adamsitnik marked this conversation as resolved.
Expand Down
Loading