Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ad79bd9
Add Process.StartAndForget APIs for fire-and-forget scenarios
Copilot Mar 25, 2026
0f8be81
Address review feedback on StartAndForget tests and ref file
Copilot Mar 25, 2026
9be6c1c
Fix StartAndForget_WithNullArguments_StartsProcess to actually pass null
Copilot Mar 25, 2026
bff4ded
Fix StartAndForget_WithNullArguments test: use hostname on Windows, l…
Copilot Mar 25, 2026
c6a0adb
Sync with main (PR #126192) and use SafeProcessHandle.Start in StartA…
Copilot Mar 30, 2026
0f411da
Revert "Sync with main (PR #126192) and use SafeProcessHandle.Start i…
adamsitnik Mar 30, 2026
295397e
Merge remote-tracking branch 'origin/main' into copilot/add-start-and…
adamsitnik Mar 30, 2026
ea70470
address my own feedback
adamsitnik Mar 30, 2026
c093f04
fix a bug discovered by code review
adamsitnik Mar 30, 2026
1a36d81
Address review feedback: fix typo, remove duplicate SerializationGuar…
Copilot Mar 30, 2026
3506a11
address code review feedback: don't call SerializationGuard.ThrowIfDe…
adamsitnik Mar 31, 2026
fd129ae
Apply suggestion from @adamsitnik
adamsitnik Mar 31, 2026
016c107
Merge remote-tracking branch 'origin/main' into copilot/add-start-and…
adamsitnik Mar 31, 2026
a4ca369
Merge remote-tracking branch 'origin/main' into copilot/add-start-and…
adamsitnik Apr 8, 2026
14d1235
use the new APIs to limit handle inheritance by default
adamsitnik Apr 8, 2026
7d9bbf0
Throw InvalidOperationException for UseShellExecute=true in StartAndF…
Copilot Apr 8, 2026
44d8f6a
Update docs and remove unused using in StartAndForget
Copilot Apr 8, 2026
8d3bbfd
Merge remote-tracking branch 'origin/main' into copilot/add-start-and…
adamsitnik Apr 16, 2026
d4a941b
update the implementation based on experience from StartDetached and …
adamsitnik Apr 16, 2026
a2b586a
Fix unused usings in Process.Scenarios.cs and improve redirect error …
Copilot Apr 16, 2026
1380afd
Address review feedback: rename fallbackToNull, simplify summaries, r…
Copilot Apr 16, 2026
398d6ac
Apply suggestion: add 'using' to Process template declaration in Star…
Copilot Apr 16, 2026
e3fff40
Add StartAndForget_WithStandardOutputHandle_CapturesOutput test
Copilot Apr 16, 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 @@ -188,6 +188,14 @@ public void Refresh() { }
[System.CLSCompliantAttribute(false)]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public static System.Diagnostics.Process? Start(string fileName, string arguments, string userName, System.Security.SecureString password, string domain) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
[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 int StartAndForget(System.Diagnostics.ProcessStartInfo startInfo) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
[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 int StartAndForget(string fileName, System.Collections.Generic.IList<string>? arguments = null) { throw null; }
Comment thread
adamsitnik marked this conversation as resolved.
public override string ToString() { throw null; }
public void WaitForExit() { }
public bool WaitForExit(int milliseconds) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ public SafeProcessHandle(IntPtr existingHandle, bool ownsHandle)
public static SafeProcessHandle Start(ProcessStartInfo startInfo)
{
ArgumentNullException.ThrowIfNull(startInfo);

return Start(startInfo, fallbackToNull: startInfo.StartDetached);
}

[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("tvos")]
[SupportedOSPlatform("maccatalyst")]
internal static SafeProcessHandle Start(ProcessStartInfo startInfo, bool fallbackToNull)
{
startInfo.ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inheritedHandles);

if (anyRedirection)
Expand All @@ -110,7 +119,7 @@ public static SafeProcessHandle Start(ProcessStartInfo startInfo)
SafeFileHandle? childOutputHandle = startInfo.StandardOutputHandle;
SafeFileHandle? childErrorHandle = startInfo.StandardErrorHandle;

using SafeFileHandle? nullDeviceHandle = startInfo.StartDetached
using SafeFileHandle? nullDeviceHandle = fallbackToNull
&& (childInputHandle is null || childOutputHandle is null || childErrorHandle is null)
? File.OpenNullHandle()
: null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@
<value>The KillOnParentExit property cannot be used with UseShellExecute.</value>
</data>
<data name="CantSetRedirectForSafeProcessHandleStart" xml:space="preserve">
<value>The RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError properties cannot be used by SafeProcessHandle.Start. Use the StandardInputHandle, StandardOutputHandle, and StandardErrorHandle properties.</value>
<value>The RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError properties cannot be used by SafeProcessHandle.Start or Process.StartAndForget. Use the StandardInputHandle, StandardOutputHandle, and StandardErrorHandle properties.</value>
</data>
<data name="StartDetachedNotCompatible" xml:space="preserve">
<value>The StartDetached property cannot be used with UseShellExecute set to true.</value>
Expand Down Expand Up @@ -363,4 +363,7 @@
<data name="InheritedHandles_MustNotContainDuplicates" xml:space="preserve">
<value>InheritedHandles must not contain duplicates.</value>
</data>
<data name="StartAndForget_UseShellExecuteNotSupported" xml:space="preserve">
<value>UseShellExecute is not supported by StartAndForget. On Windows, shell execution may not create a new process, which would make it impossible to return a valid process ID.</value>
Comment thread
adamsitnik marked this conversation as resolved.
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<Compile Include="System\Diagnostics\DataReceivedEventArgs.cs" />
<Compile Include="System\Diagnostics\Process.cs" />
<Compile Include="System\Diagnostics\Process.Multiplexing.cs" />
<Compile Include="System\Diagnostics\Process.Scenarios.cs" />
<Compile Include="System\Diagnostics\ProcessExitStatus.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
@@ -0,0 +1,99 @@
// Licensed to the .NET Foundation under one or more agreements.
Comment thread
danmoseley marked this conversation as resolved.
Comment thread
adamsitnik marked this conversation as resolved.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Runtime.Versioning;
using Microsoft.Win32.SafeHandles;

namespace System.Diagnostics
{
public partial class Process
{
/// <summary>
/// Starts the process described by <paramref name="startInfo"/>, releases all associated resources,
/// and returns the process ID.
/// </summary>
/// <param name="startInfo">The <see cref="ProcessStartInfo"/> that contains the information used to start the process.</param>
/// <returns>The process ID of the started process.</returns>
/// <exception cref="ArgumentNullException"><paramref name="startInfo"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">
/// <para>One or more of <see cref="ProcessStartInfo.RedirectStandardInput"/>,
/// <see cref="ProcessStartInfo.RedirectStandardOutput"/>, or
/// <see cref="ProcessStartInfo.RedirectStandardError"/> is set to <see langword="true"/>.
/// Stream redirection is not supported in fire-and-forget scenarios because redirected streams
/// must be drained to avoid deadlocks.</para>
/// <para>-or-</para>
/// <para><see cref="ProcessStartInfo.UseShellExecute"/> is set to <see langword="true"/>.
/// Shell execution is not supported in fire-and-forget scenarios because on Windows it may not
/// create a new process, making it impossible to return a valid process ID.</para>
/// </exception>
/// <remarks>
/// <para>
/// When a standard handle (<see cref="ProcessStartInfo.StandardInputHandle"/>,
/// <see cref="ProcessStartInfo.StandardOutputHandle"/>, or <see cref="ProcessStartInfo.StandardErrorHandle"/>)
/// is not provided, it is redirected to the null file by default.
Comment thread
adamsitnik marked this conversation as resolved.
/// </para>
Comment thread
adamsitnik marked this conversation as resolved.
/// <para>
/// This method is designed for fire-and-forget scenarios where the caller wants to launch a process
/// and does not need to interact with it further. It starts the process, releases all associated
/// resources, and returns the process ID. The started process continues to run independently.
/// </para>
/// </remarks>
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("tvos")]
[SupportedOSPlatform("maccatalyst")]
public static int StartAndForget(ProcessStartInfo startInfo)
{
ArgumentNullException.ThrowIfNull(startInfo);
Comment thread
adamsitnik marked this conversation as resolved.

if (startInfo.UseShellExecute)
{
throw new InvalidOperationException(SR.StartAndForget_UseShellExecuteNotSupported);
}

using SafeProcessHandle processHandle = SafeProcessHandle.Start(startInfo, fallbackToNull: true);
return processHandle.ProcessId;
}

/// <summary>
/// Starts a process with the specified file name and optional arguments, releases all associated resources,
/// and returns the process ID.
/// </summary>
/// <param name="fileName">The name of the application or document to start.</param>
/// <param name="arguments">
/// The command-line arguments to pass to the process. Pass <see langword="null"/> or an empty list
/// to start the process without additional arguments.
/// </param>
/// <returns>The process ID of the started process.</returns>
/// <exception cref="ArgumentNullException"><paramref name="fileName"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This method is designed for fire-and-forget scenarios where the caller wants to launch a process
/// and does not need to interact with it further. It starts the process, captures its process ID,
/// releases all associated resources, and returns the process ID. The started process continues to
/// run independently.
/// </para>
/// <para>
/// Standard handles are redirected to the null file by default.
/// </para>
Comment thread
adamsitnik marked this conversation as resolved.
/// </remarks>
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("tvos")]
[SupportedOSPlatform("maccatalyst")]
public static int StartAndForget(string fileName, IList<string>? arguments = null)
{
ArgumentNullException.ThrowIfNull(fileName);

ProcessStartInfo startInfo = new(fileName);
if (arguments is not null)
{
foreach (string argument in arguments)
{
startInfo.ArgumentList.Add(argument);
}
Comment thread
adamsitnik marked this conversation as resolved.
}

return StartAndForget(startInfo);
}
}
}
114 changes: 114 additions & 0 deletions src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using Microsoft.DotNet.RemoteExecutor;
using Microsoft.Win32.SafeHandles;
using Xunit;

namespace System.Diagnostics.Tests
{
public class StartAndForgetTests : ProcessTestBase
{
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[InlineData(true)]
[InlineData(false)]
public void StartAndForget_StartsProcessAndReturnsValidPid(bool useProcessStartInfo)
{
using Process template = CreateSleepProcess((int)TimeSpan.FromHours(1).TotalMilliseconds);
int pid = useProcessStartInfo
? Process.StartAndForget(template.StartInfo)
: Process.StartAndForget(template.StartInfo.FileName, template.StartInfo.ArgumentList);
Comment thread
adamsitnik marked this conversation as resolved.

Assert.True(pid > 0);

using Process launched = Process.GetProcessById(pid);
try
{
Assert.False(launched.HasExited);
}
finally
{
launched.Kill();
launched.WaitForExit();
Comment thread
adamsitnik marked this conversation as resolved.
}
}

Comment thread
adamsitnik marked this conversation as resolved.
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public void StartAndForget_WithStandardOutputHandle_CapturesOutput()
{
using Process template = CreateProcess(static () =>
{
Console.Write("hello");
return RemoteExecutor.SuccessExitCode;
});

SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle outputReadPipe, out SafeFileHandle outputWritePipe);

using (outputReadPipe)
using (outputWritePipe)
{
template.StartInfo.StandardOutputHandle = outputWritePipe;

int pid = Process.StartAndForget(template.StartInfo);
Assert.True(pid > 0);

outputWritePipe.Close(); // close the parent copy of child handle

using StreamReader streamReader = new(new FileStream(outputReadPipe, FileAccess.Read, bufferSize: 1, outputReadPipe.IsAsync));
Assert.Equal("hello", streamReader.ReadToEnd());
}
}

// This test does not use RemoteExecutor, but it's a simple way to filter to OSes that support Process.Start.
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public void StartAndForget_WithNullArguments_StartsProcess()
{
// cmd is available on every Windows, including Nano. When run with no parameters, it displays the Windows version/copyright banner.
// true is available on every Unix. When invoked with no arguments, it does nothing and exits successfully.
int pid = Process.StartAndForget(OperatingSystem.IsWindows() ? "cmd.exe" : "true", null);
Comment thread
adamsitnik marked this conversation as resolved.

Assert.True(pid > 0);
}

[Fact]
public void StartAndForget_WithStartInfo_NullStartInfo_ThrowsArgumentNullException()
{
AssertExtensions.Throws<ArgumentNullException>("startInfo", () => Process.StartAndForget((ProcessStartInfo)null!));
}

[Fact]
public void StartAndForget_WithFileName_NullFileName_ThrowsArgumentNullException()
{
AssertExtensions.Throws<ArgumentNullException>("fileName", () => Process.StartAndForget((string)null!));
}

[Theory]
[InlineData(true, false, false)]
[InlineData(false, true, false)]
[InlineData(false, false, true)]
public void StartAndForget_WithRedirectedStreams_ThrowsInvalidOperationException(
bool redirectInput, bool redirectOutput, bool redirectError)
{
ProcessStartInfo startInfo = new("someprocess")
{
RedirectStandardInput = redirectInput,
RedirectStandardOutput = redirectOutput,
RedirectStandardError = redirectError,
};

Assert.Throws<InvalidOperationException>(() => Process.StartAndForget(startInfo));
}

[Fact]
public void StartAndForget_WithUseShellExecute_ThrowsInvalidOperationException()
{
ProcessStartInfo startInfo = new("someprocess")
{
UseShellExecute = true,
};

Assert.Throws<InvalidOperationException>(() => Process.StartAndForget(startInfo));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<Compile Include="ProcessThreadTests.cs" />
<Compile Include="ProcessWaitingTests.cs" />
<Compile Include="RemotelyInvokable.cs" />
<Compile Include="StartAndForget.cs" />
<Compile Include="AssemblyInfo.cs" />
<Compile Include="SafeProcessHandleTests.cs" />
<Compile Include="KillOnParentExitTests.cs" />
Expand Down
Loading