From ac7a599fddc0260bab88da38c4ada636d28a1bcb Mon Sep 17 00:00:00 2001 From: Seth Jackson Date: Tue, 16 Jun 2026 12:52:10 -0400 Subject: [PATCH 1/4] Port System.Diagnostics.Process to OpenBSD --- .../src/Interop/OpenBSD/Interop.Process.cs | 182 ++++++++++++++++++ .../src/System.Diagnostics.Process.csproj | 16 +- .../src/System/Diagnostics/Process.OpenBSD.cs | 134 +++++++++++++ .../Diagnostics/ProcessManager.OpenBSD.cs | 70 +++++++ 4 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 src/libraries/Common/src/Interop/OpenBSD/Interop.Process.cs create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OpenBSD.cs create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.OpenBSD.cs diff --git a/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.cs b/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.cs new file mode 100644 index 00000000000000..0e8031d8358c10 --- /dev/null +++ b/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.cs @@ -0,0 +1,182 @@ +// 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.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; + +#pragma warning disable CA1823 // analyzer incorrectly flags fixed buffer length const (https://github.com/dotnet/roslyn/issues/37593) + +internal static partial class Interop +{ + internal static partial class Process + { + private const ulong SecondsToNanoseconds = 1000000000; + private const ulong MicroSecondsToNanoSeconds = 1000; + + internal struct proc_stats + { + internal long startTime; + internal int nice; + internal ulong userTime; /* in ticks */ + internal ulong systemTime; /* in ticks */ + } + + /// + /// Queries the OS for the list of all running processes and returns the PID for each + /// + /// Returns a list of PIDs corresponding to all running processes + internal static unsafe int[] ListAllPids() + { + kinfo_proc* entries = GetProcInfo(0, false, out int numProcesses); + try + { + if (numProcesses <= 0) + { + throw new Win32Exception(SR.CantGetAllPids); + } + + var list = new ReadOnlySpan(entries, numProcesses); + var pids = new int[numProcesses]; + + // walk through process list and skip kernel threads + int idx = 0; + for (int i = 0; i < list.Length; i++) + { + if (list[i].p_ppid == 0) + { + // skip kernel threads + numProcesses -= 1; + } + else + { + pids[idx] = list[i].p_pid; + idx += 1; + } + } + + // Remove extra elements + Array.Resize(ref pids, numProcesses); + return pids; + } + finally + { + NativeMemory.Free(entries); + } + } + + /// + /// Gets executable name for process given it's PID + /// + /// The PID of the process + public static unsafe string GetProcPath(int pid) + { + // TODO + } + + /// + /// Gets the process information for a given process + /// + /// The PID (process ID) of the process + /// + /// Returns a valid ProcessInfo struct for valid processes that the caller + /// has permission to access; otherwise, returns null + /// + public static unsafe ProcessInfo GetProcessInfoById(int pid) + { + // Negative PIDs are invalid + ArgumentOutOfRangeException.ThrowIfNegative(pid); + + ProcessInfo info; + + kinfo_proc* kinfo = GetProcInfo(pid, true, out int count); + try + { + ArgumentOutOfRangeException.ThrowIfLessThan(count, 1, nameof(pid)); + + var process = new ReadOnlySpan(kinfo, count); + + // Get the process information for the specified pid + info = new ProcessInfo(); + + info.ProcessName = Utf8StringMarshaller.ConvertToManaged(kinfo->p_comm)!; + info.BasePriority = kinfo->p_nice; + info.VirtualBytes = (long)kinfo->p_vm_map_size; + info.WorkingSet = kinfo->p_vm_rssize; + info.SessionId = kinfo->p_sid; + + for (int i = 0; i < process.Length; i++) + { + var ti = new ThreadInfo() + { + _processId = pid, + _threadId = (ulong)process[i].p_tid, + _basePriority = process[i].p_nice, + _startAddress = // TODO: this doesn't exist on OpenBSD + }; + info._threadInfoList.Add(ti); + } + } + finally + { + NativeMemory.Free(kinfo); + } + + return info; + } + + /// + /// Gets the process information for a given process + /// + /// The PID (process ID) of the process + /// The TID (thread ID) of the process + /// + /// Returns basic info about thread. If tis is 0, it will return + /// info for process e.g. main thread. + /// + public static unsafe proc_stats GetThreadInfo(int pid, int tid) + { + proc_stats ret = default; + int count; + + kinfo_proc* info = GetProcInfo(pid, (tid != 0), out count); + try + { + if (info != null && count >= 1) + { + if (tid == 0) + { + ret.startTime = (int)info->p_ustart_sec; + ret.nice = info->p_nice; + ret.userTime = (ulong)info->p_uutime_sec * SecondsToNanoseconds + (ulong)info->p_uutime_usec * MicroSecondsToNanoSeconds; + ret.systemTime = (ulong)info->p_uutime_sec * SecondsToNanoseconds + (ulong)info->p_uutime_usec * MicroSecondsToNanoSeconds; + } + else + { + var list = new ReadOnlySpan(info, count); + for (int i = 0; i < list.Length; i++) + { + if (list[i].p_tid == tid) + { + ret.startTime = (int)list[i].p_ustart_sec; + ret.nice = list[i].p_nice; + ret.userTime = (ulong)list[i].p_uutime_sec * SecondsToNanoseconds + (ulong)list[i].p_uutime_usec * MicroSecondsToNanoSeconds; + ret.systemTime = (ulong)list[i].p_uutime_sec * SecondsToNanoseconds + (ulong)list[i].p_uutime_usec * MicroSecondsToNanoSeconds; + break; + } + } + } + } + } + finally + { + NativeMemory.Free(info); + } + + return ret; + } + } +} 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 bc1e3b89bb10de..56cfade1d31a1c 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -1,7 +1,7 @@  - $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)-illumos;$(NetCoreAppCurrent)-solaris;$(NetCoreAppCurrent) + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)-openbsd;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)-illumos;$(NetCoreAppCurrent)-solaris;$(NetCoreAppCurrent) $(DefineConstants);FEATURE_REGISTRY true false @@ -392,6 +392,20 @@ Link="Common\Interop\FreeBSD\Interop.Process.GetProcInfo.cs" /> + + + + + + + + + + + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OpenBSD.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OpenBSD.cs new file mode 100644 index 00000000000000..f3ccc9cb35b613 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OpenBSD.cs @@ -0,0 +1,134 @@ +// 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.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace System.Diagnostics +{ + public partial class Process + { + /// Gets the time the associated process was started. + internal DateTime StartTimeCore + { + get + { + EnsureState(State.HaveNonExitedId); + Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, 0); + + return new DateTime(DateTime.UnixEpoch.Ticks + (stat.startTime * TimeSpan.TicksPerSecond)).ToLocalTime(); + } + } + + /// + /// Gets the amount of time the associated process has spent utilizing the CPU. + /// It is the sum of the and + /// . + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public TimeSpan TotalProcessorTime + { + get + { + if (IsCurrentProcess) + { + return Environment.CpuUsage.TotalTime; + } + + EnsureState(State.HaveNonExitedId); + Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, 0); + return Process.TicksToTimeSpan(stat.userTime + stat.systemTime); + } + } + + /// + /// Gets the amount of time the associated process has spent running code + /// inside the application portion of the process (not the operating system core). + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public TimeSpan UserProcessorTime + { + get + { + if (IsCurrentProcess) + { + return Environment.CpuUsage.UserTime; + } + + EnsureState(State.HaveNonExitedId); + + Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, 0); + return Process.TicksToTimeSpan(stat.userTime); + } + } + + /// Gets the amount of time the process has spent running code inside the operating system core. + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public TimeSpan PrivilegedProcessorTime + { + get + { + if (IsCurrentProcess) + { + return Environment.CpuUsage.PrivilegedTime; + } + + EnsureState(State.HaveNonExitedId); + + Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, 0); + return Process.TicksToTimeSpan(stat.systemTime); + } + } + + /// Gets parent process ID + private unsafe int ParentProcessId + { + get + { + EnsureState(State.HaveNonExitedId); + + Interop.Process.kinfo_proc* processInfo = Interop.Process.GetProcInfo(_processId, false, out int count); + try + { + if (count <= 0) + { + throw new Win32Exception(SR.ProcessInformationUnavailable); + } + + return processInfo->p_ppid; + } + finally + { + Marshal.FreeHGlobal((IntPtr)processInfo); + } + } + } + + // Gets execution path + internal static string? GetPathToOpenFile() + { + if (Interop.Sys.Stat("/usr/local/bin/open", out _) == 0) + { + return "/usr/local/bin/open"; + } + else + { + return null; + } + } + + // ---------------------------------- + // ---- Unix PAL layer ends here ---- + // ---------------------------------- + + + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.OpenBSD.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.OpenBSD.cs new file mode 100644 index 00000000000000..bfb355e51fd9de --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.OpenBSD.cs @@ -0,0 +1,70 @@ +// 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.IO; +using System.Runtime.InteropServices; + +namespace System.Diagnostics +{ + internal static partial class ProcessManager + { + /// Gets the IDs of all processes on the current machine. + public static int[] GetProcessIds() + { + return Interop.Process.ListAllPids(); + } + + internal static string GetProcPath(int processId) + { + return Interop.Process.GetProcPath(processId); + } + + internal static string? GetProcessName(int processId, string _ /* machineName */, bool __ /* isRemoteMachine */, ref ProcessInfo? processInfo) + { + if (processInfo is not null) + { + return processInfo.ProcessName; + } + + processInfo = CreateProcessInfo(processId); + return processInfo?.ProcessName; + } + + internal static ProcessInfo? CreateProcessInfo(int pid, string? processNameFilter = null) + { + // Negative PIDs aren't valid + ArgumentOutOfRangeException.ThrowIfNegative(pid); + + // Try to get the task info. This can fail if the user permissions don't permit + // this user context to query the specified process + ProcessInfo iinfo = Interop.Process.GetProcessInfoById(pid); + if (processNameFilter != null && !processNameFilter.Equals(iinfo.ProcessName, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + ProcessInfo procInfo = new ProcessInfo() + { + ProcessId = pid, + ProcessName = iinfo.ProcessName, + BasePriority = iinfo.BasePriority, + VirtualBytes = iinfo.VirtualBytes, + WorkingSet = iinfo.WorkingSet, + SessionId = iinfo.SessionId, + }; + + foreach (ThreadInfo ti in iinfo._threadInfoList) + { + procInfo._threadInfoList.Add(ti); + } + + return procInfo; + } + + // ---------------------------------- + // ---- Unix PAL layer ends here ---- + // ---------------------------------- + + } +} From 765ea9ab452a48df8da2a5382575a2b8fe2dafa6 Mon Sep 17 00:00:00 2001 From: Seth Jackson Date: Tue, 16 Jun 2026 20:59:00 -0400 Subject: [PATCH 2/4] Add ProcessThread.OpenBSD --- .../Diagnostics/ProcessThread.OpenBSD.cs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.OpenBSD.cs diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.OpenBSD.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.OpenBSD.cs new file mode 100644 index 00000000000000..40b74787984c71 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.OpenBSD.cs @@ -0,0 +1,83 @@ +// 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.Versioning; + +namespace System.Diagnostics +{ + public partial class ProcessThread + { + /// + /// Returns or sets the priority level of the associated thread. The priority level is + /// not an absolute level, but instead contributes to the actual thread priority by + /// considering the priority class of the process. + /// + private ThreadPriorityLevel PriorityLevelCore + { + get + { + Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, Id); + return Interop.Sys.GetThreadPriorityFromNiceValue((int)stat.nice); + } + set + { + throw new PlatformNotSupportedException(); // TODO: Is this the same as FreeBSD? We can find no API to set this + } + } + + // TODO: I'm not sure if the same holds on OpenBSD or not. + // kinfo_proc has one entry per thread but ki_start seems to be same for + // all threads e.g. reflects process start. This may be re-visited later. + private static DateTime GetStartTime() => throw new PlatformNotSupportedException(); + + /// + /// Returns the amount of time the associated thread has spent utilizing the CPU. + /// It is the sum of the System.Diagnostics.ProcessThread.UserProcessorTime and + /// System.Diagnostics.ProcessThread.PrivilegedProcessorTime. + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public TimeSpan TotalProcessorTime + { + get + { + Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, Id); + return Process.TicksToTimeSpan(stat.userTime + stat.systemTime); + } + } + + /// + /// Returns the amount of time the associated thread has spent running code + /// inside the application (not the operating system core). + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public TimeSpan UserProcessorTime + { + get + { + Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, Id); + return Process.TicksToTimeSpan(stat.userTime); + } + } + + /// + /// Returns the amount of time the thread has spent running code inside the operating + /// system core. + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public TimeSpan PrivilegedProcessorTime + { + get + { + Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, Id); + return Process.TicksToTimeSpan(stat.systemTime); + } + + } + } +} From f9bfa066d4b0c85ccb28489b6b6f3e076d14c337 Mon Sep 17 00:00:00 2001 From: Seth Jackson Date: Tue, 16 Jun 2026 23:19:27 -0400 Subject: [PATCH 3/4] Update comments --- .../Common/src/Interop/OpenBSD/Interop.Process.cs | 2 +- .../src/System/Diagnostics/ProcessThread.OpenBSD.cs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.cs b/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.cs index 0e8031d8358c10..8ebd38d80f8d5a 100644 --- a/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.cs +++ b/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.cs @@ -134,7 +134,7 @@ public static unsafe ProcessInfo GetProcessInfoById(int pid) /// The PID (process ID) of the process /// The TID (thread ID) of the process /// - /// Returns basic info about thread. If tis is 0, it will return + /// Returns basic info about thread. If tid is 0, it will return /// info for process e.g. main thread. /// public static unsafe proc_stats GetThreadInfo(int pid, int tid) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.OpenBSD.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.OpenBSD.cs index 40b74787984c71..61e63fcc0050f7 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.OpenBSD.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.OpenBSD.cs @@ -21,13 +21,12 @@ private ThreadPriorityLevel PriorityLevelCore } set { - throw new PlatformNotSupportedException(); // TODO: Is this the same as FreeBSD? We can find no API to set this + throw new PlatformNotSupportedException(); // OpenBSD does not support thread priority scheduling } } - // TODO: I'm not sure if the same holds on OpenBSD or not. - // kinfo_proc has one entry per thread but ki_start seems to be same for - // all threads e.g. reflects process start. This may be re-visited later. + // kinfo_proc has one entry per thread but p_ustart_sec is the same for + // all threads e.g. reflects process start. private static DateTime GetStartTime() => throw new PlatformNotSupportedException(); /// From a644aba480e5acda00efc6e482708f00e3cbf3d8 Mon Sep 17 00:00:00 2001 From: Adeel Mujahid <3840695+am11@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:45:37 +0000 Subject: [PATCH 4/4] Implement missing parts of S.D.Process for OpenBSD --- .../OpenBSD/Interop.Process.GetProcInfo.cs | 35 ++++-- .../src/Interop/OpenBSD/Interop.Process.cs | 115 ++++++++++++++++-- .../TestUtilities/System/PlatformDetection.cs | 3 +- .../Diagnostics/ProcessManager.OpenBSD.cs | 12 +- .../tests/ProcessTests.cs | 6 +- .../tests/ProcessThreadTests.cs | 2 +- .../src/System/Environment.OpenBSD.cs | 2 +- 7 files changed, 148 insertions(+), 27 deletions(-) diff --git a/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.GetProcInfo.cs b/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.GetProcInfo.cs index 0a7dae2df64c73..e62c4e29f8d083 100644 --- a/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.GetProcInfo.cs +++ b/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.GetProcInfo.cs @@ -14,7 +14,11 @@ internal static partial class Process // Constants from sys/sysctl.h private const int CTL_KERN = 1; private const int KERN_PROC = 66; - private const int KERN_PROC_PID = 1; + private const int KERN_PROC_ALL = 0; // everything but kernel threads + private const int KERN_PROC_PID = 1; // by process id + private const int KERN_PROC_SHOW_THREADS = unchecked((int)0x40000000); // also return threads + private const int KERN_PROC_ARGS = 55; // node: proc args and env + private const int KERN_PROC_ARGV = 1; // KERN_PROC_ARGS subtype: argv // Constants from sys/sysctl.h that determine the fixed-size members of kinfo_proc private const int KI_NGROUPS = 16; @@ -102,9 +106,9 @@ public unsafe struct @kinfo_proc private LoginBuffer p_login; /* setlogin() name */ public int p_vm_rssize; /* SEGSZ_T: current resident set size in pages */ - private int p_vm_tsize; /* SEGSZ_T: text size (pages) */ - private int p_vm_dsize; /* SEGSZ_T: data size (pages) */ - private int p_vm_ssize; /* SEGSZ_T: stack size (pages) */ + public int p_vm_tsize; /* SEGSZ_T: text size (pages) */ + public int p_vm_dsize; /* SEGSZ_T: data size (pages) */ + public int p_vm_ssize; /* SEGSZ_T: stack size (pages) */ private long p_uvalid; /* CHAR: following p_u* members are valid */ public ulong p_ustart_sec; /* STRUCT TIMEVAL: starting time. */ @@ -115,7 +119,7 @@ public unsafe struct @kinfo_proc public uint p_ustime_sec; /* STRUCT TIMEVAL: system time. */ public uint p_ustime_usec; /* STRUCT TIMEVAL: system time. */ - private ulong p_uru_maxrss; /* LONG: max resident set size. */ + public ulong p_uru_maxrss; /* LONG: max resident set size (kilobytes). */ private ulong p_uru_ixrss; /* LONG: integral shared memory size. */ private ulong p_uru_idrss; /* LONG: integral unshared data ". */ private ulong p_uru_isrss; /* LONG: integral unshared stack ". */ @@ -178,22 +182,29 @@ private struct NameBuffer } /// - /// Gets information about a single process by its PID. + /// Gets information about processes. /// - /// The PID of the process. + /// The PID of the process to query, or 0 to enumerate all processes. + /// When querying a single process, also return its threads. /// The number of kinfo_proc entries returned. - public static unsafe kinfo_proc* GetProcInfo(int pid, out int count) + public static unsafe kinfo_proc* GetProcInfo(int pid, bool threads, out int count) { // OpenBSD's KERN_PROC sysctl mib carries the element size and count inline: - // { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid, sizeof(kinfo_proc), elem_count }. - // A single PID returns at most one entry, so request a count of one. - ReadOnlySpan sysctlName = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid, sizeof(kinfo_proc), 1]; + // { CTL_KERN, KERN_PROC, op, arg, sizeof(kinfo_proc), elem_count }. + int op = pid == 0 + ? KERN_PROC_ALL + : KERN_PROC_PID | (threads ? KERN_PROC_SHOW_THREADS : 0); + int arg = pid == 0 ? 0 : pid; + + // The kernel bounds the result by the supplied buffer size, so request the + // maximum element count and let Sysctl probe, allocate, and grow the buffer. + ReadOnlySpan sysctlName = [CTL_KERN, KERN_PROC, op, arg, sizeof(kinfo_proc), int.MaxValue]; byte* pBuffer = null; uint bytesLength = 0; Interop.Sys.Sysctl(sysctlName, ref pBuffer, ref bytesLength); - count = (int)(bytesLength / sizeof(kinfo_proc)); + count = (int)(bytesLength / (uint)sizeof(kinfo_proc)); // Buffer ownership transferred to the caller return (kinfo_proc*)pBuffer; diff --git a/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.cs b/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.cs index 8ebd38d80f8d5a..9d508875c027fd 100644 --- a/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.cs +++ b/src/libraries/Common/src/Interop/OpenBSD/Interop.Process.cs @@ -74,7 +74,77 @@ internal static unsafe int[] ListAllPids() /// The PID of the process public static unsafe string GetProcPath(int pid) { - // TODO + // OpenBSD has no KERN_PROC_PATHNAME. The closest available information is the + // process argv, whose first entry is the path the process was executed with. + ReadOnlySpan sysctlName = [CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_ARGV]; + + byte* pBuffer = null; + uint bytesLength = 0; + try + { + Interop.Sys.Sysctl(sysctlName, ref pBuffer, ref bytesLength); + + if (pBuffer == null || bytesLength < (uint)sizeof(byte*)) + { + return string.Empty; + } + + // The kernel relocates the argv pointer array to point within the returned buffer. + byte* argv0 = ((byte**)pBuffer)[0]; + return argv0 is null ? string.Empty : Utf8StringMarshaller.ConvertToManaged(argv0) ?? string.Empty; + } + finally + { + NativeMemory.Free(pBuffer); + } + } + + /// + /// Attempts to recover a process name that was truncated in kinfo_proc.p_comm by reading + /// the full name from the process argv. + /// + /// The PID of the process. + /// The (possibly truncated) p_comm value used to validate the recovered name. + /// The full process name, or null if it could not be recovered. + private static unsafe string? GetUntruncatedProcessName(int pid, string prefix) + { + ReadOnlySpan sysctlName = [CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_ARGV]; + + byte* pBuffer = null; + uint bytesLength = 0; + try + { + Interop.Sys.Sysctl(sysctlName, ref pBuffer, ref bytesLength); + + if (pBuffer == null || bytesLength < (uint)sizeof(byte*)) + { + return null; + } + + // The kernel relocates the argv pointer array to point within the returned buffer. + // For native executables the name is argv[0]; for scripts argv[0] is the interpreter + // and argv[1] is the script, so check the first two NULL-terminated arguments. + byte** argv = (byte**)pBuffer; + for (int i = 0; i < 2 && argv[i] is not null; i++) + { + string arg = Utf8StringMarshaller.ConvertToManaged(argv[i]) ?? string.Empty; + + // Strip directory names. + int nameStart = arg.LastIndexOf('/') + 1; + string name = nameStart == 0 ? arg : arg.Substring(nameStart); + + if (name.StartsWith(prefix, StringComparison.Ordinal)) + { + return name; + } + } + + return null; + } + finally + { + NativeMemory.Free(pBuffer); + } } /// @@ -85,7 +155,7 @@ public static unsafe string GetProcPath(int pid) /// Returns a valid ProcessInfo struct for valid processes that the caller /// has permission to access; otherwise, returns null /// - public static unsafe ProcessInfo GetProcessInfoById(int pid) + public static unsafe ProcessInfo? GetProcessInfoById(int pid) { // Negative PIDs are invalid ArgumentOutOfRangeException.ThrowIfNegative(pid); @@ -95,7 +165,12 @@ public static unsafe ProcessInfo GetProcessInfoById(int pid) kinfo_proc* kinfo = GetProcInfo(pid, true, out int count); try { - ArgumentOutOfRangeException.ThrowIfLessThan(count, 1, nameof(pid)); + // The process may have exited between the time its PID was enumerated and now, + // in which case no entries are returned. Report it as not found rather than failing. + if (count < 1) + { + return null; + } var process = new ReadOnlySpan(kinfo, count); @@ -103,19 +178,45 @@ public static unsafe ProcessInfo GetProcessInfoById(int pid) info = new ProcessInfo(); info.ProcessName = Utf8StringMarshaller.ConvertToManaged(kinfo->p_comm)!; + + // p_comm is limited to KI_MAXCOMLEN - 1 characters. When the name is at that + // limit it may be truncated, so try to recover the full name from the process argv. + if (info.ProcessName.Length >= KI_MAXCOMLEN - 1) + { + info.ProcessName = GetUntruncatedProcessName(pid, info.ProcessName) ?? info.ProcessName; + } + info.BasePriority = kinfo->p_nice; - info.VirtualBytes = (long)kinfo->p_vm_map_size; + + // OpenBSD's KERN_PROC sysctl always reports p_vm_map_size as 0, so derive the + // virtual size from the text, data, and stack segment sizes instead. + long pageSize = Environment.SystemPageSize; + info.VirtualBytes = ((long)kinfo->p_vm_tsize + kinfo->p_vm_dsize + kinfo->p_vm_ssize) * pageSize; + // OpenBSD does not track a separate peak virtual size; report the current size. + info.VirtualBytesPeak = info.VirtualBytes; info.WorkingSet = kinfo->p_vm_rssize; + // p_uru_maxrss is the peak resident set size, reported in kilobytes. + info.WorkingSetPeak = (long)kinfo->p_uru_maxrss * 1024; + // OpenBSD does not expose a private byte count; approximate it with the + // anonymous (data + stack) segment sizes. + info.PrivateBytes = ((long)kinfo->p_vm_dsize + kinfo->p_vm_ssize) * pageSize; info.SessionId = kinfo->p_sid; for (int i = 0; i < process.Length; i++) { + // KERN_PROC_SHOW_THREADS returns a process-summary entry with p_tid == -1 + // ahead of the real per-thread entries. Skip it so only actual threads are reported. + if (process[i].p_tid < 0) + { + continue; + } + var ti = new ThreadInfo() { _processId = pid, _threadId = (ulong)process[i].p_tid, _basePriority = process[i].p_nice, - _startAddress = // TODO: this doesn't exist on OpenBSD + _startAddress = null // OpenBSD's kinfo_proc does not expose a thread start address. }; info._threadInfoList.Add(ti); } @@ -152,7 +253,7 @@ public static unsafe proc_stats GetThreadInfo(int pid, int tid) ret.startTime = (int)info->p_ustart_sec; ret.nice = info->p_nice; ret.userTime = (ulong)info->p_uutime_sec * SecondsToNanoseconds + (ulong)info->p_uutime_usec * MicroSecondsToNanoSeconds; - ret.systemTime = (ulong)info->p_uutime_sec * SecondsToNanoseconds + (ulong)info->p_uutime_usec * MicroSecondsToNanoSeconds; + ret.systemTime = (ulong)info->p_ustime_sec * SecondsToNanoseconds + (ulong)info->p_ustime_usec * MicroSecondsToNanoSeconds; } else { @@ -164,7 +265,7 @@ public static unsafe proc_stats GetThreadInfo(int pid, int tid) ret.startTime = (int)list[i].p_ustart_sec; ret.nice = list[i].p_nice; ret.userTime = (ulong)list[i].p_uutime_sec * SecondsToNanoseconds + (ulong)list[i].p_uutime_usec * MicroSecondsToNanoSeconds; - ret.systemTime = (ulong)list[i].p_uutime_sec * SecondsToNanoseconds + (ulong)list[i].p_uutime_usec * MicroSecondsToNanoSeconds; + ret.systemTime = (ulong)list[i].p_ustime_sec * SecondsToNanoseconds + (ulong)list[i].p_ustime_usec * MicroSecondsToNanoSeconds; break; } } diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index a1648c1da6d8b2..7e352915ac39e3 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -43,6 +43,7 @@ public static partial class PlatformDetection public static bool IsNotCoreClrInterpreter => !IsCoreClrInterpreter; public static bool IsFreeBSD => RuntimeInformation.IsOSPlatform(OSPlatform.Create("FREEBSD")); public static bool IsNetBSD => RuntimeInformation.IsOSPlatform(OSPlatform.Create("NETBSD")); + public static bool IsOpenBSD => RuntimeInformation.IsOSPlatform(OSPlatform.Create("OPENBSD")); public static bool IsAndroid => RuntimeInformation.IsOSPlatform(OSPlatform.Create("ANDROID")); public static bool IsNotAndroid => !IsAndroid; public static bool IsAndroidX86 => IsAndroid && IsX86Process; @@ -64,7 +65,7 @@ public static partial class PlatformDetection public static bool IsAppleMobile => IsMacCatalyst || IsiOS || IstvOS; public static bool IsNotAppleMobile => !IsAppleMobile; public static bool IsNotNetFramework => !IsNetFramework; - public static bool IsBsdLike => IsApplePlatform || IsFreeBSD || IsNetBSD; + public static bool IsBsdLike => IsApplePlatform || IsFreeBSD || IsNetBSD || IsOpenBSD; public static bool IsArmProcess => RuntimeInformation.ProcessArchitecture == Architecture.Arm; public static bool IsNotArmProcess => !IsArmProcess; diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.OpenBSD.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.OpenBSD.cs index bfb355e51fd9de..19e3933ff2e9f3 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.OpenBSD.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.OpenBSD.cs @@ -37,8 +37,13 @@ internal static string GetProcPath(int processId) ArgumentOutOfRangeException.ThrowIfNegative(pid); // Try to get the task info. This can fail if the user permissions don't permit - // this user context to query the specified process - ProcessInfo iinfo = Interop.Process.GetProcessInfoById(pid); + // this user context to query the specified process, or if the process has exited. + ProcessInfo? iinfo = Interop.Process.GetProcessInfoById(pid); + if (iinfo is null) + { + return null; + } + if (processNameFilter != null && !processNameFilter.Equals(iinfo.ProcessName, StringComparison.OrdinalIgnoreCase)) { return null; @@ -50,7 +55,10 @@ internal static string GetProcPath(int processId) ProcessName = iinfo.ProcessName, BasePriority = iinfo.BasePriority, VirtualBytes = iinfo.VirtualBytes, + VirtualBytesPeak = iinfo.VirtualBytesPeak, WorkingSet = iinfo.WorkingSet, + WorkingSetPeak = iinfo.WorkingSetPeak, + PrivateBytes = iinfo.PrivateBytes, SessionId = iinfo.SessionId, }; diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs index 89d6c78ec7d025..a8f25b6f282d98 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs @@ -887,7 +887,7 @@ public void TestMaxWorkingSet() Assert.InRange((long)p.MinWorkingSet, 0, long.MaxValue); } - if (OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD() || PlatformDetection.IsSunOS) { + if (OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD() || PlatformDetection.IsOpenBSD || PlatformDetection.IsSunOS) { return; // doesn't support getting/setting working set for other processes } @@ -935,7 +935,7 @@ public void TestMinWorkingSet() Assert.InRange((long)p.MinWorkingSet, 0, long.MaxValue); } - if (OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD() || PlatformDetection.IsSunOS) { + if (OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD() || PlatformDetection.IsOpenBSD || PlatformDetection.IsSunOS) { return; // doesn't support getting/setting working set for other processes } @@ -1299,7 +1299,7 @@ public void ExitTime_GetNotStarted_ThrowsInvalidOperationException() } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [SkipOnPlatform(TestPlatforms.OSX | TestPlatforms.FreeBSD, "getting/setting affinity not supported on OSX and BSD")] + [SkipOnPlatform(TestPlatforms.OSX | TestPlatforms.FreeBSD | TestPlatforms.OpenBSD, "getting/setting affinity not supported on OSX and BSD")] public void TestProcessorAffinity() { CreateDefaultProcess(); diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessThreadTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessThreadTests.cs index 0f9965fb8b9b24..57b24854832bd1 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessThreadTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessThreadTests.cs @@ -92,7 +92,7 @@ public void ThreadsAreDisposedWhenProcessIsDisposed() } [Fact] - [PlatformSpecific(TestPlatforms.OSX|TestPlatforms.FreeBSD)] // OSX and FreeBSD throw PNSE from StartTime + [PlatformSpecific(TestPlatforms.OSX | TestPlatforms.FreeBSD | TestPlatforms.OpenBSD)] // these platforms throw PNSE from StartTime public void TestStartTimeProperty_OSX() { using (Process p = Process.GetCurrentProcess()) diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.OpenBSD.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.OpenBSD.cs index 73c747bdfde9f6..96a0a494202bfc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.OpenBSD.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.OpenBSD.cs @@ -11,7 +11,7 @@ public static unsafe long WorkingSet { get { - Interop.Process.kinfo_proc* processInfo = Interop.Process.GetProcInfo(ProcessId, out int count); + Interop.Process.kinfo_proc* processInfo = Interop.Process.GetProcInfo(ProcessId, false, out int count); try { // p_vm_rssize is the current resident set size in pages.