diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index 7cd0b3015d5f69..f13ad9118462b2 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -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? arguments = null) { throw null; } public override string ToString() { throw null; } public void WaitForExit() { } public bool WaitForExit(int milliseconds) { throw null; } diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs index 67f8e8efa63b8a..ca28ffe6f17fd5 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs @@ -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) @@ -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; diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index f4bba47d523bb4..8e504d19fcf803 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -223,7 +223,7 @@ The KillOnParentExit property cannot be used with UseShellExecute. - The RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError properties cannot be used by SafeProcessHandle.Start. Use the StandardInputHandle, StandardOutputHandle, and StandardErrorHandle properties. + The RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError properties cannot be used by SafeProcessHandle.Start or Process.StartAndForget. Use the StandardInputHandle, StandardOutputHandle, and StandardErrorHandle properties. The StartDetached property cannot be used with UseShellExecute set to true. @@ -363,4 +363,7 @@ InheritedHandles must not contain duplicates. + + 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. + diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index 6db2a72aeb08fa..35c04ff2541007 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -21,6 +21,7 @@ + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs new file mode 100644 index 00000000000000..9df4a1f51e50dc --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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 + { + /// + /// Starts the process described by , releases all associated resources, + /// and returns the process ID. + /// + /// The that contains the information used to start the process. + /// The process ID of the started process. + /// is . + /// + /// One or more of , + /// , or + /// is set to . + /// Stream redirection is not supported in fire-and-forget scenarios because redirected streams + /// must be drained to avoid deadlocks. + /// -or- + /// is set to . + /// 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. + /// + /// + /// + /// When a standard handle (, + /// , or ) + /// is not provided, it is redirected to the null file by default. + /// + /// + /// 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. + /// + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + ArgumentNullException.ThrowIfNull(startInfo); + + if (startInfo.UseShellExecute) + { + throw new InvalidOperationException(SR.StartAndForget_UseShellExecuteNotSupported); + } + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(startInfo, fallbackToNull: true); + return processHandle.ProcessId; + } + + /// + /// Starts a process with the specified file name and optional arguments, releases all associated resources, + /// and returns the process ID. + /// + /// The name of the application or document to start. + /// + /// The command-line arguments to pass to the process. Pass or an empty list + /// to start the process without additional arguments. + /// + /// The process ID of the started process. + /// is . + /// + /// + /// 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. + /// + /// + /// Standard handles are redirected to the null file by default. + /// + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public static int StartAndForget(string fileName, IList? arguments = null) + { + ArgumentNullException.ThrowIfNull(fileName); + + ProcessStartInfo startInfo = new(fileName); + if (arguments is not null) + { + foreach (string argument in arguments) + { + startInfo.ArgumentList.Add(argument); + } + } + + return StartAndForget(startInfo); + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs new file mode 100644 index 00000000000000..0a3d26ec319365 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs @@ -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); + + Assert.True(pid > 0); + + using Process launched = Process.GetProcessById(pid); + try + { + Assert.False(launched.HasExited); + } + finally + { + launched.Kill(); + launched.WaitForExit(); + } + } + + [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); + + Assert.True(pid > 0); + } + + [Fact] + public void StartAndForget_WithStartInfo_NullStartInfo_ThrowsArgumentNullException() + { + AssertExtensions.Throws("startInfo", () => Process.StartAndForget((ProcessStartInfo)null!)); + } + + [Fact] + public void StartAndForget_WithFileName_NullFileName_ThrowsArgumentNullException() + { + AssertExtensions.Throws("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(() => Process.StartAndForget(startInfo)); + } + + [Fact] + public void StartAndForget_WithUseShellExecute_ThrowsInvalidOperationException() + { + ProcessStartInfo startInfo = new("someprocess") + { + UseShellExecute = true, + }; + + Assert.Throws(() => Process.StartAndForget(startInfo)); + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj index dd73a7baa7f533..71bf3c5d523d76 100644 --- a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj +++ b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj @@ -38,6 +38,7 @@ +