Skip to content

Commit bf3fa3f

Browse files
sethjacksonam11
andauthored
Port System.Diagnostics.Process to OpenBSD (#129470)
Port `System.Diagnostics.Process` to OpenBSD. Contributes to #124911. --------- Co-authored-by: Adeel Mujahid <3840695+am11@users.noreply.github.com>
1 parent 2608ba2 commit bf3fa3f

9 files changed

Lines changed: 620 additions & 18 deletions

File tree

src/libraries/Common/src/Interop/OpenBSD/Interop.Process.GetProcInfo.cs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ internal static partial class Process
1414
// Constants from sys/sysctl.h
1515
private const int CTL_KERN = 1;
1616
private const int KERN_PROC = 66;
17-
private const int KERN_PROC_PID = 1;
17+
private const int KERN_PROC_ALL = 0; // everything but kernel threads
18+
private const int KERN_PROC_PID = 1; // by process id
19+
private const int KERN_PROC_SHOW_THREADS = unchecked((int)0x40000000); // also return threads
20+
private const int KERN_PROC_ARGS = 55; // node: proc args and env
21+
private const int KERN_PROC_ARGV = 1; // KERN_PROC_ARGS subtype: argv
1822

1923
// Constants from sys/sysctl.h that determine the fixed-size members of kinfo_proc
2024
private const int KI_NGROUPS = 16;
@@ -102,9 +106,9 @@ public unsafe struct @kinfo_proc
102106
private LoginBuffer p_login; /* setlogin() name */
103107

104108
public int p_vm_rssize; /* SEGSZ_T: current resident set size in pages */
105-
private int p_vm_tsize; /* SEGSZ_T: text size (pages) */
106-
private int p_vm_dsize; /* SEGSZ_T: data size (pages) */
107-
private int p_vm_ssize; /* SEGSZ_T: stack size (pages) */
109+
public int p_vm_tsize; /* SEGSZ_T: text size (pages) */
110+
public int p_vm_dsize; /* SEGSZ_T: data size (pages) */
111+
public int p_vm_ssize; /* SEGSZ_T: stack size (pages) */
108112

109113
private long p_uvalid; /* CHAR: following p_u* members are valid */
110114
public ulong p_ustart_sec; /* STRUCT TIMEVAL: starting time. */
@@ -115,7 +119,7 @@ public unsafe struct @kinfo_proc
115119
public uint p_ustime_sec; /* STRUCT TIMEVAL: system time. */
116120
public uint p_ustime_usec; /* STRUCT TIMEVAL: system time. */
117121

118-
private ulong p_uru_maxrss; /* LONG: max resident set size. */
122+
public ulong p_uru_maxrss; /* LONG: max resident set size (kilobytes). */
119123
private ulong p_uru_ixrss; /* LONG: integral shared memory size. */
120124
private ulong p_uru_idrss; /* LONG: integral unshared data ". */
121125
private ulong p_uru_isrss; /* LONG: integral unshared stack ". */
@@ -178,22 +182,29 @@ private struct NameBuffer
178182
}
179183

180184
/// <summary>
181-
/// Gets information about a single process by its PID.
185+
/// Gets information about processes.
182186
/// </summary>
183-
/// <param name="pid">The PID of the process.</param>
187+
/// <param name="pid">The PID of the process to query, or 0 to enumerate all processes.</param>
188+
/// <param name="threads">When querying a single process, also return its threads.</param>
184189
/// <param name="count">The number of kinfo_proc entries returned.</param>
185-
public static unsafe kinfo_proc* GetProcInfo(int pid, out int count)
190+
public static unsafe kinfo_proc* GetProcInfo(int pid, bool threads, out int count)
186191
{
187192
// OpenBSD's KERN_PROC sysctl mib carries the element size and count inline:
188-
// { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid, sizeof(kinfo_proc), elem_count }.
189-
// A single PID returns at most one entry, so request a count of one.
190-
ReadOnlySpan<int> sysctlName = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid, sizeof(kinfo_proc), 1];
193+
// { CTL_KERN, KERN_PROC, op, arg, sizeof(kinfo_proc), elem_count }.
194+
int op = pid == 0
195+
? KERN_PROC_ALL
196+
: KERN_PROC_PID | (threads ? KERN_PROC_SHOW_THREADS : 0);
197+
int arg = pid == 0 ? 0 : pid;
198+
199+
// The kernel bounds the result by the supplied buffer size, so request the
200+
// maximum element count and let Sysctl probe, allocate, and grow the buffer.
201+
ReadOnlySpan<int> sysctlName = [CTL_KERN, KERN_PROC, op, arg, sizeof(kinfo_proc), int.MaxValue];
191202

