From c48ae3d4e3e350df59d9d41777ce2aaa5474663d Mon Sep 17 00:00:00 2001 From: Austin Wise Date: Sun, 7 Jul 2024 09:04:31 -0700 Subject: [PATCH] WIP: illumos System.Diagnostic.Process support --- ...rop.ProcFsStat.TryReadProcessStatusInfo.cs | 26 +++- .../SunOS/procfs/Interop.ProcFsStat.cs | 15 ++ .../Unix/System.Native/Interop.TimeSpec.cs | 17 +++ .../Unix/System.Native/Interop.UTimensat.cs | 6 - .../src/System.Diagnostics.Process.csproj | 6 + .../src/System/Diagnostics/Process.illumos.cs | 135 +++++++++++++++++- .../Diagnostics/ProcessManager.illumos.cs | 86 +++++++++-- .../System.Private.CoreLib.Shared.projitems | 3 + .../src/System/Environment.SunOS.cs | 2 +- src/native/libs/System.Native/pal_io.c | 10 +- src/native/libs/System.Native/pal_io.h | 10 +- 11 files changed, 292 insertions(+), 24 deletions(-) create mode 100644 src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.cs create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.TimeSpec.cs diff --git a/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.TryReadProcessStatusInfo.cs b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.TryReadProcessStatusInfo.cs index 5d835f241cda1b..797d26dbadfe68 100644 --- a/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.TryReadProcessStatusInfo.cs +++ b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.TryReadProcessStatusInfo.cs @@ -1,6 +1,8 @@ // 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.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; internal static partial class Interop @@ -12,25 +14,43 @@ internal static partial class @procfs /// /// PID of the process to read status info for. /// The pointer to processStatus instance. + /// Buffer in which to place the process name. + /// Size of in bytes. /// /// true if the process status was read; otherwise, false. /// [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_ReadProcessStatusInfo", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - private static unsafe partial bool TryReadProcessStatusInfo(int pid, ProcessStatusInfo* processStatus); + private static unsafe partial bool TryReadProcessStatusInfo(int pid, ProcessStatusInfo* processStatus, byte* nameBuf, int nameBufSize); internal struct ProcessStatusInfo { + internal int Pid; + internal int ParentPid; + internal int SessionId; internal nuint ResidentSetSize; + internal Interop.Sys.TimeSpec StartTime; // add more fields when needed. } - internal static unsafe bool TryReadProcessStatusInfo(int pid, out ProcessStatusInfo statusInfo) + internal static unsafe bool TryReadProcessStatusInfo(int pid, out ProcessStatusInfo statusInfo, [NotNullWhen(true)] out string? processName) { statusInfo = default; + processName = null; + Span buf = stackalloc byte[16]; fixed (ProcessStatusInfo* pStatusInfo = &statusInfo) + fixed (byte* pBuf = buf) { - return TryReadProcessStatusInfo(pid, pStatusInfo); + if (TryReadProcessStatusInfo(pid, pStatusInfo, pBuf, buf.Length)) + { + int terminator = buf.IndexOf((byte)0); + processName = Marshal.PtrToStringUTF8((IntPtr)pBuf, (terminator >= 0) ? terminator : buf.Length); + return true; + } + else + { + return false; + } } } } diff --git a/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.cs b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.cs new file mode 100644 index 00000000000000..8f806baafc3a65 --- /dev/null +++ b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.cs @@ -0,0 +1,15 @@ +// 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.InteropServices; + +internal static partial class Interop +{ + internal static partial class @procfs + { + internal const string RootPath = "/proc/"; + // As of July 2024, this file only exists on systems that have LX support. + private const string CmdLineFileName = "/cmdline"; + internal static string GetCmdLinePathForProcess(int pid) => string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{CmdLineFileName}"); + } +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.TimeSpec.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.TimeSpec.cs new file mode 100644 index 00000000000000..54a7299dcd4915 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.TimeSpec.cs @@ -0,0 +1,17 @@ +// 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.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + internal struct TimeSpec + { + internal long TvSec; + internal long TvNsec; + } + } +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.UTimensat.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.UTimensat.cs index 261a5ae8562df3..13c5d2fe10bce0 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.UTimensat.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.UTimensat.cs @@ -8,12 +8,6 @@ internal static partial class Interop { internal static partial class Sys { - internal struct TimeSpec - { - internal long TvSec; - internal long TvNsec; - } - /// /// Sets the last access and last modified time of a file /// 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 85306b9aabf762..9f5e3584e7c931 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -367,6 +367,12 @@ + + + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.illumos.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.illumos.cs index 9f1744d27ff9b8..4e7999b50e2f0a 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.illumos.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.illumos.cs @@ -27,7 +27,25 @@ public partial class Process : IDisposable [SupportedOSPlatform("maccatalyst")] public static Process[] GetProcessesByName(string? processName, string machineName) { - throw new PlatformNotSupportedException(); + ProcessManager.ThrowIfRemoteMachine(machineName); + + processName ??= ""; + + ArrayBuilder processes = default; + foreach (int pid in ProcessManager.EnumerateProcessIds()) + { + if (Interop.procfs.TryReadProcessStatusInfo(pid, out Interop.procfs.ProcessStatusInfo status, out string? shortProcessName)) + { + string actualProcessName = GetUntruncatedProcessName(pid, shortProcessName); + if ((processName == "" || string.Equals(processName, actualProcessName, StringComparison.OrdinalIgnoreCase))) + { + ProcessInfo processInfo = ProcessManager.CreateProcessInfo(ref status, shortProcessName, actualProcessName); + processes.Add(new Process(machineName, isRemoteMachine: false, pid, processInfo)); + } + } + } + + return processes.ToArray(); } /// Gets the amount of time the process has spent running code inside the operating system core. @@ -47,17 +65,18 @@ internal DateTime StartTimeCore { get { - throw new PlatformNotSupportedException(); + Interop.procfs.ProcessStatusInfo status = GetStatus(); + return DateTime.UnixEpoch.AddSeconds(status.StartTime.TvSec).AddTicks(status.StartTime.TvNsec / 100); } } /// Gets the parent process ID - private int ParentProcessId => throw new PlatformNotSupportedException(); + private int ParentProcessId => GetStatus().ParentPid; /// Gets execution path private static string? GetPathToOpenFile() { - throw new PlatformNotSupportedException(); + return FindProgramInPath("xdg-open"); } /// @@ -133,5 +152,113 @@ private static void SetWorkingSetLimitsCore(IntPtr? newMin, IntPtr? newMax, out // ---- Unix PAL layer ends here ---- // ---------------------------------- + /// Gets the name that was used to start the process, or null if it could not be retrieved. + /// The pid of the target process. + /// The start of the process name of the ProcessStatusInfo struct. + internal static string GetUntruncatedProcessName(int pid, string processNameStart) + { + string cmdLineFilePath = Interop.procfs.GetCmdLinePathForProcess(pid); + + byte[]? rentedArray = null; + try + { + // bufferSize == 1 used to avoid unnecessary buffer in FileStream + using (var fs = new FileStream(cmdLineFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1, useAsync: false)) + { + Span buffer = stackalloc byte[512]; + int bytesRead = 0; + while (true) + { + // Resize buffer if it was too small. + if (bytesRead == buffer.Length) + { + uint newLength = (uint)buffer.Length * 2; + + byte[] tmp = ArrayPool.Shared.Rent((int)newLength); + buffer.CopyTo(tmp); + byte[]? toReturn = rentedArray; + buffer = rentedArray = tmp; + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + Debug.Assert(bytesRead < buffer.Length); + int n = fs.Read(buffer.Slice(bytesRead)); + bytesRead += n; + + // cmdline contains the argv array separated by '\0' bytes. + // processNameStart contains a possibly truncated version of the process name. + // When the program is a native executable, the process name will be in argv[0]. + // When the program is a script, argv[0] contains the interpreter, and argv[1] contains the script name. + Span argRemainder = buffer.Slice(0, bytesRead); + int argEnd = argRemainder.IndexOf((byte)'\0'); + if (argEnd != -1) + { + // Check if argv[0] has the process name. + string? name = GetUntruncatedNameFromArg(argRemainder.Slice(0, argEnd), prefix: processNameStart); + if (name != null) + { + return name; + } + + // Check if argv[1] has the process name. + argRemainder = argRemainder.Slice(argEnd + 1); + argEnd = argRemainder.IndexOf((byte)'\0'); + if (argEnd != -1) + { + name = GetUntruncatedNameFromArg(argRemainder.Slice(0, argEnd), prefix: processNameStart); + return name ?? processNameStart; + } + } + + if (n == 0) + { + return processNameStart; + } + } + } + } + catch (IOException) + { + return processNameStart; + } + finally + { + if (rentedArray != null) + { + ArrayPool.Shared.Return(rentedArray); + } + } + + static string? GetUntruncatedNameFromArg(Span arg, string prefix) + { + // Strip directory names from arg. + int nameStart = arg.LastIndexOf((byte)'/') + 1; + string argString = Encoding.UTF8.GetString(arg.Slice(nameStart)); + + if (argString.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return argString; + } + else + { + return null; + } + } + } + + /// Reads the stats information for this process from the procfs file system. + private Interop.procfs.ProcessStatusInfo GetStatus() + { + EnsureState(State.HaveNonExitedId); + Interop.procfs.ProcessStatusInfo status; + if (Interop.procfs.TryReadProcessStatusInfo(_processId, out status, out string? _)) + { + throw new Win32Exception(SR.ProcessInformationUnavailable); + } + return status; + } } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.illumos.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.illumos.cs index 719eba40b105e1..87259d22eddfcf 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.illumos.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.illumos.cs @@ -5,18 +5,16 @@ using System.Globalization; using System.IO; -// TODO: remove +// TODO: remove or scope to just the methods that need them #pragma warning disable CA1822 +#pragma warning disable IDE0060 namespace System.Diagnostics { internal static partial class ProcessManager { /// Gets the IDs of all processes on the current machine. - public static int[] GetProcessIds() - { - throw new PlatformNotSupportedException(); - } + public static int[] GetProcessIds() => new List(EnumerateProcessIds()).ToArray(); /// Gets process infos for each process on the specified machine. /// Optional process name to use as an inclusion filter. @@ -24,7 +22,22 @@ public static int[] GetProcessIds() /// An array of process infos, one per found process. public static ProcessInfo[] GetProcessInfos(string? processNameFilter, string machineName) { - throw new PlatformNotSupportedException(); + Debug.Assert(processNameFilter is null, "Not used on Linux"); + ThrowIfRemoteMachine(machineName); + + // Iterate through all process IDs to load information about each process + IEnumerable pids = EnumerateProcessIds(); + ArrayBuilder processes = default; + foreach (int pid in pids) + { + ProcessInfo? pi = CreateProcessInfo(pid); + if (pi != null) + { + processes.Add(pi); + } + } + + return processes.ToArray(); } /// Gets an array of module infos for the specified process. @@ -32,7 +45,21 @@ public static ProcessInfo[] GetProcessInfos(string? processNameFilter, string ma /// The array of modules. internal static ProcessModuleCollection GetModules(int processId) { - throw new PlatformNotSupportedException(); + // GetModules(x)[0].FileName is often used to find the path to the executable, so at least + // get that. + // TODO: is there better way to get loaded modules? + if (Interop.procfs.TryReadProcessStatusInfo(processId, out Interop.procfs.ProcessStatusInfo _, out string? shortProcessName)) + { + string fullName = Process.GetUntruncatedProcessName(processId, shortProcessName); + if (!string.IsNullOrEmpty(fullName)) + { + return new ProcessModuleCollection(1) + { + new ProcessModule(fullName, Path.GetFileName(fullName)) + }; + } + } + return new ProcessModuleCollection(0); } /// @@ -40,11 +67,54 @@ internal static ProcessModuleCollection GetModules(int processId) /// internal static ProcessInfo? CreateProcessInfo(int pid) { - throw new PlatformNotSupportedException(); + if (Interop.procfs.TryReadProcessStatusInfo(pid, out Interop.procfs.ProcessStatusInfo status, out string? processName)) + { + return CreateProcessInfo(ref status, shortProcessName: processName); + } + return null; + } + + /// + /// Creates a ProcessInfo from the data parsed from a /proc/pid/psinfo file and the associated lwp directory. + /// + internal static ProcessInfo CreateProcessInfo(ref Interop.procfs.ProcessStatusInfo status, string shortProcessName, string? fullProcessName = null) + { + int pid = status.Pid; + + var pi = new ProcessInfo() + { + // TODO: get BasePriority from lwp? + ProcessName = fullProcessName ?? Process.GetUntruncatedProcessName(pid, shortProcessName) ?? string.Empty, + ProcessId = pid, + WorkingSet = (long)status.ResidentSetSize, + + SessionId = status.SessionId, + }; + + // TODO: translate LWP to thread + + return pi; } // ---------------------------------- // ---- Unix PAL layer ends here ---- // ---------------------------------- + + /// Enumerates the IDs of all processes on the current machine. + internal static IEnumerable EnumerateProcessIds() + { + // Parse /proc for any directory that's named with a number. Each such + // directory represents a process. + foreach (string procDir in Directory.EnumerateDirectories(Interop.procfs.RootPath)) + { + string dirName = Path.GetFileName(procDir); + int pid; + if (int.TryParse(dirName, NumberStyles.Integer, CultureInfo.InvariantCulture, out pid)) + { + Debug.Assert(pid >= 0); + yield return pid; + } + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 233d19f0d5b5f4..fde3660bb5553b 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2463,6 +2463,9 @@ Common\Interop\Unix\System.Native\Interop.SysLog.cs + + Common\Interop\Unix\System.Native\Interop.TimeSpec.cs + Common\Interop\Unix\System.Native\Interop.Threading.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs index fb7ff75cb2e6c6..df57ab1e108e27 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs @@ -8,6 +8,6 @@ namespace System public static partial class Environment { public static long WorkingSet => - (long)(Interop.procfs.TryReadProcessStatusInfo(ProcessId, out Interop.procfs.ProcessStatusInfo status) ? status.ResidentSetSize : 0); + (long)(Interop.procfs.TryReadProcessStatusInfo(ProcessId, out Interop.procfs.ProcessStatusInfo status, out string? _) ? status.ResidentSetSize : 0); } } diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c index fba2400ef23910..1d6c3a929c388c 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -1819,8 +1819,9 @@ int32_t SystemNative_CanGetHiddenFlag(void) #endif } -int32_t SystemNative_ReadProcessStatusInfo(pid_t pid, ProcessStatus* processStatus) +int32_t SystemNative_ReadProcessStatusInfo(pid_t pid, ProcessStatus* processStatus, char* nameBuf, int32_t nameBufSize) { + assert(nameBufSize >= 0); #ifdef __sun char statusFilename[64]; snprintf(statusFilename, sizeof(statusFilename), "/proc/%d/psinfo", pid); @@ -1837,7 +1838,14 @@ int32_t SystemNative_ReadProcessStatusInfo(pid_t pid, ProcessStatus* processStat close(fd); if (result >= 0) { + processStatus->Pid = status.pr_pid; + processStatus->ParentPid = status.pr_ppid; + processStatus->SessionId = status.pr_sid; processStatus->ResidentSetSize = status.pr_rssize * 1024; // pr_rssize is in Kbytes + processStatus->StartTime.tv_sec = status.pr_start.tv_sec; + processStatus->StartTime.tv_nsec = status.pr_start.tv_nsec; + assert(nameBufSize == PRFNSZ); + memcpy_s(nameBuf, nameBufSize, status.pr_fname, PRFNSZ); return 1; } diff --git a/src/native/libs/System.Native/pal_io.h b/src/native/libs/System.Native/pal_io.h index 03fd94cea25417..2c99e35bcdc033 100644 --- a/src/native/libs/System.Native/pal_io.h +++ b/src/native/libs/System.Native/pal_io.h @@ -6,6 +6,7 @@ #include "pal_compiler.h" #include "pal_types.h" #include "pal_errno.h" +#include "pal_time.h" #include #include #include @@ -36,9 +37,16 @@ typedef struct uint32_t UserFlags; // user defined flags } FileStatus; +#define ProcessNameStartSize 16 + typedef struct { + int32_t Pid; + int32_t ParentPid; + int32_t SessionId; size_t ResidentSetSize; + TimeSpec StartTime; + char ProcessNameStart[ProcessNameStartSize]; // add more fields when needed. } ProcessStatus; @@ -800,7 +808,7 @@ PALEXPORT int32_t SystemNative_CanGetHiddenFlag(void); * * Returns 1 if the process status was read; otherwise, 0. */ -PALEXPORT int32_t SystemNative_ReadProcessStatusInfo(pid_t pid, ProcessStatus* processStatus); +PALEXPORT int32_t SystemNative_ReadProcessStatusInfo(pid_t pid, ProcessStatus* processStatus, char* nameBuf, int32_t nameBufSize); /** * Reads the number of bytes specified into the provided buffer from the specified, opened file descriptor at specified offset.