diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj
index 772adf7d2..43c8f65c9 100644
--- a/src/Artemis.Core/Artemis.Core.csproj
+++ b/src/Artemis.Core/Artemis.Core.csproj
@@ -14,6 +14,7 @@
ArtemisRGB.Core
1
enable
+ true
diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings
index fe32d7dc1..32817c702 100644
--- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings
+++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings
@@ -1,5 +1,4 @@
-
+
True
True
True
@@ -80,6 +79,8 @@
True
True
True
+ True
+ True
True
True
True
diff --git a/src/Artemis.Core/Plugins/Modules/ActivationRequirements/ProcessActivationRequirement.cs b/src/Artemis.Core/Plugins/Modules/ActivationRequirements/ProcessActivationRequirement.cs
index 0be74fe7b..b9b4329c0 100644
--- a/src/Artemis.Core/Plugins/Modules/ActivationRequirements/ProcessActivationRequirement.cs
+++ b/src/Artemis.Core/Plugins/Modules/ActivationRequirements/ProcessActivationRequirement.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.IO;
using System.Linq;
using Artemis.Core.Services;
@@ -36,19 +35,17 @@ public class ProcessActivationRequirement : IModuleActivationRequirement
///
public string? Location { get; set; }
- internal static IProcessMonitorService? ProcessMonitorService { get; set; }
-
///
public bool Evaluate()
{
- if (ProcessMonitorService == null || (ProcessName == null && Location == null))
+ if (!ProcessMonitor.IsStarted || (ProcessName == null && Location == null))
return false;
- IEnumerable processes = ProcessMonitorService.GetRunningProcesses();
+ IEnumerable processes = ProcessMonitor.Processes;
if (ProcessName != null)
processes = processes.Where(p => string.Equals(p.ProcessName, ProcessName, StringComparison.InvariantCultureIgnoreCase));
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();
}
diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs
index ee1352c8d..137f339b8 100644
--- a/src/Artemis.Core/Services/CoreService.cs
+++ b/src/Artemis.Core/Services/CoreService.cs
@@ -43,8 +43,7 @@ internal class CoreService : ICoreService
IRgbService rgbService,
IProfileService profileService,
IModuleService moduleService,
- IScriptingService scriptingService,
- IProcessMonitorService _2)
+ IScriptingService scriptingService)
{
Constants.CorePlugin.Container = container;
@@ -82,8 +81,8 @@ internal class CoreService : ICoreService
string[] parts = argument.Split('=');
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!);
- LoggerFactory.LoggingLevelSwitch.MinimumLevel = (LogEventLevel) logLevelArgument;
+ _logger.Information("Setting logging level to {loggingLevel} from startup argument", (LogEventLevel)logLevelArgument!);
+ LoggerFactory.LoggingLevelSwitch.MinimumLevel = (LogEventLevel)logLevelArgument;
}
else
{
@@ -207,6 +206,8 @@ internal class CoreService : ICoreService
ApplyLoggingLevel();
+ ProcessMonitor.Start();
+
// 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
Version? hidSharpVersion = Assembly.GetAssembly(typeof(HidDevice))!.GetName().Version;
diff --git a/src/Artemis.Core/Services/ProcessMonitor/Events/ProcessEventArgs.cs b/src/Artemis.Core/Services/ProcessMonitor/Events/ProcessEventArgs.cs
deleted file mode 100644
index d4c82443e..000000000
--- a/src/Artemis.Core/Services/ProcessMonitor/Events/ProcessEventArgs.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System;
-using System.Diagnostics;
-
-namespace Artemis.Core.Services;
-
-///
-/// Contains data for the ProcessMonitor process events
-///
-public class ProcessEventArgs : EventArgs
-{
- internal ProcessEventArgs(Process process)
- {
- Process = process;
- }
-
- ///
- /// Gets the process related to the event
- ///
- public Process Process { get; }
-}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/ProcessMonitor/Interfaces/IProcessMonitorService.cs b/src/Artemis.Core/Services/ProcessMonitor/Interfaces/IProcessMonitorService.cs
deleted file mode 100644
index e3d7ccf00..000000000
--- a/src/Artemis.Core/Services/ProcessMonitor/Interfaces/IProcessMonitorService.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-
-namespace Artemis.Core.Services;
-
-///
-/// A service that provides events for started and stopped processes and a list of all running processes.
-///
-public interface IProcessMonitorService : IArtemisService
-{
- ///
- /// Occurs when a process starts.
- ///
- event EventHandler ProcessStarted;
-
- ///
- /// Occurs when a process stops.
- ///
- event EventHandler ProcessStopped;
-
- ///
- /// Returns an enumerable with the processes running on the system.
- ///
- IEnumerable GetRunningProcesses();
-}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/ProcessMonitor/ProcessComparer.cs b/src/Artemis.Core/Services/ProcessMonitor/ProcessComparer.cs
deleted file mode 100644
index 14ac5e765..000000000
--- a/src/Artemis.Core/Services/ProcessMonitor/ProcessComparer.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System.Collections.Generic;
-using System.Diagnostics;
-
-namespace Artemis.Core.Services;
-
-internal class ProcessComparer : IEqualityComparer
-{
- 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;
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/ProcessMonitor/ProcessMonitorService.cs b/src/Artemis.Core/Services/ProcessMonitor/ProcessMonitorService.cs
deleted file mode 100644
index fffcc100d..000000000
--- a/src/Artemis.Core/Services/ProcessMonitor/ProcessMonitorService.cs
+++ /dev/null
@@ -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? ProcessStarted;
- public event EventHandler? ProcessStopped;
-
- public IEnumerable GetRunningProcesses()
- {
- return _lastScannedProcesses;
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/ProcessMonitoring/Events/ProcessEventArgs.cs b/src/Artemis.Core/Services/ProcessMonitoring/Events/ProcessEventArgs.cs
new file mode 100644
index 000000000..fbacefbce
--- /dev/null
+++ b/src/Artemis.Core/Services/ProcessMonitoring/Events/ProcessEventArgs.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Artemis.Core.Services;
+
+///
+/// Contains data for the ProcessMonitor process events
+///
+public class ProcessEventArgs : EventArgs
+{
+ #region Properties & Fields
+
+ ///
+ /// Gets the process info related to the event
+ ///
+ public ProcessInfo ProcessInfo { get; }
+
+ #endregion
+
+ #region Constructors
+
+ internal ProcessEventArgs(ProcessInfo processInfo)
+ {
+ this.ProcessInfo = processInfo;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/ProcessMonitoring/ProcessInfo.cs b/src/Artemis.Core/Services/ProcessMonitoring/ProcessInfo.cs
new file mode 100644
index 000000000..c4cc6de1b
--- /dev/null
+++ b/src/Artemis.Core/Services/ProcessMonitoring/ProcessInfo.cs
@@ -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
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..ee265e90d
--- /dev/null
+++ b/src/Artemis.Core/Services/ProcessMonitoring/ProcessMonitor.cs
@@ -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 _processes = new();
+
+ public static ImmutableArray 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? ProcessStarted;
+ public static event EventHandler? 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 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
+}
\ No newline at end of file
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