192203
byte* pBuffer = null;
193204
uint bytesLength = 0;
194205
Interop.Sys.Sysctl(sysctlName, ref pBuffer, ref bytesLength);
195206

196-
count = (int)(bytesLength / sizeof(kinfo_proc));
207+
count = (int)(bytesLength / (uint)sizeof(kinfo_proc));
197208

198209
// Buffer ownership transferred to the caller
199210
return (kinfo_proc*)pBuffer;
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.ComponentModel;
7+
using System.Diagnostics;
8+
using System.Runtime.InteropServices;
9+
using System.Runtime.InteropServices.Marshalling;
10+
11+
#pragma warning disable CA1823 // analyzer incorrectly flags fixed buffer length const (https://github.com/dotnet/roslyn/issues/37593)
12+
13+
internal static partial class Interop
14+
{
15+
internal static partial class Process
16+
{
17+
private const ulong SecondsToNanoseconds = 1000000000;
18+
private const ulong MicroSecondsToNanoSeconds = 1000;
19+
20+
internal struct proc_stats
21+
{
22+
internal long startTime;
23+
internal int nice;
24+
internal ulong userTime; /* in ticks */
25+
internal ulong systemTime; /* in ticks */
26+
}
27+
28+
/// <summary>
29+
/// Queries the OS for the list of all running processes and returns the PID for each
30+
/// </summary>
31+
/// <returns>Returns a list of PIDs corresponding to all running processes</returns>
32+
internal static unsafe int[] ListAllPids()
33+
{
34+
kinfo_proc* entries = GetProcInfo(0, false, out int numProcesses);
35+
try
36+
{
37+
if (numProcesses <= 0)
38+
{
39+
throw new Win32Exception(SR.CantGetAllPids);
40+
}
41+
42+
var list = new ReadOnlySpan<kinfo_proc>(entries, numProcesses);
43+
var pids = new int[numProcesses];
44+
45+
// walk through process list and skip kernel threads
46+
int idx = 0;
47+
for (int i = 0; i < list.Length; i++)
48+
{
49+
if (list[i].p_ppid == 0)
50+
{
51+
// skip kernel threads
52+
numProcesses -= 1;
53+
}
54+
else
55+
{
56+
pids[idx] = list[i].p_pid;
57+
idx += 1;
58+
}
59+
}
60+
61+
// Remove extra elements
62+
Array.Resize<int>(ref pids, numProcesses);
63+
return pids;
64+
}
65+
finally
66+
{
67+
NativeMemory.Free(entries);
68+
}
69+
}
70+
71+
/// <summary>
72+
/// Gets executable name for process given it's PID
73+
/// </summary>
74+
/// <param name="pid">The PID of the process</param>
75+
public static unsafe string GetProcPath(int pid)
76+
{
77+
// OpenBSD has no KERN_PROC_PATHNAME. The closest available information is the
78+
// process argv, whose first entry is the path the process was executed with.
79+
ReadOnlySpan<int> sysctlName = [CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_ARGV];
80+
81+
byte* pBuffer = null;
82+
uint bytesLength = 0;
83+
try
84+
{
85+
Interop.Sys.Sysctl(sysctlName, ref pBuffer, ref bytesLength);
86+
87+
if (pBuffer == null || bytesLength < (uint)sizeof(byte*))
88+
{
89+
return string.Empty;
90+
}
91+
92+
// The kernel relocates the argv pointer array to point within the returned buffer.
93+
byte* argv0 = ((byte**)pBuffer)[0];
94+
return argv0 is null ? string.Empty : Utf8StringMarshaller.ConvertToManaged(argv0) ?? string.Empty;
95+
}
96+
finally
97+
{
98+
NativeMemory.Free(pBuffer);
99+
}
100+
}
101+
102+
/// <summary>
103+
/// Attempts to recover a process name that was truncated in kinfo_proc.p_comm by reading
104+
/// the full name from the process argv.
105+
/// </summary>
106+
/// <param name="pid">The PID of the process.</param>
107+
/// <param name="prefix">The (possibly truncated) p_comm value used to validate the recovered name.</param>
108+
/// <returns>The full process name, or null if it could not be recovered.</returns>
109+
private static unsafe string? GetUntruncatedProcessName(int pid, string prefix)
110+
{
111+
ReadOnlySpan<int> sysctlName = [CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_ARGV];
112+
113+
byte* pBuffer = null;
114+
uint bytesLength = 0;
115+
try
116+
{
117+
Interop.Sys.Sysctl(sysctlName, ref pBuffer, ref bytesLength);
118+
119+
if (pBuffer == null || bytesLength < (uint)sizeof(byte*))
120+
{
121+
return null;
122+
}
123+
124+
// The kernel relocates the argv pointer array to point within the returned buffer.
125+
// For native executables the name is argv[0]; for scripts argv[0] is the interpreter
126+
// and argv[1] is the script, so check the first two NULL-terminated arguments.
127+
byte** argv = (byte**)pBuffer;
128+
for (int i = 0; i < 2 && argv[i] is not null; i++)
129+
{
130+
string arg = Utf8StringMarshaller.ConvertToManaged(argv[i]) ?? string.Empty;
131+
132+
// Strip directory names.
133+
int nameStart = arg.LastIndexOf('/') + 1;
134+
string name = nameStart == 0 ? arg : arg.Substring(nameStart);
135+
136+
if (name.StartsWith(prefix, StringComparison.Ordinal))
137+
{
138+
return name;
139+
}
140+
}
141+
142+
return null;
143+
}
144+
finally
145+
{
146+
NativeMemory.Free(pBuffer);
147+
}
148+
}
149+
150+
/// <summary>
151+
/// Gets the process information for a given process
152+
/// </summary>
153+
/// <param name="pid">The PID (process ID) of the process</param>
154+
/// <returns>
155+
/// Returns a valid ProcessInfo struct for valid processes that the caller
156+
/// has permission to access; otherwise, returns null
157+
/// </returns>
158+
public static unsafe ProcessInfo? GetProcessInfoById(int pid)
159+
{
160+
// Negative PIDs are invalid
161+
ArgumentOutOfRangeException.ThrowIfNegative(pid);
162+
163+
ProcessInfo info;
164+
165+
kinfo_proc* kinfo = GetProcInfo(pid, true, out int count);
166+
try
167+
{
168+
// The process may have exited between the time its PID was enumerated and now,
169+
// in which case no entries are returned. Report it as not found rather than failing.
170+
if (count < 1)
171+
{
172+
return null;
173+
}
174+
175+
var process = new ReadOnlySpan<kinfo_proc>(kinfo, count);
176+
177+
// Get the process information for the specified pid
178+
info = new ProcessInfo();
179+
180+
info.ProcessName = Utf8StringMarshaller.ConvertToManaged(kinfo->p_comm)!;
181+
182+
// p_comm is limited to KI_MAXCOMLEN - 1 characters. When the name is at that
183+
// limit it may be truncated, so try to recover the full name from the process argv.
184+
if (info.ProcessName.Length >= KI_MAXCOMLEN - 1)
185+
{
186+
info.ProcessName = GetUntruncatedProcessName(pid, info.ProcessName) ?? info.ProcessName;
187+
}
188+
189+
info.BasePriority = kinfo->p_nice;
190+
191+
// OpenBSD's KERN_PROC sysctl always reports p_vm_map_size as 0, so derive the
192+
// virtual size from the text, data, and stack segment sizes instead.
193+
long pageSize = Environment.SystemPageSize;
194+
info.VirtualBytes = ((long)kinfo->p_vm_tsize + kinfo->p_vm_dsize + kinfo->p_vm_ssize) * pageSize;
195+
// OpenBSD does not track a separate peak virtual size; report the current size.
196+
info.VirtualBytesPeak = info.VirtualBytes;
197+
info.WorkingSet = kinfo->p_vm_rssize;
198+
// p_uru_maxrss is the peak resident set size, reported in kilobytes.
199+
info.WorkingSetPeak = (long)kinfo->p_uru_maxrss * 1024;
200+
// OpenBSD does not expose a private byte count; approximate it with the
201+
// anonymous (data + stack) segment sizes.
202+
info.PrivateBytes = ((long)kinfo->p_vm_dsize + kinfo->p_vm_ssize) * pageSize;
203+
info.SessionId = kinfo->p_sid;
204+
205+
for (int i = 0; i < process.Length; i++)
206+
{
207+
// KERN_PROC_SHOW_THREADS returns a process-summary entry with p_tid == -1
208+
// ahead of the real per-thread entries. Skip it so only actual threads are reported.
209+
if (process[i].p_tid < 0)
210+
{
211+
continue;
212+
}
213+
214+
var ti = new ThreadInfo()
215+
{
216+
_processId = pid,
217+
_threadId = (ulong)process[i].p_tid,
218+
_basePriority = process[i].p_nice,
219+
_startAddress = null // OpenBSD's kinfo_proc does not expose a thread start address.
220+
};
221+
info._threadInfoList.Add(ti);
222+
}
223+
}
224+
finally
225+
{
226+
NativeMemory.Free(kinfo);
227+
}
228+
229+
return info;
230+
}
231+
232+
/// <summary>
233+
/// Gets the process information for a given process
234+
/// </summary>
235+
/// <param name="pid">The PID (process ID) of the process</param>
236+
/// <param name="tid">The TID (thread ID) of the process</param>
237+
/// <returns>
238+
/// Returns basic info about thread. If tid is 0, it will return
239+
/// info for process e.g. main thread.
240+
/// </returns>
241+
public static unsafe proc_stats GetThreadInfo(int pid, int tid)
242+
{
243+
proc_stats ret = default;
244+
int count;
245+
246+
kinfo_proc* info = GetProcInfo(pid, (tid != 0), out count);
247+
try
248+
{
249+
if (info != null && count >= 1)
250+
{
251+
if (tid == 0)
252+
{
253+
ret.startTime = (int)info->p_ustart_sec;
254+
ret.nice = info->p_nice;
255+
ret.userTime = (ulong)info->p_uutime_sec * SecondsToNanoseconds + (ulong)info->p_uutime_usec * MicroSecondsToNanoSeconds;
256+
ret.systemTime = (ulong)info->p_ustime_sec * SecondsToNanoseconds + (ulong)info->p_ustime_usec * MicroSecondsToNanoSeconds;
257+
}
258+
else
259+
{
260+
var list = new ReadOnlySpan<kinfo_proc>(info, count);
261+
for (int i = 0; i < list.Length; i++)
262+
{
263+
if (list[i].p_tid == tid)
264+
{
265+
ret.startTime = (int)list[i].p_ustart_sec;
266+
ret.nice = list[i].p_nice;
267+
ret.userTime = (ulong)list[i].p_uutime_sec * SecondsToNanoseconds + (ulong)list[i].p_uutime_usec * MicroSecondsToNanoSeconds;
268+
ret.systemTime = (ulong)list[i].p_ustime_sec * SecondsToNanoseconds + (ulong)list[i].p_ustime_usec * MicroSecondsToNanoSeconds;
269+
break;
270+
}
271+
}
272+
}
273+
}
274+
}
275+
finally
276+
{
277+
NativeMemory.Free(info);
278+
}
279+
280+
return ret;
281+
}
282+
}
283+
}

