diff --git a/src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.Win32.cs b/src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.Win32.cs
new file mode 100644
index 000000000..457970df8
--- /dev/null
+++ b/src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.Win32.cs
@@ -0,0 +1,231 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Runtime.InteropServices;
+
+namespace Artemis.Core.Services;
+
+// DarthAffe 31.08.2023: Based on how it's done in the framework:
+// https://github.com/dotnet/runtime/blob/f0463a98d105f26037f9d3e63213421a3a7d4dff/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Win32.cs#L263
+public static unsafe partial class ProcessMonitor
+{
+ #region Native
+
+ [LibraryImport("ntdll.dll", EntryPoint = "NtQuerySystemInformation")]
+ private static partial uint NtQuerySystemInformation(int systemInformationClass, void* systemInformation, uint systemInformationLength, uint* returnLength);
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct SystemProcessInformation
+ {
+ // ReSharper disable MemberCanBePrivate.Local
+ public uint NextEntryOffset;
+ public uint NumberOfThreads;
+ private fixed byte Reserved1[48];
+ public UnicodeString ImageName;
+ public int BasePriority;
+ public nint UniqueProcessId;
+ private readonly nuint Reserved2;
+ public uint HandleCount;
+ public uint SessionId;
+ private readonly nuint Reserved3;
+ public nuint PeakVirtualSize; // SIZE_T
+ public nuint VirtualSize;
+ private readonly uint Reserved4;
+ public nuint PeakWorkingSetSize; // SIZE_T
+ public nuint WorkingSetSize; // SIZE_T
+ private readonly nuint Reserved5;
+ public nuint QuotaPagedPoolUsage; // SIZE_T
+ private readonly nuint Reserved6;
+ public nuint QuotaNonPagedPoolUsage; // SIZE_T
+ public nuint PagefileUsage; // SIZE_T
+ public nuint PeakPagefileUsage; // SIZE_T
+ public nuint PrivatePageCount; // SIZE_T
+ private fixed long Reserved7[6];
+ // ReSharper restore MemberCanBePrivate.Local
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct UnicodeString
+ {
+ // ReSharper disable MemberCanBePrivate.Local
+ ///
+ /// Length in bytes, not including the null terminator, if any.
+ ///
+ public ushort Length;
+
+ ///
+ /// Max size of the buffer in bytes
+ ///
+ public ushort MaximumLength;
+ public nint Buffer;
+ // ReSharper restore MemberCanBePrivate.Local
+ }
+
+ [LibraryImport("kernel32.dll", EntryPoint = "QueryFullProcessImageNameW", StringMarshalling = StringMarshalling.Utf16)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool QueryFullProcessImageName(nint hProcess, int dwFlags, [Out] char[] lpExeName, ref int lpdwSize);
+
+ [LibraryImport("kernel32.dll")]
+ private static partial nint OpenProcess(ProcessAccessFlags processAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int processId);
+
+ [Flags]
+ private enum ProcessAccessFlags : uint
+ {
+ QueryLimitedInformation = 0x00001000
+ }
+
+ #endregion
+
+ #region Constants
+
+ private const int SYSTEM_PROCESS_INFORMATION = 5;
+ private const uint STATUS_INFO_LENGTH_MISMATCH = 0xC0000004;
+ private const int SYSTEM_PROCESS_ID = 4;
+ private const int IDLE_PROCESS_ID = 0;
+ private const int DEFAULT_BUFFER_SIZE = 1024 * 1024;
+
+ #endregion
+
+ #region Properties & Fields
+
+ private static void* _buffer;
+ private static uint _bufferSize;
+
+ #endregion
+
+ #region Methods
+
+ private static void InitializeBuffer()
+ {
+ _bufferSize = DEFAULT_BUFFER_SIZE;
+ _buffer = NativeMemory.Alloc(_bufferSize);
+ }
+
+ private static void FreeBuffer()
+ {
+ NativeMemory.Free(_buffer);
+ _bufferSize = 0;
+ }
+
+ private static void UpdateProcessInfosWin32(object? o)
+ {
+ lock (LOCK)
+ {
+ try
+ {
+ if (_bufferSize == 0) return;
+
+ while (true)
+ {
+ uint actualSize = 0;
+ uint status = NtQuerySystemInformation(SYSTEM_PROCESS_INFORMATION, _buffer, _bufferSize, &actualSize);
+
+ if (status != STATUS_INFO_LENGTH_MISMATCH)
+ {
+ if ((int)status < 0)
+ throw new InvalidOperationException("Error", new Win32Exception((int)status));
+
+ UpdateProcessInfosWin32(new ReadOnlySpan(_buffer, (int)actualSize));
+ return;
+ }
+
+ ResizeBuffer(GetEstimatedBufferSize(actualSize));
+ }
+ }
+ catch { /* Should we throw here? I guess no ... */ }
+ }
+ }
+
+ private static void UpdateProcessInfosWin32(ReadOnlySpan data)
+ {
+ int processInformationOffset = 0;
+
+ HashSet processIds = new(_processes.Count);
+
+ while (true)
+ {
+ ref readonly SystemProcessInformation pi = ref MemoryMarshal.AsRef(data[processInformationOffset..]);
+
+ int processId = pi.UniqueProcessId.ToInt32();
+ processIds.Add(processId);
+
+ if (!_processes.ContainsKey(processId))
+ {
+ string imageName;
+ string processName;
+ if (pi.ImageName.Buffer != nint.Zero)
+ {
+ imageName = new ReadOnlySpan(pi.ImageName.Buffer.ToPointer(), pi.ImageName.Length / sizeof(char)).ToString();
+ processName = GetProcessShortName(imageName).ToString();
+ }
+ else
+ {
+ imageName = string.Empty;
+ processName = processId switch
+ {
+ SYSTEM_PROCESS_ID => "System",
+ IDLE_PROCESS_ID => "Idle",
+ _ => processId.ToString(CultureInfo.InvariantCulture) // use the process ID for a normal process without a name
+ };
+ }
+
+ string executable = GetProcessFilename(processId);
+ ProcessInfo processInfo = new(processId, processName, imageName, executable);
+ _processes.Add(processId, processInfo);
+
+ OnProcessStarted(processInfo);
+ }
+
+ if (pi.NextEntryOffset == 0) break;
+ processInformationOffset += (int)pi.NextEntryOffset;
+ }
+
+ HandleStoppedProcesses(processIds);
+
+ LastUpdate = DateTime.Now;
+ }
+
+ private static string GetProcessFilename(int processId)
+ {
+ int capacity = byte.MaxValue;
+ char[] buffer = new char[capacity];
+ nint ptr = OpenProcess(ProcessAccessFlags.QueryLimitedInformation, false, processId);
+
+ return QueryFullProcessImageName(ptr, 0, buffer, ref capacity)
+ ? new string(buffer, 0, capacity)
+ : string.Empty;
+ }
+
+ // This function generates the short form of process name.
+ //
+ // This is from GetProcessShortName in NT code base.
+ // Check base\screg\winreg\perfdlls\process\perfsprc.c for details.
+ private static ReadOnlySpan GetProcessShortName(ReadOnlySpan name)
+ {
+ // Trim off everything up to and including the last slash, if there is one.
+ // If there isn't, LastIndexOf will return -1 and this will end up as a nop.
+ name = name[(name.LastIndexOf('\\') + 1)..];
+
+ // If the name ends with the ".exe" extension, then drop it, otherwise include
+ // it in the name.
+ if (name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
+ name = name[..^4];
+
+ return name;
+ }
+
+ private static void ResizeBuffer(uint size)
+ {
+ NativeMemory.Free(_buffer);
+
+ _bufferSize = size;
+ _buffer = NativeMemory.Alloc(_bufferSize);
+ }
+
+ // allocating a few more kilo bytes just in case there are some new process
+ // kicked in since new call to NtQuerySystemInformation
+ private static uint GetEstimatedBufferSize(uint actualSize) => actualSize + (1024 * 10);
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.cs b/src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.cs
index b6129cd6e..ee265e90d 100644
--- a/src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.cs
+++ b/src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.cs
@@ -1,105 +1,19 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
-using System.ComponentModel;
-using System.Diagnostics;
-using System.Globalization;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Threading;
namespace Artemis.Core.Services;
-// DarthAffe 31.08.2023: Based on how it's done in the framework:
-// https://github.com/dotnet/runtime/blob/f0463a98d105f26037f9d3e63213421a3a7d4dff/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Win32.cs#L263
-public static unsafe partial class ProcessMonitor
+public static partial class ProcessMonitor
{
- #region Native
-
- [LibraryImport("ntdll.dll", EntryPoint = "NtQuerySystemInformation")]
- private static partial uint NtQuerySystemInformation(int systemInformationClass, void* systemInformation, uint systemInformationLength, uint* returnLength);
-
- [StructLayout(LayoutKind.Sequential)]
- private struct SystemProcessInformation
- {
- // ReSharper disable MemberCanBePrivate.Local
- public uint NextEntryOffset;
- public uint NumberOfThreads;
- private fixed byte Reserved1[48];
- public UnicodeString ImageName;
- public int BasePriority;
- public nint UniqueProcessId;
- private readonly nuint Reserved2;
- public uint HandleCount;
- public uint SessionId;
- private readonly nuint Reserved3;
- public nuint PeakVirtualSize; // SIZE_T
- public nuint VirtualSize;
- private readonly uint Reserved4;
- public nuint PeakWorkingSetSize; // SIZE_T
- public nuint WorkingSetSize; // SIZE_T
- private readonly nuint Reserved5;
- public nuint QuotaPagedPoolUsage; // SIZE_T
- private readonly nuint Reserved6;
- public nuint QuotaNonPagedPoolUsage; // SIZE_T
- public nuint PagefileUsage; // SIZE_T
- public nuint PeakPagefileUsage; // SIZE_T
- public nuint PrivatePageCount; // SIZE_T
- private fixed long Reserved7[6];
- // ReSharper restore MemberCanBePrivate.Local
- }
-
- [StructLayout(LayoutKind.Sequential)]
- private struct UnicodeString
- {
- // ReSharper disable MemberCanBePrivate.Local
- ///
- /// Length in bytes, not including the null terminator, if any.
- ///
- public ushort Length;
-
- ///
- /// Max size of the buffer in bytes
- ///
- public ushort MaximumLength;
- public nint Buffer;
- // ReSharper restore MemberCanBePrivate.Local
- }
-
- [LibraryImport("kernel32.dll", EntryPoint = "QueryFullProcessImageNameW", StringMarshalling = StringMarshalling.Utf16)]
- [return: MarshalAs(UnmanagedType.Bool)]
- private static partial bool QueryFullProcessImageName(nint hProcess, int dwFlags, [Out] char[] lpExeName, ref int lpdwSize);
-
- [LibraryImport("kernel32.dll")]
- private static partial nint OpenProcess(ProcessAccessFlags processAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int processId);
-
- [Flags]
- private enum ProcessAccessFlags : uint
- {
- QueryLimitedInformation = 0x00001000
- }
-
- #endregion
-
- #region Constants
-
- private const int SYSTEM_PROCESS_INFORMATION = 5;
- private const uint STATUS_INFO_LENGTH_MISMATCH = 0xC0000004;
- private const int SYSTEM_PROCESS_ID = 4;
- private const int IDLE_PROCESS_ID = 0;
- private const int DEFAULT_BUFFER_SIZE = 1024 * 1024;
-
- #endregion
-
#region Properties & Fields
private static readonly object LOCK = new();
private static Timer? _timer;
- private static void* _buffer;
- private static uint _bufferSize;
-
private static Dictionary _processes = new();
public static ImmutableArray Processes
@@ -160,10 +74,15 @@ public static unsafe partial class ProcessMonitor
{
if (_timer != null) return;
- _bufferSize = DEFAULT_BUFFER_SIZE;
- _buffer = NativeMemory.Alloc(_bufferSize);
-
- _timer = new Timer(Environment.OSVersion.Platform == PlatformID.Win32NT ? UpdateProcessInfosNative : UpdateProcessInfosSafe, null, TimeSpan.Zero, UpdateInterval);
+ if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+ {
+ InitializeBuffer();
+ _timer = new Timer(UpdateProcessInfosWin32, null, TimeSpan.Zero, UpdateInterval);
+ }
+ else
+ {
+ _timer = new Timer(UpdateProcessInfosCrossPlatform, null, TimeSpan.Zero, UpdateInterval);
+ }
}
}
@@ -174,174 +93,13 @@ public static unsafe partial class ProcessMonitor
if (_timer == null) return;
_timer.Dispose();
-
- NativeMemory.Free(_buffer);
- _bufferSize = 0;
_timer = null;
+
+ if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+ FreeBuffer();
}
}
- #region Native ProcessInfo Update
-
- private static void UpdateProcessInfosNative(object? o)
- {
- lock (LOCK)
- {
- try
- {
- if (_bufferSize == 0) return;
-
- while (true)
- {
- uint actualSize = 0;
- uint status = NtQuerySystemInformation(SYSTEM_PROCESS_INFORMATION, _buffer, _bufferSize, &actualSize);
-
- if (status != STATUS_INFO_LENGTH_MISMATCH)
- {
- if ((int)status < 0)
- throw new InvalidOperationException("Error", new Win32Exception((int)status));
-
- UpdateProcessInfosNative(new ReadOnlySpan(_buffer, (int)actualSize));
- return;
- }
-
- ResizeBuffer(GetEstimatedBufferSize(actualSize));
- }
- }
- catch { /* Should we throw here? I guess no ... */ }
- }
- }
-
- private static void UpdateProcessInfosNative(ReadOnlySpan data)
- {
- int processInformationOffset = 0;
-
- HashSet processIds = new(_processes.Count);
-
- while (true)
- {
- ref readonly SystemProcessInformation pi = ref MemoryMarshal.AsRef(data[processInformationOffset..]);
-
- int processId = pi.UniqueProcessId.ToInt32();
- processIds.Add(processId);
-
- if (!_processes.ContainsKey(processId))
- {
- string imageName;
- string processName;
- if (pi.ImageName.Buffer != nint.Zero)
- {
- imageName = new ReadOnlySpan(pi.ImageName.Buffer.ToPointer(), pi.ImageName.Length / sizeof(char)).ToString();
- processName = GetProcessShortName(imageName).ToString();
- }
- else
- {
- imageName = string.Empty;
- processName = processId switch
- {
- SYSTEM_PROCESS_ID => "System",
- IDLE_PROCESS_ID => "Idle",
- _ => processId.ToString(CultureInfo.InvariantCulture) // use the process ID for a normal process without a name
- };
- }
-
- string executable = GetProcessFilename(processId);
- ProcessInfo processInfo = new(processId, processName, imageName, executable);
- _processes.Add(processId, processInfo);
-
- OnProcessStarted(processInfo);
- }
-
- if (pi.NextEntryOffset == 0) break;
- processInformationOffset += (int)pi.NextEntryOffset;
- }
-
- HandleStoppedProcesses(processIds);
-
- LastUpdate = DateTime.Now;
- }
-
- private static string GetProcessFilename(int processId)
- {
- int capacity = byte.MaxValue;
- char[] buffer = new char[capacity];
- nint ptr = OpenProcess(ProcessAccessFlags.QueryLimitedInformation, false, processId);
-
- return QueryFullProcessImageName(ptr, 0, buffer, ref capacity)
- ? new string(buffer, 0, capacity)
- : string.Empty;
- }
-
- // This function generates the short form of process name.
- //
- // This is from GetProcessShortName in NT code base.
- // Check base\screg\winreg\perfdlls\process\perfsprc.c for details.
- private static ReadOnlySpan GetProcessShortName(ReadOnlySpan name)
- {
- // Trim off everything up to and including the last slash, if there is one.
- // If there isn't, LastIndexOf will return -1 and this will end up as a nop.
- name = name[(name.LastIndexOf('\\') + 1)..];
-
- // If the name ends with the ".exe" extension, then drop it, otherwise include
- // it in the name.
- if (name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
- name = name[..^4];
-
- return name;
- }
-
- private static void ResizeBuffer(uint size)
- {
- NativeMemory.Free(_buffer);
-
- _bufferSize = size;
- _buffer = NativeMemory.Alloc(_bufferSize);
- }
-
- // allocating a few more kilo bytes just in case there are some new process
- // kicked in since new call to NtQuerySystemInformation
- private static uint GetEstimatedBufferSize(uint actualSize) => actualSize + (1024 * 10);
-
- #endregion
-
- #region Safe ProcessInfo Update
-
- private static void UpdateProcessInfosSafe(object? o)
- {
- lock (LOCK)
- {
- try
- {
- HashSet processIds = new(_processes.Count);
-
- foreach (Process process in Process.GetProcesses())
- {
- int processId = process.Id;
- processIds.Add(processId);
-
- if (!_processes.ContainsKey(processId))
- {
- string imageName = string.Empty;
- string processName = process.ProcessName;
- string executable = string.Empty; //TODO DarthAffe 01.09.2023: Is there a crossplatform way to do this?
-
- ProcessInfo processInfo = new(processId, processName, imageName, executable);
- _processes.Add(processId, processInfo);
-
- OnProcessStarted(processInfo);
- }
- }
-
- HandleStoppedProcesses(processIds);
-
- LastUpdate = DateTime.Now;
- }
- catch { /* Should we throw here? I guess no ... */ }
- }
- }
-
- #endregion
-
// ReSharper disable once SuggestBaseTypeForParameter
private static void HandleStoppedProcesses(HashSet currentProcessIds)
{
diff --git a/src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.xPlatform.cs b/src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.xPlatform.cs
new file mode 100644
index 000000000..90f72bed6
--- /dev/null
+++ b/src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.xPlatform.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Artemis.Core.Services;
+
+public static partial class ProcessMonitor
+{
+ #region Methods
+
+ private static void UpdateProcessInfosCrossPlatform(object? o)
+ {
+ lock (LOCK)
+ {
+ try
+ {
+ HashSet processIds = new(_processes.Count);
+
+ foreach (Process process in Process.GetProcesses())
+ {
+ int processId = process.Id;
+ processIds.Add(processId);
+
+ if (!_processes.ContainsKey(processId))
+ {
+ string imageName = string.Empty;
+ string processName = process.ProcessName;
+ string executable = string.Empty; //TODO DarthAffe 01.09.2023: Is there a crossplatform way to do this?
+
+ ProcessInfo processInfo = new(processId, processName, imageName, executable);
+ _processes.Add(processId, processInfo);
+
+ OnProcessStarted(processInfo);
+ }
+ }
+
+ HandleStoppedProcesses(processIds);
+
+ LastUpdate = DateTime.Now;
+ }
+ catch { /* Should we throw here? I guess no ... */ }
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file