Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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<string>? 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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <summary>
/// Attempts to open an existing process by its process ID.
/// </summary>
/// <param name="processId">The process ID of the process to open.</param>
/// <param name="processHandle">When this method returns <see langword="true"/>, contains the <see cref="SafeProcessHandle"/> for the opened process; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if the process was successfully opened; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="processId"/> is negative or zero.</exception>
/// <exception cref="UnauthorizedAccessException">Thrown when the process exists but the caller does not have permissions to open it.</exception>
/// <remarks>
/// <para>
/// This method does not throw when the process does not exist. Instead, it returns <see langword="false"/>.
/// </para>
/// <para>
/// On Windows, if the process has already exited, the method may still succeed and return a valid handle representing the terminated process.
/// </para>
/// </remarks>
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("tvos")]
[SupportedOSPlatform("maccatalyst")]
public static bool TryOpen(int processId, [NotNullWhen(true)] out SafeProcessHandle? processHandle)
{
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(processId, 0);

Expand All @@ -83,7 +112,7 @@ public static SafeProcessHandle Open(int processId)
throw new PlatformNotSupportedException();
}

return OpenCore(processId);
return TryOpenCore(processId, out processHandle);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -936,6 +942,29 @@ public static Process GetProcessById(int processId)
return new Process(".", false, processId, null);
}

/// <summary>
/// Attempts to get a <see cref="Process"/> instance for an existing process with the specified process ID.
/// </summary>
/// <param name="processId">The process ID of the process to open.</param>
/// <param name="process">When this method returns <see langword="true"/>, contains a <see cref="Process"/> representing the opened process; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if the process was found and opened successfully; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="processId"/> is negative or zero.</exception>
/// <exception cref="UnauthorizedAccessException">Thrown when the process exists but the caller does not have permissions to open it.</exception>
[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;
}

/// <devdoc>
/// <para>
/// Creates an array of <see cref='System.Diagnostics.Process'/> components that are
Expand Down
211 changes: 211 additions & 0 deletions src/libraries/System.Diagnostics.Process/tests/ProcessOpenTests.cs
Original file line number Diff line number Diff line change
@@ -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<ArgumentOutOfRangeException>(() => SafeProcessHandle.Open(processId));
}

[Theory]
[InlineData(0)]
[InlineData(-1)]
public void TryOpen_InvalidProcessId_ThrowsArgumentOutOfRangeException(int processId)
{
Assert.Throws<ArgumentOutOfRangeException>(() => SafeProcessHandle.TryOpen(processId, out _));
}

[Theory]
[InlineData(0)]
[InlineData(-1)]
public void TryGetProcessById_InvalidProcessId_ThrowsArgumentOutOfRangeException(int processId)
{
Assert.Throws<ArgumentOutOfRangeException>(() => 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<Win32Exception>(() => SafeProcessHandle.Open(int.MaxValue));
}
Comment thread
adamsitnik marked this conversation as resolved.

[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<UnauthorizedAccessException>(() => 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<UnauthorizedAccessException>(() => 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);
Comment thread
adamsitnik marked this conversation as resolved.

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();
}
Comment thread
adamsitnik marked this conversation as resolved.
}

[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<Win32Exception>(() => SafeProcessHandle.Open(process.Id));
}
}
}
}
}
Loading
Loading