src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)-illumos;$(NetCoreAppCurrent)-solaris;$(NetCoreAppCurrent)</TargetFrameworks>
4+
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)-openbsd;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)-illumos;$(NetCoreAppCurrent)-solaris;$(NetCoreAppCurrent)</TargetFrameworks>
55
<DefineConstants>$(DefineConstants);FEATURE_REGISTRY</DefineConstants>
66
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
77
<UseCompilerGeneratedDocXmlFile>false</UseCompilerGeneratedDocXmlFile>
@@ -392,6 +392,20 @@
392392
Link="Common\Interop\FreeBSD\Interop.Process.GetProcInfo.cs" />
393393
</ItemGroup>
394394

395+
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'openbsd'">
396+
<Compile Include="System\Diagnostics\Process.BSD.cs" />
397+
<Compile Include="System\Diagnostics\Process.OpenBSD.cs" />
398+
<Compile Include="System\Diagnostics\ProcessManager.BSD.cs" />
399+
<Compile Include="System\Diagnostics\ProcessManager.OpenBSD.cs" />
400+
<Compile Include="System\Diagnostics\ProcessThread.OpenBSD.cs" />
401+
<Compile Include="$(CommonPath)Interop\BSD\System.Native\Interop.Sysctl.cs"
402+
Link="Common\Interop\BSD\System.Native\Interop.Sysctl.cs" />
403+
<Compile Include="$(CommonPath)Interop\OpenBSD\Interop.Process.cs"
404+
Link="Common\Interop\OpenBSD\Interop.Process.cs" />
405+
<Compile Include="$(CommonPath)Interop\OpenBSD\Interop.Process.GetProcInfo.cs"
406+
Link="Common\Interop\OpenBSD\Interop.Process.GetProcInfo.cs" />
407+
</ItemGroup>
408+
395409
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'illumos' or '$(TargetPlatformIdentifier)' == 'solaris'">
396410
<Compile Include="System\Diagnostics\Process.BSD.cs" />
397411
<Compile Include="System\Diagnostics\Process.SunOS.cs" />

0 commit comments

Comments
 (0)