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 6a0abcb60fe39d..c2ef66f997772b 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -27,6 +27,10 @@ public void Kill() { } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] [System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] + public static bool TryOpen(int processId, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out Microsoft.Win32.SafeHandles.SafeProcessHandle? processHandle) { 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; } public bool TryWaitForExit(System.TimeSpan timeout, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Diagnostics.ProcessExitStatus? exitStatus) { throw null; } public System.Diagnostics.ProcessExitStatus WaitForExit() { throw null; } @@ -246,6 +250,10 @@ public void Refresh() { } [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; } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] + [System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] + public static bool TryGetProcessById(int processId, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Diagnostics.Process? process) { throw null; } public void WaitForExit() { } public bool WaitForExit(int milliseconds) { throw null; } public bool WaitForExit(System.TimeSpan timeout) { throw null; } 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 cf7afba459e35d..32ff0c3c7a122f 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 @@ -76,17 +76,26 @@ protected override bool ReleaseHandle() return true; } - private static SafeProcessHandle OpenCore(int processId) + private static bool TryOpenCore(int processId, [NotNullWhen(true)] out SafeProcessHandle? processHandle) { int result = Interop.Sys.OpenProcess(processId); if (result == -1) { - throw new Win32Exception(); + Interop.Error error = Interop.Sys.GetLastError(); + + if (error == Interop.Error.EPERM) + { + throw new UnauthorizedAccessException(); + } + + processHandle = null; + return false; } ProcessWaitState.Holder waitStateHolder = new(processId); - return new SafeProcessHandle(waitStateHolder); + processHandle = new SafeProcessHandle(waitStateHolder); + return true; } 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 60679088bb156f..e2ed071d25184e 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 @@ -51,7 +51,7 @@ protected override bool ReleaseHandle() return Interop.Kernel32.CloseHandle(handle); } - private static SafeProcessHandle OpenCore(int processId) + private static bool TryOpenCore(int processId, [NotNullWhen(true)] out SafeProcessHandle? processHandle) { const int desiredAccess = Interop.Advapi32.ProcessOptions.PROCESS_QUERY_LIMITED_INFORMATION | Interop.Advapi32.ProcessOptions.SYNCHRONIZE @@ -63,11 +63,19 @@ private static SafeProcessHandle OpenCore(int processId) { int error = Marshal.GetLastPInvokeError(); safeHandle.Dispose(); - throw new Win32Exception(error); + + if (error == Interop.Errors.ERROR_ACCESS_DENIED) + { + throw new UnauthorizedAccessException(); + } + + processHandle = null; + return false; } safeHandle.ProcessId = processId; - return safeHandle; + processHandle = safeHandle; + return true; } private static unsafe Interop.Kernel32.SafeJobHandle CreateKillOnParentExitJob() 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 10e9da16d6ea35..a957c11cc757d8 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 @@ -75,6 +75,35 @@ public SafeProcessHandle(IntPtr existingHandle, bool ownsHandle) [UnsupportedOSPlatform("tvos")] [SupportedOSPlatform("maccatalyst")] public static SafeProcessHandle Open(int processId) + { + if (!TryOpen(processId, out SafeProcessHandle? handle)) + { + throw new Win32Exception(); + } + + return handle; + } + + /// + /// Attempts to open an existing process by its process ID. + /// + /// The process ID of the process to open. + /// When this method returns , contains the for the opened process; otherwise, . + /// if the process was successfully opened; otherwise, . + /// Thrown when is negative or zero. + /// Thrown when the process exists but the caller does not have permissions to open it. + /// + /// + /// This method does not throw when the process does not exist. Instead, it returns . + /// + /// + /// On Windows, if the process has already exited, the method may still succeed and return a valid handle representing the terminated process. + /// + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public static bool TryOpen(int processId, [NotNullWhen(true)] out SafeProcessHandle? processHandle) { ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(processId, 0); @@ -83,7 +112,7 @@ public static SafeProcessHandle Open(int processId) throw new PlatformNotSupportedException(); } - return OpenCore(processId); + return TryOpenCore(processId, out processHandle); } /// 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 8966e578389e92..c13176e6912af2 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -119,6 +119,12 @@ private Process(string machineName, bool isRemoteMachine, int processId, Process _errorStreamReadMode = StreamReadMode.Undefined; } + private Process(SafeProcessHandle processHandle) : this(".", false, processHandle.ProcessId, null) + { + _processHandle = processHandle; + _haveProcessHandle = true; + } + public SafeProcessHandle SafeHandle { get @@ -936,6 +942,29 @@ public static Process GetProcessById(int processId) return new Process(".", false, processId, null); } + /// + /// Attempts to get a instance for an existing process with the specified process ID. + /// + /// The process ID of the process to open. + /// When this method returns , contains a representing the opened process; otherwise, . + /// if the process was found and opened successfully; otherwise, . + /// Thrown when is negative or zero. + /// Thrown when the process exists but the caller does not have permissions to open it. + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public static bool TryGetProcessById(int processId, [NotNullWhen(true)] out Process? process) + { + if (!SafeProcessHandle.TryOpen(processId, out SafeProcessHandle? processHandle)) + { + process = null; + return false; + } + + process = new Process(processHandle); + return true; + } + /// /// /// Creates an array of components that are diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessOpenTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessOpenTests.cs new file mode 100644 index 00000000000000..be443367eb2563 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessOpenTests.cs @@ -0,0 +1,211 @@ +// 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.Threading; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.Diagnostics.Tests +{ + public class ProcessOpenTests : ProcessTestBase + { + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void Open_InvalidProcessId_ThrowsArgumentOutOfRangeException(int processId) + { + Assert.Throws(() => SafeProcessHandle.Open(processId)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void TryOpen_InvalidProcessId_ThrowsArgumentOutOfRangeException(int processId) + { + Assert.Throws(() => SafeProcessHandle.TryOpen(processId, out _)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void TryGetProcessById_InvalidProcessId_ThrowsArgumentOutOfRangeException(int processId) + { + Assert.Throws(() => Process.TryGetProcessById(processId, out _)); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void Open_NonExistentProcessId_ThrowsWin32Exception() + { + // Use an unlikely process ID that should not exist. + Assert.Throws(() => SafeProcessHandle.Open(int.MaxValue)); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TryOpen_NonExistentProcessId_ReturnsFalse() + { + bool result = SafeProcessHandle.TryOpen(int.MaxValue, out SafeProcessHandle? handle); + Assert.False(result); + Assert.Null(handle); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TryGetProcessById_NonExistentProcessId_ReturnsFalse() + { + bool result = Process.TryGetProcessById(int.MaxValue, out Process? process); + Assert.False(result); + Assert.Null(process); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows), nameof(PlatformDetection.IsNotPrivilegedProcess))] + public void Open_ProtectedProcess_ThrowsUnauthorizedAccessException() + { + int pid = Assert.Single(Process.GetProcessesByName("lsass")).Id; + Assert.Throws(() => SafeProcessHandle.Open(pid)); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows), nameof(PlatformDetection.IsNotPrivilegedProcess))] + public void TryOpen_ProtectedProcess_ThrowsUnauthorizedAccessException() + { + int pid = Assert.Single(Process.GetProcessesByName("lsass")).Id; + Assert.Throws(() => SafeProcessHandle.TryOpen(pid, out _)); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public void Open_RunningProcess_ReturnsValidHandle(bool tryOpen) + { + using Process process = CreateProcess(static () => + { + Thread.Sleep(Timeout.Infinite); + return RemoteExecutor.SuccessExitCode; + }); + process.Start(); + + SafeProcessHandle? handle = null; + try + { + if (tryOpen) + { + Assert.True(SafeProcessHandle.TryOpen(process.Id, out handle)); + } + else + { + handle = SafeProcessHandle.Open(process.Id); + } + + Assert.False(handle.IsInvalid); + Assert.Equal(process.Id, handle.ProcessId); + + handle.Kill(); + Assert.True(handle.TryWaitForExit(TimeSpan.FromMilliseconds(WaitInMS), out _)); + } + finally + { + process.Kill(); + process.WaitForExit(); + handle?.Dispose(); + } + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public void GetProcessById_RunningProcess_ReturnsProcess(bool tryGet) + { + using Process process = CreateProcess(static () => + { + Thread.Sleep(Timeout.Infinite); + return RemoteExecutor.SuccessExitCode; + }); + process.Start(); + + Process? found = null; + try + { + if (tryGet) + { + Assert.True(Process.TryGetProcessById(process.Id, out found)); + } + else + { + found = Process.GetProcessById(process.Id); + } + + Assert.NotNull(found); + Assert.Equal(process.Id, found.Id); + Assert.Equal(process.ProcessName, found.ProcessName); + + found.Kill(); + found.WaitForExit(); + } + finally + { + process.Kill(); + process.WaitForExit(); + found?.Dispose(); + } + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void Open_ExitedProcess_BehaviorDependsOnPlatform(bool tryOpen, bool kill) + { + using Process process = kill + ? CreateProcess(static () => { Thread.Sleep(Timeout.Infinite); return RemoteExecutor.SuccessExitCode; }) + : CreateProcess(static () => RemoteExecutor.SuccessExitCode); + process.Start(); + + if (kill) + { + process.Kill(); + } + + process.WaitForExit(); + + if (OperatingSystem.IsWindows()) + { + SafeProcessHandle? handle = null; + try + { + if (tryOpen) + { + Assert.True(SafeProcessHandle.TryOpen(process.Id, out handle)); + } + else + { + handle = SafeProcessHandle.Open(process.Id); + } + + // On Windows, the kernel process object persists as long as at least one handle is open. + Assert.NotNull(handle); + Assert.False(handle.IsInvalid); + } + finally + { + handle?.Dispose(); + } + } + else + { + // On Unix, once the process has been waited for (reaped), it is removed from the process + // table and its PID may be reused. + if (tryOpen) + { + bool result = SafeProcessHandle.TryOpen(process.Id, out SafeProcessHandle? handle); + Assert.False(result); + Assert.Null(handle); + } + else + { + Assert.Throws(() => SafeProcessHandle.Open(process.Id)); + } + } + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs index f7129caacd3fb5..367c5646bc46c8 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs @@ -1446,28 +1446,6 @@ public void TestGetCurrentProcess() Assert.Equal(currentProcessId, current.Id); } - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void TestGetProcessById() - { - CreateDefaultProcess(); - - Process p = Process.GetProcessById(_process.Id); - Assert.Equal(_process.Id, p.Id); - Assert.Equal(_process.ProcessName, p.ProcessName); - } - - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void GetProcessById_KilledProcess_ThrowsArgumentException() - { - Process process = CreateDefaultProcess(); - var handle = process.SafeHandle; - int processId = process.Id; - process.Kill(); - process.WaitForExit(WaitInMS); - Assert.Throws(() => Process.GetProcessById(processId)); - GC.KeepAlive(handle); - } - [Fact] [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "libproc is not supported on iOS/tvOS")] public void TestGetProcesses() diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs index cd749a2d22d418..a9dacea578c48f 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs @@ -270,83 +270,6 @@ public void Kill_HandleWithoutTerminatePermission_ThrowsWin32Exception() } } - [Theory] - [InlineData(0)] - [InlineData(-1)] - public void Open_InvalidProcessId_ThrowsArgumentOutOfRangeException(int processId) - { - Assert.Throws(() => SafeProcessHandle.Open(processId)); - } - - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void Open_NonExistentProcessId_ThrowsWin32Exception() - { - // Use an unlikely process ID that should not exist. - Assert.Throws(() => SafeProcessHandle.Open(int.MaxValue)); - } - - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void Open_RunningProcess_ReturnsValidHandle() - { - using Process process = CreateProcess(static () => - { - Thread.Sleep(Timeout.Infinite); - return RemoteExecutor.SuccessExitCode; - }); - process.Start(); - - try - { - using SafeProcessHandle handle = SafeProcessHandle.Open(process.Id); - Assert.False(handle.IsInvalid); - Assert.Equal(process.Id, handle.ProcessId); - } - finally - { - process.Kill(); - process.WaitForExit(); - } - } - - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void Open_ExitedProcess_BehaviorDependsOnPlatform() - { - using Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); - process.Start(); - process.WaitForExit(); - - if (OperatingSystem.IsWindows()) - { - // On Windows, the kernel process object persists as long as at least one handle is open. - // Since Process.WaitForExit() doesn't release the handle, OpenProcess succeeds and returns - // a valid handle to the terminated process. - using SafeProcessHandle handle = SafeProcessHandle.Open(process.Id); - Assert.False(handle.IsInvalid); - } - else - { - // On Unix, once the process has been waited for (reaped), it is removed from the process - // table and its PID may be reused. Open throws because the process no longer exists. - Assert.Throws(() => SafeProcessHandle.Open(process.Id)); - } - } - - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void Open_ThenKill_TerminatesProcess() - { - using Process process = CreateProcess(static () => - { - Thread.Sleep(Timeout.Infinite); - return RemoteExecutor.SuccessExitCode; - }); - process.Start(); - - using SafeProcessHandle handle = SafeProcessHandle.Open(process.Id); - handle.Kill(); - - Assert.True(handle.TryWaitForExit(TimeSpan.FromMilliseconds(WaitInMS), out _)); - } - [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [InlineData(true)] [InlineData(false)] 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 f1ed6055933db3..33132969ff9ec7 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 @@ -43,6 +43,7 @@ +