mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Debugger - Added performance debugging
Windows - Added back forced shutdown
This commit is contained in:
parent
4325005b0e
commit
90d028fd58
@ -636,26 +636,6 @@
|
||||
is registered
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:Artemis.UI.Shared.Events.DataModelInputDynamicEventArgs">
|
||||
<summary>
|
||||
Provides data about selection events raised by <see cref="!:DataModelDynamicViewModel" />
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:Artemis.UI.Shared.Events.DataModelInputDynamicEventArgs.DataModelPath">
|
||||
<summary>
|
||||
Gets the data model path that was selected
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:Artemis.UI.Shared.Events.DataModelInputStaticEventArgs">
|
||||
<summary>
|
||||
Provides data about submit events raised by <see cref="!:DataModelStaticViewModel" />
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:Artemis.UI.Shared.Events.DataModelInputStaticEventArgs.Value">
|
||||
<summary>
|
||||
The value that was submitted
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:Artemis.UI.Shared.Events.LedClickedEventArgs">
|
||||
<summary>
|
||||
Provides data on LED click events raised by the device visualizer
|
||||
|
||||
@ -94,7 +94,7 @@ namespace Artemis.UI
|
||||
{
|
||||
CancellationTokenSource cts = new();
|
||||
cts.CancelAfter(2000);
|
||||
|
||||
|
||||
HttpResponseMessage httpResponseMessage = client.Send(new HttpRequestMessage(HttpMethod.Post, url + "remote/bring-to-foreground"), cts.Token);
|
||||
httpResponseMessage.EnsureSuccessStatusCode();
|
||||
return true;
|
||||
|
||||
@ -22,8 +22,8 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance
|
||||
|
||||
private void UpdateTimerOnElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
foreach (PerformanceDebugPluginViewModel viewModel in Items)
|
||||
viewModel.Update();
|
||||
foreach (PerformanceDebugPluginViewModel viewModel in Items)
|
||||
viewModel.Update();
|
||||
}
|
||||
|
||||
private void FeatureToggled(object sender, PluginFeatureEventArgs e)
|
||||
|
||||
@ -28,6 +28,12 @@
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.card-condensed">
|
||||
<Setter Property="Padding" Value="15" />
|
||||
<Setter Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Separator.card-separator">
|
||||
<Setter Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}" />
|
||||
<Setter Property="Margin" Value="-12 15" />
|
||||
|
||||
@ -3,15 +3,19 @@ using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.Styling;
|
||||
using Ninject;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Windows
|
||||
{
|
||||
public class App : Application
|
||||
{
|
||||
private StandardKernel _kernel;
|
||||
private ApplicationStateManager _stateManager;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
ArtemisBootstrapper.Bootstrap();
|
||||
_kernel = ArtemisBootstrapper.Bootstrap();
|
||||
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
@ -22,6 +26,8 @@ namespace Artemis.UI.Windows
|
||||
{
|
||||
ArtemisBootstrapper.ConfigureApplicationLifetime(desktop);
|
||||
AvaloniaLocator.Current.GetService<FluentAvaloniaTheme>().ForceNativeTitleBarToTheme(desktop.MainWindow, "Dark");
|
||||
|
||||
_stateManager = new ApplicationStateManager(_kernel, desktop.Args);
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
|
||||
186
src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs
Normal file
186
src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs
Normal file
@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Windows.Utilities;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Threading;
|
||||
using Ninject;
|
||||
|
||||
namespace Artemis.UI.Windows
|
||||
{
|
||||
public class ApplicationStateManager
|
||||
{
|
||||
private readonly IWindowService _windowService;
|
||||
|
||||
// ReSharper disable once NotAccessedField.Local - Kept in scope to ensure it does not get released
|
||||
private Mutex? _artemisMutex;
|
||||
|
||||
public ApplicationStateManager(IKernel kernel, string[] startupArguments)
|
||||
{
|
||||
_windowService = kernel.Get<IWindowService>();
|
||||
StartupArguments = startupArguments;
|
||||
IsElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
|
||||
Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested;
|
||||
Core.Utilities.RestartRequested += UtilitiesOnRestartRequested;
|
||||
|
||||
// On Windows shutdown dispose the kernel just so device providers get a chance to clean up
|
||||
if (Application.Current.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime)
|
||||
{
|
||||
controlledApplicationLifetime.Exit += (_, _) =>
|
||||
{
|
||||
RunForcedShutdownIfEnabled();
|
||||
kernel.Dispose();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string[] StartupArguments { get; }
|
||||
public bool IsElevated { get; }
|
||||
|
||||
public bool FocusExistingInstance()
|
||||
{
|
||||
_artemisMutex = new Mutex(true, "Artemis-3c24b502-64e6-4587-84bf-9072970e535d", out bool createdNew);
|
||||
if (createdNew)
|
||||
return false;
|
||||
|
||||
return RemoteFocus();
|
||||
}
|
||||
|
||||
public void DisplayException(Exception e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_windowService.ShowExceptionDialog("An unhandled exception occured", e);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored, we tried
|
||||
}
|
||||
}
|
||||
|
||||
private bool RemoteFocus()
|
||||
{
|
||||
// At this point we cannot read the database yet to retrieve the web server port.
|
||||
// Instead use the method external applications should use as well.
|
||||
if (!File.Exists(Path.Combine(Constants.DataFolder, "webserver.txt")))
|
||||
{
|
||||
KillOtherInstances();
|
||||
return false;
|
||||
}
|
||||
|
||||
string url = File.ReadAllText(Path.Combine(Constants.DataFolder, "webserver.txt"));
|
||||
using HttpClient client = new();
|
||||
try
|
||||
{
|
||||
CancellationTokenSource cts = new();
|
||||
cts.CancelAfter(2000);
|
||||
|
||||
HttpResponseMessage httpResponseMessage = client.Send(new HttpRequestMessage(HttpMethod.Post, url + "remote/bring-to-foreground"), cts.Token);
|
||||
httpResponseMessage.EnsureSuccessStatusCode();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
KillOtherInstances();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void KillOtherInstances()
|
||||
{
|
||||
// Kill everything else heh
|
||||
List<Process> processes = Process.GetProcessesByName("Artemis.UI").Where(p => p.Id != Process.GetCurrentProcess().Id).ToList();
|
||||
foreach (Process process in processes)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill(true);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UtilitiesOnRestartRequested(object? sender, RestartEventArgs e)
|
||||
{
|
||||
List<string> argsList = new();
|
||||
argsList.AddRange(StartupArguments);
|
||||
if (e.ExtraArgs != null)
|
||||
argsList.AddRange(e.ExtraArgs.Except(argsList));
|
||||
string args = argsList.Any() ? "-ArgumentList " + string.Join(',', argsList) : "";
|
||||
string command =
|
||||
$"-Command \"& {{Start-Sleep -Milliseconds {(int) e.Delay.TotalMilliseconds}; " +
|
||||
"(Get-Process 'Artemis.UI').kill(); " +
|
||||
$"Start-Process -FilePath '{Constants.ExecutablePath}' -WorkingDirectory '{Constants.ApplicationFolder}' {args}}}\"";
|
||||
// Elevated always runs with RunAs
|
||||
if (e.Elevate)
|
||||
{
|
||||
ProcessStartInfo info = new()
|
||||
{
|
||||
Arguments = command.Replace("}\"", " -Verb RunAs}\""),
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
CreateNoWindow = true,
|
||||
FileName = "PowerShell.exe"
|
||||
};
|
||||
Process.Start(info);
|
||||
}
|
||||
// Non-elevated runs regularly if currently not elevated
|
||||
else if (!IsElevated)
|
||||
{
|
||||
ProcessStartInfo info = new()
|
||||
{
|
||||
Arguments = command,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
CreateNoWindow = true,
|
||||
FileName = "PowerShell.exe"
|
||||
};
|
||||
Process.Start(info);
|
||||
}
|
||||
// Non-elevated runs via a utility method is currently elevated (de-elevating is hacky)
|
||||
else
|
||||
{
|
||||
string powerShell = Path.Combine(Environment.SystemDirectory, "WindowsPowerShell", "v1.0", "powershell.exe");
|
||||
ProcessUtilities.RunAsDesktopUser(powerShell, command, true);
|
||||
}
|
||||
|
||||
// Lets try a graceful shutdown, PowerShell will kill if needed
|
||||
if (Application.Current.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime)
|
||||
Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown());
|
||||
}
|
||||
|
||||
private void UtilitiesOnShutdownRequested(object? sender, EventArgs e)
|
||||
{
|
||||
// Use PowerShell to kill the process after 8 sec just in case
|
||||
RunForcedShutdownIfEnabled();
|
||||
|
||||
if (Application.Current.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime)
|
||||
Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown());
|
||||
}
|
||||
|
||||
private void RunForcedShutdownIfEnabled()
|
||||
{
|
||||
if (StartupArguments.Contains("--disable-forced-shutdown"))
|
||||
return;
|
||||
|
||||
ProcessStartInfo info = new()
|
||||
{
|
||||
Arguments = "-Command \"& {Start-Sleep -s 8; (Get-Process -Id " + Process.GetCurrentProcess().Id + ").kill()}",
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
CreateNoWindow = true,
|
||||
FileName = "PowerShell.exe"
|
||||
};
|
||||
Process.Start(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
|
||||
235
src/Avalonia/Artemis.UI.Windows/Utilities/ProcessUtilities.cs
Normal file
235
src/Avalonia/Artemis.UI.Windows/Utilities/ProcessUtilities.cs
Normal file
@ -0,0 +1,235 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Artemis.UI.Windows.Utilities
|
||||
{
|
||||
public static class ProcessUtilities
|
||||
{
|
||||
public static Process RunAsDesktopUser(string fileName, string arguments, bool hideWindow)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
throw new ArgumentException("Value cannot be null or whitespace.", nameof(fileName));
|
||||
|
||||
// To start process as shell user you will need to carry out these steps:
|
||||
// 1. Enable the SeIncreaseQuotaPrivilege in your current token
|
||||
// 2. Get an HWND representing the desktop shell (GetShellWindow)
|
||||
// 3. Get the Process ID(PID) of the process associated with that window(GetWindowThreadProcessId)
|
||||
// 4. Open that process(OpenProcess)
|
||||
// 5. Get the access token from that process (OpenProcessToken)
|
||||
// 6. Make a primary token with that token(DuplicateTokenEx)
|
||||
// 7. Start the new process with that primary token(CreateProcessWithTokenW)
|
||||
|
||||
IntPtr hProcessToken = IntPtr.Zero;
|
||||
// Enable SeIncreaseQuotaPrivilege in this process. (This won't work if current process is not elevated.)
|
||||
try
|
||||
{
|
||||
IntPtr process = GetCurrentProcess();
|
||||
if (!OpenProcessToken(process, 0x0020, ref hProcessToken))
|
||||
return null;
|
||||
|
||||
TOKEN_PRIVILEGES tkp = new()
|
||||
{
|
||||
PrivilegeCount = 1,
|
||||
Privileges = new LUID_AND_ATTRIBUTES[1]
|
||||
};
|
||||
|
||||
if (!LookupPrivilegeValue(null, "SeIncreaseQuotaPrivilege", ref tkp.Privileges[0].Luid))
|
||||
return null;
|
||||
|
||||
tkp.Privileges[0].Attributes = 0x00000002;
|
||||
|
||||
if (!AdjustTokenPrivileges(hProcessToken, false, ref tkp, 0, IntPtr.Zero, IntPtr.Zero))
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(hProcessToken);
|
||||
}
|
||||
|
||||
// Get an HWND representing the desktop shell.
|
||||
// CAVEATS: This will fail if the shell is not running (crashed or terminated), or the default shell has been
|
||||
// replaced with a custom shell. This also won't return what you probably want if Explorer has been terminated and
|
||||
// restarted elevated.
|
||||
IntPtr hwnd = GetShellWindow();
|
||||
if (hwnd == IntPtr.Zero)
|
||||
return null;
|
||||
|
||||
IntPtr hShellProcess = IntPtr.Zero;
|
||||
IntPtr hShellProcessToken = IntPtr.Zero;
|
||||
IntPtr hPrimaryToken = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
// Get the PID of the desktop shell process.
|
||||
uint dwPID;
|
||||
if (GetWindowThreadProcessId(hwnd, out dwPID) == 0)
|
||||
return null;
|
||||
|
||||
// Open the desktop shell process in order to query it (get the token)
|
||||
hShellProcess = OpenProcess(ProcessAccessFlags.QueryInformation, false, dwPID);
|
||||
if (hShellProcess == IntPtr.Zero)
|
||||
return null;
|
||||
|
||||
// Get the process token of the desktop shell.
|
||||
if (!OpenProcessToken(hShellProcess, 0x0002, ref hShellProcessToken))
|
||||
return null;
|
||||
|
||||
uint dwTokenRights = 395U;
|
||||
|
||||
// Duplicate the shell's process token to get a primary token.
|
||||
// Based on experimentation, this is the minimal set of rights required for CreateProcessWithTokenW (contrary to current documentation).
|
||||
if (!DuplicateTokenEx(hShellProcessToken, dwTokenRights, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out hPrimaryToken))
|
||||
return null;
|
||||
|
||||
// Start the target process with the new token.
|
||||
STARTUPINFO si = new();
|
||||
if (hideWindow)
|
||||
{
|
||||
si.dwFlags = 0x00000001;
|
||||
si.wShowWindow = 0;
|
||||
}
|
||||
|
||||
PROCESS_INFORMATION pi = new();
|
||||
if (!CreateProcessWithTokenW(hPrimaryToken, 0, fileName, $"\"{fileName}\" {arguments}", 0, IntPtr.Zero, Path.GetDirectoryName(fileName), ref si, out pi))
|
||||
{
|
||||
// Get the last error and display it.
|
||||
int error = Marshal.GetLastWin32Error();
|
||||
return null;
|
||||
}
|
||||
|
||||
return Process.GetProcessById(pi.dwProcessId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(hShellProcessToken);
|
||||
CloseHandle(hPrimaryToken);
|
||||
CloseHandle(hShellProcess);
|
||||
}
|
||||
}
|
||||
|
||||
#region Interop
|
||||
|
||||
private struct TOKEN_PRIVILEGES
|
||||
{
|
||||
public uint PrivilegeCount;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
|
||||
public LUID_AND_ATTRIBUTES[] Privileges;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
private struct LUID_AND_ATTRIBUTES
|
||||
{
|
||||
public LUID Luid;
|
||||
public uint Attributes;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct LUID
|
||||
{
|
||||
public readonly uint LowPart;
|
||||
public readonly int HighPart;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum ProcessAccessFlags : uint
|
||||
{
|
||||
All = 0x001F0FFF,
|
||||
Terminate = 0x00000001,
|
||||
CreateThread = 0x00000002,
|
||||
VirtualMemoryOperation = 0x00000008,
|
||||
VirtualMemoryRead = 0x00000010,
|
||||
VirtualMemoryWrite = 0x00000020,
|
||||
DuplicateHandle = 0x00000040,
|
||||
CreateProcess = 0x000000080,
|
||||
SetQuota = 0x00000100,
|
||||
SetInformation = 0x00000200,
|
||||
QueryInformation = 0x00000400,
|
||||
QueryLimitedInformation = 0x00001000,
|
||||
Synchronize = 0x00100000
|
||||
}
|
||||
|
||||
private enum SECURITY_IMPERSONATION_LEVEL
|
||||
{
|
||||
SecurityAnonymous,
|
||||
SecurityIdentification,
|
||||
SecurityImpersonation,
|
||||
SecurityDelegation
|
||||
}
|
||||
|
||||
private enum TOKEN_TYPE
|
||||
{
|
||||
TokenPrimary = 1,
|
||||
TokenImpersonation
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct PROCESS_INFORMATION
|
||||
{
|
||||
public readonly IntPtr hProcess;
|
||||
public readonly IntPtr hThread;
|
||||
public readonly int dwProcessId;
|
||||
public readonly int dwThreadId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct STARTUPINFO
|
||||
{
|
||||
public readonly int cb;
|
||||
public readonly string lpReserved;
|
||||
public readonly string lpDesktop;
|
||||
public readonly string lpTitle;
|
||||
public readonly int dwX;
|
||||
public readonly int dwY;
|
||||
public readonly int dwXSize;
|
||||
public readonly int dwYSize;
|
||||
public readonly int dwXCountChars;
|
||||
public readonly int dwYCountChars;
|
||||
public readonly int dwFillAttribute;
|
||||
public int dwFlags;
|
||||
public short wShowWindow;
|
||||
public readonly short cbReserved2;
|
||||
public readonly IntPtr lpReserved2;
|
||||
public readonly IntPtr hStdInput;
|
||||
public readonly IntPtr hStdOutput;
|
||||
public readonly IntPtr hStdError;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", ExactSpelling = true)]
|
||||
private static extern IntPtr GetCurrentProcess();
|
||||
|
||||
[DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
|
||||
private static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool LookupPrivilegeValue(string host, string name, ref LUID pluid);
|
||||
|
||||
[DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
|
||||
private static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TOKEN_PRIVILEGES newst, int len, IntPtr prev, IntPtr relen);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetShellWindow();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, uint processId);
|
||||
|
||||
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes, SECURITY_IMPERSONATION_LEVEL impersonationLevel, TOKEN_TYPE tokenType,
|
||||
out IntPtr phNewToken);
|
||||
|
||||
[DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool CreateProcessWithTokenW(IntPtr hToken, int dwLogonFlags, string lpApplicationName, string lpCommandLine, int dwCreationFlags, IntPtr lpEnvironment,
|
||||
string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,7 @@ namespace Artemis.UI
|
||||
{
|
||||
private static StandardKernel? _kernel;
|
||||
|
||||
public static void Bootstrap()
|
||||
public static StandardKernel Bootstrap()
|
||||
{
|
||||
if (_kernel != null)
|
||||
throw new ArtemisUIException("UI already bootstrapped");
|
||||
@ -26,6 +26,8 @@ namespace Artemis.UI
|
||||
_kernel.Load<SharedUIModule>();
|
||||
|
||||
_kernel.UseNinjectDependencyResolver();
|
||||
|
||||
return _kernel;
|
||||
}
|
||||
|
||||
public static void ConfigureApplicationLifetime(IClassicDesktopStyleApplicationLifetime applicationLifetime)
|
||||
|
||||
@ -10,6 +10,14 @@
|
||||
<converters:TypeToStringConverter x:Key="TypeToStringConverter" />
|
||||
</UserControl.Resources>
|
||||
<StackPanel>
|
||||
<TextBlock Classes="h3">Data Model</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
On this page you can view the contents of the Artemis data model.
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap" Classes="subtitle" Margin="0 10">
|
||||
Please note that having this window open can have a performance impact on your system.
|
||||
</TextBlock>
|
||||
|
||||
<TreeView Items="{Binding MainDataModel.Children}">
|
||||
<TreeView.Styles>
|
||||
<Style Selector="TreeViewItem">
|
||||
|
||||
@ -4,5 +4,13 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Debugger.Tabs.Logs.LogsDebugView">
|
||||
<TextBlock>Logs</TextBlock>
|
||||
<StackPanel>
|
||||
<TextBlock Classes="h3">Logs</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
On this page you can view Artemis's logs in real-time. Logging can come from Artemis itself, plugins and scripts.
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Margin="0 20 0 0">TODO as there's no FlowDocumentScrollViewer in Avalonia and I'm too lazy to come up with an alternative.</TextBlock>
|
||||
<TextBlock Classes="subtitle">#feelsbadman</TextBlock>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -0,0 +1,61 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Debugger.Tabs.Performance
|
||||
{
|
||||
public class PerformanceDebugMeasurementViewModel : ViewModelBase
|
||||
{
|
||||
private string? _average;
|
||||
private string? _last;
|
||||
private string? _max;
|
||||
private string? _min;
|
||||
private string? _percentile;
|
||||
|
||||
public PerformanceDebugMeasurementViewModel(ProfilingMeasurement measurement)
|
||||
{
|
||||
Measurement = measurement;
|
||||
}
|
||||
|
||||
public ProfilingMeasurement Measurement { get; }
|
||||
|
||||
public string? Last
|
||||
{
|
||||
get => _last;
|
||||
set => this.RaiseAndSetIfChanged(ref _last, value);
|
||||
}
|
||||
|
||||
public string? Average
|
||||
{
|
||||
get => _average;
|
||||
set => this.RaiseAndSetIfChanged(ref _average, value);
|
||||
}
|
||||
|
||||
public string? Min
|
||||
{
|
||||
get => _min;
|
||||
set => this.RaiseAndSetIfChanged(ref _min, value);
|
||||
}
|
||||
|
||||
public string? Max
|
||||
{
|
||||
get => _max;
|
||||
set => this.RaiseAndSetIfChanged(ref _max, value);
|
||||
}
|
||||
|
||||
public string? Percentile
|
||||
{
|
||||
get => _percentile;
|
||||
set => this.RaiseAndSetIfChanged(ref _percentile, value);
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
Last = Measurement.GetLast().TotalMilliseconds + " ms";
|
||||
Average = Measurement.GetAverage().TotalMilliseconds + " ms";
|
||||
Min = Measurement.GetMin().TotalMilliseconds + " ms";
|
||||
Max = Measurement.GetMax().TotalMilliseconds + " ms";
|
||||
Percentile = Measurement.GetPercentile(0.95).TotalMilliseconds + " ms";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Debugger.Tabs.Performance.PerformanceDebugPluginView">
|
||||
<Border Classes="card-condensed" Margin="0 5">
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="40,*">
|
||||
<controls:ArtemisIcon Grid.Column="0" Icon="{Binding Plugin.Info.ResolvedIcon}" Width="24" Height="24" />
|
||||
<TextBlock Grid.Column="1" VerticalAlignment="Center" Classes="h5" Text="{Binding Plugin.Info.Name}" />
|
||||
</Grid>
|
||||
|
||||
<ItemsControl Items="{Binding Profilers}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@ -0,0 +1,19 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Artemis.UI.Screens.Debugger.Tabs.Performance
|
||||
{
|
||||
public partial class PerformanceDebugPluginView : UserControl
|
||||
{
|
||||
public PerformanceDebugPluginView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
|
||||
namespace Artemis.UI.Screens.Debugger.Tabs.Performance
|
||||
{
|
||||
public class PerformanceDebugPluginViewModel : ViewModelBase
|
||||
{
|
||||
public PerformanceDebugPluginViewModel(Plugin plugin)
|
||||
{
|
||||
Plugin = plugin;
|
||||
}
|
||||
|
||||
public Plugin Plugin { get; }
|
||||
|
||||
public ObservableCollection<PerformanceDebugProfilerViewModel> Profilers { get; } = new();
|
||||
|
||||
public void Update()
|
||||
{
|
||||
foreach (Profiler pluginProfiler in Plugin.Profilers.Where(p => p.Measurements.Any()))
|
||||
{
|
||||
if (Profilers.All(p => p.Profiler != pluginProfiler))
|
||||
Profilers.Add(new PerformanceDebugProfilerViewModel(pluginProfiler));
|
||||
}
|
||||
|
||||
foreach (PerformanceDebugProfilerViewModel profilerViewModel in Profilers)
|
||||
profilerViewModel.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Artemis.UI.Screens.Debugger.Tabs.Performance"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Debugger.Tabs.Performance.PerformanceDebugProfilerView">
|
||||
<StackPanel>
|
||||
<TextBlock Classes="subtitle" Text="{Binding Profiler.Name}" Margin="10 10 0 0" />
|
||||
|
||||
<DataGrid Items="{Binding Measurements}"
|
||||
d:DataContext="{d:DesignInstance Type={x:Type local:PerformanceDebugMeasurementViewModel}}"
|
||||
CanUserSortColumns="True"
|
||||
IsReadOnly="True"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserResizeColumns="False"
|
||||
Margin="10 5 10 10">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Binding="{Binding Measurement.Identifier}" Header="Identifier" />
|
||||
<DataGridTextColumn Binding="{Binding Last}" Header="Last" />
|
||||
<DataGridTextColumn Binding="{Binding Min}" Header="Min" />
|
||||
<DataGridTextColumn Binding="{Binding Max}" Header="Max" />
|
||||
<DataGridTextColumn Binding="{Binding Average}" Header="Average" />
|
||||
<DataGridTextColumn Binding="{Binding Percentile}" Header="95th percentile" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -0,0 +1,19 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Artemis.UI.Screens.Debugger.Tabs.Performance
|
||||
{
|
||||
public partial class PerformanceDebugProfilerView : UserControl
|
||||
{
|
||||
public PerformanceDebugProfilerView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
|
||||
namespace Artemis.UI.Screens.Debugger.Tabs.Performance
|
||||
{
|
||||
public class PerformanceDebugProfilerViewModel : ViewModelBase
|
||||
{
|
||||
public PerformanceDebugProfilerViewModel(Profiler profiler)
|
||||
{
|
||||
Profiler = profiler;
|
||||
}
|
||||
|
||||
public Profiler Profiler { get; }
|
||||
|
||||
public ObservableCollection<PerformanceDebugMeasurementViewModel> Measurements { get; } = new();
|
||||
|
||||
public void Update()
|
||||
{
|
||||
foreach ((string _, ProfilingMeasurement measurement) in Profiler.Measurements)
|
||||
{
|
||||
if (Measurements.All(m => m.Measurement != measurement))
|
||||
Measurements.Add(new PerformanceDebugMeasurementViewModel(measurement));
|
||||
}
|
||||
|
||||
foreach (PerformanceDebugMeasurementViewModel profilingMeasurementViewModel in Measurements)
|
||||
profilingMeasurementViewModel.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,25 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Debugger.Tabs.Performance.PerformanceDebugView">
|
||||
<TextBlock>Performance</TextBlock>
|
||||
<StackPanel>
|
||||
<TextBlock Classes="h3">Performance</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
On this page you can see how much CPU time different plugin features are taking. If you are having performance issues, below you can find out which plugin might be the culprit.
|
||||
</TextBlock>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock TextWrapping="Wrap" Classes="subtitle" Margin="0 10">
|
||||
These performance stats are rather basic, for advanced performance profiling check out the wiki.
|
||||
</TextBlock>
|
||||
<controls:HyperlinkButton Grid.Column="1" NavigateUri="https://wiki.artemis-rgb.com/en/guides/user/plugins/profiling">
|
||||
JetBrains Profiling Guide
|
||||
</controls:HyperlinkButton>
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl Items="{Binding Items}" Margin="0 0 10 0" />
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -1,16 +1,78 @@
|
||||
using Artemis.UI.Shared;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Timers;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Shared;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Debugger.Tabs.Performance
|
||||
{
|
||||
public class PerformanceDebugViewModel : ActivatableViewModelBase, IRoutableViewModel
|
||||
{
|
||||
public PerformanceDebugViewModel(IScreen hostScreen)
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
|
||||
public PerformanceDebugViewModel(IScreen hostScreen, IPluginManagementService pluginManagementService)
|
||||
{
|
||||
HostScreen = hostScreen;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
|
||||
Timer updateTimer = new(500);
|
||||
updateTimer.Elapsed += UpdateTimerOnElapsed;
|
||||
|
||||
this.WhenActivated(disposables =>
|
||||
{
|
||||
Observable.FromEventPattern<PluginFeatureEventArgs>(x => pluginManagementService.PluginFeatureEnabled += x, x => pluginManagementService.PluginFeatureEnabled -= x)
|
||||
.Subscribe(_ => Repopulate())
|
||||
.DisposeWith(disposables);
|
||||
Observable.FromEventPattern<PluginFeatureEventArgs>(x => pluginManagementService.PluginFeatureDisabled += x, x => pluginManagementService.PluginFeatureDisabled -= x)
|
||||
.Subscribe(_ => Repopulate())
|
||||
.DisposeWith(disposables);
|
||||
Observable.FromEventPattern<PluginEventArgs>(x => pluginManagementService.PluginEnabled += x, x => pluginManagementService.PluginEnabled -= x)
|
||||
.Subscribe(_ => Repopulate())
|
||||
.DisposeWith(disposables);
|
||||
Observable.FromEventPattern<PluginEventArgs>(x => pluginManagementService.PluginDisabled += x, x => pluginManagementService.PluginDisabled -= x)
|
||||
.Subscribe(_ => Repopulate())
|
||||
.DisposeWith(disposables);
|
||||
|
||||
PopulateItems();
|
||||
updateTimer.Start();
|
||||
|
||||
Disposable.Create(() =>
|
||||
{
|
||||
updateTimer.Stop();
|
||||
Items.Clear();
|
||||
}).DisposeWith(disposables);
|
||||
});
|
||||
}
|
||||
|
||||
public ObservableCollection<PerformanceDebugPluginViewModel> Items { get; } = new();
|
||||
|
||||
public string UrlPathSegment => "performance";
|
||||
public IScreen HostScreen { get; }
|
||||
|
||||
private void PopulateItems()
|
||||
{
|
||||
foreach (PerformanceDebugPluginViewModel performanceDebugPluginViewModel in _pluginManagementService.GetAllPlugins()
|
||||
.Where(p => p.IsEnabled && p.Profilers.Any(pr => pr.Measurements.Any()))
|
||||
.OrderBy(p => p.Info.Name)
|
||||
.Select(p => new PerformanceDebugPluginViewModel(p)))
|
||||
Items.Add(performanceDebugPluginViewModel);
|
||||
}
|
||||
|
||||
private void Repopulate()
|
||||
{
|
||||
Items.Clear();
|
||||
PopulateItems();
|
||||
}
|
||||
|
||||
private void UpdateTimerOnElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
foreach (PerformanceDebugPluginViewModel viewModel in Items)
|
||||
viewModel.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user