Skip to content

Add Process.StartAndForget APIs for fire-and-forget process launching#126078

Draft
Copilot wants to merge 4 commits intomainfrom
copilot/add-start-and-forget-apis
Draft

Add Process.StartAndForget APIs for fire-and-forget process launching#126078
Copilot wants to merge 4 commits intomainfrom
copilot/add-start-and-forget-apis

Conversation

Copy link
Contributor

Copilot AI commented Mar 25, 2026

Adds two new static methods to Process for scenarios where you want to launch a process, get its PID, and immediately release all resources without waiting for it to exit.

Description

New APIs (Process.Scenarios.cs):

  • Process.StartAndForget(ProcessStartInfo) — validates no stream redirection is configured (redirected streams must be drained to prevent deadlocks, incompatible with fire-and-forget), starts the process, captures the PID, disposes the Process instance, and returns the PID.
  • Process.StartAndForget(string fileName, IList<string>? arguments = null) — builds a ProcessStartInfo and delegates to the ProcessStartInfo overload above.
// Before: resource leak unless caller explicitly disposes
Process p = Process.Start("myapp", args);
int pid = p.Id;
p.Dispose(); // easy to forget

// After: idiomatic, guaranteed cleanup
int pid = Process.StartAndForget("myapp", args);

Redirection guard — throws InvalidOperationException if any RedirectStandardInput/Output/Error flag is set, with a message explaining the deadlock risk.

Supporting changes:

  • Reference assembly updated with both signatures, including the // this needs to come after the ios attribute due to limitations in the platform analyzer comment on [SupportedOSPlatformAttribute("maccatalyst")], consistent with other Start overloads in the file
  • New string resource StartAndForget_RedirectNotSupported in Strings.resx
  • Process.Scenarios.cs added to System.Diagnostics.Process.csproj
  • Tests in StartAndForget.cs:
    • Happy paths (both overloads) merged into a single [ConditionalTheory] parameterized by bool useProcessStartInfo, with explicit Kill()/WaitForExit() cleanup in try/finally
    • ArgumentNullException tests use AssertExtensions.Throws to verify parameter names ("startInfo" and "fileName")
    • InvalidOperationException for each redirect flag combination via [Theory]
    • Null-arguments test is a plain [Fact] (no RemoteExecutor dependency) that passes null directly; uses hostname on Windows and ls on Unix (hostname is not available on Android or Azure Linux)

⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.

@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address my feedback

Copilot AI requested a review from adamsitnik March 25, 2026 09:19
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address my feedback

Copilot AI requested a review from adamsitnik March 25, 2026 10:28
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address my feedback

public void StartAndForget_WithNullArguments_StartsProcess()
{
// hostname is available on all platforms and requires no arguments
int pid = Process.StartAndForget("hostname", null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test fails on Azure Linux:

    System.Diagnostics.Tests.StartAndForgetTests.StartAndForget_WithNullArguments_StartsProcess [FAIL]
      System.ComponentModel.Win32Exception : An error occurred trying to start process 'hostname' with working directory '/root/helix/work/workitem/e'. No such file or directory
      Stack Trace:
        /_/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs(561,0): at System.Diagnostics.Process.ForkAndExecProcess(ProcessStartInfo startInfo, String resolvedFilename, String[] argv, String[] envp, String cwd, Boolean setCredentials, UInt32 userId, UInt32 groupId, UInt32[] groups, Int32& stdinFd, Int32& stdoutFd, Int32& stderrFd, Boolean usesTerminal, Boolean throwOnNoExec)
        /_/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs(456,0): at System.Diagnostics.Process.StartCore(ProcessStartInfo startInfo)
        /_/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs(1284,0): at System.Diagnostics.Process.Start()
        /_/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs(53,0): at System.Diagnostics.Process.StartAndForget(ProcessStartInfo startInfo)
        /_/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs(98,0): at System.Diagnostics.Process.StartAndForget(String fileName, IList`1 arguments)
        /_/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs(40,0): at System.Diagnostics.Tests.StartAndForgetTests.StartAndForget_WithNullArguments_StartsProcess()
        /_/src/mono/System.Private.CoreLib/src/System/Reflection/MethodBaseInvoker.Mono.cs(22,0): at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
        /_/src/libraries/System.Private.CoreLib/src/System/Reflection/MethodBaseInvoker.cs(57,0): at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

@copilot use "hostname" on Windows and "ls" on other OSes

Suggested change
int pid = Process.StartAndForget("hostname", null);
// hostname is not available on Android or Azure Linux.
// ls is available on every Unix.
int pid = Process.StartAndForget(OperatingSystem.IsWindows() ? "hostname" : "ls", null);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bff4ded — applied the suggestion exactly: hostname on Windows, ls on Unix.

<value>Invalid performance counter data with type '{0}'.</value>
</data>
<data name="StartAndForget_RedirectNotSupported" xml:space="preserve">
<value>Stream redirection is not supported by StartAndForget. Redirected streams must be drained to avoid deadlocks, which is incompatible with fire-and-forget semantics.</value>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would someone be trying to do with stream redirection? I wonder if it makes sense to nudge folks towards setting StandardXxHandle here, possible to OpenNullHandle?

Comment on lines +51 to +54
using Process process = new Process();
process.StartInfo = startInfo;
process.Start();
return process.Id;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't we want to use the SafeProcessHandle.Start functionality here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't we want to use the SafeProcessHandle.Start functionality here?

Yes, as soon as we have SafeProcessHandle.Start, this API should use it instead of Process.

And FWIW I want to expose it as soon as #125848 gets merged (just move it from Process.StartCore as is to SafeProcessHandle.Start

foreach (string argument in arguments)
{
startInfo.ArgumentList.Add(argument);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eiriktsarpalis / @tannergooding, this kind of loop comes up all the time. We should really have an AddRange for IList. We've held off on adding it as an extension method because of the possibility of instead adding it as a DIM, and decisions there have been blocked on the whole I{ReadOnly}List consolidation, but it'd be a shame if we delayed getting AddRange even further. Can we come up with a decision for how to add AddRange regardless of the I{ReadOnly}List decision?


if (startInfo.RedirectStandardInput || startInfo.RedirectStandardOutput || startInfo.RedirectStandardError)
{
throw new InvalidOperationException(SR.StartAndForget_RedirectNotSupported);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to throw? Presumably if redirection is set, the user is really just asking for it to not go to the default stdin / stdout / stderr? In which case, we could make these convenient by just setting these up to be null handles, handling the disposal of those, etc.?

Copy link
Member

@adamsitnik adamsitnik Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My take is following: so far, RedirectStandardX always meant "I want to interact with the X". With the new APIs, the users will be allowed to make it explicit (info.StandardXHandle = File.OpenNullHandle()); So I would prefer not to choose any deafult other than pipe when RedirectStandardX is set to true

I would prefer to throw for RedirectStandardX here, but I am supportive of setting all standard handles to NUL by default.

…s on Unix

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/a8a7e105-927e-424f-986b-434912ec8a8e
Copilot AI requested a review from adamsitnik March 25, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants