diff --git a/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.Definitions.cs b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.Definitions.cs new file mode 100644 index 0000000000000..7e7a98bda7c64 --- /dev/null +++ b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.Definitions.cs @@ -0,0 +1,155 @@ +// 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; + +// C# equivalents for structures. See: struct lwpsinfo, struct psinfo. +// We read directly onto these from procfs, so the layouts and sizes of these structures +// must _exactly_ match those in + +// analyzer incorrectly flags fixed buffer length const +// (https://github.com/dotnet/roslyn/issues/37593) +#pragma warning disable CA1823 + +internal static partial class Interop +{ + internal static partial class @procfs + { + internal const string RootPath = "/proc/"; + private const string psinfoFileName = "/psinfo"; + private const string lwpDirName = "/lwp"; + private const string lwpsinfoFileName = "/lwpsinfo"; + + // Constants from sys/procfs.h + private const int PRARGSZ = 80; + private const int PRCLSZ = 8; + private const int PRFNSZ = 16; + + [StructLayout(LayoutKind.Sequential)] + internal struct @timestruc_t + { + public long tv_sec; + public long tv_nsec; + } + + // lwp ps(1) information file. /proc//lwp//lwpsinfo + // Equivalent to sys/procfs.h struct lwpsinfo + // "unsafe" because it has fixed sized arrays. + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct @lwpsinfo + { + private int pr_flag; /* lwp flags (DEPRECATED; do not use) */ + public uint pr_lwpid; /* lwp id */ + private long pr_addr; /* internal address of lwp */ + private long pr_wchan; /* wait addr for sleeping lwp */ + public byte pr_stype; /* synchronization event type */ + public byte pr_state; /* numeric lwp state */ + public byte pr_sname; /* printable character for pr_state */ + public byte pr_nice; /* nice for cpu usage */ + private short pr_syscall; /* system call number (if in syscall) */ + private byte pr_oldpri; /* pre-SVR4, low value is high priority */ + private byte pr_cpu; /* pre-SVR4, cpu usage for scheduling */ + public int pr_pri; /* priority, high value is high priority */ + private ushort pr_pctcpu; /* fixed pt. % of recent cpu time */ + private ushort pr_pad; + public timestruc_t pr_start; /* lwp start time, from the epoch */ + public timestruc_t pr_time; /* usr+sys cpu time for this lwp */ + private fixed byte pr_clname[PRCLSZ]; /* scheduling class name */ + private fixed byte pr_name[PRFNSZ]; /* name of system lwp */ + private int pr_onpro; /* processor which last ran this lwp */ + private int pr_bindpro; /* processor to which lwp is bound */ + private int pr_bindpset; /* processor set to which lwp is bound */ + private int pr_lgrp; /* lwp home lgroup */ + private fixed int pr_filler[4]; /* reserved for future use */ + } + private const int PR_LWPSINFO_SIZE = 128; // for debug assertions + + // process ps(1) information file. /proc//psinfo + // Equivalent to sys/procfs.h struct psinfo + // "unsafe" because it has fixed sized arrays. + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct @psinfo + { + private int pr_flag; /* process flags (DEPRECATED; do not use) */ + public int pr_nlwp; /* number of active lwps in the process */ + public int pr_pid; /* unique process id */ + public int pr_ppid; /* process id of parent */ + public int pr_pgid; /* pid of process group leader */ + public int pr_sid; /* session id */ + public uint pr_uid; /* real user id */ + public uint pr_euid; /* effective user id */ + public uint pr_gid; /* real group id */ + public uint pr_egid; /* effective group id */ + private long pr_addr; /* address of process */ + public ulong pr_size; /* size of process image in Kbytes */ + public ulong pr_rssize; /* resident set size in Kbytes */ + private ulong pr_pad1; + private ulong pr_ttydev; /* controlling tty device (or PRNODEV) */ + private ushort pr_pctcpu; /* % of recent cpu time used by all lwps */ + private ushort pr_pctmem; /* % of system memory used by process */ + public timestruc_t pr_start; /* process start time, from the epoch */ + public timestruc_t pr_time; /* usr+sys cpu time for this process */ + public timestruc_t pr_ctime; /* usr+sys cpu time for reaped children */ + public fixed byte pr_fname[PRFNSZ]; /* name of execed file */ + public fixed byte pr_psargs[PRARGSZ]; /* initial characters of arg list */ + public int pr_wstat; /* if zombie, the wait() status */ + public int pr_argc; /* initial argument count */ + private long pr_argv; /* address of initial argument vector */ + private long pr_envp; /* address of initial environment vector */ + private byte pr_dmodel; /* data model of the process */ + private fixed byte pr_pad2[3]; + public int pr_taskid; /* task id */ + public int pr_projid; /* project id */ + public int pr_nzomb; /* number of zombie lwps in the process */ + public int pr_poolid; /* pool id */ + public int pr_zoneid; /* zone id */ + public int pr_contract; /* process contract */ + private fixed int pr_filler[1]; /* reserved for future use */ + public lwpsinfo pr_lwp; /* information for representative lwp */ + // C# magic: Accessor method to get a Span for pr_psargs[] + // Does not affect the size or layout of this struct. + internal ReadOnlySpan PsArgsSpan => + MemoryMarshal.CreateReadOnlySpan(ref pr_psargs[0], PRARGSZ); + } + private const int PR_PSINFO_SIZE = 416; // for debug assertions + + // Ouput type for TryGetThreadInfoById() + internal struct ThreadInfo + { + internal uint Tid; + internal int Priority; + internal int NiceVal; + internal char Status; + internal Interop.Sys.TimeSpec StartTime; + internal Interop.Sys.TimeSpec CpuTotalTime; // user+sys + // add more fields when needed. + } + + // Ouput type for TryGetProcessInfoById() + internal struct ProcessInfo + { + internal int Pid; + internal int ParentPid; + internal int SessionId; + internal int Priority; + internal int NiceVal; + internal nuint VirtualSize; + internal nuint ResidentSetSize; + internal Interop.Sys.TimeSpec StartTime; + internal Interop.Sys.TimeSpec CpuTotalTime; // user+sys + internal string? Args; + // add more fields when needed. + } + + internal static string GetInfoFilePathForProcess(int pid) => + $"{RootPath}{(uint)pid}{psinfoFileName}"; + + internal static string GetLwpDirForProcess(int pid) => + $"{RootPath}{(uint)pid}{lwpDirName}"; + + internal static string GetInfoFilePathForThread(int pid, int tid) => + $"{RootPath}{(uint)pid}{lwpDirName}/{(uint)tid}{lwpsinfoFileName}"; + + } +} diff --git a/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.TryGetProcessInfoById.cs b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.TryGetProcessInfoById.cs new file mode 100644 index 0000000000000..d9989c03ae5cf --- /dev/null +++ b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.TryGetProcessInfoById.cs @@ -0,0 +1,73 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.IO; +using System.Text; + +internal static partial class Interop +{ + internal static partial class @procfs + { + + /// + /// Attempts to get status info for the specified process ID. + /// + /// PID of the process to read status info for. + /// The pointer to ProcessInfo instance. + /// + /// true if the process info was read; otherwise, false. + /// + + // ProcessManager.SunOS.cs calls this + internal static bool TryGetProcessInfoById(int pid, out ProcessInfo result) + { + result = default; + + try + { + string fileName = GetInfoFilePathForProcess(pid); + using FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read); + psinfo pr; + Unsafe.SkipInit(out pr); + Span prspan = MemoryMarshal.AsBytes(new Span(ref pr)); + Debug.Assert(prspan.Length == PR_PSINFO_SIZE, + $"psinfo struct size {prspan.Length} bytes not {PR_PSINFO_SIZE}."); + fs.ReadExactly(prspan); + + result.Pid = pr.pr_pid; + result.ParentPid = pr.pr_ppid; + result.SessionId = pr.pr_sid; + result.VirtualSize = (nuint)pr.pr_size * 1024; // pr_size is in Kbytes + result.ResidentSetSize = (nuint)pr.pr_rssize * 1024; // pr_rssize is in Kbytes + result.StartTime.TvSec = pr.pr_start.tv_sec; + result.StartTime.TvNsec = pr.pr_start.tv_nsec; + result.CpuTotalTime.TvSec = pr.pr_time.tv_sec; + result.CpuTotalTime.TvNsec = pr.pr_time.tv_nsec; + + // Get Args as a managed string, using accessor for pr_psargs[] + ReadOnlySpan argspan = pr.PsArgsSpan; + int argslen = argspan.IndexOf((byte)0); + argslen = (argslen >= 0) ? argslen : argspan.Length; + result.Args = Encoding.UTF8.GetString(argspan.Slice(0, argslen)); + + // A couple things from pr_lwp + result.Priority = pr.pr_lwp.pr_pri; + result.NiceVal = (int)pr.pr_lwp.pr_nice; + + return true; + } + catch (Exception e) + { + Debug.Fail($"Failed to read process info for PID {pid}: {e}"); + } + + return false; + } + + } +} diff --git a/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.TryGetThreadInfoById.cs b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.TryGetThreadInfoById.cs new file mode 100644 index 0000000000000..a3ab094fcef5e --- /dev/null +++ b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.TryGetThreadInfoById.cs @@ -0,0 +1,62 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.IO; + +internal static partial class Interop +{ + internal static partial class @procfs + { + + /// + /// Attempts to get status info for the specified thread ID. + /// + /// PID of the process to read status info for. + /// TID of the thread to read status info for. + /// The pointer to ThreadInfo instance. + /// + /// true if the thread info was read; otherwise, false. + /// + + // ProcessManager.SunOS.cs calls this + internal static bool TryGetThreadInfoById(int pid, int tid, out ThreadInfo result) + { + result = default; + + try + { + string fileName = GetInfoFilePathForThread(pid, tid); + using FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read); + lwpsinfo pr; + Unsafe.SkipInit(out pr); + Span prspan = MemoryMarshal.AsBytes(new Span(ref pr)); + Debug.Assert(prspan.Length == PR_LWPSINFO_SIZE, + $"psinfo struct size {prspan.Length} bytes not {PR_LWPSINFO_SIZE}."); + fs.ReadExactly(prspan); + + result.Tid = pr.pr_lwpid; + result.Priority = pr.pr_pri; + result.NiceVal = (int)pr.pr_nice; + result.Status = (char)pr.pr_sname; + result.StartTime.TvSec = pr.pr_start.tv_sec; + result.StartTime.TvNsec = pr.pr_start.tv_nsec; + result.CpuTotalTime.TvSec = pr.pr_time.tv_sec; + result.CpuTotalTime.TvNsec = pr.pr_time.tv_nsec; + + return true; + } + catch (Exception e) + { + Debug.Fail($"Failed to read thread info for PID {pid} TID {tid}: {e}"); + } + + return false; + } + + } +} 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 deleted file mode 100644 index 5d835f241cda1..0000000000000 --- a/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.TryReadProcessStatusInfo.cs +++ /dev/null @@ -1,37 +0,0 @@ -// 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 - { - /// - /// Attempts to get status info for the specified process ID. - /// - /// PID of the process to read status info for. - /// The pointer to processStatus instance. - /// - /// 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); - - internal struct ProcessStatusInfo - { - internal nuint ResidentSetSize; - // add more fields when needed. - } - - internal static unsafe bool TryReadProcessStatusInfo(int pid, out ProcessStatusInfo statusInfo) - { - statusInfo = default; - fixed (ProcessStatusInfo* pStatusInfo = &statusInfo) - { - return TryReadProcessStatusInfo(pid, pStatusInfo); - } - } - } -} 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 0000000000000..54a7299dcd491 --- /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 261a5ae8562df..13c5d2fe10bce 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/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index 2b3c1e4e00072..74ce8835d6efc 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -47,6 +47,7 @@ public static partial class PlatformDetection public static bool IsNotMacCatalyst => !IsMacCatalyst; public static bool Isillumos => RuntimeInformation.IsOSPlatform(OSPlatform.Create("ILLUMOS")); public static bool IsSolaris => RuntimeInformation.IsOSPlatform(OSPlatform.Create("SOLARIS")); + public static bool IsSunOS => Isillumos || IsSolaris; public static bool IsBrowser => RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER")); public static bool IsWasi => RuntimeInformation.IsOSPlatform(OSPlatform.Create("WASI")); public static bool IsNotBrowser => !IsBrowser; 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 97e289045e324..685ac0bd71f58 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) + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)-illumos;$(NetCoreAppCurrent)-solaris;$(NetCoreAppCurrent) $(DefineConstants);FEATURE_REGISTRY true false @@ -363,6 +363,21 @@ Link="Common\Interop\FreeBSD\Interop.Process.GetProcInfo.cs" /> + + + + + + + + + + + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs new file mode 100644 index 0000000000000..e36029c501bea --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs @@ -0,0 +1,137 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; + +namespace System.Diagnostics +{ + public partial class Process : IDisposable + { + + /// Gets the time the associated process was started. + internal DateTime StartTimeCore + { + get + { + Interop.procfs.ProcessInfo iinfo = GetProcInfo(); + + DateTime startTime = DateTime.UnixEpoch + + TimeSpan.FromSeconds(iinfo.StartTime.TvSec) + + TimeSpan.FromMicroseconds(iinfo.StartTime.TvNsec / 1000); + + // The return value is expected to be in the local time zone. + return startTime.ToLocalTime(); + } + } + + /// Gets the parent process ID + private int ParentProcessId => GetProcInfo().ParentPid; + + /// Gets execution path + private static string? GetPathToOpenFile() + { + return FindProgramInPath("xdg-open"); + } + + /// + /// 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 + { + // a.k.a. "user" + "system" time + Interop.procfs.ProcessInfo iinfo = GetProcInfo(); + TimeSpan ts = TimeSpan.FromSeconds(iinfo.CpuTotalTime.TvSec) + + TimeSpan.FromMicroseconds(iinfo.CpuTotalTime.TvNsec / 1000); + return ts; + } + } + + /// + /// 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 + { + // a.k.a. "user" time + // Could get this from /proc/$pid/status + // Just say it's all user time for now + return TotalProcessorTime; + } + } + + /// + /// 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 + { + // a.k.a. "system" time + // Could get this from /proc/$pid/status + // Just say it's all user time for now + EnsureState(State.HaveNonExitedId); + return TimeSpan.Zero; + } + } + + // ---------------------------------- + // ---- Unix PAL layer ends here ---- + // ---------------------------------- + + /// Gets the name that was used to start the process, or null if it could not be retrieved. + internal static string GetUntruncatedProcessName(ref Interop.procfs.ProcessInfo iinfo) + { + // This assumes the process name is the first part of the Args string + // ending at the first space. That seems to work well enough for now. + // If someday this need to support a process name containing spaces, + // this could call a new Interop function that reads /proc/$pid/auxv + // (sys/auxv.h) and gets the AT_SUN_EXECNAME string from that file. + if (!string.IsNullOrEmpty(iinfo.Args)) + { + string[] argv = iinfo.Args.Split(' ', 2); + if (!string.IsNullOrEmpty(argv[0])) + { + return Path.GetFileName(argv[0]); + } + } + return "?"; + } + + /// Reads the information for this process from the procfs file system. + private Interop.procfs.ProcessInfo GetProcInfo() + { + EnsureState(State.HaveNonExitedId); + Interop.procfs.ProcessInfo iinfo; + if (!Interop.procfs.TryGetProcessInfoById(_processId, out iinfo)) + { + throw new Win32Exception(SR.ProcessInformationUnavailable); + } + return iinfo; + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.SunOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.SunOS.cs new file mode 100644 index 0000000000000..4d5b8025c9218 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.SunOS.cs @@ -0,0 +1,244 @@ +// 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.Globalization; +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() + { + IEnumerable pids = EnumerateProcessIds(); + return new List(pids).ToArray(); + } + + /// Gets process infos for each process on the specified machine. + /// Optional process name to use as an inclusion filter. + /// The target machine. + /// An array of process infos, one per found process. + public static ProcessInfo[] GetProcessInfos(string? processNameFilter, string machineName) + { + 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, processNameFilter); + if (pi != null) + { + processes.Add(pi); + } + } + + return processes.ToArray(); + } + + /// Gets an array of module infos for the specified process. + /// The ID of the process whose modules should be enumerated. + /// The array of modules. + internal static ProcessModuleCollection GetModules(int processId) + { + + // Negative PIDs aren't valid + ArgumentOutOfRangeException.ThrowIfNegative(processId); + + // GetModules(x)[0].FileName is often used to find the path to the executable, + // so at least get that. That appears to be sufficient, at least for now. + // If needed, the full list of loaded modules could be obtained using another + // Interop function to read /proc/$pid/auxv similar to how the "pargs" and "pldd" + // commands do their work. + + Interop.procfs.ProcessInfo iProcInfo; + if (Interop.procfs.TryGetProcessInfoById(processId, out iProcInfo)) + { + string fullName = Process.GetUntruncatedProcessName(ref iProcInfo); + if (!string.IsNullOrEmpty(fullName)) + { + return new ProcessModuleCollection(1) + { + new ProcessModule(fullName, Path.GetFileName(fullName)) + }; + } + } + return new ProcessModuleCollection(0); + } + + /// + /// Creates a ProcessInfo from the specified process ID. + /// + internal static ProcessInfo? CreateProcessInfo(int pid, string? processNameFilter = null) + { + // Negative PIDs aren't valid + ArgumentOutOfRangeException.ThrowIfNegative(pid); + + Interop.procfs.ProcessInfo iProcInfo; + if (!Interop.procfs.TryGetProcessInfoById(pid, out iProcInfo)) + { + return null; + } + + string processName = Process.GetUntruncatedProcessName(ref iProcInfo); + if (!string.IsNullOrEmpty(processNameFilter) && + !string.Equals(processName, processNameFilter, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return CreateProcessInfo(ref iProcInfo); + } + + // ---------------------------------- + // ---- 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; + } + } + } + + /// Enumerates the IDs of all threads in the specified process. + internal static IEnumerable EnumerateThreadIds(int pid) + { + // Parse /proc/$pid/lwp for any directory that's named with a number. + // Each such directory represents a thread. + string dir = Interop.procfs.GetLwpDirForProcess(pid); + foreach (string lwpDir in Directory.EnumerateDirectories(dir)) + { + string dirName = Path.GetFileName(lwpDir); + int tid; + if (int.TryParse(dirName, NumberStyles.Integer, CultureInfo.InvariantCulture, out tid)) + { + Debug.Assert(tid >= 0); + yield return tid; + } + } + } + + /// + /// Creates a ProcessInfo from the data read from a /proc/pid/psinfo file and the associated lwp directory. + /// + internal static ProcessInfo CreateProcessInfo(ref Interop.procfs.ProcessInfo iProcInfo) + { + int pid = iProcInfo.Pid; + + string name = Process.GetUntruncatedProcessName(ref iProcInfo); + var pi = new ProcessInfo() + { + ProcessId = pid, + ProcessName = name, + BasePriority = iProcInfo.Priority, + SessionId = iProcInfo.SessionId, + VirtualBytes = (long)iProcInfo.VirtualSize, + WorkingSet = (long)iProcInfo.ResidentSetSize, + // StartTime: See Process.StartTimeCore() + }; + + // Then read through /proc/pid/lwp/ to find each thread in the process... + // Can we use a "get" method to avoid loading this for every process until it's asked for? + try + { + + // Iterate through all thread IDs to load information about each thread + IEnumerable tids = EnumerateThreadIds(pid); + + foreach (int tid in tids) + { + Interop.procfs.ThreadInfo iThrInfo; + ThreadInfo? ti; + + if (!Interop.procfs.TryGetThreadInfoById(pid, tid, out iThrInfo)) + { + continue; + } + + ti = CreateThreadInfo(ref iProcInfo, ref iThrInfo); + if (ti != null) + { + pi._threadInfoList.Add(ti); + } + } + } + catch (IOException) + { + // Between the time that we get an ID and the time that we try to read the associated + // directories and files in procfs, the process could be gone. + } + + // Finally return what we've built up + return pi; + } + + /// + /// Creates a ThreadInfo from the data read from a /proc/pid/lwp/lwpsinfo file. + /// + internal static ThreadInfo CreateThreadInfo(ref Interop.procfs.ProcessInfo iProcInfo, + ref Interop.procfs.ThreadInfo iThrInfo) + { + + var ti = new ThreadInfo() + { + _processId = iProcInfo.Pid, + _threadId = (ulong)iThrInfo.Tid, + _basePriority = iThrInfo.Priority, + _currentPriority = iThrInfo.Priority, + _startAddress = null, + _threadState = ProcFsStateToThreadState(iThrInfo.Status), + _threadWaitReason = ThreadWaitReason.Unknown + }; + + return ti; + } + + /// Gets a ThreadState to represent the value returned from the status field of /proc/pid/stat. + /// The status field value. + /// + private static ThreadState ProcFsStateToThreadState(char c) + { + // Information on these in fs/proc/array.c + // `man proc` does not document them all + switch (c) + { + case 'O': // On-CPU + case 'R': // Runnable + return ThreadState.Running; + + case 'S': // Sleeping in a wait + case 'T': // Stopped on a signal + return ThreadState.Wait; + + case 'Z': // Zombie + return ThreadState.Terminated; + + case 'W': // Waiting for CPU + return ThreadState.Transition; + + case '\0': // new, not started yet + return ThreadState.Initialized; + + default: + Debug.Fail($"Unexpected status character: {(int)c}"); + return ThreadState.Unknown; + } + } + + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.SunOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.SunOS.cs new file mode 100644 index 0000000000000..ad428290f3271 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.SunOS.cs @@ -0,0 +1,133 @@ +// 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 + { + + /// Gets the time this thread was started. + internal DateTime GetStartTime() + { + Interop.procfs.ThreadInfo iinfo = GetThreadInfo(); + + DateTime startTime = DateTime.UnixEpoch + + TimeSpan.FromSeconds(iinfo.StartTime.TvSec) + + TimeSpan.FromMicroseconds(iinfo.StartTime.TvNsec / 1000); + + // The return value is expected to be in the local time zone. + return startTime.ToLocalTime(); + } + + /// + /// 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.procfs.ThreadInfo iinfo = GetThreadInfo(); + return GetThreadPriorityFromSysPri(iinfo.Priority); + } + set + { + // Raising priority is a privileged operation. + // Might be able to adjust our "nice" value. Maybe later... + throw new PlatformNotSupportedException(); + } + } + + /// + /// Gets 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 + { + // a.k.a. "user" + "system" time + Interop.procfs.ThreadInfo iinfo = GetThreadInfo(); + TimeSpan ts = TimeSpan.FromSeconds(iinfo.CpuTotalTime.TvSec) + + TimeSpan.FromMicroseconds(iinfo.CpuTotalTime.TvNsec / 1000); + return ts; + } + } + + /// + /// Gets 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 + { + // a.k.a. "user" time + // Could get this from /proc/$pid/lwp/$lwpid/lwpstatus + // Just say it's all user time for now + return TotalProcessorTime; + } + } + + /// + /// Gets 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 + { + // a.k.a. "system" time + // Could get this from /proc/$pid/lwp/$lwpid/lwpstatus + // Just say it's all user time for now + return TimeSpan.Zero; + } + + } + + // ---------------------------------- + // ---- exported stuff ends here ---- + // ---------------------------------- + + // System priorities go from 1 to 100, where 60 and above are for "system" things + // These mappingsare relatively arbitrary. Normal user processes run at priority 59. + // and the other values above and below are simply distributed somewhat evenly. + private static System.Diagnostics.ThreadPriorityLevel GetThreadPriorityFromSysPri(int pri) + { + Debug.Assert((pri >= 0) && (pri <= 100)); + return + (pri >= 90) ? ThreadPriorityLevel.TimeCritical : + (pri >= 80) ? ThreadPriorityLevel.Highest : + (pri >= 60) ? ThreadPriorityLevel.AboveNormal : + (pri == 59) ? ThreadPriorityLevel.Normal : + (pri >= 40) ? ThreadPriorityLevel.BelowNormal : + (pri >= 20) ? ThreadPriorityLevel.Lowest : + ThreadPriorityLevel.Idle; + } + + /// Reads the information for this thread from the procfs file system. + private Interop.procfs.ThreadInfo GetThreadInfo() + { + Interop.procfs.ThreadInfo iinfo; + if (!Interop.procfs.TryGetThreadInfoById(_processId, tid: Id, out iinfo)) + { + throw new InvalidOperationException(SR.Format(SR.ThreadExited, Id)); + } + return iinfo; + } + + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs index 6907a43b63040..ae1aa51f4ff49 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs @@ -575,7 +575,7 @@ public void TestMaxWorkingSet() Assert.InRange((long)p.MinWorkingSet, 0, long.MaxValue); } - if (OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD()) { + if (OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD() || PlatformDetection.IsSunOS) { return; // doesn't support getting/setting working set for other processes } @@ -631,7 +631,7 @@ public void TestMinWorkingSet() Assert.InRange((long)p.MinWorkingSet, 0, long.MaxValue); } - if (OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD()) { + if (OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD() || PlatformDetection.IsSunOS) { return; // doesn't support getting/setting working set for other processes } 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 d920dc227a376..04ea20eb85b13 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 @@ -2465,6 +2465,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 @@ -2564,7 +2567,8 @@ - + + 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 fb7ff75cb2e6c..d072f52f0c48c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs @@ -7,7 +7,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); + public static long WorkingSet => (long)(Interop.procfs.TryGetProcessInfoById(ProcessId, out Interop.procfs.ProcessInfo iProcInfo) ? iProcInfo.ResidentSetSize : 0); } } diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index 2165430d65f56..9a254223bcb73 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -120,7 +120,6 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_LChflagsCanSetHiddenFlag) DllImportEntry(SystemNative_FChflags) DllImportEntry(SystemNative_CanGetHiddenFlag) - DllImportEntry(SystemNative_ReadProcessStatusInfo) DllImportEntry(SystemNative_Log) DllImportEntry(SystemNative_LogError) DllImportEntry(SystemNative_AlignedAlloc) diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c index 17e5f77d2d842..4c87a6791b0bd 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -1895,36 +1895,6 @@ int32_t SystemNative_CanGetHiddenFlag(void) #endif } -int32_t SystemNative_ReadProcessStatusInfo(pid_t pid, ProcessStatus* processStatus) -{ -#ifdef __sun - char statusFilename[64]; - snprintf(statusFilename, sizeof(statusFilename), "/proc/%d/psinfo", pid); - - intptr_t fd; - while ((fd = open(statusFilename, O_RDONLY)) < 0 && errno == EINTR); - if (fd < 0) - { - return 0; - } - - psinfo_t status; - int result = Common_Read(fd, &status, sizeof(psinfo_t)); - close(fd); - if (result >= 0) - { - processStatus->ResidentSetSize = status.pr_rssize * 1024; // pr_rssize is in Kbytes - return 1; - } - - return 0; -#else - (void)pid, (void)processStatus; - errno = ENOTSUP; - return -1; -#endif // __sun -} - int32_t SystemNative_PRead(intptr_t fd, void* buffer, int32_t bufferSize, int64_t fileOffset) { assert(buffer != NULL); diff --git a/src/native/libs/System.Native/pal_io.h b/src/native/libs/System.Native/pal_io.h index a07aa7c170219..6dbc61c283611 100644 --- a/src/native/libs/System.Native/pal_io.h +++ b/src/native/libs/System.Native/pal_io.h @@ -36,12 +36,6 @@ typedef struct uint32_t UserFlags; // user defined flags } FileStatus; -typedef struct -{ - size_t ResidentSetSize; - // add more fields when needed. -} ProcessStatus; - // NOTE: the layout of this type is intended to exactly match the layout of a `struct iovec`. There are // assertions in pal_networking.c that validate this. typedef struct @@ -809,13 +803,6 @@ PALEXPORT int32_t SystemNative_LChflagsCanSetHiddenFlag(void); */ PALEXPORT int32_t SystemNative_CanGetHiddenFlag(void); -/** - * Reads the psinfo_t struct and converts into ProcessStatus. - * - * Returns 1 if the process status was read; otherwise, 0. - */ -PALEXPORT int32_t SystemNative_ReadProcessStatusInfo(pid_t pid, ProcessStatus* processStatus); - /** * Reads the number of bytes specified into the provided buffer from the specified, opened file descriptor at specified offset. *