Add minidump generation, more error handling and logging
parent
6f364833a8
commit
31f97060b8
37
App.xaml.cs
37
App.xaml.cs
|
@ -1,5 +1,5 @@
|
|||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
|
||||
namespace WpfMaiTouchEmulator;
|
||||
|
@ -8,5 +8,38 @@ namespace WpfMaiTouchEmulator;
|
|||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
public App()
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
DispatcherUnhandledException += App_DispatcherUnhandledException;
|
||||
Logger.CleanupOldLogFiles();
|
||||
}
|
||||
|
||||
private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
CreateDump(e.Exception);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
CreateDump(e.ExceptionObject as Exception);
|
||||
}
|
||||
|
||||
private void CreateDump(Exception exception)
|
||||
{
|
||||
var dumpFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "CrashDump.dmp");
|
||||
|
||||
using (var fs = new FileStream(dumpFilePath, FileMode.Create))
|
||||
{
|
||||
var process = Process.GetCurrentProcess();
|
||||
DumpCreator.MiniDumpWriteDump(process.Handle, (uint)process.Id, fs.SafeFileHandle, DumpCreator.Typ.MiniDumpNormal, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
|
||||
Logger.Fatal("App encountered a fatal exception", exception);
|
||||
MessageBox.Show($"A uncaught exception was thrown: {exception.Message}. \n\n {exception.StackTrace}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
Current.Shutdown(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.IO;
|
||||
using System.Diagnostics;
|
||||
|
||||
class DumpCreator
|
||||
{
|
||||
[Flags]
|
||||
public enum Typ : uint
|
||||
{
|
||||
// Add the MiniDump flags you need, for example:
|
||||
MiniDumpNormal = 0x00000000,
|
||||
MiniDumpWithDataSegs = 0x00000001,
|
||||
// etc.
|
||||
}
|
||||
|
||||
[DllImport("DbgHelp.dll")]
|
||||
public static extern bool MiniDumpWriteDump(IntPtr hProcess, uint ProcessId, SafeHandle hFile, Typ DumpType,
|
||||
IntPtr ExceptionParam, IntPtr UserStreamParam, IntPtr CallbackParam);
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
public static class Logger
|
||||
{
|
||||
private static readonly object lockObj = new();
|
||||
private static string? logFilePath;
|
||||
|
||||
private static string GetLogFilePath()
|
||||
{
|
||||
if (logFilePath == null)
|
||||
{
|
||||
var fileName = $"app_{DateTime.Now:yyyy-MM-dd}.log";
|
||||
logFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
|
||||
}
|
||||
return logFilePath;
|
||||
}
|
||||
|
||||
|
||||
public static void CleanupOldLogFiles()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
|
||||
var oldFiles = directory.GetFiles("app_*.log")
|
||||
.Where(f => f.CreationTime < DateTime.Now.AddDays(-7))
|
||||
.ToList();
|
||||
|
||||
foreach (var file in oldFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
file.Delete();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error("Failed to delete log file", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void Info(string message)
|
||||
{
|
||||
Log("INFO", message);
|
||||
}
|
||||
|
||||
public static void Warn(string message)
|
||||
{
|
||||
Log("WARN", message);
|
||||
}
|
||||
|
||||
public static void Error(string message, Exception? ex = null)
|
||||
{
|
||||
var logMessage = new StringBuilder(message);
|
||||
if (ex != null)
|
||||
{
|
||||
logMessage.AppendLine(); // Ensure the exception starts on a new line
|
||||
logMessage.AppendLine($"Exception: {ex.Message}");
|
||||
logMessage.AppendLine($"StackTrace: {ex.StackTrace}");
|
||||
}
|
||||
|
||||
Log("ERROR", logMessage.ToString());
|
||||
}
|
||||
|
||||
public static void Fatal(string message, Exception? ex = null)
|
||||
{
|
||||
var logMessage = new StringBuilder(message);
|
||||
if (ex != null)
|
||||
{
|
||||
logMessage.AppendLine(); // Ensure the exception starts on a new line
|
||||
logMessage.AppendLine($"Exception: {ex.Message}");
|
||||
logMessage.AppendLine($"StackTrace: {ex.StackTrace}");
|
||||
}
|
||||
Log("FATAL", logMessage.ToString());
|
||||
}
|
||||
|
||||
private static void Log(string level, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (lockObj)
|
||||
{
|
||||
using var sw = new StreamWriter(GetLogFilePath(), true, Encoding.UTF8);
|
||||
sw.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{level}] {message}");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,54 +2,52 @@
|
|||
using System.Windows;
|
||||
|
||||
namespace WpfMaiTouchEmulator;
|
||||
internal class MaiTouchComConnector
|
||||
internal class MaiTouchComConnector(MaiTouchSensorButtonStateManager buttonState)
|
||||
{
|
||||
private static SerialPort? serialPort;
|
||||
private bool isActiveMode;
|
||||
private bool _connected;
|
||||
private bool _shouldReconnect = true;
|
||||
private readonly MaiTouchSensorButtonStateManager _buttonState;
|
||||
private readonly MaiTouchSensorButtonStateManager _buttonState = buttonState;
|
||||
|
||||
public Action<string> OnConnectStatusChange
|
||||
public Action<string>? OnConnectStatusChange
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public Action OnConnectError
|
||||
public Action? OnConnectError
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public Action<string> OnDataSent
|
||||
public Action<string>? OnDataSent
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public Action<string> OnDataRecieved
|
||||
public Action<string>? OnDataRecieved
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
public MaiTouchComConnector(MaiTouchSensorButtonStateManager buttonState)
|
||||
{
|
||||
_buttonState = buttonState;
|
||||
}
|
||||
|
||||
public async Task StartTouchSensorPolling()
|
||||
{
|
||||
if (!_connected && _shouldReconnect)
|
||||
{
|
||||
Logger.Info("Trying to connect to COM port...");
|
||||
var virtualPort = "COM23"; // Adjust as needed
|
||||
try
|
||||
{
|
||||
OnConnectStatusChange("Conecting...");
|
||||
serialPort = new SerialPort(virtualPort, 9600, Parity.None, 8, StopBits.One);
|
||||
serialPort.WriteTimeout = 100;
|
||||
OnConnectStatusChange?.Invoke("Conecting...");
|
||||
serialPort = new SerialPort(virtualPort, 9600, Parity.None, 8, StopBits.One)
|
||||
{
|
||||
WriteTimeout = 100
|
||||
};
|
||||
serialPort.DataReceived += SerialPort_DataReceived;
|
||||
serialPort.Open();
|
||||
Console.WriteLine("Serial port opened successfully.");
|
||||
OnConnectStatusChange("Connected to port");
|
||||
Logger.Info("Serial port opened successfully.");
|
||||
OnConnectStatusChange?.Invoke("Connected to port");
|
||||
_connected = true;
|
||||
|
||||
while (true)
|
||||
|
@ -69,7 +67,7 @@ internal class MaiTouchComConnector
|
|||
catch (TimeoutException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnConnectError();
|
||||
OnConnectError?.Invoke();
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
MessageBox.Show(ex.Message, "Error connecting to COM port", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
|
@ -78,10 +76,13 @@ internal class MaiTouchComConnector
|
|||
}
|
||||
finally
|
||||
{
|
||||
Logger.Info("Disconnecting from COM port");
|
||||
_connected = false;
|
||||
OnConnectStatusChange("Not Connected");
|
||||
OnConnectStatusChange?.Invoke("Not Connected");
|
||||
if (serialPort?.IsOpen == true)
|
||||
{
|
||||
serialPort.DiscardInBuffer();
|
||||
serialPort.DiscardOutBuffer();
|
||||
serialPort.Close();
|
||||
}
|
||||
}
|
||||
|
@ -90,9 +91,12 @@ internal class MaiTouchComConnector
|
|||
|
||||
public async Task Disconnect()
|
||||
{
|
||||
Logger.Info("Disconnecting from COM port");
|
||||
_shouldReconnect = false;
|
||||
_connected = false;
|
||||
try
|
||||
{
|
||||
if (serialPort != null)
|
||||
{
|
||||
serialPort.DtrEnable = false;
|
||||
serialPort.RtsEnable = false;
|
||||
|
@ -105,21 +109,26 @@ internal class MaiTouchComConnector
|
|||
serialPort.Close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Error whilst disconnecting from COM port", ex);
|
||||
MessageBox.Show(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
|
||||
{
|
||||
var recievedData = serialPort.ReadExisting();
|
||||
var commands = recievedData.Split(new[] { '}' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var recievedData = serialPort?.ReadExisting();
|
||||
var commands = recievedData?.Split(new[] { '}' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (commands != null)
|
||||
{
|
||||
foreach (var command in commands)
|
||||
{
|
||||
var cleanedCommand = command.TrimStart('{');
|
||||
Console.WriteLine($"Received data: {cleanedCommand}");
|
||||
OnDataRecieved(cleanedCommand);
|
||||
Logger.Info($"Received serial data: {cleanedCommand}");
|
||||
OnDataRecieved?.Invoke(cleanedCommand);
|
||||
|
||||
if (cleanedCommand == "STAT")
|
||||
{
|
||||
|
@ -140,12 +149,13 @@ internal class MaiTouchComConnector
|
|||
var ratio = cleanedCommand[3];
|
||||
|
||||
var newString = $"({leftOrRight}{sensor}{cleanedCommand[2]}{ratio})";
|
||||
serialPort.Write(newString);
|
||||
OnDataSent(newString);
|
||||
serialPort?.Write(newString);
|
||||
OnDataSent?.Invoke(newString);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(cleanedCommand);
|
||||
Logger.Warn($"Unhandled serial data command {cleanedCommand}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,12 +47,14 @@ public partial class MainWindow : Window
|
|||
|
||||
if (Properties.Settings.Default.FirstOpen)
|
||||
{
|
||||
Logger.Info("First open occurred");
|
||||
MessageBox.Show("Please remove any COM devices using the COM3 port before installing the virtual COM port. In Device Manager click \"View\" then enabled \"Show hidden devices\" and uninstall any devices that are using the COM3 port.\n\nAfter ensuring COM3 is free please use the install COM port button in the app to register the app.\n\nThe app needs to connect to the port prior to Sinmai.exe being opened.", "First time setup", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
Properties.Settings.Default.FirstOpen = false;
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
|
||||
Loaded += (s, e) => {
|
||||
Logger.Info("Main window loaded, creating touch panel");
|
||||
_touchPanel = new TouchPanel();
|
||||
_touchPanel.onTouch = (value) => { buttonState.PressButton(value); };
|
||||
_touchPanel.onRelease = (value) => { buttonState.ReleaseButton(value); };
|
||||
|
@ -90,6 +92,7 @@ public partial class MainWindow : Window
|
|||
var processes = Process.GetProcessesByName("Sinmai");
|
||||
if (processes.Length > 0)
|
||||
{
|
||||
Logger.Info("Found sinmai process to exit alongside with");
|
||||
sinamiProcess = processes[0];
|
||||
}
|
||||
else
|
||||
|
@ -98,10 +101,13 @@ public partial class MainWindow : Window
|
|||
}
|
||||
}
|
||||
await sinamiProcess.WaitForExitAsync();
|
||||
Logger.Info("Sinmai exited");
|
||||
var dataContext = (MainWindowViewModel)DataContext;
|
||||
if (dataContext.IsExitWithSinmaiEnabled)
|
||||
{
|
||||
Logger.Info("Disconnecting from COM port before shutting down");
|
||||
await connector.Disconnect();
|
||||
Logger.Info("Shutting down...");
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
}
|
||||
|
@ -178,6 +184,8 @@ public partial class MainWindow : Window
|
|||
|
||||
private async void buttonInstallComPort_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
throw new Exception("Test exception for crash dump generation");
|
||||
|
||||
await comPortManager.InstallComPort();
|
||||
}
|
||||
|
||||
|
|
|
@ -11,10 +11,10 @@ namespace WpfMaiTouchEmulator;
|
|||
/// </summary>
|
||||
public partial class TouchPanel : Window
|
||||
{
|
||||
internal Action<TouchValue> onTouch;
|
||||
internal Action<TouchValue> onRelease;
|
||||
internal Action<TouchValue>? onTouch;
|
||||
internal Action<TouchValue>? onRelease;
|
||||
|
||||
private readonly Dictionary<int, System.Windows.Controls.Image> activeTouches = [];
|
||||
private readonly Dictionary<int, Image> activeTouches = [];
|
||||
private readonly TouchPanelPositionManager _positionManager;
|
||||
private List<Image> buttons = [];
|
||||
|
||||
|
@ -44,10 +44,10 @@ public partial class TouchPanel : Window
|
|||
{
|
||||
while (true)
|
||||
{
|
||||
if (activeTouches.Any() && !TouchesOver.Any())
|
||||
if (activeTouches.Count != 0 && !TouchesOver.Any())
|
||||
{
|
||||
await Task.Delay(100);
|
||||
if (activeTouches.Any() && !TouchesOver.Any())
|
||||
if (activeTouches.Count != 0 && !TouchesOver.Any())
|
||||
{
|
||||
DeselectAllItems();
|
||||
}
|
||||
|
@ -64,8 +64,11 @@ public partial class TouchPanel : Window
|
|||
public void PositionTouchPanel()
|
||||
{
|
||||
var position = _positionManager.GetSinMaiWindowPosition();
|
||||
if (position != null)
|
||||
if (position != null &&
|
||||
(Top != position.Value.Top || Left != position.Value.Left || Width != position.Value.Width || Height != position.Value.Height)
|
||||
)
|
||||
{
|
||||
Logger.Info("Touch panel not over sinmai window, repositioning");
|
||||
Top = position.Value.Top;
|
||||
Left = position.Value.Left;
|
||||
Width = position.Value.Width;
|
||||
|
@ -103,7 +106,7 @@ public partial class TouchPanel : Window
|
|||
{
|
||||
// Highlight the element and add it to the active touches tracking.
|
||||
HighlightElement(element, true);
|
||||
onTouch((TouchValue)element.Tag);
|
||||
onTouch?.Invoke((TouchValue)element.Tag);
|
||||
activeTouches[e.TouchDevice.Id] = element;
|
||||
}
|
||||
e.Handled = true;
|
||||
|
@ -125,7 +128,7 @@ public partial class TouchPanel : Window
|
|||
HighlightElement(previousElement, false);
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
onRelease((TouchValue)previousElement.Tag);
|
||||
onRelease?.Invoke((TouchValue)previousElement.Tag);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -133,7 +136,7 @@ public partial class TouchPanel : Window
|
|||
|
||||
// Highlight the new element and update the tracking.
|
||||
HighlightElement(newElement, true);
|
||||
onTouch((TouchValue)newElement.Tag);
|
||||
onTouch?.Invoke((TouchValue)newElement.Tag);
|
||||
activeTouches[e.TouchDevice.Id] = newElement;
|
||||
}
|
||||
|
||||
|
@ -146,29 +149,20 @@ public partial class TouchPanel : Window
|
|||
if (activeTouches.TryGetValue(e.TouchDevice.Id, out var element))
|
||||
{
|
||||
HighlightElement(element, false);
|
||||
onRelease((TouchValue)element.Tag);
|
||||
onRelease?.Invoke((TouchValue)element.Tag);
|
||||
activeTouches.Remove(e.TouchDevice.Id);
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private bool IsTouchInsideWindow(Point touchPoint)
|
||||
{
|
||||
// Define the window's bounds
|
||||
var windowBounds = new Rect(0, 0, ActualWidth, ActualHeight);
|
||||
|
||||
// Check if the touch point is within the window's bounds
|
||||
return windowBounds.Contains(touchPoint);
|
||||
}
|
||||
|
||||
private void DeselectAllItems()
|
||||
{
|
||||
// Logic to deselect all items or the last touched item
|
||||
foreach (var element in activeTouches.Values)
|
||||
{
|
||||
HighlightElement(element, false);
|
||||
onRelease((TouchValue)element.Tag);
|
||||
onRelease?.Invoke((TouchValue)element.Tag);
|
||||
}
|
||||
activeTouches.Clear();
|
||||
}
|
||||
|
@ -181,7 +175,7 @@ public partial class TouchPanel : Window
|
|||
});
|
||||
}
|
||||
|
||||
private void HighlightElement(System.Windows.Controls.Image element, bool highlight)
|
||||
private void HighlightElement(Image element, bool highlight)
|
||||
{
|
||||
if (Properties.Settings.Default.IsDebugEnabled)
|
||||
{
|
||||
|
|
|
@ -22,6 +22,8 @@ class TouchPanelPositionManager
|
|||
}
|
||||
|
||||
public Rect? GetSinMaiWindowPosition()
|
||||
{
|
||||
try
|
||||
{
|
||||
var hWnd = FindWindow(null, "Sinmai");
|
||||
if (hWnd != IntPtr.Zero)
|
||||
|
@ -30,13 +32,19 @@ class TouchPanelPositionManager
|
|||
if (GetWindowRect(hWnd, out rect))
|
||||
{
|
||||
// Calculate the desired size and position based on the other application's window
|
||||
var width = Convert.ToInt32((rect.Right - rect.Left));
|
||||
var width = rect.Right - rect.Left;
|
||||
var height = width;
|
||||
var left = rect.Left + ((rect.Right - rect.Left) - width) / 2; // Center horizontally
|
||||
var top = rect.Bottom - height;
|
||||
return new Rect(left, top, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Failed top get sinmai window position", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,57 +28,69 @@ internal class VirtualComPortManager
|
|||
|
||||
public async Task InstallComPort()
|
||||
{
|
||||
Logger.Info("Trying to install virtual COM port.");
|
||||
if (await CheckIfPortInstalled("COM3", false))
|
||||
{
|
||||
Logger.Warn("Port COM3 already registered.");
|
||||
MessageBox.Show("Port COM3 already registered. Either remove it via Device Manager or uninstall the virutal port.");
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
Logger.Info("Calling com0com to install virtual COM ports");
|
||||
await ExecuteCommandAsync("setupc.exe", $"install PortName=COM3 PortName=COM23");
|
||||
if (await CheckIfPortInstalled("COM3", true))
|
||||
{
|
||||
Logger.Info("Port COM3 successfully installed.");
|
||||
MessageBox.Show("Port COM3 successfully installed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("Port COM3 failed to install");
|
||||
MessageBox.Show($"Port COM3 failed to install", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Port COM3 failed to install", ex);
|
||||
MessageBox.Show($"Port COM3 failed to install. {ex}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UninstallVirtualPorts()
|
||||
{
|
||||
|
||||
Logger.Info("Trying to uninstall virtual COM port.");
|
||||
if (!await CheckIfPortInstalled("COM3", true))
|
||||
{
|
||||
Logger.Warn("Port COM3 not found. No need to uninstall.");
|
||||
MessageBox.Show("Port COM3 not found. No need to uninstall.");
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
Logger.Info("Calling com0com to uninstall virtual COM ports");
|
||||
await ExecuteCommandAsync("setupc.exe", $"uninstall");
|
||||
if (!await CheckIfPortInstalled("COM3", false))
|
||||
{
|
||||
Logger.Info("Port COM3 successfully uninstalled.");
|
||||
MessageBox.Show("Port COM3 successfully uninstalled.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("Port COM3 failed to uninstall");
|
||||
MessageBox.Show($"Port COM3 failed to uninstall. It may be a real device, uninstall it from Device Manager", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Port COM3 failed to uninstall", ex);
|
||||
MessageBox.Show($"Port COM3 failed to uninstall. {ex}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteCommandAsync(string command, string arguments)
|
||||
{
|
||||
Logger.Info($"Executing command {command} with arguments {arguments}");
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = command,
|
||||
|
@ -92,5 +104,6 @@ internal class VirtualComPortManager
|
|||
process.Start();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
Logger.Info($"Command {command} completed");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue