mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge branch 'development'
This commit is contained in:
commit
7b7ef2045a
@ -14,6 +14,7 @@
|
|||||||
<PackageId>ArtemisRGB.Core</PackageId>
|
<PackageId>ArtemisRGB.Core</PackageId>
|
||||||
<PluginApiVersion>1</PluginApiVersion>
|
<PluginApiVersion>1</PluginApiVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib"
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=colorscience_005Cquantization/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=colorscience_005Cquantization/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=colorscience_005Csorting/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=colorscience_005Csorting/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=defaulttypes/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=defaulttypes/@EntryIndexedValue">True</s:Boolean>
|
||||||
@ -80,6 +79,8 @@
|
|||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cinput_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cinput_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cprocessmonitor/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cprocessmonitor/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cprocessmonitoring/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cprocessmonitoring_005Cevents/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cprocessmonitor_005Cevents/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cprocessmonitor_005Cevents/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cprocessmonitor_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cprocessmonitor_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cregistration/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cregistration/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
@ -36,19 +35,17 @@ public class ProcessActivationRequirement : IModuleActivationRequirement
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Location { get; set; }
|
public string? Location { get; set; }
|
||||||
|
|
||||||
internal static IProcessMonitorService? ProcessMonitorService { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Evaluate()
|
public bool Evaluate()
|
||||||
{
|
{
|
||||||
if (ProcessMonitorService == null || (ProcessName == null && Location == null))
|
if (!ProcessMonitor.IsStarted || (ProcessName == null && Location == null))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
IEnumerable<Process> processes = ProcessMonitorService.GetRunningProcesses();
|
IEnumerable<ProcessInfo> processes = ProcessMonitor.Processes;
|
||||||
if (ProcessName != null)
|
if (ProcessName != null)
|
||||||
processes = processes.Where(p => string.Equals(p.ProcessName, ProcessName, StringComparison.InvariantCultureIgnoreCase));
|
processes = processes.Where(p => string.Equals(p.ProcessName, ProcessName, StringComparison.InvariantCultureIgnoreCase));
|
||||||
if (Location != null)
|
if (Location != null)
|
||||||
processes = processes.Where(p => string.Equals(Path.GetDirectoryName(p.GetProcessFilename()), Location, StringComparison.InvariantCultureIgnoreCase));
|
processes = processes.Where(p => string.Equals(Path.GetDirectoryName(p.Executable), Location, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
return processes.Any();
|
return processes.Any();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ public class ProfilingMeasurement
|
|||||||
private long _last;
|
private long _last;
|
||||||
private bool _open;
|
private bool _open;
|
||||||
private long _start;
|
private long _start;
|
||||||
|
private ulong _count;
|
||||||
|
|
||||||
internal ProfilingMeasurement(string identifier)
|
internal ProfilingMeasurement(string identifier)
|
||||||
{
|
{
|
||||||
@ -59,6 +60,7 @@ public class ProfilingMeasurement
|
|||||||
_filledArray = true;
|
_filledArray = true;
|
||||||
_index = 0;
|
_index = 0;
|
||||||
}
|
}
|
||||||
|
_count++;
|
||||||
|
|
||||||
_last = difference;
|
_last = difference;
|
||||||
return difference;
|
return difference;
|
||||||
@ -126,6 +128,14 @@ public class ProfilingMeasurement
|
|||||||
return new TimeSpan((long) Percentile(collection, percentile));
|
return new TimeSpan((long) Percentile(collection, percentile));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of measurements taken
|
||||||
|
/// </summary>
|
||||||
|
public ulong GetCount()
|
||||||
|
{
|
||||||
|
return _count;
|
||||||
|
}
|
||||||
|
|
||||||
private static double Percentile(long[] elements, double percentile)
|
private static double Percentile(long[] elements, double percentile)
|
||||||
{
|
{
|
||||||
Array.Sort(elements);
|
Array.Sort(elements);
|
||||||
|
|||||||
@ -43,8 +43,7 @@ internal class CoreService : ICoreService
|
|||||||
IRgbService rgbService,
|
IRgbService rgbService,
|
||||||
IProfileService profileService,
|
IProfileService profileService,
|
||||||
IModuleService moduleService,
|
IModuleService moduleService,
|
||||||
IScriptingService scriptingService,
|
IScriptingService scriptingService)
|
||||||
IProcessMonitorService _2)
|
|
||||||
{
|
{
|
||||||
Constants.CorePlugin.Container = container;
|
Constants.CorePlugin.Container = container;
|
||||||
|
|
||||||
@ -82,8 +81,8 @@ internal class CoreService : ICoreService
|
|||||||
string[] parts = argument.Split('=');
|
string[] parts = argument.Split('=');
|
||||||
if (parts.Length == 2 && Enum.TryParse(typeof(LogEventLevel), parts[1], true, out object? logLevelArgument))
|
if (parts.Length == 2 && Enum.TryParse(typeof(LogEventLevel), parts[1], true, out object? logLevelArgument))
|
||||||
{
|
{
|
||||||
_logger.Information("Setting logging level to {loggingLevel} from startup argument", (LogEventLevel) logLevelArgument!);
|
_logger.Information("Setting logging level to {loggingLevel} from startup argument", (LogEventLevel)logLevelArgument!);
|
||||||
LoggerFactory.LoggingLevelSwitch.MinimumLevel = (LogEventLevel) logLevelArgument;
|
LoggerFactory.LoggingLevelSwitch.MinimumLevel = (LogEventLevel)logLevelArgument;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -207,6 +206,8 @@ internal class CoreService : ICoreService
|
|||||||
|
|
||||||
ApplyLoggingLevel();
|
ApplyLoggingLevel();
|
||||||
|
|
||||||
|
ProcessMonitor.Start();
|
||||||
|
|
||||||
// Don't remove even if it looks useless
|
// Don't remove even if it looks useless
|
||||||
// Just this line should prevent a certain someone from removing HidSharp as an unused dependency as well
|
// Just this line should prevent a certain someone from removing HidSharp as an unused dependency as well
|
||||||
Version? hidSharpVersion = Assembly.GetAssembly(typeof(HidDevice))!.GetName().Version;
|
Version? hidSharpVersion = Assembly.GetAssembly(typeof(HidDevice))!.GetName().Version;
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains data for the ProcessMonitor process events
|
|
||||||
/// </summary>
|
|
||||||
public class ProcessEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
internal ProcessEventArgs(Process process)
|
|
||||||
{
|
|
||||||
Process = process;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the process related to the event
|
|
||||||
/// </summary>
|
|
||||||
public Process Process { get; }
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A service that provides events for started and stopped processes and a list of all running processes.
|
|
||||||
/// </summary>
|
|
||||||
public interface IProcessMonitorService : IArtemisService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Occurs when a process starts.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<ProcessEventArgs> ProcessStarted;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Occurs when a process stops.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<ProcessEventArgs> ProcessStopped;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns an enumerable with the processes running on the system.
|
|
||||||
/// </summary>
|
|
||||||
IEnumerable<Process> GetRunningProcesses();
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
|
||||||
|
|
||||||
internal class ProcessComparer : IEqualityComparer<Process>
|
|
||||||
{
|
|
||||||
public bool Equals(Process? x, Process? y)
|
|
||||||
{
|
|
||||||
if (x == null && y == null) return true;
|
|
||||||
if (x == null || y == null) return false;
|
|
||||||
return x.Id == y.Id && x.ProcessName == y.ProcessName && x.SessionId == y.SessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetHashCode(Process? obj)
|
|
||||||
{
|
|
||||||
if (obj == null) return 0;
|
|
||||||
return obj.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Timers;
|
|
||||||
using Artemis.Core.Modules;
|
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
|
||||||
|
|
||||||
internal class ProcessMonitorService : IProcessMonitorService
|
|
||||||
{
|
|
||||||
private readonly ProcessComparer _comparer;
|
|
||||||
private Process[] _lastScannedProcesses;
|
|
||||||
|
|
||||||
public ProcessMonitorService()
|
|
||||||
{
|
|
||||||
_comparer = new ProcessComparer();
|
|
||||||
_lastScannedProcesses = Process.GetProcesses();
|
|
||||||
Timer processScanTimer = new(1000);
|
|
||||||
processScanTimer.Elapsed += OnTimerElapsed;
|
|
||||||
processScanTimer.Start();
|
|
||||||
|
|
||||||
ProcessActivationRequirement.ProcessMonitorService = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTimerElapsed(object? sender, ElapsedEventArgs e)
|
|
||||||
{
|
|
||||||
Process[] newProcesses = Process.GetProcesses();
|
|
||||||
foreach (Process startedProcess in newProcesses.Except(_lastScannedProcesses, _comparer))
|
|
||||||
ProcessStarted?.Invoke(this, new ProcessEventArgs(startedProcess));
|
|
||||||
foreach (Process stoppedProcess in _lastScannedProcesses.Except(newProcesses, _comparer))
|
|
||||||
ProcessStopped?.Invoke(this, new ProcessEventArgs(stoppedProcess));
|
|
||||||
|
|
||||||
_lastScannedProcesses = newProcesses;
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler<ProcessEventArgs>? ProcessStarted;
|
|
||||||
public event EventHandler<ProcessEventArgs>? ProcessStopped;
|
|
||||||
|
|
||||||
public IEnumerable<Process> GetRunningProcesses()
|
|
||||||
{
|
|
||||||
return _lastScannedProcesses;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Artemis.Core.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains data for the ProcessMonitor process events
|
||||||
|
/// </summary>
|
||||||
|
public class ProcessEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
#region Properties & Fields
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the process info related to the event
|
||||||
|
/// </summary>
|
||||||
|
public ProcessInfo ProcessInfo { get; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
|
||||||
|
internal ProcessEventArgs(ProcessInfo processInfo)
|
||||||
|
{
|
||||||
|
this.ProcessInfo = processInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
25
src/Artemis.Core/Services/ProcessMonitoring/ProcessInfo.cs
Normal file
25
src/Artemis.Core/Services/ProcessMonitoring/ProcessInfo.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
namespace Artemis.Core.Services;
|
||||||
|
|
||||||
|
public readonly struct ProcessInfo
|
||||||
|
{
|
||||||
|
#region Properties & Fields
|
||||||
|
|
||||||
|
public readonly int ProcessId;
|
||||||
|
public readonly string ProcessName;
|
||||||
|
public readonly string ImageName; //TODO DarthAffe 01.09.2023: Do we need this if we can't get it through Process.GetProcesses()?
|
||||||
|
public readonly string Executable;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
|
||||||
|
public ProcessInfo(int processId, string processName, string imageName, string executable)
|
||||||
|
{
|
||||||
|
this.ProcessId = processId;
|
||||||
|
this.ProcessName = processName;
|
||||||
|
this.ImageName = imageName;
|
||||||
|
this.Executable = executable;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
/// <summary>
|
||||||
|
/// Length in bytes, not including the null terminator, if any.
|
||||||
|
/// </summary>
|
||||||
|
public ushort Length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Max size of the buffer in bytes
|
||||||
|
/// </summary>
|
||||||
|
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<byte>(_buffer, (int)actualSize));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResizeBuffer(GetEstimatedBufferSize(actualSize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* Should we throw here? I guess no ... */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateProcessInfosWin32(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
int processInformationOffset = 0;
|
||||||
|
|
||||||
|
HashSet<int> processIds = new(_processes.Count);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
ref readonly SystemProcessInformation pi = ref MemoryMarshal.AsRef<SystemProcessInformation>(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<char>(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<char> GetProcessShortName(ReadOnlySpan<char> 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
|
||||||
|
}
|
||||||
135
src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.cs
Normal file
135
src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.cs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Artemis.Core.Services;
|
||||||
|
|
||||||
|
public static partial class ProcessMonitor
|
||||||
|
{
|
||||||
|
#region Properties & Fields
|
||||||
|
|
||||||
|
private static readonly object LOCK = new();
|
||||||
|
|
||||||
|
private static Timer? _timer;
|
||||||
|
|
||||||
|
private static Dictionary<int, ProcessInfo> _processes = new();
|
||||||
|
|
||||||
|
public static ImmutableArray<ProcessInfo> Processes
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (LOCK)
|
||||||
|
return _processes.Values.ToImmutableArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static DateTime LastUpdate { get; private set; }
|
||||||
|
|
||||||
|
private static TimeSpan _updateInterval = TimeSpan.FromSeconds(1);
|
||||||
|
public static TimeSpan UpdateInterval
|
||||||
|
{
|
||||||
|
get => _updateInterval;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_updateInterval = value;
|
||||||
|
|
||||||
|
lock (LOCK)
|
||||||
|
_timer?.Change(TimeSpan.Zero, _updateInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsStarted
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (LOCK)
|
||||||
|
return _timer != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
public static event EventHandler<ProcessEventArgs>? ProcessStarted;
|
||||||
|
public static event EventHandler<ProcessEventArgs>? ProcessStopped;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
|
||||||
|
static ProcessMonitor()
|
||||||
|
{
|
||||||
|
Utilities.ShutdownRequested += (_, _) => Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Methods
|
||||||
|
|
||||||
|
public static void Start()
|
||||||
|
{
|
||||||
|
lock (LOCK)
|
||||||
|
{
|
||||||
|
if (_timer != null) return;
|
||||||
|
|
||||||
|
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
|
||||||
|
{
|
||||||
|
InitializeBuffer();
|
||||||
|
_timer = new Timer(UpdateProcessInfosWin32, null, TimeSpan.Zero, UpdateInterval);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_timer = new Timer(UpdateProcessInfosCrossPlatform, null, TimeSpan.Zero, UpdateInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Stop()
|
||||||
|
{
|
||||||
|
lock (LOCK)
|
||||||
|
{
|
||||||
|
if (_timer == null) return;
|
||||||
|
|
||||||
|
_timer.Dispose();
|
||||||
|
_timer = null;
|
||||||
|
|
||||||
|
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
|
||||||
|
FreeBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter
|
||||||
|
private static void HandleStoppedProcesses(HashSet<int> currentProcessIds)
|
||||||
|
{
|
||||||
|
int[] oldProcessIds = _processes.Keys.ToArray();
|
||||||
|
foreach (int id in oldProcessIds)
|
||||||
|
if (!currentProcessIds.Contains(id))
|
||||||
|
{
|
||||||
|
ProcessInfo info = _processes[id];
|
||||||
|
_processes.Remove(id);
|
||||||
|
OnProcessStopped(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnProcessStarted(ProcessInfo processInfo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProcessStarted?.Invoke(null, new ProcessEventArgs(processInfo));
|
||||||
|
}
|
||||||
|
catch { /* Subscribers are idiots! */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnProcessStopped(ProcessInfo processInfo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProcessStopped?.Invoke(null, new ProcessEventArgs(processInfo));
|
||||||
|
}
|
||||||
|
catch { /* Subscribers are idiots! */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@ -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<int> 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
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Timers;
|
using System.Timers;
|
||||||
@ -22,6 +23,7 @@ public class WindowsInputProvider : InputProvider
|
|||||||
private readonly IInputService _inputService;
|
private readonly IInputService _inputService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly Timer _taskManagerTimer;
|
private readonly Timer _taskManagerTimer;
|
||||||
|
private readonly Dictionary<RawInputDeviceHandle, string> _handleToDevicePath;
|
||||||
|
|
||||||
private int _lastProcessId;
|
private int _lastProcessId;
|
||||||
delegate nint WndProc(nint hWnd, uint msg, nint wParam, nint lParam);
|
delegate nint WndProc(nint hWnd, uint msg, nint wParam, nint lParam);
|
||||||
@ -40,6 +42,7 @@ public class WindowsInputProvider : InputProvider
|
|||||||
_taskManagerTimer = new Timer(500);
|
_taskManagerTimer = new Timer(500);
|
||||||
_taskManagerTimer.Elapsed += TaskManagerTimerOnElapsed;
|
_taskManagerTimer.Elapsed += TaskManagerTimerOnElapsed;
|
||||||
_taskManagerTimer.Start();
|
_taskManagerTimer.Start();
|
||||||
|
_handleToDevicePath = new Dictionary<RawInputDeviceHandle, string>();
|
||||||
|
|
||||||
_hWnd = User32.CreateWindowEx(0, "STATIC", "", 0x80000000, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
_hWnd = User32.CreateWindowEx(0, "STATIC", "", 0x80000000, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
||||||
_hWndProcHook = User32.GetWindowLongPtr(_hWnd, GWL_WNDPROC);
|
_hWndProcHook = User32.GetWindowLongPtr(_hWnd, GWL_WNDPROC);
|
||||||
@ -106,6 +109,20 @@ public class WindowsInputProvider : InputProvider
|
|||||||
if (active?.ProcessName == "Taskmgr" || active?.ProcessName == "Idle")
|
if (active?.ProcessName == "Taskmgr" || active?.ProcessName == "Idle")
|
||||||
_inputService.ReleaseAll();
|
_inputService.ReleaseAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string? GetDeviceIdentifier(in RawInputData data)
|
||||||
|
{
|
||||||
|
if (_handleToDevicePath.TryGetValue(data.Header.DeviceHandle, out string? identifier))
|
||||||
|
return identifier;
|
||||||
|
|
||||||
|
string? newIdentifier = data.Device?.DevicePath;
|
||||||
|
if (newIdentifier == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
_handleToDevicePath.Add(data.Header.DeviceHandle, newIdentifier);
|
||||||
|
|
||||||
|
return newIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
#region Keyboard
|
#region Keyboard
|
||||||
|
|
||||||
@ -126,7 +143,7 @@ public class WindowsInputProvider : InputProvider
|
|||||||
if (key == KeyboardKey.None)
|
if (key == KeyboardKey.None)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
string? identifier = data.Device?.DevicePath;
|
string? identifier = GetDeviceIdentifier(in data);
|
||||||
|
|
||||||
// Let the core know there is an identifier so it can store new identifications if applicable
|
// Let the core know there is an identifier so it can store new identifications if applicable
|
||||||
if (identifier != null)
|
if (identifier != null)
|
||||||
@ -164,7 +181,7 @@ public class WindowsInputProvider : InputProvider
|
|||||||
|
|
||||||
private int _previousMouseX;
|
private int _previousMouseX;
|
||||||
private int _previousMouseY;
|
private int _previousMouseY;
|
||||||
|
|
||||||
private void HandleMouseData(RawInputData data, RawInputMouseData mouseData)
|
private void HandleMouseData(RawInputData data, RawInputMouseData mouseData)
|
||||||
{
|
{
|
||||||
// Only submit mouse movement 25 times per second but increment the delta
|
// Only submit mouse movement 25 times per second but increment the delta
|
||||||
@ -176,7 +193,7 @@ public class WindowsInputProvider : InputProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArtemisDevice? device = null;
|
ArtemisDevice? device = null;
|
||||||
string? identifier = data.Device?.DevicePath;
|
string? identifier = GetDeviceIdentifier(in data);
|
||||||
if (identifier != null)
|
if (identifier != null)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@ -10,6 +10,7 @@ public class PerformanceDebugMeasurementViewModel : ViewModelBase
|
|||||||
private string? _max;
|
private string? _max;
|
||||||
private string? _min;
|
private string? _min;
|
||||||
private string? _percentile;
|
private string? _percentile;
|
||||||
|
private string? _count;
|
||||||
|
|
||||||
public PerformanceDebugMeasurementViewModel(ProfilingMeasurement measurement)
|
public PerformanceDebugMeasurementViewModel(ProfilingMeasurement measurement)
|
||||||
{
|
{
|
||||||
@ -47,6 +48,12 @@ public class PerformanceDebugMeasurementViewModel : ViewModelBase
|
|||||||
get => _percentile;
|
get => _percentile;
|
||||||
set => RaiseAndSetIfChanged(ref _percentile, value);
|
set => RaiseAndSetIfChanged(ref _percentile, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string? Count
|
||||||
|
{
|
||||||
|
get => _count;
|
||||||
|
set => RaiseAndSetIfChanged(ref _count, value);
|
||||||
|
}
|
||||||
|
|
||||||
public void Update()
|
public void Update()
|
||||||
{
|
{
|
||||||
@ -55,5 +62,6 @@ public class PerformanceDebugMeasurementViewModel : ViewModelBase
|
|||||||
Min = Measurement.GetMin().TotalMilliseconds + " ms";
|
Min = Measurement.GetMin().TotalMilliseconds + " ms";
|
||||||
Max = Measurement.GetMax().TotalMilliseconds + " ms";
|
Max = Measurement.GetMax().TotalMilliseconds + " ms";
|
||||||
Percentile = Measurement.GetPercentile(0.95).TotalMilliseconds + " ms";
|
Percentile = Measurement.GetPercentile(0.95).TotalMilliseconds + " ms";
|
||||||
|
Count = Measurement.GetCount().ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -23,6 +23,7 @@
|
|||||||
<DataGridTextColumn Binding="{CompiledBinding Max}" Header="Max" />
|
<DataGridTextColumn Binding="{CompiledBinding Max}" Header="Max" />
|
||||||
<DataGridTextColumn Binding="{CompiledBinding Average}" Header="Average" />
|
<DataGridTextColumn Binding="{CompiledBinding Average}" Header="Average" />
|
||||||
<DataGridTextColumn Binding="{CompiledBinding Percentile}" Header="95th percentile" />
|
<DataGridTextColumn Binding="{CompiledBinding Percentile}" Header="95th percentile" />
|
||||||
|
<DataGridTextColumn Binding="{CompiledBinding Count}" Header="Number of Calls" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user