From ad79bd99ecf98b05138f0d8821415b910fbbf98f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:59:56 +0000 Subject: [PATCH 01/19] Add Process.StartAndForget APIs for fire-and-forget scenarios Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/095d096c-d5d9-4ad1-b5f2-45fbadf30315 --- .../ref/System.Diagnostics.Process.cs | 8 ++ .../src/Resources/Strings.resx | 3 + .../src/System.Diagnostics.Process.csproj | 1 + .../System/Diagnostics/Process.Scenarios.cs | 101 ++++++++++++++++++ .../tests/StartAndForget.cs | 88 +++++++++++++++ .../System.Diagnostics.Process.Tests.csproj | 1 + 6 files changed, 202 insertions(+) create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs create mode 100644 src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs 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 3904ae89bd0b15..c525d935079e54 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -171,6 +171,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")] + 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")] + 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/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index 24e76b6da21e1e..bfed1eff99beaa 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -336,4 +336,7 @@ Invalid performance counter data with type '{0}'. + + Stream redirection is not supported by StartAndForget. Redirected streams must be drained to avoid deadlocks, which is incompatible with fire-and-forget semantics. + 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 20397ff4889e42..19a7a7e034df0c 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -20,6 +20,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..2557bf6f5733d4 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -0,0 +1,101 @@ +// 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; + +namespace System.Diagnostics +{ + public partial class Process + { + /// + /// Starts the process described by , captures its process ID, + /// 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. + /// + /// + /// + /// 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, + /// disposes the instance to release all associated resources, and returns the + /// process ID. The started process continues to run independently. + /// + /// + /// Calling this method ensures proper resource cleanup on the caller's side: unlike calling + /// and discarding the returned object, this method guarantees + /// that the underlying operating-system resources held by the object are + /// released promptly. + /// + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + ArgumentNullException.ThrowIfNull(startInfo); + + if (startInfo.RedirectStandardInput || startInfo.RedirectStandardOutput || startInfo.RedirectStandardError) + { + throw new InvalidOperationException(SR.StartAndForget_RedirectNotSupported); + } + + using Process process = new Process(); + process.StartInfo = startInfo; + process.Start(); + return process.Id; + } + + /// + /// Starts a process with the specified file name and optional arguments, captures its process ID, + /// 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, + /// disposes the instance to release all associated resources, and returns the + /// process ID. The started process continues to run independently. + /// + /// + /// Calling this method ensures proper resource cleanup on the caller's side: unlike calling + /// and discarding the returned object, this method guarantees that the + /// underlying operating-system resources held by the object are released + /// promptly. + /// + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public static int StartAndForget(string fileName, IList? arguments = null) + { + ArgumentNullException.ThrowIfNull(fileName); + + ProcessStartInfo startInfo = new ProcessStartInfo(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..42f843d666f0a2 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs @@ -0,0 +1,88 @@ +// 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 Microsoft.DotNet.RemoteExecutor; +using Xunit; + +namespace System.Diagnostics.Tests +{ + public class StartAndForgetTests : ProcessTestBase + { + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void StartAndForget_WithProcessStartInfo_StartsProcessAndReturnsValidPid() + { + Process template = CreateProcessLong(); + ProcessStartInfo startInfo = template.StartInfo; + + int pid = Process.StartAndForget(startInfo); + + Assert.True(pid > 0); + + // Verify the process is actually running by retrieving it, then clean up. + using Process launched = Process.GetProcessById(pid); + AddProcessForDispose(launched); + Assert.False(launched.HasExited); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void StartAndForget_WithFileNameAndArguments_StartsProcessAndReturnsValidPid() + { + Process template = CreateProcessLong(); + string fileName = template.StartInfo.FileName; + IList arguments = template.StartInfo.ArgumentList; + + int pid = Process.StartAndForget(fileName, arguments); + + Assert.True(pid > 0); + + using Process launched = Process.GetProcessById(pid); + AddProcessForDispose(launched); + Assert.False(launched.HasExited); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void StartAndForget_WithNullArguments_StartsProcess() + { + // A quick process: use CreateProcess with a simple exit-immediately function + Process template = CreateProcess(() => RemoteExecutor.SuccessExitCode); + string fileName = template.StartInfo.FileName; + IList arguments = template.StartInfo.ArgumentList; + + // Passing null arguments is valid (no extra arguments beyond what fileName needs) + // Use the explicit argument list so it actually works + int pid = Process.StartAndForget(fileName, arguments); + + Assert.True(pid > 0); + } + + [Fact] + public void StartAndForget_WithStartInfo_NullStartInfo_ThrowsArgumentNullException() + { + Assert.Throws(() => Process.StartAndForget((ProcessStartInfo)null!)); + } + + [Fact] + public void StartAndForget_WithFileName_NullFileName_ThrowsArgumentNullException() + { + Assert.Throws(() => 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)); + } + } +} 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 e44431ae36405a..a1387f37a568a4 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 @@ -36,6 +36,7 @@ + From 0f8be8139092594ed27663cd79db740d1c40fe7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:17:16 +0000 Subject: [PATCH 02/19] Address review feedback on StartAndForget tests and ref file Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/942ffa7e-9504-40eb-9a58-9c4b6ceee3c1 --- .../ref/System.Diagnostics.Process.cs | 4 +- .../tests/StartAndForget.cs | 52 +++++++------------ 2 files changed, 21 insertions(+), 35 deletions(-) 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 c525d935079e54..f863af7e0c8123 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -173,11 +173,11 @@ public void Refresh() { } 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")] + [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")] + [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() { } diff --git a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs index 42f843d666f0a2..34c11b243eb09b 100644 --- a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs +++ b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs @@ -9,49 +9,35 @@ namespace System.Diagnostics.Tests { public class StartAndForgetTests : ProcessTestBase { - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void StartAndForget_WithProcessStartInfo_StartsProcessAndReturnsValidPid() + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public void StartAndForget_StartsProcessAndReturnsValidPid(bool useProcessStartInfo) { Process template = CreateProcessLong(); - ProcessStartInfo startInfo = template.StartInfo; - - int pid = Process.StartAndForget(startInfo); + int pid = useProcessStartInfo + ? Process.StartAndForget(template.StartInfo) + : Process.StartAndForget(template.StartInfo.FileName, template.StartInfo.ArgumentList); Assert.True(pid > 0); - // Verify the process is actually running by retrieving it, then clean up. using Process launched = Process.GetProcessById(pid); - AddProcessForDispose(launched); - Assert.False(launched.HasExited); - } - - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void StartAndForget_WithFileNameAndArguments_StartsProcessAndReturnsValidPid() - { - Process template = CreateProcessLong(); - string fileName = template.StartInfo.FileName; - IList arguments = template.StartInfo.ArgumentList; - - int pid = Process.StartAndForget(fileName, arguments); - - Assert.True(pid > 0); - - using Process launched = Process.GetProcessById(pid); - AddProcessForDispose(launched); - Assert.False(launched.HasExited); + try + { + Assert.False(launched.HasExited); + } + finally + { + launched.Kill(); + launched.WaitForExit(); + } } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public void StartAndForget_WithNullArguments_StartsProcess() { - // A quick process: use CreateProcess with a simple exit-immediately function Process template = CreateProcess(() => RemoteExecutor.SuccessExitCode); - string fileName = template.StartInfo.FileName; - IList arguments = template.StartInfo.ArgumentList; - - // Passing null arguments is valid (no extra arguments beyond what fileName needs) - // Use the explicit argument list so it actually works - int pid = Process.StartAndForget(fileName, arguments); + int pid = Process.StartAndForget(template.StartInfo.FileName, template.StartInfo.ArgumentList); Assert.True(pid > 0); } @@ -59,13 +45,13 @@ public void StartAndForget_WithNullArguments_StartsProcess() [Fact] public void StartAndForget_WithStartInfo_NullStartInfo_ThrowsArgumentNullException() { - Assert.Throws(() => Process.StartAndForget((ProcessStartInfo)null!)); + AssertExtensions.Throws("startInfo", () => Process.StartAndForget((ProcessStartInfo)null!)); } [Fact] public void StartAndForget_WithFileName_NullFileName_ThrowsArgumentNullException() { - Assert.Throws(() => Process.StartAndForget((string)null!)); + AssertExtensions.Throws("fileName", () => Process.StartAndForget((string)null!)); } [Theory] From 9be6c1cfc81a6b9afaaa9845df7db5bb687b0f36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 10:27:26 +0000 Subject: [PATCH 03/19] Fix StartAndForget_WithNullArguments_StartsProcess to actually pass null Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/71890478-dd77-4849-8278-bb3194c8489f --- .../System.Diagnostics.Process/tests/StartAndForget.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs index 34c11b243eb09b..d949c723065d49 100644 --- a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs +++ b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs @@ -33,11 +33,11 @@ public void StartAndForget_StartsProcessAndReturnsValidPid(bool useProcessStartI } } - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [Fact] public void StartAndForget_WithNullArguments_StartsProcess() { - Process template = CreateProcess(() => RemoteExecutor.SuccessExitCode); - int pid = Process.StartAndForget(template.StartInfo.FileName, template.StartInfo.ArgumentList); + // hostname is available on all platforms and requires no arguments + int pid = Process.StartAndForget("hostname", null); Assert.True(pid > 0); } From bff4ded6c444025a210934b253596082b9bca6ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:25:54 +0000 Subject: [PATCH 04/19] Fix StartAndForget_WithNullArguments test: use hostname on Windows, ls 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 --- .../System.Diagnostics.Process/tests/StartAndForget.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs index d949c723065d49..b7685333766186 100644 --- a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs +++ b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs @@ -36,8 +36,9 @@ public void StartAndForget_StartsProcessAndReturnsValidPid(bool useProcessStartI [Fact] public void StartAndForget_WithNullArguments_StartsProcess() { - // hostname is available on all platforms and requires no arguments - 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); Assert.True(pid > 0); } From c6a0adb2d486ef641e66a3e121eff1f7fb874719 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:50:19 +0000 Subject: [PATCH 05/19] Sync with main (PR #126192) and use SafeProcessHandle.Start in StartAndForget Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/6ec5336e-ce95-4d76-ad71-62ab2f1b4a81 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Interop.ForkAndExecProcess.cs | 152 ++++- .../Unix/System.Native/Interop.IsATty.cs | 5 + ...omicNonInheritablePipeCreationSupported.cs | 30 + .../Kernel32/Interop.HandleInformation.cs | 4 + .../ref/System.Diagnostics.Process.cs | 12 +- .../SafeHandles/SafeProcessHandle.Unix.cs | 214 +++++- .../SafeHandles/SafeProcessHandle.Windows.cs | 278 +++++++- .../Win32/SafeHandles/SafeProcessHandle.cs | 85 +++ .../src/Resources/Strings.resx | 12 + .../src/System.Diagnostics.Process.csproj | 17 +- .../src/System/Diagnostics/Process.FreeBSD.cs | 2 +- .../src/System/Diagnostics/Process.Linux.cs | 2 +- .../src/System/Diagnostics/Process.OSX.cs | 2 +- .../System/Diagnostics/Process.Scenarios.cs | 7 +- .../src/System/Diagnostics/Process.SunOS.cs | 2 +- .../src/System/Diagnostics/Process.Unix.cs | 634 +----------------- .../src/System/Diagnostics/Process.Win32.cs | 172 +---- .../src/System/Diagnostics/Process.Windows.cs | 333 +-------- .../src/System/Diagnostics/Process.cs | 163 ++++- .../src/System/Diagnostics/Process.iOS.cs | 2 +- .../System/Diagnostics/ProcessStartInfo.cs | 156 ++++- ...ConfigureTerminalForChildProcesses.Unix.cs | 64 ++ ....ConfigureTerminalForChildProcesses.iOS.cs | 20 + .../System/Diagnostics/ProcessUtils.Unix.cs | 405 +++++++++++ .../Diagnostics/ProcessUtils.Windows.cs | 101 +++ .../src/System/Diagnostics/ProcessUtils.cs | 12 + .../Diagnostics/ProcessWaitState.Unix.cs | 2 +- .../System/Diagnostics/ShellExecuteHelper.cs | 94 +++ 28 files changed, 1761 insertions(+), 1221 deletions(-) create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsAtomicNonInheritablePipeCreationSupported.cs create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ShellExecuteHelper.cs diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs index bc947d0ef4ef49..fe7b7f8fda0e4c 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs @@ -2,91 +2,169 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; +using Microsoft.Win32.SafeHandles; internal static partial class Interop { internal static partial class Sys { internal static unsafe int ForkAndExecProcess( - string filename, string[] argv, string[] envp, string? cwd, - bool redirectStdin, bool redirectStdout, bool redirectStderr, + string filename, string[] argv, IDictionary env, string? cwd, bool setUser, uint userId, uint groupId, uint[]? groups, - out int lpChildPid, out int stdinFd, out int stdoutFd, out int stderrFd, bool shouldThrow = true) + out int lpChildPid, SafeFileHandle? stdinFd, SafeFileHandle? stdoutFd, SafeFileHandle? stderrFd, bool shouldThrow = true) { byte** argvPtr = null, envpPtr = null; int result = -1; + + bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false; try { - AllocNullTerminatedArray(argv, ref argvPtr); - AllocNullTerminatedArray(envp, ref envpPtr); + int stdinRawFd = -1, stdoutRawFd = -1, stderrRawFd = -1; + + if (stdinFd is not null) + { + stdinFd.DangerousAddRef(ref stdinRefAdded); + stdinRawFd = stdinFd.DangerousGetHandle().ToInt32(); + } + + if (stdoutFd is not null) + { + stdoutFd.DangerousAddRef(ref stdoutRefAdded); + stdoutRawFd = stdoutFd.DangerousGetHandle().ToInt32(); + } + + if (stderrFd is not null) + { + stderrFd.DangerousAddRef(ref stderrRefAdded); + stderrRawFd = stderrFd.DangerousGetHandle().ToInt32(); + } + + AllocArgvArray(argv, ref argvPtr); + AllocEnvpArray(env, ref envpPtr); fixed (uint* pGroups = groups) { result = ForkAndExecProcess( filename, argvPtr, envpPtr, cwd, - redirectStdin ? 1 : 0, redirectStdout ? 1 : 0, redirectStderr ? 1 : 0, setUser ? 1 : 0, userId, groupId, pGroups, groups?.Length ?? 0, - out lpChildPid, out stdinFd, out stdoutFd, out stderrFd); + out lpChildPid, stdinRawFd, stdoutRawFd, stderrRawFd); } return result == 0 ? 0 : Marshal.GetLastPInvokeError(); } finally { - FreeArray(envpPtr, envp.Length); - FreeArray(argvPtr, argv.Length); + NativeMemory.Free(envpPtr); + NativeMemory.Free(argvPtr); + + if (stdinRefAdded) + stdinFd!.DangerousRelease(); + if (stdoutRefAdded) + stdoutFd!.DangerousRelease(); + if (stderrRefAdded) + stderrFd!.DangerousRelease(); } } [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_ForkAndExecProcess", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] private static unsafe partial int ForkAndExecProcess( string filename, byte** argv, byte** envp, string? cwd, - int redirectStdin, int redirectStdout, int redirectStderr, int setUser, uint userId, uint groupId, uint* groups, int groupsLength, - out int lpChildPid, out int stdinFd, out int stdoutFd, out int stderrFd); + out int lpChildPid, int stdinFd, int stdoutFd, int stderrFd); - private static unsafe void AllocNullTerminatedArray(string[] arr, ref byte** arrPtr) + /// + /// Allocates a single native memory block containing both a null-terminated pointer array + /// and the UTF-8 encoded string data for the given array of strings. + /// + private static unsafe void AllocArgvArray(string[] arr, ref byte** arrPtr) { - nuint arrLength = (nuint)arr.Length + 1; // +1 is for null termination - - // Allocate the unmanaged array to hold each string pointer. - // It needs to have an extra element to null terminate the array. - // Zero the memory so that if any of the individual string allocations fails, - // we can loop through the array to free any that succeeded. - // The last element will remain null. - arrPtr = (byte**)NativeMemory.AllocZeroed(arrLength, (nuint)sizeof(byte*)); - - // Now copy each string to unmanaged memory referenced from the array. - // We need the data to be an unmanaged, null-terminated array of UTF8-encoded bytes. - for (int i = 0; i < arr.Length; i++) + int count = arr.Length; + + // First pass: compute total byte length of all strings. + int dataByteLength = 0; + foreach (string str in arr) { - string str = arr[i]; + dataByteLength = checked(dataByteLength + Encoding.UTF8.GetByteCount(str) + 1); // +1 for null terminator + } + + // Allocate a single block: pointer array (count + 1 for null terminator) followed by string data. + nuint pointersByteLength = checked((nuint)(count + 1) * (nuint)sizeof(byte*)); + byte* block = (byte*)NativeMemory.Alloc(checked(pointersByteLength + (nuint)dataByteLength)); + arrPtr = (byte**)block; - int byteLength = Encoding.UTF8.GetByteCount(str); - arrPtr[i] = (byte*)NativeMemory.Alloc((nuint)byteLength + 1); //+1 for null termination + // Create spans over both portions of the block for bounds-checked access. + byte* dataPtr = block + pointersByteLength; + Span pointers = new Span(block, count + 1); + Span data = new Span(dataPtr, dataByteLength); - int bytesWritten = Encoding.UTF8.GetBytes(str, new Span(arrPtr[i], byteLength)); - Debug.Assert(bytesWritten == byteLength); + int dataOffset = 0; + for (int i = 0; i < count; i++) + { + pointers[i] = (nint)(dataPtr + dataOffset); - arrPtr[i][bytesWritten] = (byte)'\0'; // null terminate + int bytesWritten = Encoding.UTF8.GetBytes(arr[i], data.Slice(dataOffset)); + data[dataOffset + bytesWritten] = (byte)'\0'; + dataOffset += bytesWritten + 1; } + + pointers[count] = 0; // null terminator + Debug.Assert(dataOffset == dataByteLength); } - private static unsafe void FreeArray(byte** arr, int length) + /// + /// Allocates a single native memory block containing both a null-terminated pointer array + /// and the UTF-8 encoded "key=value\0" data for all non-null entries in the environment dictionary. + /// + private static unsafe void AllocEnvpArray(IDictionary env, ref byte** arrPtr) { - if (arr != null) + // First pass: count entries with non-null values and compute total buffer size. + int count = 0; + int dataByteLength = 0; + foreach (KeyValuePair pair in env) { - // Free each element of the array - for (int i = 0; i < length; i++) + if (pair.Value is not null) { - NativeMemory.Free(arr[i]); + // Each entry: UTF8(key) + '=' + UTF8(value) + '\0' + dataByteLength = checked(dataByteLength + Encoding.UTF8.GetByteCount(pair.Key) + 1 + Encoding.UTF8.GetByteCount(pair.Value) + 1); + count++; } + } + + // Allocate a single block: pointer array (count + 1 for null terminator) followed by string data. + nuint pointersByteLength = checked((nuint)(count + 1) * (nuint)sizeof(byte*)); + byte* block = (byte*)NativeMemory.Alloc(checked(pointersByteLength + (nuint)dataByteLength)); + arrPtr = (byte**)block; + + // Create spans over both portions of the block for bounds-checked access. + byte* dataPtr = block + pointersByteLength; + Span pointers = new Span(block, count + 1); + Span data = new Span(dataPtr, dataByteLength); - // And then the array itself - NativeMemory.Free(arr); + // Second pass: encode each key=value pair directly into the buffer. + int entryIndex = 0; + int dataOffset = 0; + foreach (KeyValuePair pair in env) + { + if (pair.Value is not null) + { + pointers[entryIndex] = (nint)(dataPtr + dataOffset); + + int keyBytes = Encoding.UTF8.GetBytes(pair.Key, data.Slice(dataOffset)); + data[dataOffset + keyBytes] = (byte)'='; + int valueBytes = Encoding.UTF8.GetBytes(pair.Value, data.Slice(dataOffset + keyBytes + 1)); + data[dataOffset + keyBytes + 1 + valueBytes] = (byte)'\0'; + + dataOffset += keyBytes + 1 + valueBytes + 1; + entryIndex++; + } } + + pointers[entryIndex] = 0; // null terminator + Debug.Assert(entryIndex == count); + Debug.Assert(dataOffset == dataByteLength); } } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs index beb79d77d5165b..ae905f656dc549 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; internal static partial class Interop { @@ -11,5 +12,9 @@ internal static partial class Sys [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_IsATty")] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool IsATty(IntPtr fd); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_IsATty")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool IsATty(SafeFileHandle fd); } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsAtomicNonInheritablePipeCreationSupported.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsAtomicNonInheritablePipeCreationSupported.cs new file mode 100644 index 00000000000000..1c62417d692329 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsAtomicNonInheritablePipeCreationSupported.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_IsAtomicNonInheritablePipeCreationSupported", SetLastError = false)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool IsAtomicNonInheritablePipeCreationSupportedImpl(); + + private static NullableBool s_atomicNonInheritablePipeCreationSupported; + + internal static bool IsAtomicNonInheritablePipeCreationSupported + { + get + { + NullableBool isSupported = s_atomicNonInheritablePipeCreationSupported; + if (isSupported == NullableBool.Undefined) + { + s_atomicNonInheritablePipeCreationSupported = isSupported = IsAtomicNonInheritablePipeCreationSupportedImpl() ? NullableBool.True : NullableBool.False; + } + return isSupported == NullableBool.True; + } + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs index 57ae67529f36bb..cb1b8e10bd362e 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs @@ -17,6 +17,10 @@ internal enum HandleFlags : uint HANDLE_FLAG_PROTECT_FROM_CLOSE = 2 } + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool GetHandleInformation(SafeHandle hObject, out HandleFlags lpdwFlags); + [LibraryImport(Libraries.Kernel32, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool SetHandleInformation(SafeHandle hObject, HandleFlags dwMask, HandleFlags dwFlags); 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 f863af7e0c8123..4274c9364a272c 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -11,6 +11,11 @@ public sealed partial class SafeProcessHandle : Microsoft.Win32.SafeHandles.Safe public SafeProcessHandle() : base (default(bool)) { } public SafeProcessHandle(System.IntPtr existingHandle, bool ownsHandle) : base (default(bool)) { } protected override bool ReleaseHandle() { throw null; } + public int ProcessId { get { throw null; } } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] + [System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] + public static Microsoft.Win32.SafeHandles.SafeProcessHandle Start(System.Diagnostics.ProcessStartInfo startInfo) { throw null; } } } namespace System.Diagnostics @@ -160,7 +165,7 @@ public void Refresh() { } [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 System.Diagnostics.Process Start(string fileName, string arguments) { throw null; } + public static System.Diagnostics.Process Start(string fileName, string? arguments) { 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 @@ -229,7 +234,7 @@ public sealed partial class ProcessStartInfo { public ProcessStartInfo() { } public ProcessStartInfo(string fileName) { } - public ProcessStartInfo(string fileName, string arguments) { } + public ProcessStartInfo(string fileName, string? arguments) { } public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable arguments) { } public System.Collections.ObjectModel.Collection ArgumentList { get { throw null; } } [System.Diagnostics.CodeAnalysis.AllowNullAttribute] @@ -261,6 +266,9 @@ public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable< public bool RedirectStandardInput { get { throw null; } set { } } public bool RedirectStandardOutput { get { throw null; } set { } } public System.Text.Encoding? StandardErrorEncoding { get { throw null; } set { } } + public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardErrorHandle { get { throw null; } set { } } + public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardInputHandle { get { throw null; } set { } } + public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardOutputHandle { get { throw null; } set { } } public System.Text.Encoding? StandardInputEncoding { get { throw null; } set { } } public System.Text.Encoding? StandardOutputEncoding { get { throw null; } set { } } [System.Diagnostics.CodeAnalysis.AllowNullAttribute] diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs index f3cac1f1af898b..f227eeaf4dd18d 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs @@ -1,17 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -/*============================================================ -** -** Class: SafeProcessHandle -** -** A wrapper for a process handle -** -** -===========================================================*/ - using System; +using System.Collections.Generic; +using System.ComponentModel; 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; namespace Microsoft.Win32.SafeHandles { @@ -27,6 +29,15 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali private readonly SafeWaitHandle? _handle; private readonly bool _releaseRef; + private SafeProcessHandle(int processId, ProcessWaitState.Holder waitStateHolder) : base(ownsHandle: true) + { + ProcessId = processId; + + _handle = waitStateHolder._state.EnsureExitedEvent().GetSafeWaitHandle(); + _handle.DangerousAddRef(ref _releaseRef); + SetHandle(_handle.DangerousGetHandle()); + } + internal SafeProcessHandle(int processId, SafeWaitHandle handle) : this(handle.DangerousGetHandle(), ownsHandle: true) { @@ -35,8 +46,6 @@ internal SafeProcessHandle(int processId, SafeWaitHandle handle) : handle.DangerousAddRef(ref _releaseRef); } - internal int ProcessId { get; } - protected override bool ReleaseHandle() { if (_releaseRef) @@ -46,5 +55,188 @@ protected override bool ReleaseHandle() } return true; } + + // On Unix, we don't use process descriptors yet, so we can't get PID. + private static int GetProcessIdCore() => throw new PlatformNotSupportedException(); + + private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) + { + SafeProcessHandle startedProcess = StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, out ProcessWaitState.Holder? waitStateHolder); + + // For standalone SafeProcessHandle.Start, we dispose the wait state holder immediately. + // The DangerousAddRef on the SafeWaitHandle (Unix) keeps the OS handle alive. + waitStateHolder?.Dispose(); + + return startedProcess; + } + + internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder) + { + waitStateHolder = null; + + if (ProcessUtils.PlatformDoesNotSupportProcessStartAndKill) + { + throw new PlatformNotSupportedException(); + } + + ProcessUtils.EnsureInitialized(); + + string? filename; + string[] argv; + + IDictionary env = startInfo.Environment; + string? cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null; + + bool setCredentials = !string.IsNullOrEmpty(startInfo.UserName); + uint userId = 0; + uint groupId = 0; + uint[]? groups = null; + if (setCredentials) + { + (userId, groupId, groups) = ProcessUtils.GetUserAndGroupIds(startInfo); + } + + // .NET applications don't echo characters unless there is a Console.Read operation. + // Unix applications expect the terminal to be in an echoing state by default. + // To support processes that interact with the terminal (e.g. 'vi'), we need to configure the + // terminal to echo. We keep this configuration as long as there are children possibly using the terminal. + // Handle can be null only for UseShellExecute or platforms that don't support Console.Open* methods like Android. + bool usesTerminal = (stdinHandle is not null && Interop.Sys.IsATty(stdinHandle)) + || (stdoutHandle is not null && Interop.Sys.IsATty(stdoutHandle)) + || (stderrHandle is not null && Interop.Sys.IsATty(stderrHandle)); + + if (startInfo.UseShellExecute) + { + string verb = startInfo.Verb; + if (verb != string.Empty && + !string.Equals(verb, "open", StringComparison.OrdinalIgnoreCase)) + { + throw new Win32Exception(Interop.Errors.ERROR_NO_ASSOCIATION); + } + + // On Windows, UseShellExecute of executables and scripts causes those files to be executed. + // To achieve this on Unix, we check if the file is executable (x-bit). + // Some files may have the x-bit set even when they are not executable. This happens for example + // when a Windows filesystem is mounted on Linux. To handle that, treat it as a regular file + // when exec returns ENOEXEC (file format cannot be executed). + filename = ProcessUtils.ResolveExecutableForShellExecute(startInfo.FileName, cwd); + if (filename != null) + { + argv = ProcessUtils.ParseArgv(startInfo); + + SafeProcessHandle processHandle = ForkAndExecProcess( + startInfo, filename, argv, env, cwd, + setCredentials, userId, groupId, groups, + stdinHandle, stdoutHandle, stderrHandle, usesTerminal, + out waitStateHolder, + throwOnNoExec: false); // return invalid handle instead of throwing on ENOEXEC + + if (!processHandle.IsInvalid) + { + return processHandle; + } + } + + // use default program to open file/url + filename = Process.GetPathToOpenFile(); + argv = ProcessUtils.ParseArgv(startInfo, filename, ignoreArguments: true); + + return ForkAndExecProcess( + startInfo, filename, argv, env, cwd, + setCredentials, userId, groupId, groups, + stdinHandle, stdoutHandle, stderrHandle, usesTerminal, + out waitStateHolder); + } + else + { + filename = ProcessUtils.ResolvePath(startInfo.FileName); + argv = ProcessUtils.ParseArgv(startInfo); + if (Directory.Exists(filename)) + { + throw new Win32Exception(SR.DirectoryNotValidAsInput); + } + + return ForkAndExecProcess( + startInfo, filename, argv, env, cwd, + setCredentials, userId, groupId, groups, + stdinHandle, stdoutHandle, stderrHandle, usesTerminal, + out waitStateHolder); + } + } + + private static SafeProcessHandle ForkAndExecProcess( + ProcessStartInfo startInfo, string? resolvedFilename, string[] argv, + IDictionary env, string? cwd, bool setCredentials, uint userId, + uint groupId, uint[]? groups, + SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, + bool usesTerminal, out ProcessWaitState.Holder? waitStateHolder, bool throwOnNoExec = true) + { + waitStateHolder = null; + + if (string.IsNullOrEmpty(resolvedFilename)) + { + Interop.ErrorInfo error = Interop.Error.ENOENT.Info(); + throw ProcessUtils.CreateExceptionForErrorStartingProcess(error.GetErrorMessage(), error.RawErrno, startInfo.FileName, cwd); + } + + int childPid, errno; + + // Lock to avoid races with OnSigChild + // By using a ReaderWriterLock we allow multiple processes to start concurrently. + ProcessUtils.s_processStartLock.EnterReadLock(); + try + { + if (usesTerminal) + { + ProcessUtils.ConfigureTerminalForChildProcesses(1); + } + + // Invoke the shim fork/execve routine. It will fork a child process, + // map the provided file handles onto the appropriate stdin/stdout/stderr + // descriptors, and execve to execute the requested process. The shim implementation + // is used to fork/execve as executing managed code in a forked process is not safe (only + // the calling thread will transfer, thread IDs aren't stable across the fork, etc.) + errno = Interop.Sys.ForkAndExecProcess( + resolvedFilename, argv, env, cwd, + setCredentials, userId, groupId, groups, + out childPid, stdinHandle, stdoutHandle, stderrHandle); + + if (errno == 0) + { + // Create the wait state holder while still holding the read lock. + // This ensures the child process is registered in s_childProcessWaitStates + // before the lock is released. If SIGCHLD fires after the lock is released, + // CheckChildren will find the child in the table and reap it properly. + // Without this, there is a race: SIGCHLD could fire after the lock is released + // but before the child is registered, causing WaitForExit to hang indefinitely. + waitStateHolder = new ProcessWaitState.Holder(childPid, isNewChild: true, usesTerminal); + } + } + finally + { + ProcessUtils.s_processStartLock.ExitReadLock(); + } + + if (errno != 0) + { + if (usesTerminal) + { + // We failed to launch a child that could use the terminal. + ProcessUtils.s_processStartLock.EnterWriteLock(); + ProcessUtils.ConfigureTerminalForChildProcesses(-1); + ProcessUtils.s_processStartLock.ExitWriteLock(); + } + + if (!throwOnNoExec && + new Interop.ErrorInfo(errno).Error == Interop.Error.ENOEXEC) + { + return InvalidHandle; + } + + throw ProcessUtils.CreateExceptionForErrorStartingProcess(new Interop.ErrorInfo(errno).GetErrorMessage(), errno, resolvedFilename, cwd); + } + + return new SafeProcessHandle(childPid, waitStateHolder!); + } } } diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs index 1fc7a409713278..416ec91ae11317 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs @@ -1,18 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -/*============================================================ -** -** Class: SafeProcessHandle -** -** A wrapper for a process handle -** -** -===========================================================*/ - using System; +using System.Diagnostics; using System.Runtime.InteropServices; using System.Security; +using System.Text; namespace Microsoft.Win32.SafeHandles { @@ -22,5 +15,272 @@ protected override bool ReleaseHandle() { return Interop.Kernel32.CloseHandle(handle); } + + internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) + { + return startInfo.UseShellExecute + ? StartWithShellExecuteEx(startInfo) + : StartWithCreateProcess(startInfo, stdinHandle, stdoutHandle, stderrHandle); + } + + private static unsafe SafeProcessHandle StartWithShellExecuteEx(ProcessStartInfo startInfo) + { + if (!string.IsNullOrEmpty(startInfo.UserName) || startInfo.Password != null) + throw new InvalidOperationException(SR.CantStartAsUser); + + if (startInfo.StandardInputEncoding != null) + throw new InvalidOperationException(SR.StandardInputEncodingNotAllowed); + + if (startInfo.StandardErrorEncoding != null) + throw new InvalidOperationException(SR.StandardErrorEncodingNotAllowed); + + if (startInfo.StandardOutputEncoding != null) + throw new InvalidOperationException(SR.StandardOutputEncodingNotAllowed); + + if (startInfo._environmentVariables != null) + throw new InvalidOperationException(SR.CantUseEnvVars); + + string arguments = startInfo.BuildArguments(); + + fixed (char* fileName = startInfo.FileName.Length > 0 ? startInfo.FileName : null) + fixed (char* verb = startInfo.Verb.Length > 0 ? startInfo.Verb : null) + fixed (char* parameters = arguments.Length > 0 ? arguments : null) + fixed (char* directory = startInfo.WorkingDirectory.Length > 0 ? startInfo.WorkingDirectory : null) + { + Interop.Shell32.SHELLEXECUTEINFO shellExecuteInfo = new Interop.Shell32.SHELLEXECUTEINFO() + { + cbSize = (uint)sizeof(Interop.Shell32.SHELLEXECUTEINFO), + lpFile = fileName, + lpVerb = verb, + lpParameters = parameters, + lpDirectory = directory, + fMask = Interop.Shell32.SEE_MASK_NOCLOSEPROCESS | Interop.Shell32.SEE_MASK_FLAG_DDEWAIT + }; + + if (startInfo.ErrorDialog) + shellExecuteInfo.hwnd = startInfo.ErrorDialogParentHandle; + else + shellExecuteInfo.fMask |= Interop.Shell32.SEE_MASK_FLAG_NO_UI; + + shellExecuteInfo.nShow = ProcessUtils.GetShowWindowFromWindowStyle(startInfo.WindowStyle); + ShellExecuteHelper executeHelper = new ShellExecuteHelper(&shellExecuteInfo); + if (!executeHelper.ShellExecuteOnSTAThread()) + { + int errorCode = executeHelper.ErrorCode; + if (errorCode == 0) + { + errorCode = ShellExecuteHelper.GetShellError(shellExecuteInfo.hInstApp); + } + + switch (errorCode) + { + case Interop.Errors.ERROR_CALL_NOT_IMPLEMENTED: + // This happens on Windows Nano + throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported); + default: + string nativeErrorMessage = errorCode == Interop.Errors.ERROR_BAD_EXE_FORMAT || errorCode == Interop.Errors.ERROR_EXE_MACHINE_TYPE_MISMATCH + ? SR.InvalidApplication + : Interop.Kernel32.GetMessage(errorCode); + + throw ProcessUtils.CreateExceptionForErrorStartingProcess(nativeErrorMessage, errorCode, startInfo.FileName, startInfo.WorkingDirectory); + } + } + + // From https://learn.microsoft.com/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfow: + // "In some cases, such as when execution is satisfied through a DDE conversation, no handle will be returned." + // Process.Start will return false if the handle is invalid. + return new SafeProcessHandle(shellExecuteInfo.hProcess); + } + } + + /// Starts the process using the supplied start info. + private static unsafe SafeProcessHandle StartWithCreateProcess(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) + { + // See knowledge base article Q190351 for an explanation of the following code. Noteworthy tricky points: + // * The handles are duplicated as inheritable before they are passed to CreateProcess so + // that the child process can use them + + var commandLine = new ValueStringBuilder(stackalloc char[256]); + ProcessUtils.BuildCommandLine(startInfo, ref commandLine); + + Interop.Kernel32.STARTUPINFO startupInfo = default; + Interop.Kernel32.PROCESS_INFORMATION processInfo = default; + Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default; + SafeProcessHandle procSH = new SafeProcessHandle(); + + // Inheritable copies of the child handles for CreateProcess + SafeFileHandle? inheritableStdinHandle = null; + SafeFileHandle? inheritableStdoutHandle = null; + SafeFileHandle? inheritableStderrHandle = null; + + // Take a global lock to synchronize all redirect pipe handle creations and CreateProcess + // calls. We do not want one process to inherit the handles created concurrently for another + // process, as that will impact the ownership and lifetimes of those handles now inherited + // into multiple child processes. + + ProcessUtils.s_processStartLock.EnterWriteLock(); + try + { + startupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFO); + + if (stdinHandle is not null || stdoutHandle is not null || stderrHandle is not null) + { + Debug.Assert(stdinHandle is not null && stdoutHandle is not null && stderrHandle is not null, "All or none of the standard handles must be provided."); + + ProcessUtils.DuplicateAsInheritableIfNeeded(stdinHandle, ref inheritableStdinHandle); + ProcessUtils.DuplicateAsInheritableIfNeeded(stdoutHandle, ref inheritableStdoutHandle); + ProcessUtils.DuplicateAsInheritableIfNeeded(stderrHandle, ref inheritableStderrHandle); + + startupInfo.hStdInput = (inheritableStdinHandle ?? stdinHandle).DangerousGetHandle(); + startupInfo.hStdOutput = (inheritableStdoutHandle ?? stdoutHandle).DangerousGetHandle(); + startupInfo.hStdError = (inheritableStderrHandle ?? stderrHandle).DangerousGetHandle(); + + // If STARTF_USESTDHANDLES is not set, the new process will inherit the standard handles. + startupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES; + } + + if (startInfo.WindowStyle != ProcessWindowStyle.Normal) + { + startupInfo.wShowWindow = (short)ProcessUtils.GetShowWindowFromWindowStyle(startInfo.WindowStyle); + startupInfo.dwFlags |= Interop.Advapi32.StartupInfoOptions.STARTF_USESHOWWINDOW; + } + + // set up the creation flags parameter + int creationFlags = 0; + if (startInfo.CreateNoWindow) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NO_WINDOW; + if (startInfo.CreateNewProcessGroup) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NEW_PROCESS_GROUP; + + // set up the environment block parameter + string? environmentBlock = null; + if (startInfo._environmentVariables != null) + { + creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_UNICODE_ENVIRONMENT; + environmentBlock = ProcessUtils.GetEnvironmentVariablesBlock(startInfo._environmentVariables!); + } + + string? workingDirectory = startInfo.WorkingDirectory; + if (workingDirectory.Length == 0) + { + workingDirectory = null; + } + + bool retVal; + int errorCode = 0; + + if (startInfo.UserName.Length != 0) + { + if (startInfo.Password != null && startInfo.PasswordInClearText != null) + { + throw new ArgumentException(SR.CantSetDuplicatePassword); + } + + Interop.Advapi32.LogonFlags logonFlags = (Interop.Advapi32.LogonFlags)0; + if (startInfo.LoadUserProfile && startInfo.UseCredentialsForNetworkingOnly) + { + throw new ArgumentException(SR.CantEnableConflictingLogonFlags, nameof(startInfo)); + } + else if (startInfo.LoadUserProfile) + { + logonFlags = Interop.Advapi32.LogonFlags.LOGON_WITH_PROFILE; + } + else if (startInfo.UseCredentialsForNetworkingOnly) + { + logonFlags = Interop.Advapi32.LogonFlags.LOGON_NETCREDENTIALS_ONLY; + } + + commandLine.NullTerminate(); + fixed (char* passwordInClearTextPtr = startInfo.PasswordInClearText ?? string.Empty) + fixed (char* environmentBlockPtr = environmentBlock) + fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) + { + IntPtr passwordPtr = (startInfo.Password != null) ? + Marshal.SecureStringToGlobalAllocUnicode(startInfo.Password) : IntPtr.Zero; + + try + { + retVal = Interop.Advapi32.CreateProcessWithLogonW( + startInfo.UserName, + startInfo.Domain, + (passwordPtr != IntPtr.Zero) ? passwordPtr : (IntPtr)passwordInClearTextPtr, + logonFlags, + null, // we don't need this since all the info is in commandLine + commandLinePtr, + creationFlags, + (IntPtr)environmentBlockPtr, + workingDirectory, + ref startupInfo, // pointer to STARTUPINFO + ref processInfo // pointer to PROCESS_INFORMATION + ); + if (!retVal) + errorCode = Marshal.GetLastWin32Error(); + } + finally + { + if (passwordPtr != IntPtr.Zero) + Marshal.ZeroFreeGlobalAllocUnicode(passwordPtr); + } + } + } + else + { + commandLine.NullTerminate(); + fixed (char* environmentBlockPtr = environmentBlock) + fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) + { + retVal = Interop.Kernel32.CreateProcess( + null, // we don't need this since all the info is in commandLine + commandLinePtr, // pointer to the command line string + ref unused_SecAttrs, // address to process security attributes, we don't need to inherit the handle + ref unused_SecAttrs, // address to thread security attributes. + true, // handle inheritance flag + creationFlags, // creation flags + (IntPtr)environmentBlockPtr, // pointer to new environment block + workingDirectory, // pointer to current directory name + ref startupInfo, // pointer to STARTUPINFO + ref processInfo // pointer to PROCESS_INFORMATION + ); + if (!retVal) + errorCode = Marshal.GetLastWin32Error(); + } + } + + if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != new IntPtr(-1)) + Marshal.InitHandle(procSH, processInfo.hProcess); + if (processInfo.hThread != IntPtr.Zero && processInfo.hThread != new IntPtr(-1)) + Interop.Kernel32.CloseHandle(processInfo.hThread); + + if (!retVal) + { + string nativeErrorMessage = errorCode == Interop.Errors.ERROR_BAD_EXE_FORMAT || errorCode == Interop.Errors.ERROR_EXE_MACHINE_TYPE_MISMATCH + ? SR.InvalidApplication + : Interop.Kernel32.GetMessage(errorCode); + + throw ProcessUtils.CreateExceptionForErrorStartingProcess(nativeErrorMessage, errorCode, startInfo.FileName, workingDirectory); + } + } + catch + { + procSH.Dispose(); + throw; + } + finally + { + // Only dispose duplicated handles, not the original handles passed by the caller. + // When the handle was invalid or already inheritable, no duplication was needed. + inheritableStdinHandle?.Dispose(); + inheritableStdoutHandle?.Dispose(); + inheritableStderrHandle?.Dispose(); + + ProcessUtils.s_processStartLock.ExitWriteLock(); + + commandLine.Dispose(); + } + + Debug.Assert(!procSH.IsInvalid); + procSH.ProcessId = (int)processInfo.dwProcessId; + return procSH; + } + + private int GetProcessIdCore() => Interop.Kernel32.GetProcessId(this); } } 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 c7d52e23e0b0ce..0351614413591f 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 @@ -12,12 +12,35 @@ using System; using System.Diagnostics; +using System.Runtime.Serialization; +using System.Runtime.Versioning; namespace Microsoft.Win32.SafeHandles { public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvalid { internal static readonly SafeProcessHandle InvalidHandle = new SafeProcessHandle(); + private int _processId = -1; + + /// + /// Gets the process ID. + /// + public int ProcessId + { + get + { + Validate(); + + if (_processId == -1) + { + _processId = GetProcessIdCore(); + } + + return _processId; + + } + private set => _processId = value; + } /// /// Creates a . @@ -42,5 +65,67 @@ public SafeProcessHandle(IntPtr existingHandle, bool ownsHandle) { SetHandle(existingHandle); } + + /// + /// Starts a process using the specified . + /// + /// The process start information. + /// A representing the started process. + /// + /// On Windows, when is , + /// the process is started using ShellExecuteEx. In some cases, such as when execution + /// is satisfied through a DDE conversation, the returned handle will be invalid. + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public static SafeProcessHandle Start(ProcessStartInfo startInfo) + { + ArgumentNullException.ThrowIfNull(startInfo); + startInfo.ThrowIfInvalid(out bool anyRedirection); + + if (anyRedirection) + { + // Process has .StandardInput, .StandardOutput, or .StandardError APIs that can express + // redirection of streams, but SafeProcessHandle doesn't. + // The caller can provide handles via the StandardInputHandle, StandardOutputHandle, + // and StandardErrorHandle properties. + throw new InvalidOperationException(SR.CantSetRedirectForSafeProcessHandleStart); + } + + SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); + + SafeFileHandle? childInputHandle = startInfo.StandardInputHandle; + SafeFileHandle? childOutputHandle = startInfo.StandardOutputHandle; + SafeFileHandle? childErrorHandle = startInfo.StandardErrorHandle; + + if (!startInfo.UseShellExecute) + { + if (childInputHandle is null && !OperatingSystem.IsAndroid()) + { + childInputHandle = Console.OpenStandardInputHandle(); + } + + if (childOutputHandle is null && !OperatingSystem.IsAndroid()) + { + childOutputHandle = Console.OpenStandardOutputHandle(); + } + + if (childErrorHandle is null && !OperatingSystem.IsAndroid()) + { + childErrorHandle = Console.OpenStandardErrorHandle(); + } + } + + return StartCore(startInfo, childInputHandle, childOutputHandle, childErrorHandle); + } + + private void Validate() + { + if (IsInvalid) + { + throw new InvalidOperationException(SR.InvalidProcessHandle); + } + } } } diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index bfed1eff99beaa..2f7cf93b80bf3a 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -210,6 +210,15 @@ The Process object must have the UseShellExecute property set to false in order to redirect IO streams. + + Invalid handle. + + + The StandardInputHandle, StandardOutputHandle, and StandardErrorHandle properties cannot be used together with the corresponding RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError properties. + + + The RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError properties cannot be used by SafeProcessHandle.Start. Use the StandardInputHandle, StandardOutputHandle, and StandardErrorHandle properties. + The FileName property should not be a directory unless UseShellExecute is set. @@ -336,6 +345,9 @@ Invalid performance counter data with type '{0}'. + + Invalid process handle. + Stream redirection is not supported by StartAndForget. Redirected streams must be drained to avoid deadlocks, which is incompatible with fire-and-forget semantics. 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 19a7a7e034df0c..8e68a16dfc8411 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -121,6 +121,8 @@ Link="Common\Interop\Windows\Kernel32\Interop.GetModuleBaseName.cs" /> + + Link="Common\Interop\Windows\Kernel32\Interop.HandleOptions.cs" /> + @@ -286,24 +289,28 @@ Link="Common\Interop\Unix\Interop.GetEUid.cs" /> + + + - + - + - Gets execution path - private static string? GetPathToOpenFile() + internal static string? GetPathToOpenFile() { if (Interop.Sys.Stat("/usr/local/bin/open", out _) == 0) { diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs index 93c7950b453571..449543d53db458 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs @@ -103,7 +103,7 @@ private static DateTime BootTime GetStat().ppid; /// Gets execution path - private static string? GetPathToOpenFile() + internal static string? GetPathToOpenFile() { ReadOnlySpan allowedProgramsToRun = ["xdg-open", "gnome-open", "kfmclient"]; foreach (var program in allowedProgramsToRun) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs index b84938c65e8eea..f3f236b3f93e5b 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs @@ -52,7 +52,7 @@ internal DateTime StartTimeCore } /// Gets execution path - private static string GetPathToOpenFile() + internal static string GetPathToOpenFile() { return "/usr/bin/open"; } 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 index 2557bf6f5733d4..252b74d9bbfb8b 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Runtime.Versioning; +using Microsoft.Win32.SafeHandles; namespace System.Diagnostics { @@ -48,10 +49,8 @@ public static int StartAndForget(ProcessStartInfo startInfo) throw new InvalidOperationException(SR.StartAndForget_RedirectNotSupported); } - using Process process = new Process(); - process.StartInfo = startInfo; - process.Start(); - return process.Id; + using SafeProcessHandle processHandle = SafeProcessHandle.Start(startInfo); + return processHandle.ProcessId; } /// diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs index 143e962984b0cf..fb515db23e5ccb 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs @@ -36,7 +36,7 @@ internal DateTime StartTimeCore private int ParentProcessId => GetProcInfo().ParentPid; /// Gets execution path - private static string? GetPathToOpenFile() + internal static string? GetPathToOpenFile() { return ProcessUtils.FindProgramInPath("xdg-open"); } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs index f26a8f70ffba74..64b7853340d7f5 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs @@ -16,10 +16,6 @@ namespace System.Diagnostics { public partial class Process : IDisposable { - private static volatile bool s_initialized; - private static readonly object s_initializedGate = new object(); - private static readonly ReaderWriterLockSlim s_processStartLock = new ReaderWriterLockSlim(); - /// /// Puts a Process component in state to interact with operating system processes that run in a /// special mode by enabling the native property SeDebugPrivilege on the current thread. @@ -58,7 +54,7 @@ public static Process Start(string fileName, string arguments, string userName, [SupportedOSPlatform("maccatalyst")] public void Kill() { - if (PlatformDoesNotSupportProcessStartAndKill) + if (ProcessUtils.PlatformDoesNotSupportProcessStartAndKill) { throw new PlatformNotSupportedException(); } @@ -359,376 +355,21 @@ private SafeProcessHandle GetProcessHandle() return new SafeProcessHandle(_processId, GetSafeWaitHandle()); } - /// - /// Starts the process using the supplied start info. - /// With UseShellExecute option, we'll try the shell tools to launch it(e.g. "open fileName") - /// - /// The start info with which to start the process. - private bool StartCore(ProcessStartInfo startInfo) + private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) { - if (PlatformDoesNotSupportProcessStartAndKill) - { - throw new PlatformNotSupportedException(); - } - - EnsureInitialized(); - - string? filename; - string[] argv; - - if (startInfo.UseShellExecute) - { - if (startInfo.RedirectStandardInput || startInfo.RedirectStandardOutput || startInfo.RedirectStandardError) - { - throw new InvalidOperationException(SR.CantRedirectStreams); - } - } - - int stdinFd = -1, stdoutFd = -1, stderrFd = -1; - string[] envp = CreateEnvp(startInfo); - string? cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null; - - bool setCredentials = !string.IsNullOrEmpty(startInfo.UserName); - uint userId = 0; - uint groupId = 0; - uint[]? groups = null; - if (setCredentials) - { - (userId, groupId, groups) = GetUserAndGroupIds(startInfo); - } - - // .NET applications don't echo characters unless there is a Console.Read operation. - // Unix applications expect the terminal to be in an echoing state by default. - // To support processes that interact with the terminal (e.g. 'vi'), we need to configure the - // terminal to echo. We keep this configuration as long as there are children possibly using the terminal. - bool usesTerminal = !(startInfo.RedirectStandardInput && - startInfo.RedirectStandardOutput && - startInfo.RedirectStandardError); - - if (startInfo.UseShellExecute) - { - string verb = startInfo.Verb; - if (verb != string.Empty && - !string.Equals(verb, "open", StringComparison.OrdinalIgnoreCase)) - { - throw new Win32Exception(Interop.Errors.ERROR_NO_ASSOCIATION); - } - - // On Windows, UseShellExecute of executables and scripts causes those files to be executed. - // To achieve this on Unix, we check if the file is executable (x-bit). - // Some files may have the x-bit set even when they are not executable. This happens for example - // when a Windows filesystem is mounted on Linux. To handle that, treat it as a regular file - // when exec returns ENOEXEC (file format cannot be executed). - bool isExecuting = false; - filename = ResolveExecutableForShellExecute(startInfo.FileName, cwd); - if (filename != null) - { - argv = ParseArgv(startInfo); - - isExecuting = ForkAndExecProcess( - startInfo, filename, argv, envp, cwd, - setCredentials, userId, groupId, groups, - out stdinFd, out stdoutFd, out stderrFd, usesTerminal, - throwOnNoExec: false); // return false instead of throwing on ENOEXEC - } - - // use default program to open file/url - if (!isExecuting) - { - filename = GetPathToOpenFile(); - argv = ParseArgv(startInfo, filename, ignoreArguments: true); - - ForkAndExecProcess( - startInfo, filename, argv, envp, cwd, - setCredentials, userId, groupId, groups, - out stdinFd, out stdoutFd, out stderrFd, usesTerminal); - } - } - else - { - filename = ResolvePath(startInfo.FileName); - argv = ParseArgv(startInfo); - if (Directory.Exists(filename)) - { - throw new Win32Exception(SR.DirectoryNotValidAsInput); - } - - ForkAndExecProcess( - startInfo, filename, argv, envp, cwd, - setCredentials, userId, groupId, groups, - out stdinFd, out stdoutFd, out stderrFd, usesTerminal); - } - - // Configure the parent's ends of the redirection streams. - // We use UTF8 encoding without BOM by-default(instead of Console encoding as on Windows) - // as there is no good way to get this information from the native layer - // and we do not want to take dependency on Console contract. - if (startInfo.RedirectStandardInput) - { - Debug.Assert(stdinFd >= 0); - _standardInput = new StreamWriter(OpenStream(stdinFd, PipeDirection.Out), - startInfo.StandardInputEncoding ?? Encoding.Default, StreamBufferSize) - { AutoFlush = true }; - } - if (startInfo.RedirectStandardOutput) - { - Debug.Assert(stdoutFd >= 0); - _standardOutput = new StreamReader(OpenStream(stdoutFd, PipeDirection.In), - startInfo.StandardOutputEncoding ?? Encoding.Default, true, StreamBufferSize); - } - if (startInfo.RedirectStandardError) - { - Debug.Assert(stderrFd >= 0); - _standardError = new StreamReader(OpenStream(stderrFd, PipeDirection.In), - startInfo.StandardErrorEncoding ?? Encoding.Default, true, StreamBufferSize); - } + SafeProcessHandle startedProcess = SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, out ProcessWaitState.Holder? waitStateHolder); + Debug.Assert(!startedProcess.IsInvalid); + _waitStateHolder = waitStateHolder; + SetProcessHandle(startedProcess); + SetProcessId(startedProcess.ProcessId); return true; } - private bool ForkAndExecProcess( - ProcessStartInfo startInfo, string? resolvedFilename, string[] argv, - string[] envp, string? cwd, bool setCredentials, uint userId, - uint groupId, uint[]? groups, - out int stdinFd, out int stdoutFd, out int stderrFd, - bool usesTerminal, bool throwOnNoExec = true) - { - if (string.IsNullOrEmpty(resolvedFilename)) - { - Interop.ErrorInfo errno = Interop.Error.ENOENT.Info(); - throw CreateExceptionForErrorStartingProcess(errno.GetErrorMessage(), errno.RawErrno, startInfo.FileName, cwd); - } - - // Lock to avoid races with OnSigChild - // By using a ReaderWriterLock we allow multiple processes to start concurrently. - s_processStartLock.EnterReadLock(); - try - { - if (usesTerminal) - { - ConfigureTerminalForChildProcesses(1); - } - - int childPid; - - // Invoke the shim fork/execve routine. It will create pipes for all requested - // redirects, fork a child process, map the pipe ends onto the appropriate stdin/stdout/stderr - // descriptors, and execve to execute the requested process. The shim implementation - // is used to fork/execve as executing managed code in a forked process is not safe (only - // the calling thread will transfer, thread IDs aren't stable across the fork, etc.) - int errno = Interop.Sys.ForkAndExecProcess( - resolvedFilename, argv, envp, cwd, - startInfo.RedirectStandardInput, startInfo.RedirectStandardOutput, startInfo.RedirectStandardError, - setCredentials, userId, groupId, groups, - out childPid, out stdinFd, out stdoutFd, out stderrFd); - - if (errno == 0) - { - // Ensure we'll reap this process. - // note: SetProcessId will set this if we don't set it first. - _waitStateHolder = new ProcessWaitState.Holder(childPid, isNewChild: true, usesTerminal); - - // Store the child's information into this Process object. - Debug.Assert(childPid >= 0); - SetProcessId(childPid); - SetProcessHandle(new SafeProcessHandle(_processId, GetSafeWaitHandle())); - - return true; - } - else - { - if (!throwOnNoExec && - new Interop.ErrorInfo(errno).Error == Interop.Error.ENOEXEC) - { - return false; - } - - throw CreateExceptionForErrorStartingProcess(new Interop.ErrorInfo(errno).GetErrorMessage(), errno, resolvedFilename, cwd); - } - } - finally - { - s_processStartLock.ExitReadLock(); - - if (_waitStateHolder == null && usesTerminal) - { - // We failed to launch a child that could use the terminal. - s_processStartLock.EnterWriteLock(); - ConfigureTerminalForChildProcesses(-1); - s_processStartLock.ExitWriteLock(); - } - } - } /// Finalizable holder for the underlying shared wait state object. private ProcessWaitState.Holder? _waitStateHolder; - /// Size to use for redirect streams and stream readers/writers. - private const int StreamBufferSize = 4096; - - /// Converts the filename and arguments information from a ProcessStartInfo into an argv array. - /// The ProcessStartInfo. - /// Resolved executable to open ProcessStartInfo.FileName - /// Don't pass ProcessStartInfo.Arguments - /// The argv array. - private static string[] ParseArgv(ProcessStartInfo psi, string? resolvedExe = null, bool ignoreArguments = false) - { - if (string.IsNullOrEmpty(resolvedExe) && - (ignoreArguments || (string.IsNullOrEmpty(psi.Arguments) && !psi.HasArgumentList))) - { - return new string[] { psi.FileName }; - } - - var argvList = new List(); - if (!string.IsNullOrEmpty(resolvedExe)) - { - argvList.Add(resolvedExe); - if (resolvedExe.Contains("kfmclient")) - { - argvList.Add("openURL"); // kfmclient needs OpenURL - } - } - - argvList.Add(psi.FileName); - - if (!ignoreArguments) - { - if (!string.IsNullOrEmpty(psi.Arguments)) - { - ParseArgumentsIntoList(psi.Arguments, argvList); - } - else if (psi.HasArgumentList) - { - argvList.AddRange(psi.ArgumentList); - } - } - return argvList.ToArray(); - } - - /// Converts the environment variables information from a ProcessStartInfo into an envp array. - /// The ProcessStartInfo. - /// The envp array. - private static string[] CreateEnvp(ProcessStartInfo psi) - { - var envp = new string[psi.Environment.Count]; - int index = 0; - foreach (KeyValuePair pair in psi.Environment) - { - // Ignore null values for consistency with Environment.SetEnvironmentVariable - if (pair.Value != null) - { - envp[index++] = pair.Key + "=" + pair.Value; - } - } - // Resize the array in case we skipped some entries - Array.Resize(ref envp, index); - return envp; - } - - private static string? ResolveExecutableForShellExecute(string filename, string? workingDirectory) - { - // Determine if filename points to an executable file. - // filename may be an absolute path, a relative path or a uri. - - string? resolvedFilename = null; - // filename is an absolute path - if (Path.IsPathRooted(filename)) - { - if (File.Exists(filename)) - { - resolvedFilename = filename; - } - } - // filename is a uri - else if (Uri.TryCreate(filename, UriKind.Absolute, out Uri? uri)) - { - if (uri.IsFile && uri.Host == "" && File.Exists(uri.LocalPath)) - { - resolvedFilename = uri.LocalPath; - } - } - // filename is relative - else - { - // The WorkingDirectory property specifies the location of the executable. - // If WorkingDirectory is an empty string, the current directory is understood to contain the executable. - workingDirectory = workingDirectory != null ? Path.GetFullPath(workingDirectory) : - Directory.GetCurrentDirectory(); - string filenameInWorkingDirectory = Path.Combine(workingDirectory, filename); - // filename is a relative path in the working directory - if (File.Exists(filenameInWorkingDirectory)) - { - resolvedFilename = filenameInWorkingDirectory; - } - // find filename on PATH - else - { - resolvedFilename = ProcessUtils.FindProgramInPath(filename); - } - } - - if (resolvedFilename == null) - { - return null; - } - - if (Interop.Sys.Access(resolvedFilename, Interop.Sys.AccessMode.X_OK) == 0) - { - return resolvedFilename; - } - else - { - return null; - } - } - - /// Resolves a path to the filename passed to ProcessStartInfo. - /// The filename. - /// The resolved path. It can return null in case of URLs. - private static string? ResolvePath(string filename) - { - // Follow the same resolution that Windows uses with CreateProcess: - // 1. First try the exact path provided - // 2. Then try the file relative to the executable directory - // 3. Then try the file relative to the current directory - // 4. then try the file in each of the directories specified in PATH - // Windows does additional Windows-specific steps between 3 and 4, - // and we ignore those here. - - // If the filename is a complete path, use it, regardless of whether it exists. - if (Path.IsPathRooted(filename)) - { - // In this case, it doesn't matter whether the file exists or not; - // it's what the caller asked for, so it's what they'll get - return filename; - } - - // Then check the executable's directory - string? path = Environment.ProcessPath; - if (path != null) - { - try - { - path = Path.Combine(Path.GetDirectoryName(path)!, filename); - if (File.Exists(path)) - { - return path; - } - } - catch (ArgumentException) { } // ignore any errors in data that may come from the exe path - } - - // Then check the current directory - path = Path.Combine(Directory.GetCurrentDirectory(), filename); - if (File.Exists(path)) - { - return path; - } - - // Then check each directory listed in the PATH environment variables - return ProcessUtils.FindProgramInPath(filename); - } - private static long s_ticksPerSecond; /// Convert a number of "jiffies", or ticks, to a TimeSpan. @@ -753,116 +394,21 @@ internal static TimeSpan TicksToTimeSpan(double ticks) return TimeSpan.FromSeconds(ticks / (double)ticksPerSecond); } - /// Opens a stream around the specified file descriptor and with the specified access. - /// The file descriptor. - /// The pipe direction. - /// The opened stream. - private static AnonymousPipeClientStream OpenStream(int fd, PipeDirection direction) - { - Debug.Assert(fd >= 0); - return new AnonymousPipeClientStream(direction, new SafePipeHandle((IntPtr)fd, ownsHandle: true)); - } - - /// Parses a command-line argument string into a list of arguments. - /// The argument string. - /// The list into which the component arguments should be stored. - /// - /// This follows the rules outlined in "Parsing C++ Command-Line Arguments" at - /// https://msdn.microsoft.com/en-us/library/17w5ykft.aspx. - /// - private static void ParseArgumentsIntoList(string arguments, List results) + private static AnonymousPipeClientStream OpenStream(SafeFileHandle handle, FileAccess access) { - // Iterate through all of the characters in the argument string. - for (int i = 0; i < arguments.Length; i++) - { - while (i < arguments.Length && (arguments[i] == ' ' || arguments[i] == '\t')) - i++; + PipeDirection direction = access == FileAccess.Write ? PipeDirection.Out : PipeDirection.In; - if (i == arguments.Length) - break; + // Transfer the ownership to SafePipeHandle, so that it can be properly released when the AnonymousPipeClientStream is disposed. + SafePipeHandle safePipeHandle = new(handle.DangerousGetHandle(), ownsHandle: true); + handle.SetHandleAsInvalid(); - results.Add(GetNextArgument(arguments, ref i)); - } + // Use AnonymousPipeClientStream for async, cancellable read/write support. + return new AnonymousPipeClientStream(direction, safePipeHandle); } - private static string GetNextArgument(string arguments, ref int i) - { - var currentArgument = new ValueStringBuilder(stackalloc char[256]); - bool inQuotes = false; + private static Encoding GetStandardInputEncoding() => Encoding.Default; - while (i < arguments.Length) - { - // From the current position, iterate through contiguous backslashes. - int backslashCount = 0; - while (i < arguments.Length && arguments[i] == '\\') - { - i++; - backslashCount++; - } - - if (backslashCount > 0) - { - if (i >= arguments.Length || arguments[i] != '"') - { - // Backslashes not followed by a double quote: - // they should all be treated as literal backslashes. - currentArgument.Append('\\', backslashCount); - } - else - { - // Backslashes followed by a double quote: - // - Output a literal slash for each complete pair of slashes - // - If one remains, use it to make the subsequent quote a literal. - currentArgument.Append('\\', backslashCount / 2); - if (backslashCount % 2 != 0) - { - currentArgument.Append('"'); - i++; - } - } - - continue; - } - - char c = arguments[i]; - - // If this is a double quote, track whether we're inside of quotes or not. - // Anything within quotes will be treated as a single argument, even if - // it contains spaces. - if (c == '"') - { - if (inQuotes && i < arguments.Length - 1 && arguments[i + 1] == '"') - { - // Two consecutive double quotes inside an inQuotes region should result in a literal double quote - // (the parser is left in the inQuotes region). - // This behavior is not part of the spec of code:ParseArgumentsIntoList, but is compatible with CRT - // and .NET Framework. - currentArgument.Append('"'); - i++; - } - else - { - inQuotes = !inQuotes; - } - - i++; - continue; - } - - // If this is a space/tab and we're not in quotes, we're done with the current - // argument, it should be added to the results and then reset for the next one. - if ((c == ' ' || c == '\t') && !inQuotes) - { - break; - } - - // Nothing special; add the character to the current argument. - currentArgument.Append(c); - i++; - } - - return currentArgument.ToString(); - } + private static Encoding GetStandardOutputEncoding() => Encoding.Default; /// Gets the wait state for this Process object. private ProcessWaitState GetWaitState() @@ -878,100 +424,6 @@ private ProcessWaitState GetWaitState() private SafeWaitHandle GetSafeWaitHandle() => GetWaitState().EnsureExitedEvent().GetSafeWaitHandle(); - private static (uint userId, uint groupId, uint[] groups) GetUserAndGroupIds(ProcessStartInfo startInfo) - { - Debug.Assert(!string.IsNullOrEmpty(startInfo.UserName)); - - (uint? userId, uint? groupId) = GetUserAndGroupIds(startInfo.UserName); - - Debug.Assert(userId.HasValue == groupId.HasValue, "userId and groupId both need to have values, or both need to be null."); - if (!userId.HasValue) - { - throw new Win32Exception(SR.Format(SR.UserDoesNotExist, startInfo.UserName)); - } - - uint[]? groups = Interop.Sys.GetGroupList(startInfo.UserName, groupId!.Value); - if (groups == null) - { - throw new Win32Exception(SR.Format(SR.UserGroupsCannotBeDetermined, startInfo.UserName)); - } - - return (userId.Value, groupId.Value, groups); - } - - private static unsafe (uint? userId, uint? groupId) GetUserAndGroupIds(string userName) - { - Interop.Sys.Passwd? passwd; - // First try with a buffer that should suffice for 99% of cases. - // Note: on CentOS/RedHat 7.1 systems, getpwnam_r returns 'user not found' if the buffer is too small - // see https://bugs.centos.org/view.php?id=7324 - const int BufLen = Interop.Sys.Passwd.InitialBufferSize; - byte* stackBuf = stackalloc byte[BufLen]; - if (TryGetPasswd(userName, stackBuf, BufLen, out passwd)) - { - if (passwd == null) - { - return (null, null); - } - return (passwd.Value.UserId, passwd.Value.GroupId); - } - - // Fallback to heap allocations if necessary, growing the buffer until - // we succeed. TryGetPasswd will throw if there's an unexpected error. - int lastBufLen = BufLen; - while (true) - { - lastBufLen *= 2; - byte[] heapBuf = new byte[lastBufLen]; - fixed (byte* buf = &heapBuf[0]) - { - if (TryGetPasswd(userName, buf, heapBuf.Length, out passwd)) - { - if (passwd == null) - { - return (null, null); - } - return (passwd.Value.UserId, passwd.Value.GroupId); - } - } - } - } - - private static unsafe bool TryGetPasswd(string name, byte* buf, int bufLen, out Interop.Sys.Passwd? passwd) - { - // Call getpwnam_r to get the passwd struct - Interop.Sys.Passwd tempPasswd; - int error = Interop.Sys.GetPwNamR(name, out tempPasswd, buf, bufLen); - - // If the call succeeds, give back the passwd retrieved - if (error == 0) - { - passwd = tempPasswd; - return true; - } - - // If the current user's entry could not be found, give back null, - // but still return true as false indicates the buffer was too small. - if (error == -1) - { - passwd = null; - return true; - } - - var errorInfo = new Interop.ErrorInfo(error); - - // If the call failed because the buffer was too small, return false to - // indicate the caller should try again with a larger buffer. - if (errorInfo.Error == Interop.Error.ERANGE) - { - passwd = null; - return false; - } - - // Otherwise, fail. - throw new Win32Exception(errorInfo.RawErrno, errorInfo.GetErrorMessage()); - } - public IntPtr MainWindowHandle => IntPtr.Zero; private static bool CloseMainWindowCore() => false; @@ -982,57 +434,6 @@ private static unsafe bool TryGetPasswd(string name, byte* buf, int bufLen, out private static bool WaitForInputIdleCore(int _ /*milliseconds*/) => throw new InvalidOperationException(SR.InputIdleUnknownError); - private static unsafe void EnsureInitialized() - { - if (s_initialized) - { - return; - } - - lock (s_initializedGate) - { - if (!s_initialized) - { - if (!Interop.Sys.InitializeTerminalAndSignalHandling()) - { - throw new Win32Exception(); - } - - // Register our callback. - Interop.Sys.RegisterForSigChld(&OnSigChild); - SetDelayedSigChildConsoleConfigurationHandler(); - - s_initialized = true; - } - } - } - - [UnmanagedCallersOnly] - private static int OnSigChild(int reapAll, int configureConsole) - { - // configureConsole is non zero when there are PosixSignalRegistrations that - // may Cancel the terminal configuration that happens when there are no more - // children using the terminal. - // When the registrations don't cancel the terminal configuration, - // DelayedSigChildConsoleConfiguration will be called. - - // Lock to avoid races with Process.Start - s_processStartLock.EnterWriteLock(); - try - { - bool childrenUsingTerminalPre = AreChildrenUsingTerminal; - ProcessWaitState.CheckChildren(reapAll != 0, configureConsole != 0); - bool childrenUsingTerminalPost = AreChildrenUsingTerminal; - - // return whether console configuration was skipped. - return childrenUsingTerminalPre && !childrenUsingTerminalPost && configureConsole == 0 ? 1 : 0; - } - finally - { - s_processStartLock.ExitWriteLock(); - } - } - /// Gets the friendly name of the process. public string ProcessName { @@ -1042,8 +443,5 @@ public string ProcessName return _processInfo!.ProcessName; } } - - private static bool PlatformDoesNotSupportProcessStartAndKill - => (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()) || OperatingSystem.IsTvOS(); } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs index bd551a53f8d311..fb7bf658939f0d 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs @@ -20,176 +20,22 @@ public partial class Process : IDisposable private bool _haveResponding; private bool _responding; - private bool StartCore(ProcessStartInfo startInfo) + private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) { - return startInfo.UseShellExecute - ? StartWithShellExecuteEx(startInfo) - : StartWithCreateProcess(startInfo); - } - - private unsafe bool StartWithShellExecuteEx(ProcessStartInfo startInfo) - { - if (!string.IsNullOrEmpty(startInfo.UserName) || startInfo.Password != null) - throw new InvalidOperationException(SR.CantStartAsUser); - - if (startInfo.RedirectStandardInput || startInfo.RedirectStandardOutput || startInfo.RedirectStandardError) - throw new InvalidOperationException(SR.CantRedirectStreams); - - if (startInfo.StandardInputEncoding != null) - throw new InvalidOperationException(SR.StandardInputEncodingNotAllowed); - - if (startInfo.StandardErrorEncoding != null) - throw new InvalidOperationException(SR.StandardErrorEncodingNotAllowed); - - if (startInfo.StandardOutputEncoding != null) - throw new InvalidOperationException(SR.StandardOutputEncodingNotAllowed); - - if (startInfo._environmentVariables != null) - throw new InvalidOperationException(SR.CantUseEnvVars); - - string arguments = startInfo.BuildArguments(); - - fixed (char* fileName = startInfo.FileName.Length > 0 ? startInfo.FileName : null) - fixed (char* verb = startInfo.Verb.Length > 0 ? startInfo.Verb : null) - fixed (char* parameters = arguments.Length > 0 ? arguments : null) - fixed (char* directory = startInfo.WorkingDirectory.Length > 0 ? startInfo.WorkingDirectory : null) - { - Interop.Shell32.SHELLEXECUTEINFO shellExecuteInfo = new Interop.Shell32.SHELLEXECUTEINFO() - { - cbSize = (uint)sizeof(Interop.Shell32.SHELLEXECUTEINFO), - lpFile = fileName, - lpVerb = verb, - lpParameters = parameters, - lpDirectory = directory, - fMask = Interop.Shell32.SEE_MASK_NOCLOSEPROCESS | Interop.Shell32.SEE_MASK_FLAG_DDEWAIT - }; - - if (startInfo.ErrorDialog) - shellExecuteInfo.hwnd = startInfo.ErrorDialogParentHandle; - else - shellExecuteInfo.fMask |= Interop.Shell32.SEE_MASK_FLAG_NO_UI; - - shellExecuteInfo.nShow = GetShowWindowFromWindowStyle(startInfo.WindowStyle); - ShellExecuteHelper executeHelper = new ShellExecuteHelper(&shellExecuteInfo); - if (!executeHelper.ShellExecuteOnSTAThread()) - { - int errorCode = executeHelper.ErrorCode; - if (errorCode == 0) - { - errorCode = GetShellError(shellExecuteInfo.hInstApp); - } - - switch (errorCode) - { - case Interop.Errors.ERROR_CALL_NOT_IMPLEMENTED: - // This happens on Windows Nano - throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported); - default: - string nativeErrorMessage = errorCode == Interop.Errors.ERROR_BAD_EXE_FORMAT || errorCode == Interop.Errors.ERROR_EXE_MACHINE_TYPE_MISMATCH - ? SR.InvalidApplication - : GetErrorMessage(errorCode); - - throw CreateExceptionForErrorStartingProcess(nativeErrorMessage, errorCode, startInfo.FileName, startInfo.WorkingDirectory); - } - } - - if (shellExecuteInfo.hProcess != IntPtr.Zero) - { - SetProcessHandle(new SafeProcessHandle(shellExecuteInfo.hProcess)); - return true; - } - } - - return false; - } - - private static int GetShowWindowFromWindowStyle(ProcessWindowStyle windowStyle) => windowStyle switch - { - ProcessWindowStyle.Hidden => Interop.Shell32.SW_HIDE, - ProcessWindowStyle.Minimized => Interop.Shell32.SW_SHOWMINIMIZED, - ProcessWindowStyle.Maximized => Interop.Shell32.SW_SHOWMAXIMIZED, - _ => Interop.Shell32.SW_SHOWNORMAL, - }; - - private static int GetShellError(IntPtr error) - { - switch ((long)error) - { - case Interop.Shell32.SE_ERR_FNF: - return Interop.Errors.ERROR_FILE_NOT_FOUND; - case Interop.Shell32.SE_ERR_PNF: - return Interop.Errors.ERROR_PATH_NOT_FOUND; - case Interop.Shell32.SE_ERR_ACCESSDENIED: - return Interop.Errors.ERROR_ACCESS_DENIED; - case Interop.Shell32.SE_ERR_OOM: - return Interop.Errors.ERROR_NOT_ENOUGH_MEMORY; - case Interop.Shell32.SE_ERR_DDEFAIL: - case Interop.Shell32.SE_ERR_DDEBUSY: - case Interop.Shell32.SE_ERR_DDETIMEOUT: - return Interop.Errors.ERROR_DDE_FAIL; - case Interop.Shell32.SE_ERR_SHARE: - return Interop.Errors.ERROR_SHARING_VIOLATION; - case Interop.Shell32.SE_ERR_NOASSOC: - return Interop.Errors.ERROR_NO_ASSOCIATION; - case Interop.Shell32.SE_ERR_DLLNOTFOUND: - return Interop.Errors.ERROR_DLL_NOT_FOUND; - default: - return (int)(long)error; - } - } - - internal sealed unsafe class ShellExecuteHelper - { - private readonly Interop.Shell32.SHELLEXECUTEINFO* _executeInfo; - private bool _succeeded; - private bool _notpresent; - - public ShellExecuteHelper(Interop.Shell32.SHELLEXECUTEINFO* executeInfo) - { - _executeInfo = executeInfo; - } + SafeProcessHandle startedProcess = SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle); - private void ShellExecuteFunction() + if (startedProcess.IsInvalid) { - try - { - if (!(_succeeded = Interop.Shell32.ShellExecuteExW(_executeInfo))) - ErrorCode = Marshal.GetLastWin32Error(); - } - catch (EntryPointNotFoundException) - { - _notpresent = true; - } + Debug.Assert(startInfo.UseShellExecute); + return false; } - public bool ShellExecuteOnSTAThread() + SetProcessHandle(startedProcess); + if (!startInfo.UseShellExecute) { - // ShellExecute() requires STA in order to work correctly. - - if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) - { - ThreadStart threadStart = new ThreadStart(ShellExecuteFunction); - Thread executionThread = new Thread(threadStart) - { - IsBackground = true, - Name = ".NET Process STA" - }; - executionThread.SetApartmentState(ApartmentState.STA); - executionThread.Start(); - executionThread.Join(); - } - else - { - ShellExecuteFunction(); - } - - if (_notpresent) - throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported); - - return _succeeded; + SetProcessId(startedProcess.ProcessId); } - - public int ErrorCode { get; private set; } + return true; } private string GetMainWindowTitle() diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index f073bd1e8d54f5..27701f20649280 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -16,8 +16,6 @@ namespace System.Diagnostics { public partial class Process : IDisposable { - private static readonly object s_createProcessLock = new object(); - private string? _processName; /// @@ -423,244 +421,6 @@ private void SetWorkingSetLimitsCore(IntPtr? newMin, IntPtr? newMax, out IntPtr } } - /// Starts the process using the supplied start info. - /// The start info with which to start the process. - private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) - { - // See knowledge base article Q190351 for an explanation of the following code. Noteworthy tricky points: - // * The handles are duplicated as inheritable before they are passed to CreateProcess so - // that the child process can use them - // * CreateProcess allows you to redirect all or none of the standard IO handles, so we use - // Console.OpenStandard*Handle for the handles that are not being redirected - - var commandLine = new ValueStringBuilder(stackalloc char[256]); - BuildCommandLine(startInfo, ref commandLine); - - Interop.Kernel32.STARTUPINFO startupInfo = default; - Interop.Kernel32.PROCESS_INFORMATION processInfo = default; - Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default; - SafeProcessHandle procSH = new SafeProcessHandle(); - - // handles used in parent process - SafeFileHandle? parentInputPipeHandle = null; - SafeFileHandle? childInputPipeHandle = null; - SafeFileHandle? parentOutputPipeHandle = null; - SafeFileHandle? childOutputPipeHandle = null; - SafeFileHandle? parentErrorPipeHandle = null; - SafeFileHandle? childErrorPipeHandle = null; - - // Take a global lock to synchronize all redirect pipe handle creations and CreateProcess - // calls. We do not want one process to inherit the handles created concurrently for another - // process, as that will impact the ownership and lifetimes of those handles now inherited - // into multiple child processes. - lock (s_createProcessLock) - { - try - { - startupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFO); - - // set up the streams - if (startInfo.RedirectStandardInput || startInfo.RedirectStandardOutput || startInfo.RedirectStandardError) - { - if (startInfo.RedirectStandardInput) - { - CreatePipe(out parentInputPipeHandle, out childInputPipeHandle, true); - } - else - { - childInputPipeHandle = Console.OpenStandardInputHandle(); - } - - if (startInfo.RedirectStandardOutput) - { - CreatePipe(out parentOutputPipeHandle, out childOutputPipeHandle, false); - } - else - { - childOutputPipeHandle = Console.OpenStandardOutputHandle(); - } - - if (startInfo.RedirectStandardError) - { - CreatePipe(out parentErrorPipeHandle, out childErrorPipeHandle, false); - } - else - { - childErrorPipeHandle = Console.OpenStandardErrorHandle(); - } - - startupInfo.hStdInput = childInputPipeHandle.DangerousGetHandle(); - startupInfo.hStdOutput = childOutputPipeHandle.DangerousGetHandle(); - startupInfo.hStdError = childErrorPipeHandle.DangerousGetHandle(); - - startupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES; - } - - if (startInfo.WindowStyle != ProcessWindowStyle.Normal) - { - startupInfo.wShowWindow = (short)GetShowWindowFromWindowStyle(startInfo.WindowStyle); - startupInfo.dwFlags |= Interop.Advapi32.StartupInfoOptions.STARTF_USESHOWWINDOW; - } - - // set up the creation flags parameter - int creationFlags = 0; - if (startInfo.CreateNoWindow) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NO_WINDOW; - if (startInfo.CreateNewProcessGroup) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NEW_PROCESS_GROUP; - - // set up the environment block parameter - string? environmentBlock = null; - if (startInfo._environmentVariables != null) - { - creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_UNICODE_ENVIRONMENT; - environmentBlock = GetEnvironmentVariablesBlock(startInfo._environmentVariables!); - } - - string? workingDirectory = startInfo.WorkingDirectory; - if (workingDirectory.Length == 0) - { - workingDirectory = null; - } - - bool retVal; - int errorCode = 0; - - if (startInfo.UserName.Length != 0) - { - if (startInfo.Password != null && startInfo.PasswordInClearText != null) - { - throw new ArgumentException(SR.CantSetDuplicatePassword); - } - - Interop.Advapi32.LogonFlags logonFlags = (Interop.Advapi32.LogonFlags)0; - if (startInfo.LoadUserProfile && startInfo.UseCredentialsForNetworkingOnly) - { - throw new ArgumentException(SR.CantEnableConflictingLogonFlags, nameof(startInfo)); - } - else if (startInfo.LoadUserProfile) - { - logonFlags = Interop.Advapi32.LogonFlags.LOGON_WITH_PROFILE; - } - else if (startInfo.UseCredentialsForNetworkingOnly) - { - logonFlags = Interop.Advapi32.LogonFlags.LOGON_NETCREDENTIALS_ONLY; - } - - commandLine.NullTerminate(); - fixed (char* passwordInClearTextPtr = startInfo.PasswordInClearText ?? string.Empty) - fixed (char* environmentBlockPtr = environmentBlock) - fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) - { - IntPtr passwordPtr = (startInfo.Password != null) ? - Marshal.SecureStringToGlobalAllocUnicode(startInfo.Password) : IntPtr.Zero; - - try - { - retVal = Interop.Advapi32.CreateProcessWithLogonW( - startInfo.UserName, - startInfo.Domain, - (passwordPtr != IntPtr.Zero) ? passwordPtr : (IntPtr)passwordInClearTextPtr, - logonFlags, - null, // we don't need this since all the info is in commandLine - commandLinePtr, - creationFlags, - (IntPtr)environmentBlockPtr, - workingDirectory, - ref startupInfo, // pointer to STARTUPINFO - ref processInfo // pointer to PROCESS_INFORMATION - ); - if (!retVal) - errorCode = Marshal.GetLastWin32Error(); - } - finally - { - if (passwordPtr != IntPtr.Zero) - Marshal.ZeroFreeGlobalAllocUnicode(passwordPtr); - } - } - } - else - { - commandLine.NullTerminate(); - fixed (char* environmentBlockPtr = environmentBlock) - fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) - { - retVal = Interop.Kernel32.CreateProcess( - null, // we don't need this since all the info is in commandLine - commandLinePtr, // pointer to the command line string - ref unused_SecAttrs, // address to process security attributes, we don't need to inherit the handle - ref unused_SecAttrs, // address to thread security attributes. - true, // handle inheritance flag - creationFlags, // creation flags - (IntPtr)environmentBlockPtr, // pointer to new environment block - workingDirectory, // pointer to current directory name - ref startupInfo, // pointer to STARTUPINFO - ref processInfo // pointer to PROCESS_INFORMATION - ); - if (!retVal) - errorCode = Marshal.GetLastWin32Error(); - } - } - - if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != new IntPtr(-1)) - Marshal.InitHandle(procSH, processInfo.hProcess); - if (processInfo.hThread != IntPtr.Zero && processInfo.hThread != new IntPtr(-1)) - Interop.Kernel32.CloseHandle(processInfo.hThread); - - if (!retVal) - { - string nativeErrorMessage = errorCode == Interop.Errors.ERROR_BAD_EXE_FORMAT || errorCode == Interop.Errors.ERROR_EXE_MACHINE_TYPE_MISMATCH - ? SR.InvalidApplication - : GetErrorMessage(errorCode); - - throw CreateExceptionForErrorStartingProcess(nativeErrorMessage, errorCode, startInfo.FileName, workingDirectory); - } - } - catch - { - parentInputPipeHandle?.Dispose(); - parentOutputPipeHandle?.Dispose(); - parentErrorPipeHandle?.Dispose(); - procSH.Dispose(); - throw; - } - finally - { - childInputPipeHandle?.Dispose(); - childOutputPipeHandle?.Dispose(); - childErrorPipeHandle?.Dispose(); - } - } - - if (startInfo.RedirectStandardInput) - { - Encoding enc = startInfo.StandardInputEncoding ?? GetEncoding((int)Interop.Kernel32.GetConsoleCP()); - _standardInput = new StreamWriter(new FileStream(parentInputPipeHandle!, FileAccess.Write, 4096, false), enc, 4096); - _standardInput.AutoFlush = true; - } - if (startInfo.RedirectStandardOutput) - { - Encoding enc = startInfo.StandardOutputEncoding ?? GetEncoding((int)Interop.Kernel32.GetConsoleOutputCP()); - _standardOutput = new StreamReader(new FileStream(parentOutputPipeHandle!, FileAccess.Read, 4096, parentOutputPipeHandle!.IsAsync), enc, true, 4096); - } - if (startInfo.RedirectStandardError) - { - Encoding enc = startInfo.StandardErrorEncoding ?? GetEncoding((int)Interop.Kernel32.GetConsoleOutputCP()); - _standardError = new StreamReader(new FileStream(parentErrorPipeHandle!, FileAccess.Read, 4096, parentErrorPipeHandle!.IsAsync), enc, true, 4096); - } - - commandLine.Dispose(); - - if (procSH.IsInvalid) - { - procSH.Dispose(); - return false; - } - - SetProcessHandle(procSH); - SetProcessId((int)processInfo.dwProcessId); - return true; - } - private static ConsoleEncoding GetEncoding(int codePage) { Encoding enc = EncodingHelper.GetSupportedConsoleEncoding(codePage); @@ -669,30 +429,6 @@ private static ConsoleEncoding GetEncoding(int codePage) private bool _signaled; - private static void BuildCommandLine(ProcessStartInfo startInfo, ref ValueStringBuilder commandLine) - { - // Construct a StringBuilder with the appropriate command line - // to pass to CreateProcess. If the filename isn't already - // in quotes, we quote it here. This prevents some security - // problems (it specifies exactly which part of the string - // is the file to execute). - ReadOnlySpan fileName = startInfo.FileName.AsSpan().Trim(); - bool fileNameIsQuoted = fileName.StartsWith('"') && fileName.EndsWith('"'); - if (!fileNameIsQuoted) - { - commandLine.Append('"'); - } - - commandLine.Append(fileName); - - if (!fileNameIsQuoted) - { - commandLine.Append('"'); - } - - startInfo.AppendArgumentsTo(ref commandLine); - } - /// Gets timing information for the current process. private ProcessThreadTimes GetProcessTimes() { @@ -800,74 +536,11 @@ private SafeProcessHandle GetProcessHandle(int access, bool throwIfExited = true } } - // Using synchronous Anonymous pipes for process input/output redirection means we would end up - // wasting a worker threadpool thread per pipe instance. Overlapped pipe IO is desirable, since - // it will take advantage of the NT IO completion port infrastructure. But we can't really use - // Overlapped I/O for process input/output as it would break Console apps (managed Console class - // methods such as WriteLine as well as native CRT functions like printf) which are making an - // assumption that the console standard handles (obtained via GetStdHandle()) are opened - // for synchronous I/O and hence they can work fine with ReadFile/WriteFile synchronously! - // We therefore only open the parent's end of the pipe for async I/O (overlapped), while the - // child's end is always opened for synchronous I/O so the child process can use it normally. - private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHandle childHandle, bool parentInputs) - { - // Only the parent's read end benefits from async I/O; stdin is always sync. - // asyncRead applies to the read handle; asyncWrite to the write handle. - SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: !parentInputs, asyncWrite: false); - - // parentInputs=true: parent writes to pipe, child reads (stdin redirect). - // parentInputs=false: parent reads from pipe, child writes (stdout/stderr redirect). - parentHandle = parentInputs ? writeHandle : readHandle; - SafeFileHandle hTmpChild = parentInputs ? readHandle : writeHandle; - - // Duplicate the child handle to be inheritable so that the child process - // has access. The original non-inheritable handle is closed afterwards. - IntPtr currentProcHandle = Interop.Kernel32.GetCurrentProcess(); - if (!Interop.Kernel32.DuplicateHandle(currentProcHandle, - hTmpChild, - currentProcHandle, - out childHandle, - 0, - bInheritHandle: true, - Interop.Kernel32.HandleOptions.DUPLICATE_SAME_ACCESS)) - { - int lastError = Marshal.GetLastWin32Error(); - parentHandle.Dispose(); - hTmpChild.Dispose(); - throw new Win32Exception(lastError); - } - - hTmpChild.Dispose(); - } - - private static string GetEnvironmentVariablesBlock(DictionaryWrapper sd) - { - // https://learn.microsoft.com/windows/win32/procthread/changing-environment-variables - // "All strings in the environment block must be sorted alphabetically by name. The sort is - // case-insensitive, Unicode order, without regard to locale. Because the equal sign is a - // separator, it must not be used in the name of an environment variable." - - var keys = new string[sd.Count]; - sd.Keys.CopyTo(keys, 0); - Array.Sort(keys, StringComparer.OrdinalIgnoreCase); - - // Join the null-terminated "key=val\0" strings - var result = new StringBuilder(8 * keys.Length); - foreach (string key in keys) - { - string? value = sd[key]; - - // Ignore null values for consistency with Environment.SetEnvironmentVariable - if (value != null) - { - result.Append(key).Append('=').Append(value).Append('\0'); - } - } + private static FileStream OpenStream(SafeFileHandle handle, FileAccess access) => new(handle, access, StreamBufferSize, handle.IsAsync); - return result.ToString(); - } + private static ConsoleEncoding GetStandardInputEncoding() => GetEncoding((int)Interop.Kernel32.GetConsoleCP()); - private static string GetErrorMessage(int error) => Interop.Kernel32.GetMessage(error); + private static ConsoleEncoding GetStandardOutputEncoding() => GetEncoding((int)Interop.Kernel32.GetConsoleOutputCP()); /// Gets the friendly name of the process. public string ProcessName diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index 4218596c129644..a093dac7ceacb8 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -83,8 +83,6 @@ public partial class Process : Component internal bool _pendingOutputRead; internal bool _pendingErrorRead; - private static int s_cachedSerializationSwitch; - /// /// /// Initializes a new instance of the class. @@ -1227,6 +1225,9 @@ private void SetProcessId(int processId) /// Additional optional configuration hook after a process ID is set. partial void ConfigureAfterProcessIdSet(); + /// Size to use for redirect streams and stream readers/writers. + private const int StreamBufferSize = 4096; + /// /// /// Starts a process specified by the property of this @@ -1244,44 +1245,143 @@ public bool Start() Close(); ProcessStartInfo startInfo = StartInfo; - if (startInfo.FileName.Length == 0) + startInfo.ThrowIfInvalid(out bool anyRedirection); + + //Cannot start a new process and store its handle if the object has been disposed, since finalization has been suppressed. + CheckDisposed(); + + SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); + + SafeFileHandle? parentInputPipeHandle = null; + SafeFileHandle? parentOutputPipeHandle = null; + SafeFileHandle? parentErrorPipeHandle = null; + + SafeFileHandle? childInputHandle = null; + SafeFileHandle? childOutputHandle = null; + SafeFileHandle? childErrorHandle = null; + + try { - throw new InvalidOperationException(SR.FileNameMissing); + if (!startInfo.UseShellExecute) + { + // Windows supports creating non-inheritable pipe in atomic way. + // When it comes to Unixes, it depends whether they support pipe2 sys-call or not. + // If they don't, the pipe is created as inheritable and made non-inheritable with another sys-call. + // Some process could be started in the meantime, so in order to prevent accidental handle inheritance, + // a writer lock is used around the pipe creation code. + + bool requiresLock = anyRedirection && !ProcessUtils.SupportsAtomicNonInheritablePipeCreation; + + if (requiresLock) + { + ProcessUtils.s_processStartLock.EnterWriteLock(); + } + + try + { + if (startInfo.StandardInputHandle is not null) + { + childInputHandle = startInfo.StandardInputHandle; + } + else if (startInfo.RedirectStandardInput) + { + SafeFileHandle.CreateAnonymousPipe(out childInputHandle, out parentInputPipeHandle); + } + else if (!OperatingSystem.IsAndroid()) + { + childInputHandle = Console.OpenStandardInputHandle(); + } + + if (startInfo.StandardOutputHandle is not null) + { + childOutputHandle = startInfo.StandardOutputHandle; + } + else if (startInfo.RedirectStandardOutput) + { + SafeFileHandle.CreateAnonymousPipe(out parentOutputPipeHandle, out childOutputHandle, asyncRead: OperatingSystem.IsWindows()); + } + else if (!OperatingSystem.IsAndroid()) + { + childOutputHandle = Console.OpenStandardOutputHandle(); + } + + if (startInfo.StandardErrorHandle is not null) + { + childErrorHandle = startInfo.StandardErrorHandle; + } + else if (startInfo.RedirectStandardError) + { + SafeFileHandle.CreateAnonymousPipe(out parentErrorPipeHandle, out childErrorHandle, asyncRead: OperatingSystem.IsWindows()); + } + else if (!OperatingSystem.IsAndroid()) + { + childErrorHandle = Console.OpenStandardErrorHandle(); + } + } + finally + { + if (requiresLock) + { + ProcessUtils.s_processStartLock.ExitWriteLock(); + } + } + } + + if (!StartCore(startInfo, childInputHandle, childOutputHandle, childErrorHandle)) + { + return false; + } } - if (startInfo.StandardInputEncoding != null && !startInfo.RedirectStandardInput) + catch { - throw new InvalidOperationException(SR.StandardInputEncodingNotAllowed); + parentInputPipeHandle?.Dispose(); + parentOutputPipeHandle?.Dispose(); + parentErrorPipeHandle?.Dispose(); + + throw; } - if (startInfo.StandardOutputEncoding != null && !startInfo.RedirectStandardOutput) + finally { - throw new InvalidOperationException(SR.StandardOutputEncodingNotAllowed); + // We MUST close the child handles, otherwise the parent + // process will not receive EOF when the child process closes its handles. + // It's OK to do it for handles returned by Console.OpenStandard*Handle APIs, + // because these handles are not owned and won't be closed by Dispose. + // We don't dispose handles that were passed in + // by the caller via StartInfo.StandardInputHandle/OutputHandle/ErrorHandle. + if (startInfo.StandardInputHandle is null) + { + childInputHandle?.Dispose(); + } + if (startInfo.StandardOutputHandle is null) + { + childOutputHandle?.Dispose(); + } + if (startInfo.StandardErrorHandle is null) + { + childErrorHandle?.Dispose(); + } } - if (startInfo.StandardErrorEncoding != null && !startInfo.RedirectStandardError) + + if (startInfo.RedirectStandardInput) { - throw new InvalidOperationException(SR.StandardErrorEncodingNotAllowed); + _standardInput = new StreamWriter(OpenStream(parentInputPipeHandle!, FileAccess.Write), + startInfo.StandardInputEncoding ?? GetStandardInputEncoding(), StreamBufferSize) + { + AutoFlush = true + }; } - if (!string.IsNullOrEmpty(startInfo.Arguments) && startInfo.HasArgumentList) + if (startInfo.RedirectStandardOutput) { - throw new InvalidOperationException(SR.ArgumentAndArgumentListInitialized); + _standardOutput = new StreamReader(OpenStream(parentOutputPipeHandle!, FileAccess.Read), + startInfo.StandardOutputEncoding ?? GetStandardOutputEncoding(), true, StreamBufferSize); } - if (startInfo.HasArgumentList) + if (startInfo.RedirectStandardError) { - int argumentCount = startInfo.ArgumentList.Count; - for (int i = 0; i < argumentCount; i++) - { - if (startInfo.ArgumentList[i] is null) - { - throw new ArgumentNullException("item", SR.ArgumentListMayNotContainNull); - } - } + _standardError = new StreamReader(OpenStream(parentErrorPipeHandle!, FileAccess.Read), + startInfo.StandardErrorEncoding ?? GetStandardOutputEncoding(), true, StreamBufferSize); } - //Cannot start a new process and store its handle if the object has been disposed, since finalization has been suppressed. - CheckDisposed(); - - SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref s_cachedSerializationSwitch); - - return StartCore(startInfo); + return true; } /// @@ -1313,7 +1413,7 @@ public static Process Start(string fileName) [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("tvos")] [SupportedOSPlatform("maccatalyst")] - public static Process Start(string fileName, string arguments) + public static Process Start(string fileName, string? arguments) { // the underlying Start method can only return null on Windows platforms, // when the ProcessStartInfo.UseShellExecute property is set to true. @@ -1740,13 +1840,6 @@ private void CheckDisposed() ObjectDisposedException.ThrowIf(_disposed, this); } - private static Win32Exception CreateExceptionForErrorStartingProcess(string errorMessage, int errorCode, string fileName, string? workingDirectory) - { - string directoryForException = string.IsNullOrEmpty(workingDirectory) ? Directory.GetCurrentDirectory() : workingDirectory; - string msg = SR.Format(SR.ErrorStartingProcess, fileName, directoryForException, errorMessage); - return new Win32Exception(errorCode, msg); - } - /// /// This enum defines the operation mode for redirected process stream. /// We don't support switching between synchronous mode and asynchronous mode. diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.iOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.iOS.cs index b94531f135685a..49519cfc243b29 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.iOS.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.iOS.cs @@ -114,7 +114,7 @@ private static void SetWorkingSetLimitsCore(IntPtr? newMin, IntPtr? newMax, out #pragma warning restore IDE0060 /// Gets execution path - private static string GetPathToOpenFile() + internal static string GetPathToOpenFile() { throw new PlatformNotSupportedException(); } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs index 0f4fc0a0cb72c1..e564f6201faa72 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs @@ -8,6 +8,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Text; +using Microsoft.Win32.SafeHandles; namespace System.Diagnostics { @@ -49,7 +50,7 @@ public ProcessStartInfo(string fileName) /// Specifies the name of the application that is to be started, as well as a set /// of command line arguments to pass to the application. /// - public ProcessStartInfo(string fileName, string arguments) + public ProcessStartInfo(string fileName, string? arguments) { _fileName = fileName; _arguments = arguments; @@ -117,6 +118,84 @@ public string Arguments public bool RedirectStandardOutput { get; set; } public bool RedirectStandardError { get; set; } + /// + /// Gets or sets a that will be used as the standard input of the child process. + /// When set, the handle is passed directly to the child process and must be . + /// + /// + /// + /// The handle does not need to be inheritable; the runtime will duplicate it as inheritable if needed. + /// + /// + /// Use to create a pair of connected pipe handles, + /// to open a file handle, + /// to provide an empty input, + /// or to inherit the parent's standard input + /// (the default behavior when this property is ). + /// + /// + /// It's recommended to dispose the handle right after starting the process. + /// + /// + /// This property cannot be used together with + /// and requires to be . + /// + /// + /// A to use as the standard input handle of the child process, or to use the default behavior. + public SafeFileHandle? StandardInputHandle { get; set; } + + /// + /// Gets or sets a that will be used as the standard output of the child process. + /// When set, the handle is passed directly to the child process and must be . + /// + /// + /// + /// The handle does not need to be inheritable; the runtime will duplicate it as inheritable if needed. + /// + /// + /// Use to create a pair of connected pipe handles, + /// to open a file handle, + /// to discard output, + /// or to inherit the parent's standard output + /// (the default behavior when this property is ). + /// + /// + /// It's recommended to dispose the handle right after starting the process. + /// + /// + /// This property cannot be used together with + /// and requires to be . + /// + /// + /// A to use as the standard output handle of the child process, or to use the default behavior. + public SafeFileHandle? StandardOutputHandle { get; set; } + + /// + /// Gets or sets a that will be used as the standard error of the child process. + /// When set, the handle is passed directly to the child process and must be . + /// + /// + /// + /// The handle does not need to be inheritable; the runtime will duplicate it as inheritable if needed. + /// + /// + /// Use to create a pair of connected pipe handles, + /// to open a file handle, + /// to discard error output, + /// or to inherit the parent's standard error + /// (the default behavior when this property is ). + /// + /// + /// It's recommended to dispose the handle right after starting the process. + /// + /// + /// This property cannot be used together with + /// and requires to be . + /// + /// + /// A to use as the standard error handle of the child process, or to use the default behavior. + public SafeFileHandle? StandardErrorHandle { get; set; } + public Encoding? StandardInputEncoding { get; set; } public Encoding? StandardErrorEncoding { get; set; } @@ -214,5 +293,80 @@ internal void AppendArgumentsTo(ref ValueStringBuilder stringBuilder) stringBuilder.Append(Arguments); } } + + internal void ThrowIfInvalid(out bool anyRedirection) + { + if (FileName.Length == 0) + { + throw new InvalidOperationException(SR.FileNameMissing); + } + if (StandardInputEncoding != null && !RedirectStandardInput) + { + throw new InvalidOperationException(SR.StandardInputEncodingNotAllowed); + } + if (StandardOutputEncoding != null && !RedirectStandardOutput) + { + throw new InvalidOperationException(SR.StandardOutputEncodingNotAllowed); + } + if (StandardErrorEncoding != null && !RedirectStandardError) + { + throw new InvalidOperationException(SR.StandardErrorEncodingNotAllowed); + } + if (!string.IsNullOrEmpty(Arguments) && HasArgumentList) + { + throw new InvalidOperationException(SR.ArgumentAndArgumentListInitialized); + } + if (HasArgumentList) + { + int argumentCount = ArgumentList.Count; + for (int i = 0; i < argumentCount; i++) + { + if (ArgumentList[i] is null) + { + throw new ArgumentNullException("item", SR.ArgumentListMayNotContainNull); + } + } + } + + anyRedirection = RedirectStandardInput || RedirectStandardOutput || RedirectStandardError; + bool anyHandle = StandardInputHandle is not null || StandardOutputHandle is not null || StandardErrorHandle is not null; + if (UseShellExecute && (anyRedirection || anyHandle)) + { + throw new InvalidOperationException(SR.CantRedirectStreams); + } + + if (anyHandle) + { + if (StandardInputHandle is not null && RedirectStandardInput) + { + throw new InvalidOperationException(SR.CantSetHandleAndRedirect); + } + if (StandardOutputHandle is not null && RedirectStandardOutput) + { + throw new InvalidOperationException(SR.CantSetHandleAndRedirect); + } + if (StandardErrorHandle is not null && RedirectStandardError) + { + throw new InvalidOperationException(SR.CantSetHandleAndRedirect); + } + + ValidateHandle(StandardInputHandle, nameof(StandardInputHandle)); + ValidateHandle(StandardOutputHandle, nameof(StandardOutputHandle)); + ValidateHandle(StandardErrorHandle, nameof(StandardErrorHandle)); + } + + static void ValidateHandle(SafeFileHandle? handle, string paramName) + { + if (handle is not null) + { + if (handle.IsInvalid) + { + throw new ArgumentException(SR.Arg_InvalidHandle, paramName); + } + + ObjectDisposedException.ThrowIf(handle.IsClosed, handle); + } + } + } } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs new file mode 100644 index 00000000000000..91d98013eb57b0 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Threading; + +namespace System.Diagnostics +{ + internal static partial class ProcessUtils + { + private static int s_childrenUsingTerminalCount; + + internal static void ConfigureTerminalForChildProcesses(int increment, bool configureConsole = true) + { + Debug.Assert(increment != 0); + + int childrenUsingTerminalRemaining = Interlocked.Add(ref s_childrenUsingTerminalCount, increment); + if (increment > 0) + { + Debug.Assert(s_processStartLock.IsReadLockHeld); + Debug.Assert(configureConsole); + + // At least one child is using the terminal. + Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: true); + } + else + { + Debug.Assert(s_processStartLock.IsWriteLockHeld); + + if (childrenUsingTerminalRemaining == 0 && configureConsole) + { + // No more children are using the terminal. + Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: false); + } + } + } + + private static unsafe void SetDelayedSigChildConsoleConfigurationHandler() + { + Interop.Sys.SetDelayedSigChildConsoleConfigurationHandler(&DelayedSigChildConsoleConfiguration); + } + + [UnmanagedCallersOnly] + private static void DelayedSigChildConsoleConfiguration() + { + // Lock to avoid races with Process.Start + s_processStartLock.EnterWriteLock(); + try + { + if (s_childrenUsingTerminalCount == 0) + { + // No more children are using the terminal. + Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: false); + } + } + finally + { + s_processStartLock.ExitWriteLock(); + } + } + + private static bool AreChildrenUsingTerminal => s_childrenUsingTerminalCount > 0; + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs new file mode 100644 index 00000000000000..671630a2f00719 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics +{ + internal static partial class ProcessUtils + { + /// These methods are used on other Unix systems to track how many children use the terminal, + /// and update the terminal configuration when necessary. + + [Conditional("unnecessary")] + internal static void ConfigureTerminalForChildProcesses(int increment, bool configureConsole = true) + { + } + + static partial void SetDelayedSigChildConsoleConfigurationHandler(); + + private static bool AreChildrenUsingTerminal => false; + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs index eb4c06a033566e..2c0717c684c66f 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs @@ -1,12 +1,29 @@ // 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.ComponentModel; 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; namespace System.Diagnostics { internal static partial class ProcessUtils { + private static volatile bool s_initialized; + private static readonly object s_initializedGate = new object(); + + internal static bool SupportsAtomicNonInheritablePipeCreation => Interop.Sys.IsAtomicNonInheritablePipeCreationSupported; + + internal static bool PlatformDoesNotSupportProcessStartAndKill + => (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()) || OperatingSystem.IsTvOS(); + private static bool IsExecutable(string fullPath) { Interop.Sys.FileStatus fileinfo; @@ -68,5 +85,393 @@ private static bool IsExecutable(string fullPath) } } + internal static unsafe void EnsureInitialized() + { + if (s_initialized) + { + return; + } + + lock (s_initializedGate) + { + if (!s_initialized) + { + if (!Interop.Sys.InitializeTerminalAndSignalHandling()) + { + throw new Win32Exception(); + } + + // Register our callback. + Interop.Sys.RegisterForSigChld(&OnSigChild); + SetDelayedSigChildConsoleConfigurationHandler(); + + s_initialized = true; + } + } + } + + internal static (uint userId, uint groupId, uint[] groups) GetUserAndGroupIds(ProcessStartInfo startInfo) + { + Debug.Assert(!string.IsNullOrEmpty(startInfo.UserName)); + + (uint? userId, uint? groupId) = GetUserAndGroupIds(startInfo.UserName); + + Debug.Assert(userId.HasValue == groupId.HasValue, "userId and groupId both need to have values, or both need to be null."); + if (!userId.HasValue) + { + throw new Win32Exception(SR.Format(SR.UserDoesNotExist, startInfo.UserName)); + } + + uint[]? groups = Interop.Sys.GetGroupList(startInfo.UserName, groupId!.Value); + if (groups == null) + { + throw new Win32Exception(SR.Format(SR.UserGroupsCannotBeDetermined, startInfo.UserName)); + } + + return (userId.Value, groupId.Value, groups); + } + + private static unsafe (uint? userId, uint? groupId) GetUserAndGroupIds(string userName) + { + Interop.Sys.Passwd? passwd; + // First try with a buffer that should suffice for 99% of cases. + // Note: on CentOS/RedHat 7.1 systems, getpwnam_r returns 'user not found' if the buffer is too small + // see https://bugs.centos.org/view.php?id=7324 + const int BufLen = Interop.Sys.Passwd.InitialBufferSize; + byte* stackBuf = stackalloc byte[BufLen]; + if (TryGetPasswd(userName, stackBuf, BufLen, out passwd)) + { + if (passwd == null) + { + return (null, null); + } + return (passwd.Value.UserId, passwd.Value.GroupId); + } + + // Fallback to heap allocations if necessary, growing the buffer until + // we succeed. TryGetPasswd will throw if there's an unexpected error. + int lastBufLen = BufLen; + while (true) + { + lastBufLen *= 2; + byte[] heapBuf = new byte[lastBufLen]; + fixed (byte* buf = &heapBuf[0]) + { + if (TryGetPasswd(userName, buf, heapBuf.Length, out passwd)) + { + if (passwd == null) + { + return (null, null); + } + return (passwd.Value.UserId, passwd.Value.GroupId); + } + } + } + } + + private static unsafe bool TryGetPasswd(string name, byte* buf, int bufLen, out Interop.Sys.Passwd? passwd) + { + // Call getpwnam_r to get the passwd struct + Interop.Sys.Passwd tempPasswd; + int error = Interop.Sys.GetPwNamR(name, out tempPasswd, buf, bufLen); + + // If the call succeeds, give back the passwd retrieved + if (error == 0) + { + passwd = tempPasswd; + return true; + } + + // If the current user's entry could not be found, give back null, + // but still return true as false indicates the buffer was too small. + if (error == -1) + { + passwd = null; + return true; + } + + var errorInfo = new Interop.ErrorInfo(error); + + // If the call failed because the buffer was too small, return false to + // indicate the caller should try again with a larger buffer. + if (errorInfo.Error == Interop.Error.ERANGE) + { + passwd = null; + return false; + } + + // Otherwise, fail. + throw new Win32Exception(errorInfo.RawErrno, errorInfo.GetErrorMessage()); + } + + internal static string? ResolveExecutableForShellExecute(string filename, string? workingDirectory) + { + // Determine if filename points to an executable file. + // filename may be an absolute path, a relative path or a uri. + + string? resolvedFilename = null; + // filename is an absolute path + if (Path.IsPathRooted(filename)) + { + if (File.Exists(filename)) + { + resolvedFilename = filename; + } + } + // filename is a uri + else if (Uri.TryCreate(filename, UriKind.Absolute, out Uri? uri)) + { + if (uri.IsFile && uri.Host == "" && File.Exists(uri.LocalPath)) + { + resolvedFilename = uri.LocalPath; + } + } + // filename is relative + else + { + // The WorkingDirectory property specifies the location of the executable. + // If WorkingDirectory is an empty string, the current directory is understood to contain the executable. + workingDirectory = workingDirectory != null ? Path.GetFullPath(workingDirectory) : + Directory.GetCurrentDirectory(); + string filenameInWorkingDirectory = Path.Combine(workingDirectory, filename); + // filename is a relative path in the working directory + if (File.Exists(filenameInWorkingDirectory)) + { + resolvedFilename = filenameInWorkingDirectory; + } + // find filename on PATH + else + { + resolvedFilename = FindProgramInPath(filename); + } + } + + if (resolvedFilename == null) + { + return null; + } + + if (Interop.Sys.Access(resolvedFilename, Interop.Sys.AccessMode.X_OK) == 0) + { + return resolvedFilename; + } + else + { + return null; + } + } + + [UnmanagedCallersOnly] + private static int OnSigChild(int reapAll, int configureConsole) + { + // configureConsole is non zero when there are PosixSignalRegistrations that + // may Cancel the terminal configuration that happens when there are no more + // children using the terminal. + // When the registrations don't cancel the terminal configuration, + // DelayedSigChildConsoleConfiguration will be called. + + // Lock to avoid races with Process.Start + s_processStartLock.EnterWriteLock(); + try + { + bool childrenUsingTerminalPre = AreChildrenUsingTerminal; + ProcessWaitState.CheckChildren(reapAll != 0, configureConsole != 0); + bool childrenUsingTerminalPost = AreChildrenUsingTerminal; + + // return whether console configuration was skipped. + return childrenUsingTerminalPre && !childrenUsingTerminalPost && configureConsole == 0 ? 1 : 0; + } + finally + { + s_processStartLock.ExitWriteLock(); + } + } + + /// Converts the filename and arguments information from a ProcessStartInfo into an argv array. + /// The ProcessStartInfo. + /// Resolved executable to open ProcessStartInfo.FileName + /// Don't pass ProcessStartInfo.Arguments + /// The argv array. + internal static string[] ParseArgv(ProcessStartInfo psi, string? resolvedExe = null, bool ignoreArguments = false) + { + if (string.IsNullOrEmpty(resolvedExe) && + (ignoreArguments || (string.IsNullOrEmpty(psi.Arguments) && !psi.HasArgumentList))) + { + return new string[] { psi.FileName }; + } + + var argvList = new List(); + if (!string.IsNullOrEmpty(resolvedExe)) + { + argvList.Add(resolvedExe); + if (resolvedExe.Contains("kfmclient")) + { + argvList.Add("openURL"); // kfmclient needs OpenURL + } + } + + argvList.Add(psi.FileName); + + if (!ignoreArguments) + { + if (!string.IsNullOrEmpty(psi.Arguments)) + { + ParseArgumentsIntoList(psi.Arguments, argvList); + } + else if (psi.HasArgumentList) + { + argvList.AddRange(psi.ArgumentList); + } + } + return argvList.ToArray(); + } + + /// Resolves a path to the filename passed to ProcessStartInfo. + /// The filename. + /// The resolved path. It can return null in case of URLs. + internal static string? ResolvePath(string filename) + { + // Follow the same resolution that Windows uses with CreateProcess: + // 1. First try the exact path provided + // 2. Then try the file relative to the executable directory + // 3. Then try the file relative to the current directory + // 4. then try the file in each of the directories specified in PATH + // Windows does additional Windows-specific steps between 3 and 4, + // and we ignore those here. + + // If the filename is a complete path, use it, regardless of whether it exists. + if (Path.IsPathRooted(filename)) + { + // In this case, it doesn't matter whether the file exists or not; + // it's what the caller asked for, so it's what they'll get + return filename; + } + + // Then check the executable's directory + string? path = Environment.ProcessPath; + if (path != null) + { + try + { + path = Path.Combine(Path.GetDirectoryName(path)!, filename); + if (File.Exists(path)) + { + return path; + } + } + catch (ArgumentException) { } // ignore any errors in data that may come from the exe path + } + + // Then check the current directory + path = Path.Combine(Directory.GetCurrentDirectory(), filename); + if (File.Exists(path)) + { + return path; + } + + // Then check each directory listed in the PATH environment variables + return FindProgramInPath(filename); + } + + /// Parses a command-line argument string into a list of arguments. + /// The argument string. + /// The list into which the component arguments should be stored. + /// + /// This follows the rules outlined in "Parsing C++ Command-Line Arguments" at + /// https://msdn.microsoft.com/en-us/library/17w5ykft.aspx. + /// + private static void ParseArgumentsIntoList(string arguments, List results) + { + // Iterate through all of the characters in the argument string. + for (int i = 0; i < arguments.Length; i++) + { + while (i < arguments.Length && (arguments[i] == ' ' || arguments[i] == '\t')) + i++; + + if (i == arguments.Length) + break; + + results.Add(GetNextArgument(arguments, ref i)); + } + } + + private static string GetNextArgument(string arguments, ref int i) + { + var currentArgument = new ValueStringBuilder(stackalloc char[256]); + bool inQuotes = false; + + while (i < arguments.Length) + { + // From the current position, iterate through contiguous backslashes. + int backslashCount = 0; + while (i < arguments.Length && arguments[i] == '\\') + { + i++; + backslashCount++; + } + + if (backslashCount > 0) + { + if (i >= arguments.Length || arguments[i] != '"') + { + // Backslashes not followed by a double quote: + // they should all be treated as literal backslashes. + currentArgument.Append('\\', backslashCount); + } + else + { + // Backslashes followed by a double quote: + // - Output a literal slash for each complete pair of slashes + // - If one remains, use it to make the subsequent quote a literal. + currentArgument.Append('\\', backslashCount / 2); + if (backslashCount % 2 != 0) + { + currentArgument.Append('"'); + i++; + } + } + + continue; + } + + char c = arguments[i]; + + // If this is a double quote, track whether we're inside of quotes or not. + // Anything within quotes will be treated as a single argument, even if + // it contains spaces. + if (c == '"') + { + if (inQuotes && i < arguments.Length - 1 && arguments[i + 1] == '"') + { + // Two consecutive double quotes inside an inQuotes region should result in a literal double quote + // (the parser is left in the inQuotes region). + // This behavior is not part of the spec of code:ParseArgumentsIntoList, but is compatible with CRT + // and .NET Framework. + currentArgument.Append('"'); + i++; + } + else + { + inQuotes = !inQuotes; + } + + i++; + continue; + } + + // If this is a space/tab and we're not in quotes, we're done with the current + // argument, it should be added to the results and then reset for the next one. + if ((c == ' ' || c == '\t') && !inQuotes) + { + break; + } + + // Nothing special; add the character to the current argument. + currentArgument.Append(c); + i++; + } + + return currentArgument.ToString(); + } } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs index 6208deff5d094d..702d579c095c54 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs @@ -1,15 +1,116 @@ // 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.Collections.Specialized; +using System.ComponentModel; using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using Microsoft.Win32.SafeHandles; namespace System.Diagnostics { internal static partial class ProcessUtils { + internal static bool SupportsAtomicNonInheritablePipeCreation => true; + private static bool IsExecutable(string fullPath) { return File.Exists(fullPath); } + + internal static int GetShowWindowFromWindowStyle(ProcessWindowStyle windowStyle) => windowStyle switch + { + ProcessWindowStyle.Hidden => Interop.Shell32.SW_HIDE, + ProcessWindowStyle.Minimized => Interop.Shell32.SW_SHOWMINIMIZED, + ProcessWindowStyle.Maximized => Interop.Shell32.SW_SHOWMAXIMIZED, + _ => Interop.Shell32.SW_SHOWNORMAL, + }; + + internal static void BuildCommandLine(ProcessStartInfo startInfo, ref ValueStringBuilder commandLine) + { + // Construct a StringBuilder with the appropriate command line + // to pass to CreateProcess. If the filename isn't already + // in quotes, we quote it here. This prevents some security + // problems (it specifies exactly which part of the string + // is the file to execute). + ReadOnlySpan fileName = startInfo.FileName.AsSpan().Trim(); + bool fileNameIsQuoted = fileName.StartsWith('"') && fileName.EndsWith('"'); + if (!fileNameIsQuoted) + { + commandLine.Append('"'); + } + + commandLine.Append(fileName); + + if (!fileNameIsQuoted) + { + commandLine.Append('"'); + } + + startInfo.AppendArgumentsTo(ref commandLine); + } + + /// Duplicates a handle as inheritable if it's valid and not inheritable. + internal static void DuplicateAsInheritableIfNeeded(SafeFileHandle sourceHandle, ref SafeFileHandle? duplicatedHandle) + { + // The user can't specify invalid handle via ProcessStartInfo.Standard*Handle APIs. + // However, Console.OpenStandard*Handle() can return INVALID_HANDLE_VALUE for a process + // that was started with INVALID_HANDLE_VALUE as given standard handle. + if (sourceHandle.IsInvalid) + { + return; + } + + // When we know for sure that the handle is inheritable, we don't need to duplicate. + // When GetHandleInformation fails, we still attempt to call DuplicateHandle, + // just to keep throwing the same exception (backward compatibility). + if (Interop.Kernel32.GetHandleInformation(sourceHandle, out Interop.Kernel32.HandleFlags flags) + && (flags & Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT) != 0) + { + return; + } + + IntPtr currentProcHandle = Interop.Kernel32.GetCurrentProcess(); + if (!Interop.Kernel32.DuplicateHandle(currentProcHandle, + sourceHandle, + currentProcHandle, + out duplicatedHandle, + 0, + bInheritHandle: true, + Interop.Kernel32.HandleOptions.DUPLICATE_SAME_ACCESS)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + internal static string GetEnvironmentVariablesBlock(DictionaryWrapper sd) + { + // https://learn.microsoft.com/windows/win32/procthread/changing-environment-variables + // "All strings in the environment block must be sorted alphabetically by name. The sort is + // case-insensitive, Unicode order, without regard to locale. Because the equal sign is a + // separator, it must not be used in the name of an environment variable." + + var keys = new string[sd.Count]; + sd.Keys.CopyTo(keys, 0); + Array.Sort(keys, StringComparer.OrdinalIgnoreCase); + + // Join the null-terminated "key=val\0" strings + var result = new StringBuilder(8 * keys.Length); + foreach (string key in keys) + { + string? value = sd[key]; + + // Ignore null values for consistency with Environment.SetEnvironmentVariable + if (value != null) + { + result.Append(key).Append('=').Append(value).Append('\0'); + } + } + + return result.ToString(); + } } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs index c3ead1de0030bf..8b585fc2c14d21 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs @@ -1,12 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.ComponentModel; using System.IO; +using System.Threading; namespace System.Diagnostics { internal static partial class ProcessUtils { + internal static readonly ReaderWriterLockSlim s_processStartLock = new ReaderWriterLockSlim(); + internal static int s_cachedSerializationSwitch; + internal static string? FindProgramInPath(string program) { string? pathEnvVar = System.Environment.GetEnvironmentVariable("PATH"); @@ -28,5 +33,12 @@ internal static partial class ProcessUtils return null; } + + internal static Win32Exception CreateExceptionForErrorStartingProcess(string errorMessage, int errorCode, string fileName, string? workingDirectory) + { + string directoryForException = string.IsNullOrEmpty(workingDirectory) ? Directory.GetCurrentDirectory() : workingDirectory; + string msg = SR.Format(SR.ErrorStartingProcess, fileName, directoryForException, errorMessage); + return new Win32Exception(errorCode, msg); + } } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs index 042a95b1950c64..94c1355558dfc2 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs @@ -550,7 +550,7 @@ private void ChildReaped(int exitCode, bool configureConsole) if (_usesTerminal) { // Update terminal settings before calling SetExited. - Process.ConfigureTerminalForChildProcesses(-1, configureConsole); + ProcessUtils.ConfigureTerminalForChildProcesses(-1, configureConsole); } SetExited(); diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ShellExecuteHelper.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ShellExecuteHelper.cs new file mode 100644 index 00000000000000..e31eb4fd6684ac --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ShellExecuteHelper.cs @@ -0,0 +1,94 @@ +// 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.ComponentModel; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using Microsoft.Win32.SafeHandles; + +namespace System.Diagnostics +{ + internal sealed unsafe class ShellExecuteHelper + { + private readonly Interop.Shell32.SHELLEXECUTEINFO* _executeInfo; + private bool _succeeded; + private bool _notpresent; + + public ShellExecuteHelper(Interop.Shell32.SHELLEXECUTEINFO* executeInfo) + { + _executeInfo = executeInfo; + } + + private void ShellExecuteFunction() + { + try + { + if (!(_succeeded = Interop.Shell32.ShellExecuteExW(_executeInfo))) + ErrorCode = Marshal.GetLastWin32Error(); + } + catch (EntryPointNotFoundException) + { + _notpresent = true; + } + } + + public bool ShellExecuteOnSTAThread() + { + // ShellExecute() requires STA in order to work correctly. + + if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) + { + ThreadStart threadStart = new ThreadStart(ShellExecuteFunction); + Thread executionThread = new Thread(threadStart) + { + IsBackground = true, + Name = ".NET Process STA" + }; + executionThread.SetApartmentState(ApartmentState.STA); + executionThread.Start(); + executionThread.Join(); + } + else + { + ShellExecuteFunction(); + } + + if (_notpresent) + throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported); + + return _succeeded; + } + + internal static int GetShellError(IntPtr error) + { + switch ((long)error) + { + case Interop.Shell32.SE_ERR_FNF: + return Interop.Errors.ERROR_FILE_NOT_FOUND; + case Interop.Shell32.SE_ERR_PNF: + return Interop.Errors.ERROR_PATH_NOT_FOUND; + case Interop.Shell32.SE_ERR_ACCESSDENIED: + return Interop.Errors.ERROR_ACCESS_DENIED; + case Interop.Shell32.SE_ERR_OOM: + return Interop.Errors.ERROR_NOT_ENOUGH_MEMORY; + case Interop.Shell32.SE_ERR_DDEFAIL: + case Interop.Shell32.SE_ERR_DDEBUSY: + case Interop.Shell32.SE_ERR_DDETIMEOUT: + return Interop.Errors.ERROR_DDE_FAIL; + case Interop.Shell32.SE_ERR_SHARE: + return Interop.Errors.ERROR_SHARING_VIOLATION; + case Interop.Shell32.SE_ERR_NOASSOC: + return Interop.Errors.ERROR_NO_ASSOCIATION; + case Interop.Shell32.SE_ERR_DLLNOTFOUND: + return Interop.Errors.ERROR_DLL_NOT_FOUND; + default: + return (int)(long)error; + } + } + + public int ErrorCode { get; private set; } + } +} From 0f411da3ca3d566a70dc6298dfc4ab61d6b78b48 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 30 Mar 2026 16:16:38 +0200 Subject: [PATCH 06/19] Revert "Sync with main (PR #126192) and use SafeProcessHandle.Start in StartAndForget" This reverts commit c6a0adb2d486ef641e66a3e121eff1f7fb874719. --- .../Interop.ForkAndExecProcess.cs | 152 +---- .../Unix/System.Native/Interop.IsATty.cs | 5 - ...omicNonInheritablePipeCreationSupported.cs | 30 - .../Kernel32/Interop.HandleInformation.cs | 4 - .../ref/System.Diagnostics.Process.cs | 12 +- .../SafeHandles/SafeProcessHandle.Unix.cs | 214 +----- .../SafeHandles/SafeProcessHandle.Windows.cs | 278 +------- .../Win32/SafeHandles/SafeProcessHandle.cs | 85 --- .../src/Resources/Strings.resx | 12 - .../src/System.Diagnostics.Process.csproj | 17 +- .../src/System/Diagnostics/Process.FreeBSD.cs | 2 +- .../src/System/Diagnostics/Process.Linux.cs | 2 +- .../src/System/Diagnostics/Process.OSX.cs | 2 +- .../System/Diagnostics/Process.Scenarios.cs | 7 +- .../src/System/Diagnostics/Process.SunOS.cs | 2 +- .../src/System/Diagnostics/Process.Unix.cs | 634 +++++++++++++++++- .../src/System/Diagnostics/Process.Win32.cs | 172 ++++- .../src/System/Diagnostics/Process.Windows.cs | 333 ++++++++- .../src/System/Diagnostics/Process.cs | 163 +---- .../src/System/Diagnostics/Process.iOS.cs | 2 +- .../System/Diagnostics/ProcessStartInfo.cs | 156 +---- ...ConfigureTerminalForChildProcesses.Unix.cs | 64 -- ....ConfigureTerminalForChildProcesses.iOS.cs | 20 - .../System/Diagnostics/ProcessUtils.Unix.cs | 405 ----------- .../Diagnostics/ProcessUtils.Windows.cs | 101 --- .../src/System/Diagnostics/ProcessUtils.cs | 12 - .../Diagnostics/ProcessWaitState.Unix.cs | 2 +- .../System/Diagnostics/ShellExecuteHelper.cs | 94 --- 28 files changed, 1221 insertions(+), 1761 deletions(-) delete mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsAtomicNonInheritablePipeCreationSupported.cs delete mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs delete mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs delete mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ShellExecuteHelper.cs diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs index fe7b7f8fda0e4c..bc947d0ef4ef49 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs @@ -2,169 +2,91 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; -using Microsoft.Win32.SafeHandles; internal static partial class Interop { internal static partial class Sys { internal static unsafe int ForkAndExecProcess( - string filename, string[] argv, IDictionary env, string? cwd, + string filename, string[] argv, string[] envp, string? cwd, + bool redirectStdin, bool redirectStdout, bool redirectStderr, bool setUser, uint userId, uint groupId, uint[]? groups, - out int lpChildPid, SafeFileHandle? stdinFd, SafeFileHandle? stdoutFd, SafeFileHandle? stderrFd, bool shouldThrow = true) + out int lpChildPid, out int stdinFd, out int stdoutFd, out int stderrFd, bool shouldThrow = true) { byte** argvPtr = null, envpPtr = null; int result = -1; - - bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false; try { - int stdinRawFd = -1, stdoutRawFd = -1, stderrRawFd = -1; - - if (stdinFd is not null) - { - stdinFd.DangerousAddRef(ref stdinRefAdded); - stdinRawFd = stdinFd.DangerousGetHandle().ToInt32(); - } - - if (stdoutFd is not null) - { - stdoutFd.DangerousAddRef(ref stdoutRefAdded); - stdoutRawFd = stdoutFd.DangerousGetHandle().ToInt32(); - } - - if (stderrFd is not null) - { - stderrFd.DangerousAddRef(ref stderrRefAdded); - stderrRawFd = stderrFd.DangerousGetHandle().ToInt32(); - } - - AllocArgvArray(argv, ref argvPtr); - AllocEnvpArray(env, ref envpPtr); + AllocNullTerminatedArray(argv, ref argvPtr); + AllocNullTerminatedArray(envp, ref envpPtr); fixed (uint* pGroups = groups) { result = ForkAndExecProcess( filename, argvPtr, envpPtr, cwd, + redirectStdin ? 1 : 0, redirectStdout ? 1 : 0, redirectStderr ? 1 : 0, setUser ? 1 : 0, userId, groupId, pGroups, groups?.Length ?? 0, - out lpChildPid, stdinRawFd, stdoutRawFd, stderrRawFd); + out lpChildPid, out stdinFd, out stdoutFd, out stderrFd); } return result == 0 ? 0 : Marshal.GetLastPInvokeError(); } finally { - NativeMemory.Free(envpPtr); - NativeMemory.Free(argvPtr); - - if (stdinRefAdded) - stdinFd!.DangerousRelease(); - if (stdoutRefAdded) - stdoutFd!.DangerousRelease(); - if (stderrRefAdded) - stderrFd!.DangerousRelease(); + FreeArray(envpPtr, envp.Length); + FreeArray(argvPtr, argv.Length); } } [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_ForkAndExecProcess", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] private static unsafe partial int ForkAndExecProcess( string filename, byte** argv, byte** envp, string? cwd, + int redirectStdin, int redirectStdout, int redirectStderr, int setUser, uint userId, uint groupId, uint* groups, int groupsLength, - out int lpChildPid, int stdinFd, int stdoutFd, int stderrFd); + out int lpChildPid, out int stdinFd, out int stdoutFd, out int stderrFd); - /// - /// Allocates a single native memory block containing both a null-terminated pointer array - /// and the UTF-8 encoded string data for the given array of strings. - /// - private static unsafe void AllocArgvArray(string[] arr, ref byte** arrPtr) + private static unsafe void AllocNullTerminatedArray(string[] arr, ref byte** arrPtr) { - int count = arr.Length; - - // First pass: compute total byte length of all strings. - int dataByteLength = 0; - foreach (string str in arr) + nuint arrLength = (nuint)arr.Length + 1; // +1 is for null termination + + // Allocate the unmanaged array to hold each string pointer. + // It needs to have an extra element to null terminate the array. + // Zero the memory so that if any of the individual string allocations fails, + // we can loop through the array to free any that succeeded. + // The last element will remain null. + arrPtr = (byte**)NativeMemory.AllocZeroed(arrLength, (nuint)sizeof(byte*)); + + // Now copy each string to unmanaged memory referenced from the array. + // We need the data to be an unmanaged, null-terminated array of UTF8-encoded bytes. + for (int i = 0; i < arr.Length; i++) { - dataByteLength = checked(dataByteLength + Encoding.UTF8.GetByteCount(str) + 1); // +1 for null terminator - } - - // Allocate a single block: pointer array (count + 1 for null terminator) followed by string data. - nuint pointersByteLength = checked((nuint)(count + 1) * (nuint)sizeof(byte*)); - byte* block = (byte*)NativeMemory.Alloc(checked(pointersByteLength + (nuint)dataByteLength)); - arrPtr = (byte**)block; + string str = arr[i]; - // Create spans over both portions of the block for bounds-checked access. - byte* dataPtr = block + pointersByteLength; - Span pointers = new Span(block, count + 1); - Span data = new Span(dataPtr, dataByteLength); + int byteLength = Encoding.UTF8.GetByteCount(str); + arrPtr[i] = (byte*)NativeMemory.Alloc((nuint)byteLength + 1); //+1 for null termination - int dataOffset = 0; - for (int i = 0; i < count; i++) - { - pointers[i] = (nint)(dataPtr + dataOffset); + int bytesWritten = Encoding.UTF8.GetBytes(str, new Span(arrPtr[i], byteLength)); + Debug.Assert(bytesWritten == byteLength); - int bytesWritten = Encoding.UTF8.GetBytes(arr[i], data.Slice(dataOffset)); - data[dataOffset + bytesWritten] = (byte)'\0'; - dataOffset += bytesWritten + 1; + arrPtr[i][bytesWritten] = (byte)'\0'; // null terminate } - - pointers[count] = 0; // null terminator - Debug.Assert(dataOffset == dataByteLength); } - /// - /// Allocates a single native memory block containing both a null-terminated pointer array - /// and the UTF-8 encoded "key=value\0" data for all non-null entries in the environment dictionary. - /// - private static unsafe void AllocEnvpArray(IDictionary env, ref byte** arrPtr) + private static unsafe void FreeArray(byte** arr, int length) { - // First pass: count entries with non-null values and compute total buffer size. - int count = 0; - int dataByteLength = 0; - foreach (KeyValuePair pair in env) + if (arr != null) { - if (pair.Value is not null) + // Free each element of the array + for (int i = 0; i < length; i++) { - // Each entry: UTF8(key) + '=' + UTF8(value) + '\0' - dataByteLength = checked(dataByteLength + Encoding.UTF8.GetByteCount(pair.Key) + 1 + Encoding.UTF8.GetByteCount(pair.Value) + 1); - count++; + NativeMemory.Free(arr[i]); } - } - - // Allocate a single block: pointer array (count + 1 for null terminator) followed by string data. - nuint pointersByteLength = checked((nuint)(count + 1) * (nuint)sizeof(byte*)); - byte* block = (byte*)NativeMemory.Alloc(checked(pointersByteLength + (nuint)dataByteLength)); - arrPtr = (byte**)block; - - // Create spans over both portions of the block for bounds-checked access. - byte* dataPtr = block + pointersByteLength; - Span pointers = new Span(block, count + 1); - Span data = new Span(dataPtr, dataByteLength); - // Second pass: encode each key=value pair directly into the buffer. - int entryIndex = 0; - int dataOffset = 0; - foreach (KeyValuePair pair in env) - { - if (pair.Value is not null) - { - pointers[entryIndex] = (nint)(dataPtr + dataOffset); - - int keyBytes = Encoding.UTF8.GetBytes(pair.Key, data.Slice(dataOffset)); - data[dataOffset + keyBytes] = (byte)'='; - int valueBytes = Encoding.UTF8.GetBytes(pair.Value, data.Slice(dataOffset + keyBytes + 1)); - data[dataOffset + keyBytes + 1 + valueBytes] = (byte)'\0'; - - dataOffset += keyBytes + 1 + valueBytes + 1; - entryIndex++; - } + // And then the array itself + NativeMemory.Free(arr); } - - pointers[entryIndex] = 0; // null terminator - Debug.Assert(entryIndex == count); - Debug.Assert(dataOffset == dataByteLength); } } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs index ae905f656dc549..beb79d77d5165b 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs @@ -3,7 +3,6 @@ using System; using System.Runtime.InteropServices; -using Microsoft.Win32.SafeHandles; internal static partial class Interop { @@ -12,9 +11,5 @@ internal static partial class Sys [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_IsATty")] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool IsATty(IntPtr fd); - - [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_IsATty")] - [return: MarshalAs(UnmanagedType.Bool)] - internal static partial bool IsATty(SafeFileHandle fd); } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsAtomicNonInheritablePipeCreationSupported.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsAtomicNonInheritablePipeCreationSupported.cs deleted file mode 100644 index 1c62417d692329..00000000000000 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsAtomicNonInheritablePipeCreationSupported.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Runtime.InteropServices; - -internal static partial class Interop -{ - internal static partial class Sys - { - [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_IsAtomicNonInheritablePipeCreationSupported", SetLastError = false)] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool IsAtomicNonInheritablePipeCreationSupportedImpl(); - - private static NullableBool s_atomicNonInheritablePipeCreationSupported; - - internal static bool IsAtomicNonInheritablePipeCreationSupported - { - get - { - NullableBool isSupported = s_atomicNonInheritablePipeCreationSupported; - if (isSupported == NullableBool.Undefined) - { - s_atomicNonInheritablePipeCreationSupported = isSupported = IsAtomicNonInheritablePipeCreationSupportedImpl() ? NullableBool.True : NullableBool.False; - } - return isSupported == NullableBool.True; - } - } - } -} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs index cb1b8e10bd362e..57ae67529f36bb 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.HandleInformation.cs @@ -17,10 +17,6 @@ internal enum HandleFlags : uint HANDLE_FLAG_PROTECT_FROM_CLOSE = 2 } - [LibraryImport(Libraries.Kernel32, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static partial bool GetHandleInformation(SafeHandle hObject, out HandleFlags lpdwFlags); - [LibraryImport(Libraries.Kernel32, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool SetHandleInformation(SafeHandle hObject, HandleFlags dwMask, HandleFlags dwFlags); 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 4274c9364a272c..f863af7e0c8123 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -11,11 +11,6 @@ public sealed partial class SafeProcessHandle : Microsoft.Win32.SafeHandles.Safe public SafeProcessHandle() : base (default(bool)) { } public SafeProcessHandle(System.IntPtr existingHandle, bool ownsHandle) : base (default(bool)) { } protected override bool ReleaseHandle() { throw null; } - public int ProcessId { get { throw null; } } - [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] - [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] - [System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] - public static Microsoft.Win32.SafeHandles.SafeProcessHandle Start(System.Diagnostics.ProcessStartInfo startInfo) { throw null; } } } namespace System.Diagnostics @@ -165,7 +160,7 @@ public void Refresh() { } [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 System.Diagnostics.Process Start(string fileName, string? arguments) { throw null; } + public static System.Diagnostics.Process Start(string fileName, string arguments) { 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 @@ -234,7 +229,7 @@ public sealed partial class ProcessStartInfo { public ProcessStartInfo() { } public ProcessStartInfo(string fileName) { } - public ProcessStartInfo(string fileName, string? arguments) { } + public ProcessStartInfo(string fileName, string arguments) { } public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable arguments) { } public System.Collections.ObjectModel.Collection ArgumentList { get { throw null; } } [System.Diagnostics.CodeAnalysis.AllowNullAttribute] @@ -266,9 +261,6 @@ public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable< public bool RedirectStandardInput { get { throw null; } set { } } public bool RedirectStandardOutput { get { throw null; } set { } } public System.Text.Encoding? StandardErrorEncoding { get { throw null; } set { } } - public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardErrorHandle { get { throw null; } set { } } - public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardInputHandle { get { throw null; } set { } } - public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardOutputHandle { get { throw null; } set { } } public System.Text.Encoding? StandardInputEncoding { get { throw null; } set { } } public System.Text.Encoding? StandardOutputEncoding { get { throw null; } set { } } [System.Diagnostics.CodeAnalysis.AllowNullAttribute] diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs index f227eeaf4dd18d..f3cac1f1af898b 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs @@ -1,19 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +/*============================================================ +** +** Class: SafeProcessHandle +** +** A wrapper for a process handle +** +** +===========================================================*/ + using System; -using System.Collections.Generic; -using System.ComponentModel; 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; namespace Microsoft.Win32.SafeHandles { @@ -29,15 +27,6 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali private readonly SafeWaitHandle? _handle; private readonly bool _releaseRef; - private SafeProcessHandle(int processId, ProcessWaitState.Holder waitStateHolder) : base(ownsHandle: true) - { - ProcessId = processId; - - _handle = waitStateHolder._state.EnsureExitedEvent().GetSafeWaitHandle(); - _handle.DangerousAddRef(ref _releaseRef); - SetHandle(_handle.DangerousGetHandle()); - } - internal SafeProcessHandle(int processId, SafeWaitHandle handle) : this(handle.DangerousGetHandle(), ownsHandle: true) { @@ -46,6 +35,8 @@ internal SafeProcessHandle(int processId, SafeWaitHandle handle) : handle.DangerousAddRef(ref _releaseRef); } + internal int ProcessId { get; } + protected override bool ReleaseHandle() { if (_releaseRef) @@ -55,188 +46,5 @@ protected override bool ReleaseHandle() } return true; } - - // On Unix, we don't use process descriptors yet, so we can't get PID. - private static int GetProcessIdCore() => throw new PlatformNotSupportedException(); - - private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) - { - SafeProcessHandle startedProcess = StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, out ProcessWaitState.Holder? waitStateHolder); - - // For standalone SafeProcessHandle.Start, we dispose the wait state holder immediately. - // The DangerousAddRef on the SafeWaitHandle (Unix) keeps the OS handle alive. - waitStateHolder?.Dispose(); - - return startedProcess; - } - - internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder) - { - waitStateHolder = null; - - if (ProcessUtils.PlatformDoesNotSupportProcessStartAndKill) - { - throw new PlatformNotSupportedException(); - } - - ProcessUtils.EnsureInitialized(); - - string? filename; - string[] argv; - - IDictionary env = startInfo.Environment; - string? cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null; - - bool setCredentials = !string.IsNullOrEmpty(startInfo.UserName); - uint userId = 0; - uint groupId = 0; - uint[]? groups = null; - if (setCredentials) - { - (userId, groupId, groups) = ProcessUtils.GetUserAndGroupIds(startInfo); - } - - // .NET applications don't echo characters unless there is a Console.Read operation. - // Unix applications expect the terminal to be in an echoing state by default. - // To support processes that interact with the terminal (e.g. 'vi'), we need to configure the - // terminal to echo. We keep this configuration as long as there are children possibly using the terminal. - // Handle can be null only for UseShellExecute or platforms that don't support Console.Open* methods like Android. - bool usesTerminal = (stdinHandle is not null && Interop.Sys.IsATty(stdinHandle)) - || (stdoutHandle is not null && Interop.Sys.IsATty(stdoutHandle)) - || (stderrHandle is not null && Interop.Sys.IsATty(stderrHandle)); - - if (startInfo.UseShellExecute) - { - string verb = startInfo.Verb; - if (verb != string.Empty && - !string.Equals(verb, "open", StringComparison.OrdinalIgnoreCase)) - { - throw new Win32Exception(Interop.Errors.ERROR_NO_ASSOCIATION); - } - - // On Windows, UseShellExecute of executables and scripts causes those files to be executed. - // To achieve this on Unix, we check if the file is executable (x-bit). - // Some files may have the x-bit set even when they are not executable. This happens for example - // when a Windows filesystem is mounted on Linux. To handle that, treat it as a regular file - // when exec returns ENOEXEC (file format cannot be executed). - filename = ProcessUtils.ResolveExecutableForShellExecute(startInfo.FileName, cwd); - if (filename != null) - { - argv = ProcessUtils.ParseArgv(startInfo); - - SafeProcessHandle processHandle = ForkAndExecProcess( - startInfo, filename, argv, env, cwd, - setCredentials, userId, groupId, groups, - stdinHandle, stdoutHandle, stderrHandle, usesTerminal, - out waitStateHolder, - throwOnNoExec: false); // return invalid handle instead of throwing on ENOEXEC - - if (!processHandle.IsInvalid) - { - return processHandle; - } - } - - // use default program to open file/url - filename = Process.GetPathToOpenFile(); - argv = ProcessUtils.ParseArgv(startInfo, filename, ignoreArguments: true); - - return ForkAndExecProcess( - startInfo, filename, argv, env, cwd, - setCredentials, userId, groupId, groups, - stdinHandle, stdoutHandle, stderrHandle, usesTerminal, - out waitStateHolder); - } - else - { - filename = ProcessUtils.ResolvePath(startInfo.FileName); - argv = ProcessUtils.ParseArgv(startInfo); - if (Directory.Exists(filename)) - { - throw new Win32Exception(SR.DirectoryNotValidAsInput); - } - - return ForkAndExecProcess( - startInfo, filename, argv, env, cwd, - setCredentials, userId, groupId, groups, - stdinHandle, stdoutHandle, stderrHandle, usesTerminal, - out waitStateHolder); - } - } - - private static SafeProcessHandle ForkAndExecProcess( - ProcessStartInfo startInfo, string? resolvedFilename, string[] argv, - IDictionary env, string? cwd, bool setCredentials, uint userId, - uint groupId, uint[]? groups, - SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, - bool usesTerminal, out ProcessWaitState.Holder? waitStateHolder, bool throwOnNoExec = true) - { - waitStateHolder = null; - - if (string.IsNullOrEmpty(resolvedFilename)) - { - Interop.ErrorInfo error = Interop.Error.ENOENT.Info(); - throw ProcessUtils.CreateExceptionForErrorStartingProcess(error.GetErrorMessage(), error.RawErrno, startInfo.FileName, cwd); - } - - int childPid, errno; - - // Lock to avoid races with OnSigChild - // By using a ReaderWriterLock we allow multiple processes to start concurrently. - ProcessUtils.s_processStartLock.EnterReadLock(); - try - { - if (usesTerminal) - { - ProcessUtils.ConfigureTerminalForChildProcesses(1); - } - - // Invoke the shim fork/execve routine. It will fork a child process, - // map the provided file handles onto the appropriate stdin/stdout/stderr - // descriptors, and execve to execute the requested process. The shim implementation - // is used to fork/execve as executing managed code in a forked process is not safe (only - // the calling thread will transfer, thread IDs aren't stable across the fork, etc.) - errno = Interop.Sys.ForkAndExecProcess( - resolvedFilename, argv, env, cwd, - setCredentials, userId, groupId, groups, - out childPid, stdinHandle, stdoutHandle, stderrHandle); - - if (errno == 0) - { - // Create the wait state holder while still holding the read lock. - // This ensures the child process is registered in s_childProcessWaitStates - // before the lock is released. If SIGCHLD fires after the lock is released, - // CheckChildren will find the child in the table and reap it properly. - // Without this, there is a race: SIGCHLD could fire after the lock is released - // but before the child is registered, causing WaitForExit to hang indefinitely. - waitStateHolder = new ProcessWaitState.Holder(childPid, isNewChild: true, usesTerminal); - } - } - finally - { - ProcessUtils.s_processStartLock.ExitReadLock(); - } - - if (errno != 0) - { - if (usesTerminal) - { - // We failed to launch a child that could use the terminal. - ProcessUtils.s_processStartLock.EnterWriteLock(); - ProcessUtils.ConfigureTerminalForChildProcesses(-1); - ProcessUtils.s_processStartLock.ExitWriteLock(); - } - - if (!throwOnNoExec && - new Interop.ErrorInfo(errno).Error == Interop.Error.ENOEXEC) - { - return InvalidHandle; - } - - throw ProcessUtils.CreateExceptionForErrorStartingProcess(new Interop.ErrorInfo(errno).GetErrorMessage(), errno, resolvedFilename, cwd); - } - - return new SafeProcessHandle(childPid, waitStateHolder!); - } } } diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs index 416ec91ae11317..1fc7a409713278 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs @@ -1,11 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +/*============================================================ +** +** Class: SafeProcessHandle +** +** A wrapper for a process handle +** +** +===========================================================*/ + using System; -using System.Diagnostics; using System.Runtime.InteropServices; using System.Security; -using System.Text; namespace Microsoft.Win32.SafeHandles { @@ -15,272 +22,5 @@ protected override bool ReleaseHandle() { return Interop.Kernel32.CloseHandle(handle); } - - internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) - { - return startInfo.UseShellExecute - ? StartWithShellExecuteEx(startInfo) - : StartWithCreateProcess(startInfo, stdinHandle, stdoutHandle, stderrHandle); - } - - private static unsafe SafeProcessHandle StartWithShellExecuteEx(ProcessStartInfo startInfo) - { - if (!string.IsNullOrEmpty(startInfo.UserName) || startInfo.Password != null) - throw new InvalidOperationException(SR.CantStartAsUser); - - if (startInfo.StandardInputEncoding != null) - throw new InvalidOperationException(SR.StandardInputEncodingNotAllowed); - - if (startInfo.StandardErrorEncoding != null) - throw new InvalidOperationException(SR.StandardErrorEncodingNotAllowed); - - if (startInfo.StandardOutputEncoding != null) - throw new InvalidOperationException(SR.StandardOutputEncodingNotAllowed); - - if (startInfo._environmentVariables != null) - throw new InvalidOperationException(SR.CantUseEnvVars); - - string arguments = startInfo.BuildArguments(); - - fixed (char* fileName = startInfo.FileName.Length > 0 ? startInfo.FileName : null) - fixed (char* verb = startInfo.Verb.Length > 0 ? startInfo.Verb : null) - fixed (char* parameters = arguments.Length > 0 ? arguments : null) - fixed (char* directory = startInfo.WorkingDirectory.Length > 0 ? startInfo.WorkingDirectory : null) - { - Interop.Shell32.SHELLEXECUTEINFO shellExecuteInfo = new Interop.Shell32.SHELLEXECUTEINFO() - { - cbSize = (uint)sizeof(Interop.Shell32.SHELLEXECUTEINFO), - lpFile = fileName, - lpVerb = verb, - lpParameters = parameters, - lpDirectory = directory, - fMask = Interop.Shell32.SEE_MASK_NOCLOSEPROCESS | Interop.Shell32.SEE_MASK_FLAG_DDEWAIT - }; - - if (startInfo.ErrorDialog) - shellExecuteInfo.hwnd = startInfo.ErrorDialogParentHandle; - else - shellExecuteInfo.fMask |= Interop.Shell32.SEE_MASK_FLAG_NO_UI; - - shellExecuteInfo.nShow = ProcessUtils.GetShowWindowFromWindowStyle(startInfo.WindowStyle); - ShellExecuteHelper executeHelper = new ShellExecuteHelper(&shellExecuteInfo); - if (!executeHelper.ShellExecuteOnSTAThread()) - { - int errorCode = executeHelper.ErrorCode; - if (errorCode == 0) - { - errorCode = ShellExecuteHelper.GetShellError(shellExecuteInfo.hInstApp); - } - - switch (errorCode) - { - case Interop.Errors.ERROR_CALL_NOT_IMPLEMENTED: - // This happens on Windows Nano - throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported); - default: - string nativeErrorMessage = errorCode == Interop.Errors.ERROR_BAD_EXE_FORMAT || errorCode == Interop.Errors.ERROR_EXE_MACHINE_TYPE_MISMATCH - ? SR.InvalidApplication - : Interop.Kernel32.GetMessage(errorCode); - - throw ProcessUtils.CreateExceptionForErrorStartingProcess(nativeErrorMessage, errorCode, startInfo.FileName, startInfo.WorkingDirectory); - } - } - - // From https://learn.microsoft.com/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfow: - // "In some cases, such as when execution is satisfied through a DDE conversation, no handle will be returned." - // Process.Start will return false if the handle is invalid. - return new SafeProcessHandle(shellExecuteInfo.hProcess); - } - } - - /// Starts the process using the supplied start info. - private static unsafe SafeProcessHandle StartWithCreateProcess(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) - { - // See knowledge base article Q190351 for an explanation of the following code. Noteworthy tricky points: - // * The handles are duplicated as inheritable before they are passed to CreateProcess so - // that the child process can use them - - var commandLine = new ValueStringBuilder(stackalloc char[256]); - ProcessUtils.BuildCommandLine(startInfo, ref commandLine); - - Interop.Kernel32.STARTUPINFO startupInfo = default; - Interop.Kernel32.PROCESS_INFORMATION processInfo = default; - Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default; - SafeProcessHandle procSH = new SafeProcessHandle(); - - // Inheritable copies of the child handles for CreateProcess - SafeFileHandle? inheritableStdinHandle = null; - SafeFileHandle? inheritableStdoutHandle = null; - SafeFileHandle? inheritableStderrHandle = null; - - // Take a global lock to synchronize all redirect pipe handle creations and CreateProcess - // calls. We do not want one process to inherit the handles created concurrently for another - // process, as that will impact the ownership and lifetimes of those handles now inherited - // into multiple child processes. - - ProcessUtils.s_processStartLock.EnterWriteLock(); - try - { - startupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFO); - - if (stdinHandle is not null || stdoutHandle is not null || stderrHandle is not null) - { - Debug.Assert(stdinHandle is not null && stdoutHandle is not null && stderrHandle is not null, "All or none of the standard handles must be provided."); - - ProcessUtils.DuplicateAsInheritableIfNeeded(stdinHandle, ref inheritableStdinHandle); - ProcessUtils.DuplicateAsInheritableIfNeeded(stdoutHandle, ref inheritableStdoutHandle); - ProcessUtils.DuplicateAsInheritableIfNeeded(stderrHandle, ref inheritableStderrHandle); - - startupInfo.hStdInput = (inheritableStdinHandle ?? stdinHandle).DangerousGetHandle(); - startupInfo.hStdOutput = (inheritableStdoutHandle ?? stdoutHandle).DangerousGetHandle(); - startupInfo.hStdError = (inheritableStderrHandle ?? stderrHandle).DangerousGetHandle(); - - // If STARTF_USESTDHANDLES is not set, the new process will inherit the standard handles. - startupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES; - } - - if (startInfo.WindowStyle != ProcessWindowStyle.Normal) - { - startupInfo.wShowWindow = (short)ProcessUtils.GetShowWindowFromWindowStyle(startInfo.WindowStyle); - startupInfo.dwFlags |= Interop.Advapi32.StartupInfoOptions.STARTF_USESHOWWINDOW; - } - - // set up the creation flags parameter - int creationFlags = 0; - if (startInfo.CreateNoWindow) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NO_WINDOW; - if (startInfo.CreateNewProcessGroup) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NEW_PROCESS_GROUP; - - // set up the environment block parameter - string? environmentBlock = null; - if (startInfo._environmentVariables != null) - { - creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_UNICODE_ENVIRONMENT; - environmentBlock = ProcessUtils.GetEnvironmentVariablesBlock(startInfo._environmentVariables!); - } - - string? workingDirectory = startInfo.WorkingDirectory; - if (workingDirectory.Length == 0) - { - workingDirectory = null; - } - - bool retVal; - int errorCode = 0; - - if (startInfo.UserName.Length != 0) - { - if (startInfo.Password != null && startInfo.PasswordInClearText != null) - { - throw new ArgumentException(SR.CantSetDuplicatePassword); - } - - Interop.Advapi32.LogonFlags logonFlags = (Interop.Advapi32.LogonFlags)0; - if (startInfo.LoadUserProfile && startInfo.UseCredentialsForNetworkingOnly) - { - throw new ArgumentException(SR.CantEnableConflictingLogonFlags, nameof(startInfo)); - } - else if (startInfo.LoadUserProfile) - { - logonFlags = Interop.Advapi32.LogonFlags.LOGON_WITH_PROFILE; - } - else if (startInfo.UseCredentialsForNetworkingOnly) - { - logonFlags = Interop.Advapi32.LogonFlags.LOGON_NETCREDENTIALS_ONLY; - } - - commandLine.NullTerminate(); - fixed (char* passwordInClearTextPtr = startInfo.PasswordInClearText ?? string.Empty) - fixed (char* environmentBlockPtr = environmentBlock) - fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) - { - IntPtr passwordPtr = (startInfo.Password != null) ? - Marshal.SecureStringToGlobalAllocUnicode(startInfo.Password) : IntPtr.Zero; - - try - { - retVal = Interop.Advapi32.CreateProcessWithLogonW( - startInfo.UserName, - startInfo.Domain, - (passwordPtr != IntPtr.Zero) ? passwordPtr : (IntPtr)passwordInClearTextPtr, - logonFlags, - null, // we don't need this since all the info is in commandLine - commandLinePtr, - creationFlags, - (IntPtr)environmentBlockPtr, - workingDirectory, - ref startupInfo, // pointer to STARTUPINFO - ref processInfo // pointer to PROCESS_INFORMATION - ); - if (!retVal) - errorCode = Marshal.GetLastWin32Error(); - } - finally - { - if (passwordPtr != IntPtr.Zero) - Marshal.ZeroFreeGlobalAllocUnicode(passwordPtr); - } - } - } - else - { - commandLine.NullTerminate(); - fixed (char* environmentBlockPtr = environmentBlock) - fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) - { - retVal = Interop.Kernel32.CreateProcess( - null, // we don't need this since all the info is in commandLine - commandLinePtr, // pointer to the command line string - ref unused_SecAttrs, // address to process security attributes, we don't need to inherit the handle - ref unused_SecAttrs, // address to thread security attributes. - true, // handle inheritance flag - creationFlags, // creation flags - (IntPtr)environmentBlockPtr, // pointer to new environment block - workingDirectory, // pointer to current directory name - ref startupInfo, // pointer to STARTUPINFO - ref processInfo // pointer to PROCESS_INFORMATION - ); - if (!retVal) - errorCode = Marshal.GetLastWin32Error(); - } - } - - if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != new IntPtr(-1)) - Marshal.InitHandle(procSH, processInfo.hProcess); - if (processInfo.hThread != IntPtr.Zero && processInfo.hThread != new IntPtr(-1)) - Interop.Kernel32.CloseHandle(processInfo.hThread); - - if (!retVal) - { - string nativeErrorMessage = errorCode == Interop.Errors.ERROR_BAD_EXE_FORMAT || errorCode == Interop.Errors.ERROR_EXE_MACHINE_TYPE_MISMATCH - ? SR.InvalidApplication - : Interop.Kernel32.GetMessage(errorCode); - - throw ProcessUtils.CreateExceptionForErrorStartingProcess(nativeErrorMessage, errorCode, startInfo.FileName, workingDirectory); - } - } - catch - { - procSH.Dispose(); - throw; - } - finally - { - // Only dispose duplicated handles, not the original handles passed by the caller. - // When the handle was invalid or already inheritable, no duplication was needed. - inheritableStdinHandle?.Dispose(); - inheritableStdoutHandle?.Dispose(); - inheritableStderrHandle?.Dispose(); - - ProcessUtils.s_processStartLock.ExitWriteLock(); - - commandLine.Dispose(); - } - - Debug.Assert(!procSH.IsInvalid); - procSH.ProcessId = (int)processInfo.dwProcessId; - return procSH; - } - - private int GetProcessIdCore() => Interop.Kernel32.GetProcessId(this); } } 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 0351614413591f..c7d52e23e0b0ce 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 @@ -12,35 +12,12 @@ using System; using System.Diagnostics; -using System.Runtime.Serialization; -using System.Runtime.Versioning; namespace Microsoft.Win32.SafeHandles { public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvalid { internal static readonly SafeProcessHandle InvalidHandle = new SafeProcessHandle(); - private int _processId = -1; - - /// - /// Gets the process ID. - /// - public int ProcessId - { - get - { - Validate(); - - if (_processId == -1) - { - _processId = GetProcessIdCore(); - } - - return _processId; - - } - private set => _processId = value; - } /// /// Creates a . @@ -65,67 +42,5 @@ public SafeProcessHandle(IntPtr existingHandle, bool ownsHandle) { SetHandle(existingHandle); } - - /// - /// Starts a process using the specified . - /// - /// The process start information. - /// A representing the started process. - /// - /// On Windows, when is , - /// the process is started using ShellExecuteEx. In some cases, such as when execution - /// is satisfied through a DDE conversation, the returned handle will be invalid. - /// - [UnsupportedOSPlatform("ios")] - [UnsupportedOSPlatform("tvos")] - [SupportedOSPlatform("maccatalyst")] - public static SafeProcessHandle Start(ProcessStartInfo startInfo) - { - ArgumentNullException.ThrowIfNull(startInfo); - startInfo.ThrowIfInvalid(out bool anyRedirection); - - if (anyRedirection) - { - // Process has .StandardInput, .StandardOutput, or .StandardError APIs that can express - // redirection of streams, but SafeProcessHandle doesn't. - // The caller can provide handles via the StandardInputHandle, StandardOutputHandle, - // and StandardErrorHandle properties. - throw new InvalidOperationException(SR.CantSetRedirectForSafeProcessHandleStart); - } - - SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); - - SafeFileHandle? childInputHandle = startInfo.StandardInputHandle; - SafeFileHandle? childOutputHandle = startInfo.StandardOutputHandle; - SafeFileHandle? childErrorHandle = startInfo.StandardErrorHandle; - - if (!startInfo.UseShellExecute) - { - if (childInputHandle is null && !OperatingSystem.IsAndroid()) - { - childInputHandle = Console.OpenStandardInputHandle(); - } - - if (childOutputHandle is null && !OperatingSystem.IsAndroid()) - { - childOutputHandle = Console.OpenStandardOutputHandle(); - } - - if (childErrorHandle is null && !OperatingSystem.IsAndroid()) - { - childErrorHandle = Console.OpenStandardErrorHandle(); - } - } - - return StartCore(startInfo, childInputHandle, childOutputHandle, childErrorHandle); - } - - private void Validate() - { - if (IsInvalid) - { - throw new InvalidOperationException(SR.InvalidProcessHandle); - } - } } } diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index 2f7cf93b80bf3a..bfed1eff99beaa 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -210,15 +210,6 @@ The Process object must have the UseShellExecute property set to false in order to redirect IO streams. - - Invalid handle. - - - The StandardInputHandle, StandardOutputHandle, and StandardErrorHandle properties cannot be used together with the corresponding RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError properties. - - - The RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError properties cannot be used by SafeProcessHandle.Start. Use the StandardInputHandle, StandardOutputHandle, and StandardErrorHandle properties. - The FileName property should not be a directory unless UseShellExecute is set. @@ -345,9 +336,6 @@ Invalid performance counter data with type '{0}'. - - Invalid process handle. - Stream redirection is not supported by StartAndForget. Redirected streams must be drained to avoid deadlocks, which is incompatible with fire-and-forget semantics. 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 8e68a16dfc8411..19a7a7e034df0c 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -121,8 +121,6 @@ Link="Common\Interop\Windows\Kernel32\Interop.GetModuleBaseName.cs" /> - + Link="Common\Interop\Windows\kernel32\Interop.ProcessOptions.cs" /> - @@ -289,28 +286,24 @@ Link="Common\Interop\Unix\Interop.GetEUid.cs" /> - - - - + - + + Gets execution path - internal static string? GetPathToOpenFile() + private static string? GetPathToOpenFile() { if (Interop.Sys.Stat("/usr/local/bin/open", out _) == 0) { diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs index 449543d53db458..93c7950b453571 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs @@ -103,7 +103,7 @@ private static DateTime BootTime GetStat().ppid; /// Gets execution path - internal static string? GetPathToOpenFile() + private static string? GetPathToOpenFile() { ReadOnlySpan allowedProgramsToRun = ["xdg-open", "gnome-open", "kfmclient"]; foreach (var program in allowedProgramsToRun) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs index f3f236b3f93e5b..b84938c65e8eea 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs @@ -52,7 +52,7 @@ internal DateTime StartTimeCore } /// Gets execution path - internal static string GetPathToOpenFile() + private static string GetPathToOpenFile() { return "/usr/bin/open"; } 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 index 252b74d9bbfb8b..2557bf6f5733d4 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Runtime.Versioning; -using Microsoft.Win32.SafeHandles; namespace System.Diagnostics { @@ -49,8 +48,10 @@ public static int StartAndForget(ProcessStartInfo startInfo) throw new InvalidOperationException(SR.StartAndForget_RedirectNotSupported); } - using SafeProcessHandle processHandle = SafeProcessHandle.Start(startInfo); - return processHandle.ProcessId; + using Process process = new Process(); + process.StartInfo = startInfo; + process.Start(); + return process.Id; } /// diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs index fb515db23e5ccb..143e962984b0cf 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs @@ -36,7 +36,7 @@ internal DateTime StartTimeCore private int ParentProcessId => GetProcInfo().ParentPid; /// Gets execution path - internal static string? GetPathToOpenFile() + private static string? GetPathToOpenFile() { return ProcessUtils.FindProgramInPath("xdg-open"); } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs index 64b7853340d7f5..f26a8f70ffba74 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs @@ -16,6 +16,10 @@ namespace System.Diagnostics { public partial class Process : IDisposable { + private static volatile bool s_initialized; + private static readonly object s_initializedGate = new object(); + private static readonly ReaderWriterLockSlim s_processStartLock = new ReaderWriterLockSlim(); + /// /// Puts a Process component in state to interact with operating system processes that run in a /// special mode by enabling the native property SeDebugPrivilege on the current thread. @@ -54,7 +58,7 @@ public static Process Start(string fileName, string arguments, string userName, [SupportedOSPlatform("maccatalyst")] public void Kill() { - if (ProcessUtils.PlatformDoesNotSupportProcessStartAndKill) + if (PlatformDoesNotSupportProcessStartAndKill) { throw new PlatformNotSupportedException(); } @@ -355,21 +359,376 @@ private SafeProcessHandle GetProcessHandle() return new SafeProcessHandle(_processId, GetSafeWaitHandle()); } - private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) + /// + /// Starts the process using the supplied start info. + /// With UseShellExecute option, we'll try the shell tools to launch it(e.g. "open fileName") + /// + /// The start info with which to start the process. + private bool StartCore(ProcessStartInfo startInfo) { - SafeProcessHandle startedProcess = SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, out ProcessWaitState.Holder? waitStateHolder); - Debug.Assert(!startedProcess.IsInvalid); + if (PlatformDoesNotSupportProcessStartAndKill) + { + throw new PlatformNotSupportedException(); + } + + EnsureInitialized(); + + string? filename; + string[] argv; + + if (startInfo.UseShellExecute) + { + if (startInfo.RedirectStandardInput || startInfo.RedirectStandardOutput || startInfo.RedirectStandardError) + { + throw new InvalidOperationException(SR.CantRedirectStreams); + } + } + + int stdinFd = -1, stdoutFd = -1, stderrFd = -1; + string[] envp = CreateEnvp(startInfo); + string? cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null; + + bool setCredentials = !string.IsNullOrEmpty(startInfo.UserName); + uint userId = 0; + uint groupId = 0; + uint[]? groups = null; + if (setCredentials) + { + (userId, groupId, groups) = GetUserAndGroupIds(startInfo); + } + + // .NET applications don't echo characters unless there is a Console.Read operation. + // Unix applications expect the terminal to be in an echoing state by default. + // To support processes that interact with the terminal (e.g. 'vi'), we need to configure the + // terminal to echo. We keep this configuration as long as there are children possibly using the terminal. + bool usesTerminal = !(startInfo.RedirectStandardInput && + startInfo.RedirectStandardOutput && + startInfo.RedirectStandardError); + + if (startInfo.UseShellExecute) + { + string verb = startInfo.Verb; + if (verb != string.Empty && + !string.Equals(verb, "open", StringComparison.OrdinalIgnoreCase)) + { + throw new Win32Exception(Interop.Errors.ERROR_NO_ASSOCIATION); + } + + // On Windows, UseShellExecute of executables and scripts causes those files to be executed. + // To achieve this on Unix, we check if the file is executable (x-bit). + // Some files may have the x-bit set even when they are not executable. This happens for example + // when a Windows filesystem is mounted on Linux. To handle that, treat it as a regular file + // when exec returns ENOEXEC (file format cannot be executed). + bool isExecuting = false; + filename = ResolveExecutableForShellExecute(startInfo.FileName, cwd); + if (filename != null) + { + argv = ParseArgv(startInfo); + + isExecuting = ForkAndExecProcess( + startInfo, filename, argv, envp, cwd, + setCredentials, userId, groupId, groups, + out stdinFd, out stdoutFd, out stderrFd, usesTerminal, + throwOnNoExec: false); // return false instead of throwing on ENOEXEC + } + + // use default program to open file/url + if (!isExecuting) + { + filename = GetPathToOpenFile(); + argv = ParseArgv(startInfo, filename, ignoreArguments: true); + + ForkAndExecProcess( + startInfo, filename, argv, envp, cwd, + setCredentials, userId, groupId, groups, + out stdinFd, out stdoutFd, out stderrFd, usesTerminal); + } + } + else + { + filename = ResolvePath(startInfo.FileName); + argv = ParseArgv(startInfo); + if (Directory.Exists(filename)) + { + throw new Win32Exception(SR.DirectoryNotValidAsInput); + } + + ForkAndExecProcess( + startInfo, filename, argv, envp, cwd, + setCredentials, userId, groupId, groups, + out stdinFd, out stdoutFd, out stderrFd, usesTerminal); + } + + // Configure the parent's ends of the redirection streams. + // We use UTF8 encoding without BOM by-default(instead of Console encoding as on Windows) + // as there is no good way to get this information from the native layer + // and we do not want to take dependency on Console contract. + if (startInfo.RedirectStandardInput) + { + Debug.Assert(stdinFd >= 0); + _standardInput = new StreamWriter(OpenStream(stdinFd, PipeDirection.Out), + startInfo.StandardInputEncoding ?? Encoding.Default, StreamBufferSize) + { AutoFlush = true }; + } + if (startInfo.RedirectStandardOutput) + { + Debug.Assert(stdoutFd >= 0); + _standardOutput = new StreamReader(OpenStream(stdoutFd, PipeDirection.In), + startInfo.StandardOutputEncoding ?? Encoding.Default, true, StreamBufferSize); + } + if (startInfo.RedirectStandardError) + { + Debug.Assert(stderrFd >= 0); + _standardError = new StreamReader(OpenStream(stderrFd, PipeDirection.In), + startInfo.StandardErrorEncoding ?? Encoding.Default, true, StreamBufferSize); + } - _waitStateHolder = waitStateHolder; - SetProcessHandle(startedProcess); - SetProcessId(startedProcess.ProcessId); return true; } + private bool ForkAndExecProcess( + ProcessStartInfo startInfo, string? resolvedFilename, string[] argv, + string[] envp, string? cwd, bool setCredentials, uint userId, + uint groupId, uint[]? groups, + out int stdinFd, out int stdoutFd, out int stderrFd, + bool usesTerminal, bool throwOnNoExec = true) + { + if (string.IsNullOrEmpty(resolvedFilename)) + { + Interop.ErrorInfo errno = Interop.Error.ENOENT.Info(); + throw CreateExceptionForErrorStartingProcess(errno.GetErrorMessage(), errno.RawErrno, startInfo.FileName, cwd); + } + + // Lock to avoid races with OnSigChild + // By using a ReaderWriterLock we allow multiple processes to start concurrently. + s_processStartLock.EnterReadLock(); + try + { + if (usesTerminal) + { + ConfigureTerminalForChildProcesses(1); + } + + int childPid; + + // Invoke the shim fork/execve routine. It will create pipes for all requested + // redirects, fork a child process, map the pipe ends onto the appropriate stdin/stdout/stderr + // descriptors, and execve to execute the requested process. The shim implementation + // is used to fork/execve as executing managed code in a forked process is not safe (only + // the calling thread will transfer, thread IDs aren't stable across the fork, etc.) + int errno = Interop.Sys.ForkAndExecProcess( + resolvedFilename, argv, envp, cwd, + startInfo.RedirectStandardInput, startInfo.RedirectStandardOutput, startInfo.RedirectStandardError, + setCredentials, userId, groupId, groups, + out childPid, out stdinFd, out stdoutFd, out stderrFd); + + if (errno == 0) + { + // Ensure we'll reap this process. + // note: SetProcessId will set this if we don't set it first. + _waitStateHolder = new ProcessWaitState.Holder(childPid, isNewChild: true, usesTerminal); + + // Store the child's information into this Process object. + Debug.Assert(childPid >= 0); + SetProcessId(childPid); + SetProcessHandle(new SafeProcessHandle(_processId, GetSafeWaitHandle())); + + return true; + } + else + { + if (!throwOnNoExec && + new Interop.ErrorInfo(errno).Error == Interop.Error.ENOEXEC) + { + return false; + } + + throw CreateExceptionForErrorStartingProcess(new Interop.ErrorInfo(errno).GetErrorMessage(), errno, resolvedFilename, cwd); + } + } + finally + { + s_processStartLock.ExitReadLock(); + + if (_waitStateHolder == null && usesTerminal) + { + // We failed to launch a child that could use the terminal. + s_processStartLock.EnterWriteLock(); + ConfigureTerminalForChildProcesses(-1); + s_processStartLock.ExitWriteLock(); + } + } + } /// Finalizable holder for the underlying shared wait state object. private ProcessWaitState.Holder? _waitStateHolder; + /// Size to use for redirect streams and stream readers/writers. + private const int StreamBufferSize = 4096; + + /// Converts the filename and arguments information from a ProcessStartInfo into an argv array. + /// The ProcessStartInfo. + /// Resolved executable to open ProcessStartInfo.FileName + /// Don't pass ProcessStartInfo.Arguments + /// The argv array. + private static string[] ParseArgv(ProcessStartInfo psi, string? resolvedExe = null, bool ignoreArguments = false) + { + if (string.IsNullOrEmpty(resolvedExe) && + (ignoreArguments || (string.IsNullOrEmpty(psi.Arguments) && !psi.HasArgumentList))) + { + return new string[] { psi.FileName }; + } + + var argvList = new List(); + if (!string.IsNullOrEmpty(resolvedExe)) + { + argvList.Add(resolvedExe); + if (resolvedExe.Contains("kfmclient")) + { + argvList.Add("openURL"); // kfmclient needs OpenURL + } + } + + argvList.Add(psi.FileName); + + if (!ignoreArguments) + { + if (!string.IsNullOrEmpty(psi.Arguments)) + { + ParseArgumentsIntoList(psi.Arguments, argvList); + } + else if (psi.HasArgumentList) + { + argvList.AddRange(psi.ArgumentList); + } + } + return argvList.ToArray(); + } + + /// Converts the environment variables information from a ProcessStartInfo into an envp array. + /// The ProcessStartInfo. + /// The envp array. + private static string[] CreateEnvp(ProcessStartInfo psi) + { + var envp = new string[psi.Environment.Count]; + int index = 0; + foreach (KeyValuePair pair in psi.Environment) + { + // Ignore null values for consistency with Environment.SetEnvironmentVariable + if (pair.Value != null) + { + envp[index++] = pair.Key + "=" + pair.Value; + } + } + // Resize the array in case we skipped some entries + Array.Resize(ref envp, index); + return envp; + } + + private static string? ResolveExecutableForShellExecute(string filename, string? workingDirectory) + { + // Determine if filename points to an executable file. + // filename may be an absolute path, a relative path or a uri. + + string? resolvedFilename = null; + // filename is an absolute path + if (Path.IsPathRooted(filename)) + { + if (File.Exists(filename)) + { + resolvedFilename = filename; + } + } + // filename is a uri + else if (Uri.TryCreate(filename, UriKind.Absolute, out Uri? uri)) + { + if (uri.IsFile && uri.Host == "" && File.Exists(uri.LocalPath)) + { + resolvedFilename = uri.LocalPath; + } + } + // filename is relative + else + { + // The WorkingDirectory property specifies the location of the executable. + // If WorkingDirectory is an empty string, the current directory is understood to contain the executable. + workingDirectory = workingDirectory != null ? Path.GetFullPath(workingDirectory) : + Directory.GetCurrentDirectory(); + string filenameInWorkingDirectory = Path.Combine(workingDirectory, filename); + // filename is a relative path in the working directory + if (File.Exists(filenameInWorkingDirectory)) + { + resolvedFilename = filenameInWorkingDirectory; + } + // find filename on PATH + else + { + resolvedFilename = ProcessUtils.FindProgramInPath(filename); + } + } + + if (resolvedFilename == null) + { + return null; + } + + if (Interop.Sys.Access(resolvedFilename, Interop.Sys.AccessMode.X_OK) == 0) + { + return resolvedFilename; + } + else + { + return null; + } + } + + /// Resolves a path to the filename passed to ProcessStartInfo. + /// The filename. + /// The resolved path. It can return null in case of URLs. + private static string? ResolvePath(string filename) + { + // Follow the same resolution that Windows uses with CreateProcess: + // 1. First try the exact path provided + // 2. Then try the file relative to the executable directory + // 3. Then try the file relative to the current directory + // 4. then try the file in each of the directories specified in PATH + // Windows does additional Windows-specific steps between 3 and 4, + // and we ignore those here. + + // If the filename is a complete path, use it, regardless of whether it exists. + if (Path.IsPathRooted(filename)) + { + // In this case, it doesn't matter whether the file exists or not; + // it's what the caller asked for, so it's what they'll get + return filename; + } + + // Then check the executable's directory + string? path = Environment.ProcessPath; + if (path != null) + { + try + { + path = Path.Combine(Path.GetDirectoryName(path)!, filename); + if (File.Exists(path)) + { + return path; + } + } + catch (ArgumentException) { } // ignore any errors in data that may come from the exe path + } + + // Then check the current directory + path = Path.Combine(Directory.GetCurrentDirectory(), filename); + if (File.Exists(path)) + { + return path; + } + + // Then check each directory listed in the PATH environment variables + return ProcessUtils.FindProgramInPath(filename); + } + private static long s_ticksPerSecond; /// Convert a number of "jiffies", or ticks, to a TimeSpan. @@ -394,21 +753,116 @@ internal static TimeSpan TicksToTimeSpan(double ticks) return TimeSpan.FromSeconds(ticks / (double)ticksPerSecond); } - private static AnonymousPipeClientStream OpenStream(SafeFileHandle handle, FileAccess access) + /// Opens a stream around the specified file descriptor and with the specified access. + /// The file descriptor. + /// The pipe direction. + /// The opened stream. + private static AnonymousPipeClientStream OpenStream(int fd, PipeDirection direction) + { + Debug.Assert(fd >= 0); + return new AnonymousPipeClientStream(direction, new SafePipeHandle((IntPtr)fd, ownsHandle: true)); + } + + /// Parses a command-line argument string into a list of arguments. + /// The argument string. + /// The list into which the component arguments should be stored. + /// + /// This follows the rules outlined in "Parsing C++ Command-Line Arguments" at + /// https://msdn.microsoft.com/en-us/library/17w5ykft.aspx. + /// + private static void ParseArgumentsIntoList(string arguments, List results) { - PipeDirection direction = access == FileAccess.Write ? PipeDirection.Out : PipeDirection.In; + // Iterate through all of the characters in the argument string. + for (int i = 0; i < arguments.Length; i++) + { + while (i < arguments.Length && (arguments[i] == ' ' || arguments[i] == '\t')) + i++; - // Transfer the ownership to SafePipeHandle, so that it can be properly released when the AnonymousPipeClientStream is disposed. - SafePipeHandle safePipeHandle = new(handle.DangerousGetHandle(), ownsHandle: true); - handle.SetHandleAsInvalid(); + if (i == arguments.Length) + break; - // Use AnonymousPipeClientStream for async, cancellable read/write support. - return new AnonymousPipeClientStream(direction, safePipeHandle); + results.Add(GetNextArgument(arguments, ref i)); + } } - private static Encoding GetStandardInputEncoding() => Encoding.Default; + private static string GetNextArgument(string arguments, ref int i) + { + var currentArgument = new ValueStringBuilder(stackalloc char[256]); + bool inQuotes = false; - private static Encoding GetStandardOutputEncoding() => Encoding.Default; + while (i < arguments.Length) + { + // From the current position, iterate through contiguous backslashes. + int backslashCount = 0; + while (i < arguments.Length && arguments[i] == '\\') + { + i++; + backslashCount++; + } + + if (backslashCount > 0) + { + if (i >= arguments.Length || arguments[i] != '"') + { + // Backslashes not followed by a double quote: + // they should all be treated as literal backslashes. + currentArgument.Append('\\', backslashCount); + } + else + { + // Backslashes followed by a double quote: + // - Output a literal slash for each complete pair of slashes + // - If one remains, use it to make the subsequent quote a literal. + currentArgument.Append('\\', backslashCount / 2); + if (backslashCount % 2 != 0) + { + currentArgument.Append('"'); + i++; + } + } + + continue; + } + + char c = arguments[i]; + + // If this is a double quote, track whether we're inside of quotes or not. + // Anything within quotes will be treated as a single argument, even if + // it contains spaces. + if (c == '"') + { + if (inQuotes && i < arguments.Length - 1 && arguments[i + 1] == '"') + { + // Two consecutive double quotes inside an inQuotes region should result in a literal double quote + // (the parser is left in the inQuotes region). + // This behavior is not part of the spec of code:ParseArgumentsIntoList, but is compatible with CRT + // and .NET Framework. + currentArgument.Append('"'); + i++; + } + else + { + inQuotes = !inQuotes; + } + + i++; + continue; + } + + // If this is a space/tab and we're not in quotes, we're done with the current + // argument, it should be added to the results and then reset for the next one. + if ((c == ' ' || c == '\t') && !inQuotes) + { + break; + } + + // Nothing special; add the character to the current argument. + currentArgument.Append(c); + i++; + } + + return currentArgument.ToString(); + } /// Gets the wait state for this Process object. private ProcessWaitState GetWaitState() @@ -424,6 +878,100 @@ private ProcessWaitState GetWaitState() private SafeWaitHandle GetSafeWaitHandle() => GetWaitState().EnsureExitedEvent().GetSafeWaitHandle(); + private static (uint userId, uint groupId, uint[] groups) GetUserAndGroupIds(ProcessStartInfo startInfo) + { + Debug.Assert(!string.IsNullOrEmpty(startInfo.UserName)); + + (uint? userId, uint? groupId) = GetUserAndGroupIds(startInfo.UserName); + + Debug.Assert(userId.HasValue == groupId.HasValue, "userId and groupId both need to have values, or both need to be null."); + if (!userId.HasValue) + { + throw new Win32Exception(SR.Format(SR.UserDoesNotExist, startInfo.UserName)); + } + + uint[]? groups = Interop.Sys.GetGroupList(startInfo.UserName, groupId!.Value); + if (groups == null) + { + throw new Win32Exception(SR.Format(SR.UserGroupsCannotBeDetermined, startInfo.UserName)); + } + + return (userId.Value, groupId.Value, groups); + } + + private static unsafe (uint? userId, uint? groupId) GetUserAndGroupIds(string userName) + { + Interop.Sys.Passwd? passwd; + // First try with a buffer that should suffice for 99% of cases. + // Note: on CentOS/RedHat 7.1 systems, getpwnam_r returns 'user not found' if the buffer is too small + // see https://bugs.centos.org/view.php?id=7324 + const int BufLen = Interop.Sys.Passwd.InitialBufferSize; + byte* stackBuf = stackalloc byte[BufLen]; + if (TryGetPasswd(userName, stackBuf, BufLen, out passwd)) + { + if (passwd == null) + { + return (null, null); + } + return (passwd.Value.UserId, passwd.Value.GroupId); + } + + // Fallback to heap allocations if necessary, growing the buffer until + // we succeed. TryGetPasswd will throw if there's an unexpected error. + int lastBufLen = BufLen; + while (true) + { + lastBufLen *= 2; + byte[] heapBuf = new byte[lastBufLen]; + fixed (byte* buf = &heapBuf[0]) + { + if (TryGetPasswd(userName, buf, heapBuf.Length, out passwd)) + { + if (passwd == null) + { + return (null, null); + } + return (passwd.Value.UserId, passwd.Value.GroupId); + } + } + } + } + + private static unsafe bool TryGetPasswd(string name, byte* buf, int bufLen, out Interop.Sys.Passwd? passwd) + { + // Call getpwnam_r to get the passwd struct + Interop.Sys.Passwd tempPasswd; + int error = Interop.Sys.GetPwNamR(name, out tempPasswd, buf, bufLen); + + // If the call succeeds, give back the passwd retrieved + if (error == 0) + { + passwd = tempPasswd; + return true; + } + + // If the current user's entry could not be found, give back null, + // but still return true as false indicates the buffer was too small. + if (error == -1) + { + passwd = null; + return true; + } + + var errorInfo = new Interop.ErrorInfo(error); + + // If the call failed because the buffer was too small, return false to + // indicate the caller should try again with a larger buffer. + if (errorInfo.Error == Interop.Error.ERANGE) + { + passwd = null; + return false; + } + + // Otherwise, fail. + throw new Win32Exception(errorInfo.RawErrno, errorInfo.GetErrorMessage()); + } + public IntPtr MainWindowHandle => IntPtr.Zero; private static bool CloseMainWindowCore() => false; @@ -434,6 +982,57 @@ private SafeWaitHandle GetSafeWaitHandle() private static bool WaitForInputIdleCore(int _ /*milliseconds*/) => throw new InvalidOperationException(SR.InputIdleUnknownError); + private static unsafe void EnsureInitialized() + { + if (s_initialized) + { + return; + } + + lock (s_initializedGate) + { + if (!s_initialized) + { + if (!Interop.Sys.InitializeTerminalAndSignalHandling()) + { + throw new Win32Exception(); + } + + // Register our callback. + Interop.Sys.RegisterForSigChld(&OnSigChild); + SetDelayedSigChildConsoleConfigurationHandler(); + + s_initialized = true; + } + } + } + + [UnmanagedCallersOnly] + private static int OnSigChild(int reapAll, int configureConsole) + { + // configureConsole is non zero when there are PosixSignalRegistrations that + // may Cancel the terminal configuration that happens when there are no more + // children using the terminal. + // When the registrations don't cancel the terminal configuration, + // DelayedSigChildConsoleConfiguration will be called. + + // Lock to avoid races with Process.Start + s_processStartLock.EnterWriteLock(); + try + { + bool childrenUsingTerminalPre = AreChildrenUsingTerminal; + ProcessWaitState.CheckChildren(reapAll != 0, configureConsole != 0); + bool childrenUsingTerminalPost = AreChildrenUsingTerminal; + + // return whether console configuration was skipped. + return childrenUsingTerminalPre && !childrenUsingTerminalPost && configureConsole == 0 ? 1 : 0; + } + finally + { + s_processStartLock.ExitWriteLock(); + } + } + /// Gets the friendly name of the process. public string ProcessName { @@ -443,5 +1042,8 @@ public string ProcessName return _processInfo!.ProcessName; } } + + private static bool PlatformDoesNotSupportProcessStartAndKill + => (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()) || OperatingSystem.IsTvOS(); } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs index fb7bf658939f0d..bd551a53f8d311 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Win32.cs @@ -20,22 +20,176 @@ public partial class Process : IDisposable private bool _haveResponding; private bool _responding; - private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) + private bool StartCore(ProcessStartInfo startInfo) { - SafeProcessHandle startedProcess = SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle); + return startInfo.UseShellExecute + ? StartWithShellExecuteEx(startInfo) + : StartWithCreateProcess(startInfo); + } + + private unsafe bool StartWithShellExecuteEx(ProcessStartInfo startInfo) + { + if (!string.IsNullOrEmpty(startInfo.UserName) || startInfo.Password != null) + throw new InvalidOperationException(SR.CantStartAsUser); + + if (startInfo.RedirectStandardInput || startInfo.RedirectStandardOutput || startInfo.RedirectStandardError) + throw new InvalidOperationException(SR.CantRedirectStreams); + + if (startInfo.StandardInputEncoding != null) + throw new InvalidOperationException(SR.StandardInputEncodingNotAllowed); + + if (startInfo.StandardErrorEncoding != null) + throw new InvalidOperationException(SR.StandardErrorEncodingNotAllowed); + + if (startInfo.StandardOutputEncoding != null) + throw new InvalidOperationException(SR.StandardOutputEncodingNotAllowed); - if (startedProcess.IsInvalid) + if (startInfo._environmentVariables != null) + throw new InvalidOperationException(SR.CantUseEnvVars); + + string arguments = startInfo.BuildArguments(); + + fixed (char* fileName = startInfo.FileName.Length > 0 ? startInfo.FileName : null) + fixed (char* verb = startInfo.Verb.Length > 0 ? startInfo.Verb : null) + fixed (char* parameters = arguments.Length > 0 ? arguments : null) + fixed (char* directory = startInfo.WorkingDirectory.Length > 0 ? startInfo.WorkingDirectory : null) { - Debug.Assert(startInfo.UseShellExecute); - return false; + Interop.Shell32.SHELLEXECUTEINFO shellExecuteInfo = new Interop.Shell32.SHELLEXECUTEINFO() + { + cbSize = (uint)sizeof(Interop.Shell32.SHELLEXECUTEINFO), + lpFile = fileName, + lpVerb = verb, + lpParameters = parameters, + lpDirectory = directory, + fMask = Interop.Shell32.SEE_MASK_NOCLOSEPROCESS | Interop.Shell32.SEE_MASK_FLAG_DDEWAIT + }; + + if (startInfo.ErrorDialog) + shellExecuteInfo.hwnd = startInfo.ErrorDialogParentHandle; + else + shellExecuteInfo.fMask |= Interop.Shell32.SEE_MASK_FLAG_NO_UI; + + shellExecuteInfo.nShow = GetShowWindowFromWindowStyle(startInfo.WindowStyle); + ShellExecuteHelper executeHelper = new ShellExecuteHelper(&shellExecuteInfo); + if (!executeHelper.ShellExecuteOnSTAThread()) + { + int errorCode = executeHelper.ErrorCode; + if (errorCode == 0) + { + errorCode = GetShellError(shellExecuteInfo.hInstApp); + } + + switch (errorCode) + { + case Interop.Errors.ERROR_CALL_NOT_IMPLEMENTED: + // This happens on Windows Nano + throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported); + default: + string nativeErrorMessage = errorCode == Interop.Errors.ERROR_BAD_EXE_FORMAT || errorCode == Interop.Errors.ERROR_EXE_MACHINE_TYPE_MISMATCH + ? SR.InvalidApplication + : GetErrorMessage(errorCode); + + throw CreateExceptionForErrorStartingProcess(nativeErrorMessage, errorCode, startInfo.FileName, startInfo.WorkingDirectory); + } + } + + if (shellExecuteInfo.hProcess != IntPtr.Zero) + { + SetProcessHandle(new SafeProcessHandle(shellExecuteInfo.hProcess)); + return true; + } + } + + return false; + } + + private static int GetShowWindowFromWindowStyle(ProcessWindowStyle windowStyle) => windowStyle switch + { + ProcessWindowStyle.Hidden => Interop.Shell32.SW_HIDE, + ProcessWindowStyle.Minimized => Interop.Shell32.SW_SHOWMINIMIZED, + ProcessWindowStyle.Maximized => Interop.Shell32.SW_SHOWMAXIMIZED, + _ => Interop.Shell32.SW_SHOWNORMAL, + }; + + private static int GetShellError(IntPtr error) + { + switch ((long)error) + { + case Interop.Shell32.SE_ERR_FNF: + return Interop.Errors.ERROR_FILE_NOT_FOUND; + case Interop.Shell32.SE_ERR_PNF: + return Interop.Errors.ERROR_PATH_NOT_FOUND; + case Interop.Shell32.SE_ERR_ACCESSDENIED: + return Interop.Errors.ERROR_ACCESS_DENIED; + case Interop.Shell32.SE_ERR_OOM: + return Interop.Errors.ERROR_NOT_ENOUGH_MEMORY; + case Interop.Shell32.SE_ERR_DDEFAIL: + case Interop.Shell32.SE_ERR_DDEBUSY: + case Interop.Shell32.SE_ERR_DDETIMEOUT: + return Interop.Errors.ERROR_DDE_FAIL; + case Interop.Shell32.SE_ERR_SHARE: + return Interop.Errors.ERROR_SHARING_VIOLATION; + case Interop.Shell32.SE_ERR_NOASSOC: + return Interop.Errors.ERROR_NO_ASSOCIATION; + case Interop.Shell32.SE_ERR_DLLNOTFOUND: + return Interop.Errors.ERROR_DLL_NOT_FOUND; + default: + return (int)(long)error; } + } - SetProcessHandle(startedProcess); - if (!startInfo.UseShellExecute) + internal sealed unsafe class ShellExecuteHelper + { + private readonly Interop.Shell32.SHELLEXECUTEINFO* _executeInfo; + private bool _succeeded; + private bool _notpresent; + + public ShellExecuteHelper(Interop.Shell32.SHELLEXECUTEINFO* executeInfo) { - SetProcessId(startedProcess.ProcessId); + _executeInfo = executeInfo; } - return true; + + private void ShellExecuteFunction() + { + try + { + if (!(_succeeded = Interop.Shell32.ShellExecuteExW(_executeInfo))) + ErrorCode = Marshal.GetLastWin32Error(); + } + catch (EntryPointNotFoundException) + { + _notpresent = true; + } + } + + public bool ShellExecuteOnSTAThread() + { + // ShellExecute() requires STA in order to work correctly. + + if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) + { + ThreadStart threadStart = new ThreadStart(ShellExecuteFunction); + Thread executionThread = new Thread(threadStart) + { + IsBackground = true, + Name = ".NET Process STA" + }; + executionThread.SetApartmentState(ApartmentState.STA); + executionThread.Start(); + executionThread.Join(); + } + else + { + ShellExecuteFunction(); + } + + if (_notpresent) + throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported); + + return _succeeded; + } + + public int ErrorCode { get; private set; } } private string GetMainWindowTitle() diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index 27701f20649280..f073bd1e8d54f5 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -16,6 +16,8 @@ namespace System.Diagnostics { public partial class Process : IDisposable { + private static readonly object s_createProcessLock = new object(); + private string? _processName; /// @@ -421,6 +423,244 @@ private void SetWorkingSetLimitsCore(IntPtr? newMin, IntPtr? newMax, out IntPtr } } + /// Starts the process using the supplied start info. + /// The start info with which to start the process. + private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) + { + // See knowledge base article Q190351 for an explanation of the following code. Noteworthy tricky points: + // * The handles are duplicated as inheritable before they are passed to CreateProcess so + // that the child process can use them + // * CreateProcess allows you to redirect all or none of the standard IO handles, so we use + // Console.OpenStandard*Handle for the handles that are not being redirected + + var commandLine = new ValueStringBuilder(stackalloc char[256]); + BuildCommandLine(startInfo, ref commandLine); + + Interop.Kernel32.STARTUPINFO startupInfo = default; + Interop.Kernel32.PROCESS_INFORMATION processInfo = default; + Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default; + SafeProcessHandle procSH = new SafeProcessHandle(); + + // handles used in parent process + SafeFileHandle? parentInputPipeHandle = null; + SafeFileHandle? childInputPipeHandle = null; + SafeFileHandle? parentOutputPipeHandle = null; + SafeFileHandle? childOutputPipeHandle = null; + SafeFileHandle? parentErrorPipeHandle = null; + SafeFileHandle? childErrorPipeHandle = null; + + // Take a global lock to synchronize all redirect pipe handle creations and CreateProcess + // calls. We do not want one process to inherit the handles created concurrently for another + // process, as that will impact the ownership and lifetimes of those handles now inherited + // into multiple child processes. + lock (s_createProcessLock) + { + try + { + startupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFO); + + // set up the streams + if (startInfo.RedirectStandardInput || startInfo.RedirectStandardOutput || startInfo.RedirectStandardError) + { + if (startInfo.RedirectStandardInput) + { + CreatePipe(out parentInputPipeHandle, out childInputPipeHandle, true); + } + else + { + childInputPipeHandle = Console.OpenStandardInputHandle(); + } + + if (startInfo.RedirectStandardOutput) + { + CreatePipe(out parentOutputPipeHandle, out childOutputPipeHandle, false); + } + else + { + childOutputPipeHandle = Console.OpenStandardOutputHandle(); + } + + if (startInfo.RedirectStandardError) + { + CreatePipe(out parentErrorPipeHandle, out childErrorPipeHandle, false); + } + else + { + childErrorPipeHandle = Console.OpenStandardErrorHandle(); + } + + startupInfo.hStdInput = childInputPipeHandle.DangerousGetHandle(); + startupInfo.hStdOutput = childOutputPipeHandle.DangerousGetHandle(); + startupInfo.hStdError = childErrorPipeHandle.DangerousGetHandle(); + + startupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES; + } + + if (startInfo.WindowStyle != ProcessWindowStyle.Normal) + { + startupInfo.wShowWindow = (short)GetShowWindowFromWindowStyle(startInfo.WindowStyle); + startupInfo.dwFlags |= Interop.Advapi32.StartupInfoOptions.STARTF_USESHOWWINDOW; + } + + // set up the creation flags parameter + int creationFlags = 0; + if (startInfo.CreateNoWindow) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NO_WINDOW; + if (startInfo.CreateNewProcessGroup) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NEW_PROCESS_GROUP; + + // set up the environment block parameter + string? environmentBlock = null; + if (startInfo._environmentVariables != null) + { + creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_UNICODE_ENVIRONMENT; + environmentBlock = GetEnvironmentVariablesBlock(startInfo._environmentVariables!); + } + + string? workingDirectory = startInfo.WorkingDirectory; + if (workingDirectory.Length == 0) + { + workingDirectory = null; + } + + bool retVal; + int errorCode = 0; + + if (startInfo.UserName.Length != 0) + { + if (startInfo.Password != null && startInfo.PasswordInClearText != null) + { + throw new ArgumentException(SR.CantSetDuplicatePassword); + } + + Interop.Advapi32.LogonFlags logonFlags = (Interop.Advapi32.LogonFlags)0; + if (startInfo.LoadUserProfile && startInfo.UseCredentialsForNetworkingOnly) + { + throw new ArgumentException(SR.CantEnableConflictingLogonFlags, nameof(startInfo)); + } + else if (startInfo.LoadUserProfile) + { + logonFlags = Interop.Advapi32.LogonFlags.LOGON_WITH_PROFILE; + } + else if (startInfo.UseCredentialsForNetworkingOnly) + { + logonFlags = Interop.Advapi32.LogonFlags.LOGON_NETCREDENTIALS_ONLY; + } + + commandLine.NullTerminate(); + fixed (char* passwordInClearTextPtr = startInfo.PasswordInClearText ?? string.Empty) + fixed (char* environmentBlockPtr = environmentBlock) + fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) + { + IntPtr passwordPtr = (startInfo.Password != null) ? + Marshal.SecureStringToGlobalAllocUnicode(startInfo.Password) : IntPtr.Zero; + + try + { + retVal = Interop.Advapi32.CreateProcessWithLogonW( + startInfo.UserName, + startInfo.Domain, + (passwordPtr != IntPtr.Zero) ? passwordPtr : (IntPtr)passwordInClearTextPtr, + logonFlags, + null, // we don't need this since all the info is in commandLine + commandLinePtr, + creationFlags, + (IntPtr)environmentBlockPtr, + workingDirectory, + ref startupInfo, // pointer to STARTUPINFO + ref processInfo // pointer to PROCESS_INFORMATION + ); + if (!retVal) + errorCode = Marshal.GetLastWin32Error(); + } + finally + { + if (passwordPtr != IntPtr.Zero) + Marshal.ZeroFreeGlobalAllocUnicode(passwordPtr); + } + } + } + else + { + commandLine.NullTerminate(); + fixed (char* environmentBlockPtr = environmentBlock) + fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) + { + retVal = Interop.Kernel32.CreateProcess( + null, // we don't need this since all the info is in commandLine + commandLinePtr, // pointer to the command line string + ref unused_SecAttrs, // address to process security attributes, we don't need to inherit the handle + ref unused_SecAttrs, // address to thread security attributes. + true, // handle inheritance flag + creationFlags, // creation flags + (IntPtr)environmentBlockPtr, // pointer to new environment block + workingDirectory, // pointer to current directory name + ref startupInfo, // pointer to STARTUPINFO + ref processInfo // pointer to PROCESS_INFORMATION + ); + if (!retVal) + errorCode = Marshal.GetLastWin32Error(); + } + } + + if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != new IntPtr(-1)) + Marshal.InitHandle(procSH, processInfo.hProcess); + if (processInfo.hThread != IntPtr.Zero && processInfo.hThread != new IntPtr(-1)) + Interop.Kernel32.CloseHandle(processInfo.hThread); + + if (!retVal) + { + string nativeErrorMessage = errorCode == Interop.Errors.ERROR_BAD_EXE_FORMAT || errorCode == Interop.Errors.ERROR_EXE_MACHINE_TYPE_MISMATCH + ? SR.InvalidApplication + : GetErrorMessage(errorCode); + + throw CreateExceptionForErrorStartingProcess(nativeErrorMessage, errorCode, startInfo.FileName, workingDirectory); + } + } + catch + { + parentInputPipeHandle?.Dispose(); + parentOutputPipeHandle?.Dispose(); + parentErrorPipeHandle?.Dispose(); + procSH.Dispose(); + throw; + } + finally + { + childInputPipeHandle?.Dispose(); + childOutputPipeHandle?.Dispose(); + childErrorPipeHandle?.Dispose(); + } + } + + if (startInfo.RedirectStandardInput) + { + Encoding enc = startInfo.StandardInputEncoding ?? GetEncoding((int)Interop.Kernel32.GetConsoleCP()); + _standardInput = new StreamWriter(new FileStream(parentInputPipeHandle!, FileAccess.Write, 4096, false), enc, 4096); + _standardInput.AutoFlush = true; + } + if (startInfo.RedirectStandardOutput) + { + Encoding enc = startInfo.StandardOutputEncoding ?? GetEncoding((int)Interop.Kernel32.GetConsoleOutputCP()); + _standardOutput = new StreamReader(new FileStream(parentOutputPipeHandle!, FileAccess.Read, 4096, parentOutputPipeHandle!.IsAsync), enc, true, 4096); + } + if (startInfo.RedirectStandardError) + { + Encoding enc = startInfo.StandardErrorEncoding ?? GetEncoding((int)Interop.Kernel32.GetConsoleOutputCP()); + _standardError = new StreamReader(new FileStream(parentErrorPipeHandle!, FileAccess.Read, 4096, parentErrorPipeHandle!.IsAsync), enc, true, 4096); + } + + commandLine.Dispose(); + + if (procSH.IsInvalid) + { + procSH.Dispose(); + return false; + } + + SetProcessHandle(procSH); + SetProcessId((int)processInfo.dwProcessId); + return true; + } + private static ConsoleEncoding GetEncoding(int codePage) { Encoding enc = EncodingHelper.GetSupportedConsoleEncoding(codePage); @@ -429,6 +669,30 @@ private static ConsoleEncoding GetEncoding(int codePage) private bool _signaled; + private static void BuildCommandLine(ProcessStartInfo startInfo, ref ValueStringBuilder commandLine) + { + // Construct a StringBuilder with the appropriate command line + // to pass to CreateProcess. If the filename isn't already + // in quotes, we quote it here. This prevents some security + // problems (it specifies exactly which part of the string + // is the file to execute). + ReadOnlySpan fileName = startInfo.FileName.AsSpan().Trim(); + bool fileNameIsQuoted = fileName.StartsWith('"') && fileName.EndsWith('"'); + if (!fileNameIsQuoted) + { + commandLine.Append('"'); + } + + commandLine.Append(fileName); + + if (!fileNameIsQuoted) + { + commandLine.Append('"'); + } + + startInfo.AppendArgumentsTo(ref commandLine); + } + /// Gets timing information for the current process. private ProcessThreadTimes GetProcessTimes() { @@ -536,11 +800,74 @@ private SafeProcessHandle GetProcessHandle(int access, bool throwIfExited = true } } - private static FileStream OpenStream(SafeFileHandle handle, FileAccess access) => new(handle, access, StreamBufferSize, handle.IsAsync); + // Using synchronous Anonymous pipes for process input/output redirection means we would end up + // wasting a worker threadpool thread per pipe instance. Overlapped pipe IO is desirable, since + // it will take advantage of the NT IO completion port infrastructure. But we can't really use + // Overlapped I/O for process input/output as it would break Console apps (managed Console class + // methods such as WriteLine as well as native CRT functions like printf) which are making an + // assumption that the console standard handles (obtained via GetStdHandle()) are opened + // for synchronous I/O and hence they can work fine with ReadFile/WriteFile synchronously! + // We therefore only open the parent's end of the pipe for async I/O (overlapped), while the + // child's end is always opened for synchronous I/O so the child process can use it normally. + private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHandle childHandle, bool parentInputs) + { + // Only the parent's read end benefits from async I/O; stdin is always sync. + // asyncRead applies to the read handle; asyncWrite to the write handle. + SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: !parentInputs, asyncWrite: false); + + // parentInputs=true: parent writes to pipe, child reads (stdin redirect). + // parentInputs=false: parent reads from pipe, child writes (stdout/stderr redirect). + parentHandle = parentInputs ? writeHandle : readHandle; + SafeFileHandle hTmpChild = parentInputs ? readHandle : writeHandle; + + // Duplicate the child handle to be inheritable so that the child process + // has access. The original non-inheritable handle is closed afterwards. + IntPtr currentProcHandle = Interop.Kernel32.GetCurrentProcess(); + if (!Interop.Kernel32.DuplicateHandle(currentProcHandle, + hTmpChild, + currentProcHandle, + out childHandle, + 0, + bInheritHandle: true, + Interop.Kernel32.HandleOptions.DUPLICATE_SAME_ACCESS)) + { + int lastError = Marshal.GetLastWin32Error(); + parentHandle.Dispose(); + hTmpChild.Dispose(); + throw new Win32Exception(lastError); + } + + hTmpChild.Dispose(); + } + + private static string GetEnvironmentVariablesBlock(DictionaryWrapper sd) + { + // https://learn.microsoft.com/windows/win32/procthread/changing-environment-variables + // "All strings in the environment block must be sorted alphabetically by name. The sort is + // case-insensitive, Unicode order, without regard to locale. Because the equal sign is a + // separator, it must not be used in the name of an environment variable." + + var keys = new string[sd.Count]; + sd.Keys.CopyTo(keys, 0); + Array.Sort(keys, StringComparer.OrdinalIgnoreCase); + + // Join the null-terminated "key=val\0" strings + var result = new StringBuilder(8 * keys.Length); + foreach (string key in keys) + { + string? value = sd[key]; + + // Ignore null values for consistency with Environment.SetEnvironmentVariable + if (value != null) + { + result.Append(key).Append('=').Append(value).Append('\0'); + } + } - private static ConsoleEncoding GetStandardInputEncoding() => GetEncoding((int)Interop.Kernel32.GetConsoleCP()); + return result.ToString(); + } - private static ConsoleEncoding GetStandardOutputEncoding() => GetEncoding((int)Interop.Kernel32.GetConsoleOutputCP()); + private static string GetErrorMessage(int error) => Interop.Kernel32.GetMessage(error); /// Gets the friendly name of the process. public string ProcessName diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index a093dac7ceacb8..4218596c129644 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -83,6 +83,8 @@ public partial class Process : Component internal bool _pendingOutputRead; internal bool _pendingErrorRead; + private static int s_cachedSerializationSwitch; + /// /// /// Initializes a new instance of the class. @@ -1225,9 +1227,6 @@ private void SetProcessId(int processId) /// Additional optional configuration hook after a process ID is set. partial void ConfigureAfterProcessIdSet(); - /// Size to use for redirect streams and stream readers/writers. - private const int StreamBufferSize = 4096; - /// /// /// Starts a process specified by the property of this @@ -1245,143 +1244,44 @@ public bool Start() Close(); ProcessStartInfo startInfo = StartInfo; - startInfo.ThrowIfInvalid(out bool anyRedirection); - - //Cannot start a new process and store its handle if the object has been disposed, since finalization has been suppressed. - CheckDisposed(); - - SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); - - SafeFileHandle? parentInputPipeHandle = null; - SafeFileHandle? parentOutputPipeHandle = null; - SafeFileHandle? parentErrorPipeHandle = null; - - SafeFileHandle? childInputHandle = null; - SafeFileHandle? childOutputHandle = null; - SafeFileHandle? childErrorHandle = null; - - try + if (startInfo.FileName.Length == 0) { - if (!startInfo.UseShellExecute) - { - // Windows supports creating non-inheritable pipe in atomic way. - // When it comes to Unixes, it depends whether they support pipe2 sys-call or not. - // If they don't, the pipe is created as inheritable and made non-inheritable with another sys-call. - // Some process could be started in the meantime, so in order to prevent accidental handle inheritance, - // a writer lock is used around the pipe creation code. - - bool requiresLock = anyRedirection && !ProcessUtils.SupportsAtomicNonInheritablePipeCreation; - - if (requiresLock) - { - ProcessUtils.s_processStartLock.EnterWriteLock(); - } - - try - { - if (startInfo.StandardInputHandle is not null) - { - childInputHandle = startInfo.StandardInputHandle; - } - else if (startInfo.RedirectStandardInput) - { - SafeFileHandle.CreateAnonymousPipe(out childInputHandle, out parentInputPipeHandle); - } - else if (!OperatingSystem.IsAndroid()) - { - childInputHandle = Console.OpenStandardInputHandle(); - } - - if (startInfo.StandardOutputHandle is not null) - { - childOutputHandle = startInfo.StandardOutputHandle; - } - else if (startInfo.RedirectStandardOutput) - { - SafeFileHandle.CreateAnonymousPipe(out parentOutputPipeHandle, out childOutputHandle, asyncRead: OperatingSystem.IsWindows()); - } - else if (!OperatingSystem.IsAndroid()) - { - childOutputHandle = Console.OpenStandardOutputHandle(); - } - - if (startInfo.StandardErrorHandle is not null) - { - childErrorHandle = startInfo.StandardErrorHandle; - } - else if (startInfo.RedirectStandardError) - { - SafeFileHandle.CreateAnonymousPipe(out parentErrorPipeHandle, out childErrorHandle, asyncRead: OperatingSystem.IsWindows()); - } - else if (!OperatingSystem.IsAndroid()) - { - childErrorHandle = Console.OpenStandardErrorHandle(); - } - } - finally - { - if (requiresLock) - { - ProcessUtils.s_processStartLock.ExitWriteLock(); - } - } - } - - if (!StartCore(startInfo, childInputHandle, childOutputHandle, childErrorHandle)) - { - return false; - } + throw new InvalidOperationException(SR.FileNameMissing); } - catch + if (startInfo.StandardInputEncoding != null && !startInfo.RedirectStandardInput) { - parentInputPipeHandle?.Dispose(); - parentOutputPipeHandle?.Dispose(); - parentErrorPipeHandle?.Dispose(); - - throw; + throw new InvalidOperationException(SR.StandardInputEncodingNotAllowed); } - finally + if (startInfo.StandardOutputEncoding != null && !startInfo.RedirectStandardOutput) { - // We MUST close the child handles, otherwise the parent - // process will not receive EOF when the child process closes its handles. - // It's OK to do it for handles returned by Console.OpenStandard*Handle APIs, - // because these handles are not owned and won't be closed by Dispose. - // We don't dispose handles that were passed in - // by the caller via StartInfo.StandardInputHandle/OutputHandle/ErrorHandle. - if (startInfo.StandardInputHandle is null) - { - childInputHandle?.Dispose(); - } - if (startInfo.StandardOutputHandle is null) - { - childOutputHandle?.Dispose(); - } - if (startInfo.StandardErrorHandle is null) - { - childErrorHandle?.Dispose(); - } + throw new InvalidOperationException(SR.StandardOutputEncodingNotAllowed); } - - if (startInfo.RedirectStandardInput) + if (startInfo.StandardErrorEncoding != null && !startInfo.RedirectStandardError) { - _standardInput = new StreamWriter(OpenStream(parentInputPipeHandle!, FileAccess.Write), - startInfo.StandardInputEncoding ?? GetStandardInputEncoding(), StreamBufferSize) - { - AutoFlush = true - }; + throw new InvalidOperationException(SR.StandardErrorEncodingNotAllowed); } - if (startInfo.RedirectStandardOutput) + if (!string.IsNullOrEmpty(startInfo.Arguments) && startInfo.HasArgumentList) { - _standardOutput = new StreamReader(OpenStream(parentOutputPipeHandle!, FileAccess.Read), - startInfo.StandardOutputEncoding ?? GetStandardOutputEncoding(), true, StreamBufferSize); + throw new InvalidOperationException(SR.ArgumentAndArgumentListInitialized); } - if (startInfo.RedirectStandardError) + if (startInfo.HasArgumentList) { - _standardError = new StreamReader(OpenStream(parentErrorPipeHandle!, FileAccess.Read), - startInfo.StandardErrorEncoding ?? GetStandardOutputEncoding(), true, StreamBufferSize); + int argumentCount = startInfo.ArgumentList.Count; + for (int i = 0; i < argumentCount; i++) + { + if (startInfo.ArgumentList[i] is null) + { + throw new ArgumentNullException("item", SR.ArgumentListMayNotContainNull); + } + } } - return true; + //Cannot start a new process and store its handle if the object has been disposed, since finalization has been suppressed. + CheckDisposed(); + + SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref s_cachedSerializationSwitch); + + return StartCore(startInfo); } /// @@ -1413,7 +1313,7 @@ public static Process Start(string fileName) [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("tvos")] [SupportedOSPlatform("maccatalyst")] - public static Process Start(string fileName, string? arguments) + public static Process Start(string fileName, string arguments) { // the underlying Start method can only return null on Windows platforms, // when the ProcessStartInfo.UseShellExecute property is set to true. @@ -1840,6 +1740,13 @@ private void CheckDisposed() ObjectDisposedException.ThrowIf(_disposed, this); } + private static Win32Exception CreateExceptionForErrorStartingProcess(string errorMessage, int errorCode, string fileName, string? workingDirectory) + { + string directoryForException = string.IsNullOrEmpty(workingDirectory) ? Directory.GetCurrentDirectory() : workingDirectory; + string msg = SR.Format(SR.ErrorStartingProcess, fileName, directoryForException, errorMessage); + return new Win32Exception(errorCode, msg); + } + /// /// This enum defines the operation mode for redirected process stream. /// We don't support switching between synchronous mode and asynchronous mode. diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.iOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.iOS.cs index 49519cfc243b29..b94531f135685a 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.iOS.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.iOS.cs @@ -114,7 +114,7 @@ private static void SetWorkingSetLimitsCore(IntPtr? newMin, IntPtr? newMax, out #pragma warning restore IDE0060 /// Gets execution path - internal static string GetPathToOpenFile() + private static string GetPathToOpenFile() { throw new PlatformNotSupportedException(); } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs index e564f6201faa72..0f4fc0a0cb72c1 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs @@ -8,7 +8,6 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Text; -using Microsoft.Win32.SafeHandles; namespace System.Diagnostics { @@ -50,7 +49,7 @@ public ProcessStartInfo(string fileName) /// Specifies the name of the application that is to be started, as well as a set /// of command line arguments to pass to the application. /// - public ProcessStartInfo(string fileName, string? arguments) + public ProcessStartInfo(string fileName, string arguments) { _fileName = fileName; _arguments = arguments; @@ -118,84 +117,6 @@ public string Arguments public bool RedirectStandardOutput { get; set; } public bool RedirectStandardError { get; set; } - /// - /// Gets or sets a that will be used as the standard input of the child process. - /// When set, the handle is passed directly to the child process and must be . - /// - /// - /// - /// The handle does not need to be inheritable; the runtime will duplicate it as inheritable if needed. - /// - /// - /// Use to create a pair of connected pipe handles, - /// to open a file handle, - /// to provide an empty input, - /// or to inherit the parent's standard input - /// (the default behavior when this property is ). - /// - /// - /// It's recommended to dispose the handle right after starting the process. - /// - /// - /// This property cannot be used together with - /// and requires to be . - /// - /// - /// A to use as the standard input handle of the child process, or to use the default behavior. - public SafeFileHandle? StandardInputHandle { get; set; } - - /// - /// Gets or sets a that will be used as the standard output of the child process. - /// When set, the handle is passed directly to the child process and must be . - /// - /// - /// - /// The handle does not need to be inheritable; the runtime will duplicate it as inheritable if needed. - /// - /// - /// Use to create a pair of connected pipe handles, - /// to open a file handle, - /// to discard output, - /// or to inherit the parent's standard output - /// (the default behavior when this property is ). - /// - /// - /// It's recommended to dispose the handle right after starting the process. - /// - /// - /// This property cannot be used together with - /// and requires to be . - /// - /// - /// A to use as the standard output handle of the child process, or to use the default behavior. - public SafeFileHandle? StandardOutputHandle { get; set; } - - /// - /// Gets or sets a that will be used as the standard error of the child process. - /// When set, the handle is passed directly to the child process and must be . - /// - /// - /// - /// The handle does not need to be inheritable; the runtime will duplicate it as inheritable if needed. - /// - /// - /// Use to create a pair of connected pipe handles, - /// to open a file handle, - /// to discard error output, - /// or to inherit the parent's standard error - /// (the default behavior when this property is ). - /// - /// - /// It's recommended to dispose the handle right after starting the process. - /// - /// - /// This property cannot be used together with - /// and requires to be . - /// - /// - /// A to use as the standard error handle of the child process, or to use the default behavior. - public SafeFileHandle? StandardErrorHandle { get; set; } - public Encoding? StandardInputEncoding { get; set; } public Encoding? StandardErrorEncoding { get; set; } @@ -293,80 +214,5 @@ internal void AppendArgumentsTo(ref ValueStringBuilder stringBuilder) stringBuilder.Append(Arguments); } } - - internal void ThrowIfInvalid(out bool anyRedirection) - { - if (FileName.Length == 0) - { - throw new InvalidOperationException(SR.FileNameMissing); - } - if (StandardInputEncoding != null && !RedirectStandardInput) - { - throw new InvalidOperationException(SR.StandardInputEncodingNotAllowed); - } - if (StandardOutputEncoding != null && !RedirectStandardOutput) - { - throw new InvalidOperationException(SR.StandardOutputEncodingNotAllowed); - } - if (StandardErrorEncoding != null && !RedirectStandardError) - { - throw new InvalidOperationException(SR.StandardErrorEncodingNotAllowed); - } - if (!string.IsNullOrEmpty(Arguments) && HasArgumentList) - { - throw new InvalidOperationException(SR.ArgumentAndArgumentListInitialized); - } - if (HasArgumentList) - { - int argumentCount = ArgumentList.Count; - for (int i = 0; i < argumentCount; i++) - { - if (ArgumentList[i] is null) - { - throw new ArgumentNullException("item", SR.ArgumentListMayNotContainNull); - } - } - } - - anyRedirection = RedirectStandardInput || RedirectStandardOutput || RedirectStandardError; - bool anyHandle = StandardInputHandle is not null || StandardOutputHandle is not null || StandardErrorHandle is not null; - if (UseShellExecute && (anyRedirection || anyHandle)) - { - throw new InvalidOperationException(SR.CantRedirectStreams); - } - - if (anyHandle) - { - if (StandardInputHandle is not null && RedirectStandardInput) - { - throw new InvalidOperationException(SR.CantSetHandleAndRedirect); - } - if (StandardOutputHandle is not null && RedirectStandardOutput) - { - throw new InvalidOperationException(SR.CantSetHandleAndRedirect); - } - if (StandardErrorHandle is not null && RedirectStandardError) - { - throw new InvalidOperationException(SR.CantSetHandleAndRedirect); - } - - ValidateHandle(StandardInputHandle, nameof(StandardInputHandle)); - ValidateHandle(StandardOutputHandle, nameof(StandardOutputHandle)); - ValidateHandle(StandardErrorHandle, nameof(StandardErrorHandle)); - } - - static void ValidateHandle(SafeFileHandle? handle, string paramName) - { - if (handle is not null) - { - if (handle.IsInvalid) - { - throw new ArgumentException(SR.Arg_InvalidHandle, paramName); - } - - ObjectDisposedException.ThrowIf(handle.IsClosed, handle); - } - } - } } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs deleted file mode 100644 index 91d98013eb57b0..00000000000000 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.InteropServices; -using System.Threading; - -namespace System.Diagnostics -{ - internal static partial class ProcessUtils - { - private static int s_childrenUsingTerminalCount; - - internal static void ConfigureTerminalForChildProcesses(int increment, bool configureConsole = true) - { - Debug.Assert(increment != 0); - - int childrenUsingTerminalRemaining = Interlocked.Add(ref s_childrenUsingTerminalCount, increment); - if (increment > 0) - { - Debug.Assert(s_processStartLock.IsReadLockHeld); - Debug.Assert(configureConsole); - - // At least one child is using the terminal. - Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: true); - } - else - { - Debug.Assert(s_processStartLock.IsWriteLockHeld); - - if (childrenUsingTerminalRemaining == 0 && configureConsole) - { - // No more children are using the terminal. - Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: false); - } - } - } - - private static unsafe void SetDelayedSigChildConsoleConfigurationHandler() - { - Interop.Sys.SetDelayedSigChildConsoleConfigurationHandler(&DelayedSigChildConsoleConfiguration); - } - - [UnmanagedCallersOnly] - private static void DelayedSigChildConsoleConfiguration() - { - // Lock to avoid races with Process.Start - s_processStartLock.EnterWriteLock(); - try - { - if (s_childrenUsingTerminalCount == 0) - { - // No more children are using the terminal. - Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: false); - } - } - finally - { - s_processStartLock.ExitWriteLock(); - } - } - - private static bool AreChildrenUsingTerminal => s_childrenUsingTerminalCount > 0; - } -} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs deleted file mode 100644 index 671630a2f00719..00000000000000 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Diagnostics -{ - internal static partial class ProcessUtils - { - /// These methods are used on other Unix systems to track how many children use the terminal, - /// and update the terminal configuration when necessary. - - [Conditional("unnecessary")] - internal static void ConfigureTerminalForChildProcesses(int increment, bool configureConsole = true) - { - } - - static partial void SetDelayedSigChildConsoleConfigurationHandler(); - - private static bool AreChildrenUsingTerminal => false; - } -} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs index 2c0717c684c66f..eb4c06a033566e 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs @@ -1,29 +1,12 @@ // 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.ComponentModel; 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; namespace System.Diagnostics { internal static partial class ProcessUtils { - private static volatile bool s_initialized; - private static readonly object s_initializedGate = new object(); - - internal static bool SupportsAtomicNonInheritablePipeCreation => Interop.Sys.IsAtomicNonInheritablePipeCreationSupported; - - internal static bool PlatformDoesNotSupportProcessStartAndKill - => (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()) || OperatingSystem.IsTvOS(); - private static bool IsExecutable(string fullPath) { Interop.Sys.FileStatus fileinfo; @@ -85,393 +68,5 @@ private static bool IsExecutable(string fullPath) } } - internal static unsafe void EnsureInitialized() - { - if (s_initialized) - { - return; - } - - lock (s_initializedGate) - { - if (!s_initialized) - { - if (!Interop.Sys.InitializeTerminalAndSignalHandling()) - { - throw new Win32Exception(); - } - - // Register our callback. - Interop.Sys.RegisterForSigChld(&OnSigChild); - SetDelayedSigChildConsoleConfigurationHandler(); - - s_initialized = true; - } - } - } - - internal static (uint userId, uint groupId, uint[] groups) GetUserAndGroupIds(ProcessStartInfo startInfo) - { - Debug.Assert(!string.IsNullOrEmpty(startInfo.UserName)); - - (uint? userId, uint? groupId) = GetUserAndGroupIds(startInfo.UserName); - - Debug.Assert(userId.HasValue == groupId.HasValue, "userId and groupId both need to have values, or both need to be null."); - if (!userId.HasValue) - { - throw new Win32Exception(SR.Format(SR.UserDoesNotExist, startInfo.UserName)); - } - - uint[]? groups = Interop.Sys.GetGroupList(startInfo.UserName, groupId!.Value); - if (groups == null) - { - throw new Win32Exception(SR.Format(SR.UserGroupsCannotBeDetermined, startInfo.UserName)); - } - - return (userId.Value, groupId.Value, groups); - } - - private static unsafe (uint? userId, uint? groupId) GetUserAndGroupIds(string userName) - { - Interop.Sys.Passwd? passwd; - // First try with a buffer that should suffice for 99% of cases. - // Note: on CentOS/RedHat 7.1 systems, getpwnam_r returns 'user not found' if the buffer is too small - // see https://bugs.centos.org/view.php?id=7324 - const int BufLen = Interop.Sys.Passwd.InitialBufferSize; - byte* stackBuf = stackalloc byte[BufLen]; - if (TryGetPasswd(userName, stackBuf, BufLen, out passwd)) - { - if (passwd == null) - { - return (null, null); - } - return (passwd.Value.UserId, passwd.Value.GroupId); - } - - // Fallback to heap allocations if necessary, growing the buffer until - // we succeed. TryGetPasswd will throw if there's an unexpected error. - int lastBufLen = BufLen; - while (true) - { - lastBufLen *= 2; - byte[] heapBuf = new byte[lastBufLen]; - fixed (byte* buf = &heapBuf[0]) - { - if (TryGetPasswd(userName, buf, heapBuf.Length, out passwd)) - { - if (passwd == null) - { - return (null, null); - } - return (passwd.Value.UserId, passwd.Value.GroupId); - } - } - } - } - - private static unsafe bool TryGetPasswd(string name, byte* buf, int bufLen, out Interop.Sys.Passwd? passwd) - { - // Call getpwnam_r to get the passwd struct - Interop.Sys.Passwd tempPasswd; - int error = Interop.Sys.GetPwNamR(name, out tempPasswd, buf, bufLen); - - // If the call succeeds, give back the passwd retrieved - if (error == 0) - { - passwd = tempPasswd; - return true; - } - - // If the current user's entry could not be found, give back null, - // but still return true as false indicates the buffer was too small. - if (error == -1) - { - passwd = null; - return true; - } - - var errorInfo = new Interop.ErrorInfo(error); - - // If the call failed because the buffer was too small, return false to - // indicate the caller should try again with a larger buffer. - if (errorInfo.Error == Interop.Error.ERANGE) - { - passwd = null; - return false; - } - - // Otherwise, fail. - throw new Win32Exception(errorInfo.RawErrno, errorInfo.GetErrorMessage()); - } - - internal static string? ResolveExecutableForShellExecute(string filename, string? workingDirectory) - { - // Determine if filename points to an executable file. - // filename may be an absolute path, a relative path or a uri. - - string? resolvedFilename = null; - // filename is an absolute path - if (Path.IsPathRooted(filename)) - { - if (File.Exists(filename)) - { - resolvedFilename = filename; - } - } - // filename is a uri - else if (Uri.TryCreate(filename, UriKind.Absolute, out Uri? uri)) - { - if (uri.IsFile && uri.Host == "" && File.Exists(uri.LocalPath)) - { - resolvedFilename = uri.LocalPath; - } - } - // filename is relative - else - { - // The WorkingDirectory property specifies the location of the executable. - // If WorkingDirectory is an empty string, the current directory is understood to contain the executable. - workingDirectory = workingDirectory != null ? Path.GetFullPath(workingDirectory) : - Directory.GetCurrentDirectory(); - string filenameInWorkingDirectory = Path.Combine(workingDirectory, filename); - // filename is a relative path in the working directory - if (File.Exists(filenameInWorkingDirectory)) - { - resolvedFilename = filenameInWorkingDirectory; - } - // find filename on PATH - else - { - resolvedFilename = FindProgramInPath(filename); - } - } - - if (resolvedFilename == null) - { - return null; - } - - if (Interop.Sys.Access(resolvedFilename, Interop.Sys.AccessMode.X_OK) == 0) - { - return resolvedFilename; - } - else - { - return null; - } - } - - [UnmanagedCallersOnly] - private static int OnSigChild(int reapAll, int configureConsole) - { - // configureConsole is non zero when there are PosixSignalRegistrations that - // may Cancel the terminal configuration that happens when there are no more - // children using the terminal. - // When the registrations don't cancel the terminal configuration, - // DelayedSigChildConsoleConfiguration will be called. - - // Lock to avoid races with Process.Start - s_processStartLock.EnterWriteLock(); - try - { - bool childrenUsingTerminalPre = AreChildrenUsingTerminal; - ProcessWaitState.CheckChildren(reapAll != 0, configureConsole != 0); - bool childrenUsingTerminalPost = AreChildrenUsingTerminal; - - // return whether console configuration was skipped. - return childrenUsingTerminalPre && !childrenUsingTerminalPost && configureConsole == 0 ? 1 : 0; - } - finally - { - s_processStartLock.ExitWriteLock(); - } - } - - /// Converts the filename and arguments information from a ProcessStartInfo into an argv array. - /// The ProcessStartInfo. - /// Resolved executable to open ProcessStartInfo.FileName - /// Don't pass ProcessStartInfo.Arguments - /// The argv array. - internal static string[] ParseArgv(ProcessStartInfo psi, string? resolvedExe = null, bool ignoreArguments = false) - { - if (string.IsNullOrEmpty(resolvedExe) && - (ignoreArguments || (string.IsNullOrEmpty(psi.Arguments) && !psi.HasArgumentList))) - { - return new string[] { psi.FileName }; - } - - var argvList = new List(); - if (!string.IsNullOrEmpty(resolvedExe)) - { - argvList.Add(resolvedExe); - if (resolvedExe.Contains("kfmclient")) - { - argvList.Add("openURL"); // kfmclient needs OpenURL - } - } - - argvList.Add(psi.FileName); - - if (!ignoreArguments) - { - if (!string.IsNullOrEmpty(psi.Arguments)) - { - ParseArgumentsIntoList(psi.Arguments, argvList); - } - else if (psi.HasArgumentList) - { - argvList.AddRange(psi.ArgumentList); - } - } - return argvList.ToArray(); - } - - /// Resolves a path to the filename passed to ProcessStartInfo. - /// The filename. - /// The resolved path. It can return null in case of URLs. - internal static string? ResolvePath(string filename) - { - // Follow the same resolution that Windows uses with CreateProcess: - // 1. First try the exact path provided - // 2. Then try the file relative to the executable directory - // 3. Then try the file relative to the current directory - // 4. then try the file in each of the directories specified in PATH - // Windows does additional Windows-specific steps between 3 and 4, - // and we ignore those here. - - // If the filename is a complete path, use it, regardless of whether it exists. - if (Path.IsPathRooted(filename)) - { - // In this case, it doesn't matter whether the file exists or not; - // it's what the caller asked for, so it's what they'll get - return filename; - } - - // Then check the executable's directory - string? path = Environment.ProcessPath; - if (path != null) - { - try - { - path = Path.Combine(Path.GetDirectoryName(path)!, filename); - if (File.Exists(path)) - { - return path; - } - } - catch (ArgumentException) { } // ignore any errors in data that may come from the exe path - } - - // Then check the current directory - path = Path.Combine(Directory.GetCurrentDirectory(), filename); - if (File.Exists(path)) - { - return path; - } - - // Then check each directory listed in the PATH environment variables - return FindProgramInPath(filename); - } - - /// Parses a command-line argument string into a list of arguments. - /// The argument string. - /// The list into which the component arguments should be stored. - /// - /// This follows the rules outlined in "Parsing C++ Command-Line Arguments" at - /// https://msdn.microsoft.com/en-us/library/17w5ykft.aspx. - /// - private static void ParseArgumentsIntoList(string arguments, List results) - { - // Iterate through all of the characters in the argument string. - for (int i = 0; i < arguments.Length; i++) - { - while (i < arguments.Length && (arguments[i] == ' ' || arguments[i] == '\t')) - i++; - - if (i == arguments.Length) - break; - - results.Add(GetNextArgument(arguments, ref i)); - } - } - - private static string GetNextArgument(string arguments, ref int i) - { - var currentArgument = new ValueStringBuilder(stackalloc char[256]); - bool inQuotes = false; - - while (i < arguments.Length) - { - // From the current position, iterate through contiguous backslashes. - int backslashCount = 0; - while (i < arguments.Length && arguments[i] == '\\') - { - i++; - backslashCount++; - } - - if (backslashCount > 0) - { - if (i >= arguments.Length || arguments[i] != '"') - { - // Backslashes not followed by a double quote: - // they should all be treated as literal backslashes. - currentArgument.Append('\\', backslashCount); - } - else - { - // Backslashes followed by a double quote: - // - Output a literal slash for each complete pair of slashes - // - If one remains, use it to make the subsequent quote a literal. - currentArgument.Append('\\', backslashCount / 2); - if (backslashCount % 2 != 0) - { - currentArgument.Append('"'); - i++; - } - } - - continue; - } - - char c = arguments[i]; - - // If this is a double quote, track whether we're inside of quotes or not. - // Anything within quotes will be treated as a single argument, even if - // it contains spaces. - if (c == '"') - { - if (inQuotes && i < arguments.Length - 1 && arguments[i + 1] == '"') - { - // Two consecutive double quotes inside an inQuotes region should result in a literal double quote - // (the parser is left in the inQuotes region). - // This behavior is not part of the spec of code:ParseArgumentsIntoList, but is compatible with CRT - // and .NET Framework. - currentArgument.Append('"'); - i++; - } - else - { - inQuotes = !inQuotes; - } - - i++; - continue; - } - - // If this is a space/tab and we're not in quotes, we're done with the current - // argument, it should be added to the results and then reset for the next one. - if ((c == ' ' || c == '\t') && !inQuotes) - { - break; - } - - // Nothing special; add the character to the current argument. - currentArgument.Append(c); - i++; - } - - return currentArgument.ToString(); - } } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs index 702d579c095c54..6208deff5d094d 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Windows.cs @@ -1,116 +1,15 @@ // 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.Collections.Specialized; -using System.ComponentModel; using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using Microsoft.Win32.SafeHandles; namespace System.Diagnostics { internal static partial class ProcessUtils { - internal static bool SupportsAtomicNonInheritablePipeCreation => true; - private static bool IsExecutable(string fullPath) { return File.Exists(fullPath); } - - internal static int GetShowWindowFromWindowStyle(ProcessWindowStyle windowStyle) => windowStyle switch - { - ProcessWindowStyle.Hidden => Interop.Shell32.SW_HIDE, - ProcessWindowStyle.Minimized => Interop.Shell32.SW_SHOWMINIMIZED, - ProcessWindowStyle.Maximized => Interop.Shell32.SW_SHOWMAXIMIZED, - _ => Interop.Shell32.SW_SHOWNORMAL, - }; - - internal static void BuildCommandLine(ProcessStartInfo startInfo, ref ValueStringBuilder commandLine) - { - // Construct a StringBuilder with the appropriate command line - // to pass to CreateProcess. If the filename isn't already - // in quotes, we quote it here. This prevents some security - // problems (it specifies exactly which part of the string - // is the file to execute). - ReadOnlySpan fileName = startInfo.FileName.AsSpan().Trim(); - bool fileNameIsQuoted = fileName.StartsWith('"') && fileName.EndsWith('"'); - if (!fileNameIsQuoted) - { - commandLine.Append('"'); - } - - commandLine.Append(fileName); - - if (!fileNameIsQuoted) - { - commandLine.Append('"'); - } - - startInfo.AppendArgumentsTo(ref commandLine); - } - - /// Duplicates a handle as inheritable if it's valid and not inheritable. - internal static void DuplicateAsInheritableIfNeeded(SafeFileHandle sourceHandle, ref SafeFileHandle? duplicatedHandle) - { - // The user can't specify invalid handle via ProcessStartInfo.Standard*Handle APIs. - // However, Console.OpenStandard*Handle() can return INVALID_HANDLE_VALUE for a process - // that was started with INVALID_HANDLE_VALUE as given standard handle. - if (sourceHandle.IsInvalid) - { - return; - } - - // When we know for sure that the handle is inheritable, we don't need to duplicate. - // When GetHandleInformation fails, we still attempt to call DuplicateHandle, - // just to keep throwing the same exception (backward compatibility). - if (Interop.Kernel32.GetHandleInformation(sourceHandle, out Interop.Kernel32.HandleFlags flags) - && (flags & Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT) != 0) - { - return; - } - - IntPtr currentProcHandle = Interop.Kernel32.GetCurrentProcess(); - if (!Interop.Kernel32.DuplicateHandle(currentProcHandle, - sourceHandle, - currentProcHandle, - out duplicatedHandle, - 0, - bInheritHandle: true, - Interop.Kernel32.HandleOptions.DUPLICATE_SAME_ACCESS)) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } - } - - internal static string GetEnvironmentVariablesBlock(DictionaryWrapper sd) - { - // https://learn.microsoft.com/windows/win32/procthread/changing-environment-variables - // "All strings in the environment block must be sorted alphabetically by name. The sort is - // case-insensitive, Unicode order, without regard to locale. Because the equal sign is a - // separator, it must not be used in the name of an environment variable." - - var keys = new string[sd.Count]; - sd.Keys.CopyTo(keys, 0); - Array.Sort(keys, StringComparer.OrdinalIgnoreCase); - - // Join the null-terminated "key=val\0" strings - var result = new StringBuilder(8 * keys.Length); - foreach (string key in keys) - { - string? value = sd[key]; - - // Ignore null values for consistency with Environment.SetEnvironmentVariable - if (value != null) - { - result.Append(key).Append('=').Append(value).Append('\0'); - } - } - - return result.ToString(); - } } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs index 8b585fc2c14d21..c3ead1de0030bf 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs @@ -1,17 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; using System.IO; -using System.Threading; namespace System.Diagnostics { internal static partial class ProcessUtils { - internal static readonly ReaderWriterLockSlim s_processStartLock = new ReaderWriterLockSlim(); - internal static int s_cachedSerializationSwitch; - internal static string? FindProgramInPath(string program) { string? pathEnvVar = System.Environment.GetEnvironmentVariable("PATH"); @@ -33,12 +28,5 @@ internal static partial class ProcessUtils return null; } - - internal static Win32Exception CreateExceptionForErrorStartingProcess(string errorMessage, int errorCode, string fileName, string? workingDirectory) - { - string directoryForException = string.IsNullOrEmpty(workingDirectory) ? Directory.GetCurrentDirectory() : workingDirectory; - string msg = SR.Format(SR.ErrorStartingProcess, fileName, directoryForException, errorMessage); - return new Win32Exception(errorCode, msg); - } } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs index 94c1355558dfc2..042a95b1950c64 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs @@ -550,7 +550,7 @@ private void ChildReaped(int exitCode, bool configureConsole) if (_usesTerminal) { // Update terminal settings before calling SetExited. - ProcessUtils.ConfigureTerminalForChildProcesses(-1, configureConsole); + Process.ConfigureTerminalForChildProcesses(-1, configureConsole); } SetExited(); diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ShellExecuteHelper.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ShellExecuteHelper.cs deleted file mode 100644 index e31eb4fd6684ac..00000000000000 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ShellExecuteHelper.cs +++ /dev/null @@ -1,94 +0,0 @@ -// 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.ComponentModel; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using Microsoft.Win32.SafeHandles; - -namespace System.Diagnostics -{ - internal sealed unsafe class ShellExecuteHelper - { - private readonly Interop.Shell32.SHELLEXECUTEINFO* _executeInfo; - private bool _succeeded; - private bool _notpresent; - - public ShellExecuteHelper(Interop.Shell32.SHELLEXECUTEINFO* executeInfo) - { - _executeInfo = executeInfo; - } - - private void ShellExecuteFunction() - { - try - { - if (!(_succeeded = Interop.Shell32.ShellExecuteExW(_executeInfo))) - ErrorCode = Marshal.GetLastWin32Error(); - } - catch (EntryPointNotFoundException) - { - _notpresent = true; - } - } - - public bool ShellExecuteOnSTAThread() - { - // ShellExecute() requires STA in order to work correctly. - - if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) - { - ThreadStart threadStart = new ThreadStart(ShellExecuteFunction); - Thread executionThread = new Thread(threadStart) - { - IsBackground = true, - Name = ".NET Process STA" - }; - executionThread.SetApartmentState(ApartmentState.STA); - executionThread.Start(); - executionThread.Join(); - } - else - { - ShellExecuteFunction(); - } - - if (_notpresent) - throw new PlatformNotSupportedException(SR.UseShellExecuteNotSupported); - - return _succeeded; - } - - internal static int GetShellError(IntPtr error) - { - switch ((long)error) - { - case Interop.Shell32.SE_ERR_FNF: - return Interop.Errors.ERROR_FILE_NOT_FOUND; - case Interop.Shell32.SE_ERR_PNF: - return Interop.Errors.ERROR_PATH_NOT_FOUND; - case Interop.Shell32.SE_ERR_ACCESSDENIED: - return Interop.Errors.ERROR_ACCESS_DENIED; - case Interop.Shell32.SE_ERR_OOM: - return Interop.Errors.ERROR_NOT_ENOUGH_MEMORY; - case Interop.Shell32.SE_ERR_DDEFAIL: - case Interop.Shell32.SE_ERR_DDEBUSY: - case Interop.Shell32.SE_ERR_DDETIMEOUT: - return Interop.Errors.ERROR_DDE_FAIL; - case Interop.Shell32.SE_ERR_SHARE: - return Interop.Errors.ERROR_SHARING_VIOLATION; - case Interop.Shell32.SE_ERR_NOASSOC: - return Interop.Errors.ERROR_NO_ASSOCIATION; - case Interop.Shell32.SE_ERR_DLLNOTFOUND: - return Interop.Errors.ERROR_DLL_NOT_FOUND; - default: - return (int)(long)error; - } - } - - public int ErrorCode { get; private set; } - } -} From ea70470e19ee840d1f5f03f986080f1c1c9329fe Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 30 Mar 2026 16:31:38 +0200 Subject: [PATCH 07/19] address my own feedback --- .../SafeHandles/SafeProcessHandle.Unix.cs | 5 +++- .../SafeHandles/SafeProcessHandle.Windows.cs | 3 ++ .../Win32/SafeHandles/SafeProcessHandle.cs | 2 -- .../System/Diagnostics/Process.Scenarios.cs | 28 +++++++++++++++---- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs index f227eeaf4dd18d..0083309ac6d7e7 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs @@ -9,6 +9,7 @@ using System.IO; using System.IO.Pipes; using System.Runtime.InteropServices; +using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Text; @@ -59,8 +60,10 @@ protected override bool ReleaseHandle() // On Unix, we don't use process descriptors yet, so we can't get PID. private static int GetProcessIdCore() => throw new PlatformNotSupportedException(); - private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) + internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) { + SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); + SafeProcessHandle startedProcess = StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, out ProcessWaitState.Holder? waitStateHolder); // For standalone SafeProcessHandle.Start, we dispose the wait state holder immediately. diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs index 416ec91ae11317..0b37a1b4ac92af 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Runtime.Serialization; using System.Security; using System.Text; @@ -18,6 +19,8 @@ protected override bool ReleaseHandle() internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) { + SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); + return startInfo.UseShellExecute ? StartWithShellExecuteEx(startInfo) : StartWithCreateProcess(startInfo, stdinHandle, stdoutHandle, stderrHandle); 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 0351614413591f..fcb199787115e4 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 @@ -93,8 +93,6 @@ public static SafeProcessHandle Start(ProcessStartInfo startInfo) throw new InvalidOperationException(SR.CantSetRedirectForSafeProcessHandleStart); } - SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); - SafeFileHandle? childInputHandle = startInfo.StandardInputHandle; SafeFileHandle? childOutputHandle = startInfo.StandardOutputHandle; SafeFileHandle? childErrorHandle = startInfo.StandardErrorHandle; 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 index 2557bf6f5733d4..763802c1bc4b7b 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -1,8 +1,10 @@ // 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 System.Collections.Generic; using System.Runtime.Versioning; +using Microsoft.Win32.SafeHandles; namespace System.Diagnostics { @@ -35,6 +37,11 @@ public partial class Process /// that the underlying operating-system resources held by the object are /// released promptly. /// + /// + /// When none of the standard handles (, + /// , or ) + /// are provided, the handles are redirected to the null file by default. + /// /// [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("tvos")] @@ -42,16 +49,24 @@ public partial class Process public static int StartAndForget(ProcessStartInfo startInfo) { ArgumentNullException.ThrowIfNull(startInfo); + startInfo.ThrowIfInvalid(out bool anyRedirection); - if (startInfo.RedirectStandardInput || startInfo.RedirectStandardOutput || startInfo.RedirectStandardError) + if (anyRedirection) { throw new InvalidOperationException(SR.StartAndForget_RedirectNotSupported); } - using Process process = new Process(); - process.StartInfo = startInfo; - process.Start(); - return process.Id; + using SafeFileHandle? nullFile = startInfo.StandardInputHandle is null || startInfo.StandardOutputHandle is null || startInfo.RedirectStandardError + ? File.OpenNullHandle() + : null; + + // Use internal StartCore to avoid the need of modyfing provided ProcessStartInfo + using SafeProcessHandle processHandle = SafeProcessHandle.StartCore(startInfo, + startInfo.StandardInputHandle ?? nullFile, + startInfo.StandardOutputHandle ?? nullFile, + startInfo.StandardErrorHandle ?? nullFile); + + return processHandle.ProcessId; } /// @@ -78,6 +93,9 @@ public static int StartAndForget(ProcessStartInfo startInfo) /// underlying operating-system resources held by the object are released /// promptly. /// + /// + /// Standard handles are redirected to the null file by default. + /// /// [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("tvos")] From c093f0461b277bb05c3e593318fa19765447dd19 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 30 Mar 2026 18:32:49 +0200 Subject: [PATCH 08/19] fix a bug discovered by code review --- .../src/System/Diagnostics/Process.Scenarios.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 763802c1bc4b7b..0bdba4ec16c877 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -56,7 +56,7 @@ public static int StartAndForget(ProcessStartInfo startInfo) throw new InvalidOperationException(SR.StartAndForget_RedirectNotSupported); } - using SafeFileHandle? nullFile = startInfo.StandardInputHandle is null || startInfo.StandardOutputHandle is null || startInfo.RedirectStandardError + using SafeFileHandle? nullFile = startInfo.StandardInputHandle is null || startInfo.StandardOutputHandle is null || startInfo.StandardErrorHandle is null ? File.OpenNullHandle() : null; From 1a36d81487bd40a7879359dce8ca37155f3b5870 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:07:15 +0000 Subject: [PATCH 09/19] Address review feedback: fix typo, remove duplicate SerializationGuard, update XML docs Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/f6d8eb50-6704-40c3-aab9-183176eea631 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../SafeHandles/SafeProcessHandle.Unix.cs | 2 ++ .../System/Diagnostics/Process.Scenarios.cs | 22 +++++-------------- .../src/System/Diagnostics/Process.cs | 2 -- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs index 0083309ac6d7e7..7f986b44e32abe 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs @@ -77,6 +77,8 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile { waitStateHolder = null; + SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); + if (ProcessUtils.PlatformDoesNotSupportProcessStartAndKill) { throw new PlatformNotSupportedException(); 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 index 0bdba4ec16c877..3d35c70618878c 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -28,14 +28,8 @@ public partial class Process /// /// 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, - /// disposes the instance to release all associated resources, and returns the - /// process ID. The started process continues to run independently. - /// - /// - /// Calling this method ensures proper resource cleanup on the caller's side: unlike calling - /// and discarding the returned object, this method guarantees - /// that the underlying operating-system resources held by the object are - /// released promptly. + /// releases all associated resources, and returns the process ID. The started process continues to + /// run independently. /// /// /// When none of the standard handles (, @@ -60,7 +54,7 @@ public static int StartAndForget(ProcessStartInfo startInfo) ? File.OpenNullHandle() : null; - // Use internal StartCore to avoid the need of modyfing provided ProcessStartInfo + // Use internal StartCore to avoid the need of modifying provided ProcessStartInfo using SafeProcessHandle processHandle = SafeProcessHandle.StartCore(startInfo, startInfo.StandardInputHandle ?? nullFile, startInfo.StandardOutputHandle ?? nullFile, @@ -84,14 +78,8 @@ public static int StartAndForget(ProcessStartInfo startInfo) /// /// 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, - /// disposes the instance to release all associated resources, and returns the - /// process ID. The started process continues to run independently. - /// - /// - /// Calling this method ensures proper resource cleanup on the caller's side: unlike calling - /// and discarding the returned object, this method guarantees that the - /// underlying operating-system resources held by the object are released - /// promptly. + /// 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. diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index a093dac7ceacb8..b299a464b8afe0 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -1250,8 +1250,6 @@ public bool Start() //Cannot start a new process and store its handle if the object has been disposed, since finalization has been suppressed. CheckDisposed(); - SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); - SafeFileHandle? parentInputPipeHandle = null; SafeFileHandle? parentOutputPipeHandle = null; SafeFileHandle? parentErrorPipeHandle = null; From 3506a11fbb10d1b88cc2a00d5ff11db85d48bfea Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 31 Mar 2026 09:15:34 +0200 Subject: [PATCH 10/19] address code review feedback: don't call SerializationGuard.ThrowIfDeserializationInProgress twice --- .../src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs index 7f986b44e32abe..7f77afc2262170 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs @@ -62,8 +62,6 @@ protected override bool ReleaseHandle() internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle) { - SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); - SafeProcessHandle startedProcess = StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, out ProcessWaitState.Holder? waitStateHolder); // For standalone SafeProcessHandle.Start, we dispose the wait state holder immediately. From fd129aee1ab33c4bd1f7883101f94f8b89bcbfd9 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 31 Mar 2026 09:33:37 +0200 Subject: [PATCH 11/19] Apply suggestion from @adamsitnik --- .../src/System/Diagnostics/Process.Scenarios.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 3d35c70618878c..382d2f1dd0d926 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -32,9 +32,9 @@ public partial class Process /// run independently. /// /// - /// When none of the standard handles (, + /// When a standard handle (, /// , or ) - /// are provided, the handles are redirected to the null file by default. + /// is not provided, it is redirected to the null file by default. /// /// [UnsupportedOSPlatform("ios")] From 14d1235ddd624d9e129712349f2297d0ac768ba2 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 8 Apr 2026 08:22:09 +0200 Subject: [PATCH 12/19] use the new APIs to limit handle inheritance by default --- .../System/Diagnostics/Process.Scenarios.cs | 23 +++++++++++++++---- .../System/Diagnostics/ProcessStartInfo.cs | 4 +++- 2 files changed, 22 insertions(+), 5 deletions(-) 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 index 382d2f1dd0d926..a18a3171911af9 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -3,6 +3,7 @@ using System.IO; using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Runtime.Versioning; using Microsoft.Win32.SafeHandles; @@ -43,7 +44,7 @@ public partial class Process public static int StartAndForget(ProcessStartInfo startInfo) { ArgumentNullException.ThrowIfNull(startInfo); - startInfo.ThrowIfInvalid(out bool anyRedirection); + startInfo.ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inheritedHandles); if (anyRedirection) { @@ -54,11 +55,22 @@ public static int StartAndForget(ProcessStartInfo startInfo) ? File.OpenNullHandle() : null; + SafeFileHandle childInputHandle = startInfo.StandardInputHandle ?? nullFile!; + SafeFileHandle childOutputHandle = startInfo.StandardOutputHandle ?? nullFile!; + SafeFileHandle childErrorHandle = startInfo.StandardErrorHandle ?? nullFile!; + + if (inheritedHandles is null && startInfo.SupportsHandleInheritanceRestriction) + { + // In case user has not provided their own allow list, + // the inheritance is restricted to standard handles only. + inheritedHandles = []; + } + + ProcessStartInfo.ValidateInheritedHandles(childInputHandle, childOutputHandle, childErrorHandle, inheritedHandles); + // Use internal StartCore to avoid the need of modifying provided ProcessStartInfo using SafeProcessHandle processHandle = SafeProcessHandle.StartCore(startInfo, - startInfo.StandardInputHandle ?? nullFile, - startInfo.StandardOutputHandle ?? nullFile, - startInfo.StandardErrorHandle ?? nullFile); + childInputHandle, childOutputHandle, childErrorHandle, inheritedHandles); return processHandle.ProcessId; } @@ -101,6 +113,9 @@ public static int StartAndForget(string fileName, IList? arguments = nul } } + // Limit the inheritance to standard handles only. + startInfo.InheritedHandles = []; + return StartAndForget(startInfo); } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs index 257257d83c892c..315728d618fe12 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs @@ -306,6 +306,8 @@ public string Verb set => _verb = value; } + internal bool SupportsHandleInheritanceRestriction => !UseShellExecute && string.IsNullOrEmpty(UserName); + [DefaultValueAttribute(System.Diagnostics.ProcessWindowStyle.Normal)] public ProcessWindowStyle WindowStyle { @@ -394,7 +396,7 @@ internal void ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inherite throw new InvalidOperationException(SR.CantRedirectStreams); } - if (InheritedHandles is not null && (UseShellExecute || !string.IsNullOrEmpty(UserName))) + if (InheritedHandles is not null && !SupportsHandleInheritanceRestriction) { throw new InvalidOperationException(SR.InheritedHandlesRequiresCreateProcess); } From 7d9bbf08b48dd5b3e76b2cd3ca960e1ea293d6e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 06:59:53 +0000 Subject: [PATCH 13/19] Throw InvalidOperationException for UseShellExecute=true in StartAndForget Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/952738e9-5ac4-4e09-bf49-1a6b3b50f662 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/Resources/Strings.resx | 3 +++ .../src/System/Diagnostics/Process.Scenarios.cs | 14 ++++++++++++-- .../tests/StartAndForget.cs | 11 +++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index a681a378ed2306..8ce5881c044fed 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -360,4 +360,7 @@ Stream redirection is not supported by StartAndForget. Redirected streams must be drained to avoid deadlocks, which is incompatible with fire-and-forget semantics. + + 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.Scenarios.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs index a18a3171911af9..a380f517ef9073 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -19,11 +19,15 @@ public partial class Process /// The process ID of the started process. /// is . /// - /// One or more of , + /// 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. + /// 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. /// /// /// @@ -44,6 +48,12 @@ public partial class Process public static int StartAndForget(ProcessStartInfo startInfo) { ArgumentNullException.ThrowIfNull(startInfo); + + if (startInfo.UseShellExecute) + { + throw new InvalidOperationException(SR.StartAndForget_UseShellExecuteNotSupported); + } + startInfo.ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inheritedHandles); if (anyRedirection) diff --git a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs index b7685333766186..a6744bb9057dd1 100644 --- a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs +++ b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs @@ -71,5 +71,16 @@ public void StartAndForget_WithRedirectedStreams_ThrowsInvalidOperationException Assert.Throws(() => Process.StartAndForget(startInfo)); } + + [Fact] + public void StartAndForget_WithUseShellExecute_ThrowsInvalidOperationException() + { + ProcessStartInfo startInfo = new("someprocess") + { + UseShellExecute = true, + }; + + Assert.Throws(() => Process.StartAndForget(startInfo)); + } } } From 44d8f6afdef03c97a39bc846c3ab15ecba549bc4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:53:26 +0000 Subject: [PATCH 14/19] Update docs and remove unused using in StartAndForget Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/c53c66b3-fc3d-4807-9d63-5ecaf4603b5f Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/System/Diagnostics/Process.Scenarios.cs | 6 ++++++ .../System.Diagnostics.Process/tests/StartAndForget.cs | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) 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 index a380f517ef9073..2e2c8135f11cf9 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -41,6 +41,9 @@ public partial class Process /// , or ) /// is not provided, it is redirected to the null file by default. /// + /// + /// When possible, handle inheritance in the new process is limited to the standard handles only. + /// /// [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("tvos")] @@ -106,6 +109,9 @@ public static int StartAndForget(ProcessStartInfo startInfo) /// /// Standard handles are redirected to the null file by default. /// + /// + /// No handles are inherited by the new process. + /// /// [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("tvos")] diff --git a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs index a6744bb9057dd1..201fb0a91a07ca 100644 --- a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs +++ b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs @@ -1,7 +1,6 @@ // 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 Microsoft.DotNet.RemoteExecutor; using Xunit; From d4a941b2a85eebc046bb4e65b264dc5c5227966c Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 16 Apr 2026 08:33:49 +0200 Subject: [PATCH 15/19] update the implementation based on experience from StartDetached and other recent PRs --- .../SafeHandles/SafeProcessHandle.Unix.cs | 5 +-- .../SafeHandles/SafeProcessHandle.Windows.cs | 3 -- .../Win32/SafeHandles/SafeProcessHandle.cs | 13 +++++- .../src/Resources/Strings.resx | 3 -- .../System/Diagnostics/Process.Scenarios.cs | 40 +------------------ .../src/System/Diagnostics/Process.cs | 2 + .../System/Diagnostics/ProcessStartInfo.cs | 4 +- .../tests/StartAndForget.cs | 11 ++--- 8 files changed, 24 insertions(+), 57 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs index 31e9d6d2ab3a23..2a290ea098724e 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs @@ -9,7 +9,6 @@ using System.IO; using System.IO.Pipes; using System.Runtime.InteropServices; -using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Text; @@ -93,7 +92,7 @@ private bool SignalCore(PosixSignal signal) private delegate SafeProcessHandle StartWithShellExecuteDelegate(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder); private static StartWithShellExecuteDelegate? s_startWithShellExecute; - internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandlesSnapshot = null) + private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandlesSnapshot = null) { SafeProcessHandle startedProcess = StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandlesSnapshot, out ProcessWaitState.Holder? waitStateHolder); @@ -109,8 +108,6 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile { waitStateHolder = null; - SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); - ProcessUtils.EnsureInitialized(); if (startInfo.UseShellExecute) diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs index 65bcbf52d1cf70..29e915a0803c0b 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs @@ -6,7 +6,6 @@ using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; -using System.Runtime.Serialization; using System.Security; using System.Text; using System.Threading; @@ -52,8 +51,6 @@ private static unsafe Interop.Kernel32.SafeJobHandle CreateKillOnParentExitJob() internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles = null) { - SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); - if (startInfo.UseShellExecute) { // Nulls are allowed only for ShellExecute. 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 dae2ec5ff450f1..177f734ceb70de 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, redirectToNull: startInfo.StartDetached); + } + + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + internal static SafeProcessHandle Start(ProcessStartInfo startInfo, bool redirectToNull) + { startInfo.ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inheritedHandles); if (anyRedirection) @@ -104,11 +113,13 @@ public static SafeProcessHandle Start(ProcessStartInfo startInfo) throw new PlatformNotSupportedException(); } + SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); + SafeFileHandle? childInputHandle = startInfo.StandardInputHandle; SafeFileHandle? childOutputHandle = startInfo.StandardOutputHandle; SafeFileHandle? childErrorHandle = startInfo.StandardErrorHandle; - using SafeFileHandle? nullDeviceHandle = startInfo.StartDetached + using SafeFileHandle? nullDeviceHandle = redirectToNull && (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 ce8c0ccff3c03c..cbc82022ca209c 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -363,9 +363,6 @@ InheritedHandles must not contain duplicates. - - Stream redirection is not supported by StartAndForget. Redirected streams must be drained to avoid deadlocks, which is incompatible with fire-and-forget semantics. - 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.Scenarios.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs index 2e2c8135f11cf9..5da31e3252a484 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -41,9 +41,6 @@ public partial class Process /// , or ) /// is not provided, it is redirected to the null file by default. /// - /// - /// When possible, handle inheritance in the new process is limited to the standard handles only. - /// /// [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("tvos")] @@ -57,34 +54,7 @@ public static int StartAndForget(ProcessStartInfo startInfo) throw new InvalidOperationException(SR.StartAndForget_UseShellExecuteNotSupported); } - startInfo.ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inheritedHandles); - - if (anyRedirection) - { - throw new InvalidOperationException(SR.StartAndForget_RedirectNotSupported); - } - - using SafeFileHandle? nullFile = startInfo.StandardInputHandle is null || startInfo.StandardOutputHandle is null || startInfo.StandardErrorHandle is null - ? File.OpenNullHandle() - : null; - - SafeFileHandle childInputHandle = startInfo.StandardInputHandle ?? nullFile!; - SafeFileHandle childOutputHandle = startInfo.StandardOutputHandle ?? nullFile!; - SafeFileHandle childErrorHandle = startInfo.StandardErrorHandle ?? nullFile!; - - if (inheritedHandles is null && startInfo.SupportsHandleInheritanceRestriction) - { - // In case user has not provided their own allow list, - // the inheritance is restricted to standard handles only. - inheritedHandles = []; - } - - ProcessStartInfo.ValidateInheritedHandles(childInputHandle, childOutputHandle, childErrorHandle, inheritedHandles); - - // Use internal StartCore to avoid the need of modifying provided ProcessStartInfo - using SafeProcessHandle processHandle = SafeProcessHandle.StartCore(startInfo, - childInputHandle, childOutputHandle, childErrorHandle, inheritedHandles); - + using SafeProcessHandle processHandle = SafeProcessHandle.Start(startInfo, redirectToNull: true); return processHandle.ProcessId; } @@ -109,9 +79,6 @@ public static int StartAndForget(ProcessStartInfo startInfo) /// /// Standard handles are redirected to the null file by default. /// - /// - /// No handles are inherited by the new process. - /// /// [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("tvos")] @@ -120,7 +87,7 @@ public static int StartAndForget(string fileName, IList? arguments = nul { ArgumentNullException.ThrowIfNull(fileName); - ProcessStartInfo startInfo = new ProcessStartInfo(fileName); + ProcessStartInfo startInfo = new(fileName); if (arguments is not null) { foreach (string argument in arguments) @@ -129,9 +96,6 @@ public static int StartAndForget(string fileName, IList? arguments = nul } } - // Limit the inheritance to standard handles only. - startInfo.InheritedHandles = []; - return StartAndForget(startInfo); } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index 82d7003e22addf..65b96e440b4061 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -1172,6 +1172,8 @@ public bool Start() //Cannot start a new process and store its handle if the object has been disposed, since finalization has been suppressed. CheckDisposed(); + SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); + SafeFileHandle? parentInputPipeHandle = null; SafeFileHandle? parentOutputPipeHandle = null; SafeFileHandle? parentErrorPipeHandle = null; diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs index b3d74a0f85187c..11ff3f54fe152e 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs @@ -350,8 +350,6 @@ public string Verb set => _verb = value; } - internal bool SupportsHandleInheritanceRestriction => !UseShellExecute && string.IsNullOrEmpty(UserName); - [DefaultValueAttribute(System.Diagnostics.ProcessWindowStyle.Normal)] public ProcessWindowStyle WindowStyle { @@ -445,7 +443,7 @@ internal void ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inherite throw new InvalidOperationException(SR.StartDetachedNotCompatible); } - if (InheritedHandles is not null && !SupportsHandleInheritanceRestriction) + if (InheritedHandles is not null && (UseShellExecute || !string.IsNullOrEmpty(UserName))) { throw new InvalidOperationException(SR.InheritedHandlesRequiresCreateProcess); } diff --git a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs index 201fb0a91a07ca..00fd5601c3ae1b 100644 --- a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs +++ b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs @@ -13,7 +13,7 @@ public class StartAndForgetTests : ProcessTestBase [InlineData(false)] public void StartAndForget_StartsProcessAndReturnsValidPid(bool useProcessStartInfo) { - Process template = CreateProcessLong(); + Process template = CreateSleepProcess((int)TimeSpan.FromHours(1).TotalMilliseconds); int pid = useProcessStartInfo ? Process.StartAndForget(template.StartInfo) : Process.StartAndForget(template.StartInfo.FileName, template.StartInfo.ArgumentList); @@ -32,12 +32,13 @@ public void StartAndForget_StartsProcessAndReturnsValidPid(bool useProcessStartI } } - [Fact] + // 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() { - // hostname is not available on Android or Azure Linux. - // ls is available on every Unix. - int pid = Process.StartAndForget(OperatingSystem.IsWindows() ? "hostname" : "ls", null); + // 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); } From a2b586a6c2115e359c8d0660f7bea6eac34c32f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 08:11:19 +0000 Subject: [PATCH 16/19] Fix unused usings in Process.Scenarios.cs and improve redirect error message Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/76d05506-679f-4ffa-baaf-17b58419e756 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../System.Diagnostics.Process/src/Resources/Strings.resx | 2 +- .../src/System/Diagnostics/Process.Scenarios.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index cbc82022ca209c..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. 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 index 5da31e3252a484..b41e9c780967ac 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -1,9 +1,7 @@ // 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 System.Collections.Generic; -using System.Runtime.InteropServices; using System.Runtime.Versioning; using Microsoft.Win32.SafeHandles; From 1380afd5dc4e9017aa2d4e8c88b4b5b1044eb011 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:09:46 +0000 Subject: [PATCH 17/19] Address review feedback: rename fallbackToNull, simplify summaries, reorder remarks Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/149a6be4-01ab-45ff-a079-343db0f8e2bc Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Win32/SafeHandles/SafeProcessHandle.cs | 6 +++--- .../System/Diagnostics/Process.Scenarios.cs | 21 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) 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 177f734ceb70de..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 @@ -89,13 +89,13 @@ public static SafeProcessHandle Start(ProcessStartInfo startInfo) { ArgumentNullException.ThrowIfNull(startInfo); - return Start(startInfo, redirectToNull: startInfo.StartDetached); + return Start(startInfo, fallbackToNull: startInfo.StartDetached); } [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("tvos")] [SupportedOSPlatform("maccatalyst")] - internal static SafeProcessHandle Start(ProcessStartInfo startInfo, bool redirectToNull) + internal static SafeProcessHandle Start(ProcessStartInfo startInfo, bool fallbackToNull) { startInfo.ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inheritedHandles); @@ -119,7 +119,7 @@ internal static SafeProcessHandle Start(ProcessStartInfo startInfo, bool redirec SafeFileHandle? childOutputHandle = startInfo.StandardOutputHandle; SafeFileHandle? childErrorHandle = startInfo.StandardErrorHandle; - using SafeFileHandle? nullDeviceHandle = redirectToNull + 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/System/Diagnostics/Process.Scenarios.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs index b41e9c780967ac..9df4a1f51e50dc 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Scenarios.cs @@ -10,8 +10,8 @@ namespace System.Diagnostics public partial class Process { /// - /// Starts the process described by , captures its process ID, - /// releases all associated resources, and returns the process ID. + /// 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. @@ -29,16 +29,15 @@ public partial class Process /// /// /// - /// 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. - /// - /// /// 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")] @@ -52,13 +51,13 @@ public static int StartAndForget(ProcessStartInfo startInfo) throw new InvalidOperationException(SR.StartAndForget_UseShellExecuteNotSupported); } - using SafeProcessHandle processHandle = SafeProcessHandle.Start(startInfo, redirectToNull: true); + using SafeProcessHandle processHandle = SafeProcessHandle.Start(startInfo, fallbackToNull: true); return processHandle.ProcessId; } /// - /// Starts a process with the specified file name and optional arguments, captures its process ID, - /// releases all associated resources, and returns the process ID. + /// 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. /// From 398d6ac09d39d7e463c630ada617b09b7a81a25a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:18:27 +0000 Subject: [PATCH 18/19] Apply suggestion: add 'using' to Process template declaration in StartAndForget test Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/3564306f-64a4-473b-8bea-cce48f30b996 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../System.Diagnostics.Process/tests/StartAndForget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs index 00fd5601c3ae1b..4b8430146569ed 100644 --- a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs +++ b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs @@ -13,7 +13,7 @@ public class StartAndForgetTests : ProcessTestBase [InlineData(false)] public void StartAndForget_StartsProcessAndReturnsValidPid(bool useProcessStartInfo) { - Process template = CreateSleepProcess((int)TimeSpan.FromHours(1).TotalMilliseconds); + using Process template = CreateSleepProcess((int)TimeSpan.FromHours(1).TotalMilliseconds); int pid = useProcessStartInfo ? Process.StartAndForget(template.StartInfo) : Process.StartAndForget(template.StartInfo.FileName, template.StartInfo.ArgumentList); From e3fff4052b807f2b83ecb0d7f5cf143a6990aec2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:32:54 +0000 Subject: [PATCH 19/19] Add StartAndForget_WithStandardOutputHandle_CapturesOutput test Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/cf4bd1f8-9847-4f51-996e-2434969a7a15 Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/StartAndForget.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs index 4b8430146569ed..0a3d26ec319365 100644 --- a/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs +++ b/src/libraries/System.Diagnostics.Process/tests/StartAndForget.cs @@ -1,7 +1,9 @@ // 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 @@ -32,6 +34,32 @@ public void StartAndForget_StartsProcessAndReturnsValidPid(bool useProcessStartI } } + [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()