Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public SafeProcessHandle(System.IntPtr existingHandle, bool ownsHandle) : base (
public void Kill() { }
public int ProcessId { get { throw null; } }
protected override bool ReleaseHandle() { throw null; }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public void Resume() { }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")]
Expand Down Expand Up @@ -354,6 +356,8 @@ 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")]
public bool StartSuspended { get { throw null; } set { } }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public bool UseCredentialsForNetworkingOnly { get { throw null; } set { } }
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
public string UserName { get { throw null; } set { } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ private bool SignalCore(PosixSignal signal)
return true;
}

private static void ResumeCore()
{
throw new PlatformNotSupportedException();
}

private ProcessExitStatus WaitForExitCore()
{
ProcessWaitState waitState = GetWaitState();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali
// by the OS, which terminates all child processes in the job.
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.
private IntPtr _mainThreadHandle;

/// <summary>
/// Gets the process ID.
/// </summary>
Expand All @@ -48,6 +53,12 @@ public int ProcessId

protected override bool ReleaseHandle()
{
IntPtr threadHandle = Interlocked.Exchange(ref _mainThreadHandle, IntPtr.Zero);
if (threadHandle != IntPtr.Zero)
{
Interop.Kernel32.CloseHandle(threadHandle);
}

return Interop.Kernel32.CloseHandle(handle);
}

Expand Down Expand Up @@ -181,6 +192,8 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S
if (startInfo.CreateNoWindow) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NO_WINDOW;
if (startInfo.CreateNewProcessGroup) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NEW_PROCESS_GROUP;
if (startInfo.StartDetached) creationFlags |= Interop.Advapi32.StartupInfoOptions.DETACHED_PROCESS;
bool startSuspended = startInfo.StartSuspended;
if (startSuspended) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_SUSPENDED;

// set up the environment block parameter
string? environmentBlock = null;
Expand Down Expand Up @@ -318,7 +331,16 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S
// assign it to the job object and then resume the thread.
if (killOnParentExit && logon)
{
AssignJobAndResumeThread(processInfo.hThread, procSH);
// Assign to the job. Resume the thread only if the user didn't request StartSuspended.
AssignJobAndResumeThread(processInfo.hThread, procSH, resume: !startSuspended);
}

if (startSuspended && !IsInvalidHandle(processInfo.hThread))
{
// Store the main thread handle so that Resume() can use it later.
// The handle will be closed either in Resume() or in ReleaseHandle().
procSH._mainThreadHandle = processInfo.hThread;
processInfo.hThread = IntPtr.Zero; // Prevent the finally block from closing it.
}
}

Expand Down Expand Up @@ -657,7 +679,7 @@ private static void DisableInheritanceAndRelease(SafeHandle?[] handlesToRelease)
}
}

private static void AssignJobAndResumeThread(IntPtr hThread, SafeProcessHandle procSH)
private static void AssignJobAndResumeThread(IntPtr hThread, SafeProcessHandle procSH, bool resume)
{
Debug.Assert(!IsInvalidHandle(hThread), "Thread handle must be valid for suspended process.");

Expand All @@ -668,7 +690,7 @@ private static void AssignJobAndResumeThread(IntPtr hThread, SafeProcessHandle p
throw new Win32Exception(Marshal.GetLastWin32Error());
}

if (Interop.Kernel32.ResumeThread(hThread) == 0xFFFFFFFF)
if (resume && Interop.Kernel32.ResumeThread(hThread) == 0xFFFFFFFF)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
Expand All @@ -681,6 +703,29 @@ private static void AssignJobAndResumeThread(IntPtr hThread, SafeProcessHandle p
}
}

private void ResumeCore()
{
Validate();

IntPtr threadHandle = Interlocked.Exchange(ref _mainThreadHandle, IntPtr.Zero);
if (threadHandle == IntPtr.Zero)
{
throw new InvalidOperationException(SR.ProcessNotStartedSuspended);
}
Comment thread
adamsitnik marked this conversation as resolved.

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

private ProcessExitStatus WaitForExitCore()
{
using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,19 @@ internal static SafeProcessHandle Start(ProcessStartInfo startInfo, bool fallbac
return StartCore(startInfo, childInputHandle, childOutputHandle, childErrorHandle, inheritedHandles);
}

/// <summary>
/// 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" />.
/// </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>
[SupportedOSPlatform("windows")]
public void Resume() => ResumeCore();

/// <summary>
/// Sends a request to the OS to terminate the process.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,12 @@
<data name="StartDetachedNotCompatible" xml:space="preserve">
<value>The StartDetached property cannot be used with UseShellExecute set to true.</value>
</data>
<data name="StartSuspendedNotCompatible" xml:space="preserve">
<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>
</data>
<data name="DirectoryNotValidAsInput" xml:space="preserve">
<value>The FileName property should not be a directory unless UseShellExecute is set.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,27 @@ public string Arguments
/// </remarks>
public bool StartDetached { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the process should be started in a suspended state.
/// </summary>
/// <value><see langword="true" /> if the process should be started suspended; otherwise, <see langword="false" />. The default is <see langword="false" />.</value>
/// <remarks>
/// <para>
/// When set to <see langword="true" />, the process is created with its main thread suspended.
/// The process will not begin execution until <see cref="SafeProcessHandle.Resume" /> is called
/// on the <see cref="SafeProcessHandle" /> returned by <see cref="SafeProcessHandle.Start(ProcessStartInfo)" />.
/// </para>
/// <para>
/// On Windows, the process is started with the
/// <see href="https://learn.microsoft.com/windows/win32/procthread/process-creation-flags">CREATE_SUSPENDED</see> flag.
/// </para>
/// <para>
/// This property cannot be used together with <see cref="UseShellExecute" /> set to <see langword="true" />.
/// </para>
/// </remarks>
[SupportedOSPlatform("windows")]
public bool StartSuspended { get; set; }

/// <summary>
/// Gets or sets a <see cref="SafeFileHandle"/> that will be used as the standard input of the child process.
/// When set, the handle is passed directly to the child process and <see cref="RedirectStandardInput"/> must be <see langword="false"/>.
Expand Down Expand Up @@ -448,6 +469,11 @@ internal void ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inherite
throw new InvalidOperationException(SR.StartDetachedNotCompatible);
}

if (OperatingSystem.IsWindows() && StartSuspended && UseShellExecute)
{
throw new InvalidOperationException(SR.StartSuspendedNotCompatible);
}

if (InheritedHandles is not null && (UseShellExecute || !string.IsNullOrEmpty(UserName)))
{
throw new InvalidOperationException(SR.InheritedHandlesRequiresCreateProcess);
Expand Down
Loading
Loading