Skip to content

Commit

Permalink
WIP: illumos System.Diagnostic.Process support
Browse files Browse the repository at this point in the history
  • Loading branch information
AustinWise committed Jul 7, 2024
1 parent 361f64a commit c48ae3d
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,25 +14,43 @@ internal static partial class @procfs
/// </summary>
/// <param name="pid">PID of the process to read status info for.</param>
/// <param name="processStatus">The pointer to processStatus instance.</param>
/// <param name="nameBuf">Buffer in which to place the process name.</param>
/// <param name="nameBufSize">Size of <paramref name="nameBuf"/> in bytes.</param>
/// <returns>
/// true if the process status was read; otherwise, false.
/// </returns>
[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<byte> 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;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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}");
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ internal static partial class Interop
{
internal static partial class Sys
{
internal struct TimeSpec
{
internal long TvSec;
internal long TvNsec;
}

/// <summary>
/// Sets the last access and last modified time of a file
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@
<Compile Include="System\Diagnostics\Process.illumos.cs" />
<Compile Include="System\Diagnostics\ProcessManager.illumos.cs" />
<Compile Include="System\Diagnostics\ProcessThread.illumos.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.TimeSpec.cs"
Link="Common\Interop\Unix\System.Native\Interop.TimeSpec.cs" />
<Compile Include="$(CommonPath)Interop\SunOS\procfs\Interop.ProcFsStat.cs"
Link="Common\Interop\SunOS\procfs\Interop.ProcFsStat.cs" />
<Compile Include="$(CommonPath)Interop\SunOS\procfs\Interop.ProcFsStat.TryReadProcessStatusInfo.cs"
Link="Common\Interop\SunOS\procfs\Interop.ProcFsStat.TryReadProcessStatusInfo.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'ios' or '$(TargetPlatformIdentifier)' == 'tvos'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Process> 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();
}

/// <summary>Gets the amount of time the process has spent running code inside the operating system core.</summary>
Expand All @@ -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);
}
}

/// <summary>Gets the parent process ID</summary>
private int ParentProcessId => throw new PlatformNotSupportedException();
private int ParentProcessId => GetStatus().ParentPid;

/// <summary>Gets execution path</summary>
private static string? GetPathToOpenFile()
{
throw new PlatformNotSupportedException();
return FindProgramInPath("xdg-open");
}

/// <summary>
Expand Down Expand Up @@ -133,5 +152,113 @@ private static void SetWorkingSetLimitsCore(IntPtr? newMin, IntPtr? newMax, out
// ---- Unix PAL layer ends here ----
// ----------------------------------

/// <summary>Gets the name that was used to start the process, or null if it could not be retrieved.</summary>
/// <param name="pid">The pid of the target process.</param>
/// <param name="processNameStart">The start of the process name of the ProcessStatusInfo struct.</param>
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<byte> 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<byte>.Shared.Rent((int)newLength);
buffer.CopyTo(tmp);
byte[]? toReturn = rentedArray;
buffer = rentedArray = tmp;
if (toReturn != null)
{
ArrayPool<byte>.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<byte> 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<byte>.Shared.Return(rentedArray);
}
}

static string? GetUntruncatedNameFromArg(Span<byte> 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;
}
}
}

/// <summary>Reads the stats information for this process from the procfs file system.</summary>
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;
}
}
}
Loading

0 comments on commit c48ae3d

Please sign in to comment.