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.