diff --git a/src/Pepperdash.Core/Debug.cs b/src/Pepperdash.Core/Debug.cs new file mode 100644 index 0000000..e580cb4 --- /dev/null +++ b/src/Pepperdash.Core/Debug.cs @@ -0,0 +1,687 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronDataStore; +using Crestron.SimplSharp.CrestronIO; +using Crestron.SimplSharp.CrestronLogger; +using PepperDash.Core.Logging; +using Serilog; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting.Compact; +using Serilog.Formatting.Json; +using Serilog.Templates; + +namespace PepperDash.Core +{ + /// + /// Contains debug commands for use in various situations + /// + public static class Debug + { + public const string DoNotLoadConfigOnNextBootKey = "DoNotLoadConfigOnNextBoot"; + public const string ConsoleLevelStoreKey = "ConsoleDebugLevel"; + public const string WebSocketLevelStoreKey = "WebsocketDebugLevel"; + public const string ErrorLogLevelStoreKey = "ErrorLogDebugLevel"; + public const string FileLevelStoreKey = "FileDebugLevel"; + + private static readonly Dictionary LogLevels = new() + { + {0, LogEventLevel.Information }, + {3, LogEventLevel.Warning }, + {4, LogEventLevel.Error }, + {5, LogEventLevel.Fatal }, + {1, LogEventLevel.Debug }, + {2, LogEventLevel.Verbose }, + }; + + private static readonly LoggingLevelSwitch ConsoleLoggingLevelSwitch = new(); + + private static readonly LoggingLevelSwitch WebsocketLoggingLevelSwitch = new(LogEventLevel.Debug); + + private static readonly LoggingLevelSwitch ErrorLogLevelSwitch = new(LogEventLevel.Error); + + private static readonly LoggingLevelSwitch FileLevelSwitch = new(LogEventLevel.Debug); + + /// + /// When this is true, the configuration file will NOT be loaded until triggered by either a console command or a signal + /// + public static bool DoNotLoadConfigOnNextBoot => + CrestronDataStoreStatic.GetLocalBoolValue(DoNotLoadConfigOnNextBootKey, out var value) switch + { + CrestronDataStore.CDS_ERROR.CDS_SUCCESS => value, + _ => false + }; + + /// + /// Version for the currently loaded PepperDashCore dll + /// + public static string PepperDashCoreVersion { get; } + + public static int WebsocketPort { get; } + + public static readonly LoggerConfiguration DefaultLoggerConfiguration; + + public static readonly bool IsRunningOnAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance; + + static Debug() + { + CrestronDataStoreStatic.InitCrestronDataStore(); + + var logFilePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? + $@"{Directory.GetApplicationRootDirectory()}{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}debug{Path.DirectorySeparatorChar}app{InitialParametersClass.ApplicationNumber}{Path.DirectorySeparatorChar}global-log.log" : + $@"{Directory.GetApplicationRootDirectory()}{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}debug{Path.DirectorySeparatorChar}room{InitialParametersClass.RoomId}{Path.DirectorySeparatorChar}global-log.log"; + + CrestronConsole.PrintLine($"Saving log files to {logFilePath}"); + + const string consoleTemplate = + "[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}"; + + var errorLogTemplate = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance + ? "{@t:fff}ms [{@l:u4}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}" + : "[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}"; + + DefaultLoggerConfiguration = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.FromLogContext() + .Enrich.With(new CrestronEnricher()) + .WriteTo.DebugConsoleSink(new ExpressionTemplate(template: consoleTemplate), levelSwitch: ConsoleLoggingLevelSwitch) + .WriteTo.Sink(new DebugErrorLogSink(new ExpressionTemplate(template: errorLogTemplate)), levelSwitch: ErrorLogLevelSwitch) + .WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath, + rollingInterval: RollingInterval.Day, + restrictedToMinimumLevel: LogEventLevel.Debug, + retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 30 : 60, + levelSwitch: FileLevelSwitch + ); + +#if NET472 + + const string certFilename = "cert.pfx"; + var certPath = IsRunningOnAppliance ? Path.Combine("user", certFilename) : Path.Combine("User", certFilename); + var websocket = new DebugNet472WebSocket(certPath); + WebsocketPort = websocket.Port; + DefaultLoggerConfiguration.WriteTo.Sink(new DebugWebsocketSink(websocket, new JsonFormatter(renderMessage: true)), levelSwitch: WebsocketLoggingLevelSwitch); +#endif + + try + { + if (InitialParametersClass.NumberOfRemovableDrives > 0) + { + CrestronConsole.PrintLine("{0} RM Drive(s) Present. Initializing Crestron Logger", InitialParametersClass.NumberOfRemovableDrives); + DefaultLoggerConfiguration.WriteTo.Sink(new DebugCrestronLoggerSink()); + } + else + CrestronConsole.PrintLine("No RM Drive(s) Present. Not using Crestron Logger"); + } + catch (Exception e) + { + CrestronConsole.PrintLine("Initializing of CrestronLogger failed: {0}", e); + } + + var storedConsoleLevel = DebugContext.GetDataForKey(ConsoleLevelStoreKey, LogEventLevel.Information); + ConsoleLoggingLevelSwitch.MinimumLevel = storedConsoleLevel.Level; + + Log.Logger = DefaultLoggerConfiguration.CreateLogger(); + PepperDashCoreVersion = GetVersion(); + + var msg = $"[App {InitialParametersClass.ApplicationNumber}] Using PepperDash_Core v{PepperDashCoreVersion}"; + + if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Server) + { + msg = $"[Room {InitialParametersClass.RoomId}] Using PepperDash_Core v{PepperDashCoreVersion}"; + } + + LogMessage(LogEventLevel.Information,msg); + + if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro) + { + CrestronConsole.AddNewConsoleCommand(SetDoNotLoadOnNextBootFromConsole, "donotloadonnextboot", + "donotloadonnextboot:P [true/false]: Should the application load on next boot", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", + "appdebug:P [0-5] --devices [devices]: Sets the application's console debug message level. Devices is an array of comma separated device keys for devices you would like to debug. An empty array will allow all devices", + ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(ShowDebugLog, "appdebuglog", + "appdebuglog:P [all] Use \"all\" for full log.", + ConsoleAccessLevelEnum.AccessOperator); + } + + if(DoNotLoadConfigOnNextBoot) + CrestronConsole.PrintLine(string.Format("Program {0} will not load config after next boot. Use console command go:{0} to load the config manually", InitialParametersClass.ApplicationNumber)); + } + + private static string GetVersion() + { + var assembly = Assembly.GetExecutingAssembly(); + var ver = + assembly + .GetCustomAttributes(typeof (AssemblyInformationalVersionAttribute), false); + + if (ver.Length > 0 && ver[0] is AssemblyInformationalVersionAttribute verAttribute) + { + return verAttribute.InformationalVersion; + } + + var name = assembly.GetName(); + return name.Version?.ToString() ?? "0.0.0"; + } + + private static void SetDebugFromConsole(string levelString) + { + try + { + if (levelString.Trim() == "?") + { + CrestronConsole.ConsoleCommandResponse( + $""" + Used to set the minimum level of debug messages to be printed to the console: + {LogLevels[0]} = 0 + {LogLevels[1]} = 1 + {LogLevels[2]} = 2 + {LogLevels[3]} = 3 + {LogLevels[4]} = 4 + {LogLevels[5]} = 5 + """); + return; + } + + if (string.IsNullOrEmpty(levelString.Trim())) + { + CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", ConsoleLoggingLevelSwitch.MinimumLevel); + return; + } + + const string pattern = @"^(\d+)(?:\s+--devices\s+([\w,]+))?$"; + var match = Regex.Match(levelString, pattern); + if (match.Success) + { + var level = int.Parse(match.Groups[1].Value); + var devices = match.Groups[2].Success + ? match.Groups[2].Value.Split(',') + : []; + + + if (LogLevels.TryGetValue(level, out var logEventLevel)) + { + SetConsoleDebugLevel(logEventLevel, devices); + } + else + { + CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level"); + } + } + + CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level"); + } + catch + { + CrestronConsole.ConsoleCommandResponse("Usage: appdebug:P [0-5] --devices [deviceKeys]"); + } + } + + public static void SetConsoleDebugLevel(LogEventLevel level, string[] includedDevices = null) + { + ConsoleLoggingLevelSwitch.MinimumLevel = level; + DebugContext.SetDataForKey(ConsoleLevelStoreKey, level, includedDevices); + + var includedDevicesMessage = includedDevices == null || includedDevices.Length == 0 ? "all" : string.Join(",", includedDevices); + CrestronConsole.ConsoleCommandResponse($"Success: set console debug level to {level} with devices:{includedDevicesMessage}"); + + } + + public static void SetWebSocketMinimumDebugLevel(LogEventLevel level) + { + WebsocketLoggingLevelSwitch.MinimumLevel = level; + LogMessage(LogEventLevel.Information, "Websocket debug level set to {0}", WebsocketLoggingLevelSwitch.MinimumLevel); + } + + public static void SetErrorLogMinimumDebugLevel(LogEventLevel level) + { + ErrorLogLevelSwitch.MinimumLevel = level; + LogMessage(LogEventLevel.Information, "Error log debug level set to {0}", WebsocketLoggingLevelSwitch.MinimumLevel); + } + + public static void SetFileMinimumDebugLevel(LogEventLevel level) + { + ErrorLogLevelSwitch.MinimumLevel = level; + LogMessage(LogEventLevel.Information, "File debug level set to {0}", WebsocketLoggingLevelSwitch.MinimumLevel); + } + + public static void SetDebugLevel(uint level) + { + if (LogLevels.TryGetValue((int)level, out var logEventLevel)) + { + SetConsoleDebugLevel(logEventLevel); + } + else + { + CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {level} to valid log level"); + } + } + + /// + /// Callback for console command + /// + /// + private static void SetDoNotLoadOnNextBootFromConsole(string stateString) + { + try + { + if (string.IsNullOrEmpty(stateString.Trim())) + { + CrestronConsole.ConsoleCommandResponse("DoNotLoadOnNextBoot = {0}", DoNotLoadConfigOnNextBoot); + return; + } + + SetDoNotLoadConfigOnNextBoot(bool.Parse(stateString)); + } + catch + { + CrestronConsole.ConsoleCommandResponse("Usage: donotloadonnextboot:P [true/false]"); + } + } + + /// + /// Sets the flag to prevent application starting on next boot + /// + /// + public static void SetDoNotLoadConfigOnNextBoot(bool state) + { + CrestronDataStoreStatic.SetLocalBoolValue(DoNotLoadConfigOnNextBootKey, state); + CrestronConsole.ConsoleCommandResponse("[Application {0}], Do Not Load Config on Next Boot set to {1}", InitialParametersClass.ApplicationNumber, DoNotLoadConfigOnNextBoot); + } + + /// + /// Prints message to console if current debug level is equal to or higher than the level of this message. + /// Uses CrestronConsole.PrintLine. + /// + /// + /// Console format string + /// Object parameters + [Obsolete("Use LogMessage methods")] + public static void Console(uint level, string format, params object[] items) + { + var logLevel = LogLevels.TryGetValue((int)level, out var value) ? value : LogEventLevel.Information; + LogMessage(logLevel, format, items); + + //if (IsRunningOnAppliance) + //{ + // CrestronConsole.PrintLine("[{0}]App {1} Lvl {2}:{3}", DateTime.Now.ToString("HH:mm:ss.fff"), + // InitialParametersClass.ApplicationNumber, + // level, + // string.Format(format, items)); + //} + } + + /// + /// Logs to Console when at-level, and all messages to error log, including device key + /// + [Obsolete("Use LogMessage methods")] + public static void Console(uint level, IKeyed dev, string format, params object[] items) + { + var logLevel = LogLevels.TryGetValue((int)level, out var value) ? value : LogEventLevel.Information; + LogMessage(logLevel, dev, format, items); + + //if (Level >= level) + // Console(level, "[{0}] {1}", dev.Key, message); + } + + /// + /// Prints message to console if current debug level is equal to or higher than the level of this message. Always sends message to Error Log. + /// Uses CrestronConsole.PrintLine. + /// + [Obsolete("Use LogMessage methods")] + public static void Console(uint level, IKeyed dev, ErrorLogLevel errorLogLevel, string format, params object[] items) + { + var logLevel = LogLevels.TryGetValue((int)level, out var value) ? value : LogEventLevel.Information; + LogMessage(logLevel, dev, format, items); + } + + /// + /// Logs to Console when at-level, and all messages to error log + /// + [Obsolete("Use LogMessage methods")] + public static void Console(uint level, ErrorLogLevel errorLogLevel, string format, params object[] items) + { + var logLevel = LogLevels.TryGetValue((int)level, out var value) ? value : LogEventLevel.Information; + LogMessage(logLevel, format, items); + } + + /// + /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at + /// or above the level provided, then the output will be written to both console and the log. Otherwise + /// it will only be written to the log. + /// + [Obsolete("Use LogMessage methods")] + public static void ConsoleWithLog(uint level, string format, params object[] items) + { + var logLevel = LogLevels.TryGetValue((int)level, out var value) ? value : LogEventLevel.Information; + LogMessage(logLevel, format, items); + + // var str = string.Format(format, items); + //if (Level >= level) + // CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); + // CrestronLogger.WriteToLog(str, level); + } + + /// + /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at + /// or above the level provided, then the output will be written to both console and the log. Otherwise + /// it will only be written to the log. + /// + [Obsolete("Use LogMessage methods")] + public static void ConsoleWithLog(uint level, IKeyed dev, string format, params object[] items) + { + var logLevel = LogLevels.TryGetValue((int)level, out var value) ? value : LogEventLevel.Information; + LogMessage(logLevel, dev, format, items); + + // var str = string.Format(format, items); + // CrestronLogger.WriteToLog(string.Format("[{0}] {1}", dev.Key, str), level); + } + + /// + /// + /// + public static void ShowDebugLog(string s) + { + var logList = CrestronLogger.PrintTheLog(s.ToLower() == "all"); + foreach (var l in logList) + CrestronConsole.ConsoleCommandResponse(l + CrestronEnvironment.NewLine); + } + + public static ILogger CreateLoggerForDevice(T device) where T : IKeyed => + Log.ForContext().ForContext("Key", device?.Key); + + //Message + public static void LogMessage(LogEventLevel level, string message, params object[] args) + { + Log.Write(level, message, args); + } + + public static void LogMessage(LogEventLevel level, IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Write(level, message, args); + } + + public static void LogMessage(LogEventLevel level, IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Write(level, ex, message, args); + } + + public static void LogMessage(LogEventLevel level, IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).ForContext().Write(level, message, args); + } + + public static void LogMessage(LogEventLevel level, string message, params object[] args) + { + Log.ForContext().Write(level, message, args); + } + + public static void LogMessage(LogEventLevel level, IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext().ForContext("Key", device?.Key).Write(level, ex, message, args); + } + + //Error + public static void LogError(string message, params object[] args) + { + Log.Error(message, args); + } + + public static void LogError(Exception ex, string message, params object[] args) + { + Log.Error(ex, message, args); + } + + public static void LogError(string message, params object[] args) + { + Log.ForContext().Error(message, args); + } + + public static void LogError(Exception ex, string message, params object[] args) + { + Log.ForContext().Error(ex, message, args); + } + + public static void LogError(IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Error(message, args); + } + + public static void LogError(IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Error(ex, message, args); + } + + public static void LogError(IKeyed device, string message, params object[] args) + { + Log.ForContext().ForContext("Key", device?.Key).Error(message, args); + } + + public static void LogError(IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext().ForContext("Key", device?.Key).Error(ex, message, args); + } + + //Verbose + public static void LogVerbose(string message, params object[] args) + { + Log.Verbose(message, args); + } + public static void LogVerbose(Exception ex, string message, params object[] args) + { + Log.Verbose(ex, message, args); + } + + public static void LogVerbose(string message, params object[] args) + { + Log.ForContext().Verbose(message, args); + } + + public static void LogVerbose(Exception ex, string message, params object[] args) + { + Log.ForContext().ForContext().Verbose(ex, message, args); + } + + public static void LogVerbose(IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Verbose(message, args); + } + public static void LogVerbose(IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Verbose(ex, message, args); + } + + public static void LogVerbose(IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).ForContext().Verbose(message, args); + } + + public static void LogVerbose(IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext().ForContext().Verbose(ex, message, args); + } + + //Debug + public static void LogDebug(IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Debug(message, args); + } + public static void LogDebug(IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Debug(ex, message, args); + } + + public static void LogDebug(IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).ForContext().Debug(message, args); + } + + public static void LogDebug(IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext().ForContext().Debug(ex, message, args); + } + + public static void LogDebug(string message, params object[] args) + { + Log.Debug(message, args); + } + + public static void LogDebug(string message, params object[] args) + { + Log.ForContext().Debug(message, args); + } + + public static void LogDebug(Exception ex, string message, params object[] args) + { + Log.Debug(ex, message, args); + } + + public static void LogDebug(Exception ex, string message, params object[] args) + { + Log.ForContext().Debug(ex, message, args); + } + + //Information + public static void LogInformation(IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Information(message, args); + } + public static void LogInformation(IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Information(ex, message, args); + } + + public static void LogInformation(IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).ForContext().Information(message, args); + } + + public static void LogInformation(IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext().ForContext().Information(ex, message, args); + } + + public static void LogInformation(string message, params object[] args) + { + Log.Information(message, args); + } + + public static void LogInformation(string message, params object[] args) + { + Log.ForContext().Information(message, args); + } + + public static void LogInformation(Exception ex, string message, params object[] args) + { + Log.Information(ex, message, args); + } + + public static void LogInformation(Exception ex, string message, params object[] args) + { + Log.ForContext().Information(ex, message, args); + } + + //Fatal + public static void LogWarning(IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Warning(message, args); + } + public static void LogWarning(IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Warning(ex, message, args); + } + + public static void LogWarning(IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).ForContext().Warning(message, args); + } + + public static void LogWarning(IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext().ForContext().Warning(ex, message, args); + } + + public static void LogWarning(string message, params object[] args) + { + Log.Warning(message, args); + } + + public static void LogWarning(string message, params object[] args) + { + Log.ForContext().Warning(message, args); + } + + public static void LogWarning(Exception ex, string message, params object[] args) + { + Log.Warning(ex, message, args); + } + public static void LogWarning(Exception ex, string message, params object[] args) + { + Log.ForContext().Warning(ex, message, args); + } + + //Fatal + public static void LogFatal(IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Fatal(message, args); + } + public static void LogFatal(IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).Fatal(ex, message, args); + } + + public static void LogFatal(IKeyed device, string message, params object[] args) + { + Log.ForContext("Key", device?.Key).ForContext().Fatal(message, args); + } + + public static void LogFatal(IKeyed device, Exception ex, string message, params object[] args) + { + Log.ForContext().ForContext().Fatal(ex, message, args); + } + + public static void LogFatal(string message, params object[] args) + { + Log.Fatal(message, args); + } + + public static void LogFatal(string message, params object[] args) + { + Log.ForContext().Fatal(message, args); + } + + public static void LogFatal(Exception ex, string message, params object[] args) + { + Log.Fatal(ex, message, args); + } + public static void LogFatal(Exception ex, string message, params object[] args) + { + Log.ForContext().Fatal(ex, message, args); + } + + public enum ErrorLogLevel + { + /// + /// Error + /// + Error, + /// + /// Warning + /// + Warning, + /// + /// Notice + /// + Notice, + /// + /// None + /// + None, + } + } +} \ No newline at end of file diff --git a/src/Pepperdash.Core/Logging/CrestronEnricher.cs b/src/Pepperdash.Core/Logging/CrestronEnricher.cs index 902ce8d..61a267a 100644 --- a/src/Pepperdash.Core/Logging/CrestronEnricher.cs +++ b/src/Pepperdash.Core/Logging/CrestronEnricher.cs @@ -1,35 +1,27 @@ using Crestron.SimplSharp; using Serilog.Core; using Serilog.Events; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace PepperDash.Core.Logging { public class CrestronEnricher : ILogEventEnricher { - static readonly string _appName; + private static readonly string AppName; static CrestronEnricher() { - switch (CrestronEnvironment.DevicePlatform) + AppName = CrestronEnvironment.DevicePlatform switch { - case eDevicePlatform.Appliance: - _appName = $"App {InitialParametersClass.ApplicationNumber}"; - break; - case eDevicePlatform.Server: - _appName = $"{InitialParametersClass.RoomId}"; - break; - } + eDevicePlatform.Appliance => $"App {InitialParametersClass.ApplicationNumber}", + eDevicePlatform.Server => $"{InitialParametersClass.RoomId}", + _ => string.Empty + }; } public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - var property = propertyFactory.CreateProperty("App", _appName); + var property = propertyFactory.CreateProperty("App", AppName); logEvent.AddOrUpdateProperty(property); } diff --git a/src/Pepperdash.Core/Logging/Debug.cs b/src/Pepperdash.Core/Logging/Debug.cs deleted file mode 100644 index 4e79a60..0000000 --- a/src/Pepperdash.Core/Logging/Debug.cs +++ /dev/null @@ -1,895 +0,0 @@ -using Crestron.SimplSharp; -using Crestron.SimplSharp.CrestronDataStore; -using Crestron.SimplSharp.CrestronIO; -using Crestron.SimplSharp.CrestronLogger; -using Newtonsoft.Json; -using PepperDash.Core.Logging; -using Serilog; -using Serilog.Context; -using Serilog.Core; -using Serilog.Events; -using Serilog.Formatting.Compact; -using Serilog.Formatting.Json; -using Serilog.Templates; -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text.RegularExpressions; - -namespace PepperDash.Core -{ - /// - /// Contains debug commands for use in various situations - /// - public static class Debug - { - private static readonly string LevelStoreKey = "ConsoleDebugLevel"; - private static readonly string WebSocketLevelStoreKey = "WebsocketDebugLevel"; - private static readonly string ErrorLogLevelStoreKey = "ErrorLogDebugLevel"; - private static readonly string FileLevelStoreKey = "FileDebugLevel"; - - private static readonly Dictionary _logLevels = new Dictionary() - { - {0, LogEventLevel.Information }, - {3, LogEventLevel.Warning }, - {4, LogEventLevel.Error }, - {5, LogEventLevel.Fatal }, - {1, LogEventLevel.Debug }, - {2, LogEventLevel.Verbose }, - }; - - private static ILogger _logger; - - private static readonly LoggingLevelSwitch _consoleLoggingLevelSwitch; - - private static readonly LoggingLevelSwitch _websocketLoggingLevelSwitch; - - private static readonly LoggingLevelSwitch _errorLogLevelSwitch; - - private static readonly LoggingLevelSwitch _fileLevelSwitch; - - public static LogEventLevel WebsocketMinimumLogLevel - { - get { return _websocketLoggingLevelSwitch.MinimumLevel; } - } - - private static readonly DebugWebsocketSink _websocketSink; - - public static DebugWebsocketSink WebsocketSink - { - get { return _websocketSink; } - } - - /// - /// Describes the folder location where a given program stores it's debug level memory. By default, the - /// file written will be named appNdebug where N is 1-10. - /// - public static string OldFilePathPrefix = @"\nvram\debug\"; - - /// - /// Describes the new folder location where a given program stores it's debug level memory. By default, the - /// file written will be named appNdebug where N is 1-10. - /// - public static string NewFilePathPrefix = @"\user\debug\"; - - /// - /// The name of the file containing the current debug settings. - /// - public static string FileName = string.Format(@"app{0}Debug.json", InitialParametersClass.ApplicationNumber); - - /// - /// Debug level to set for a given program. - /// - public static int Level { get; private set; } - - /// - /// When this is true, the configuration file will NOT be loaded until triggered by either a console command or a signal - /// - public static bool DoNotLoadConfigOnNextBoot { get; private set; } - - private static DebugContextCollection _contexts; - - private const int SaveTimeoutMs = 30000; - - public static bool IsRunningOnAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance; - - /// - /// Version for the currently loaded PepperDashCore dll - /// - public static string PepperDashCoreVersion { get; private set; } - - private static CTimer _saveTimer; - - /// - /// When true, the IncludedExcludedKeys dict will contain keys to include. - /// When false (default), IncludedExcludedKeys will contain keys to exclude. - /// - private static bool _excludeAllMode; - - //static bool ExcludeNoKeyMessages; - - private static readonly Dictionary IncludedExcludedKeys; - - private static readonly LoggerConfiguration _defaultLoggerConfiguration; - - private static LoggerConfiguration _loggerConfiguration; - - public static LoggerConfiguration LoggerConfiguration => _loggerConfiguration; - - static Debug() - { - CrestronDataStoreStatic.InitCrestronDataStore(); - - var defaultConsoleLevel = GetStoredLogEventLevel(LevelStoreKey); - - var defaultWebsocketLevel = GetStoredLogEventLevel(WebSocketLevelStoreKey); - - var defaultErrorLogLevel = GetStoredLogEventLevel(ErrorLogLevelStoreKey); - - var defaultFileLogLevel = GetStoredLogEventLevel(FileLevelStoreKey); - - _consoleLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultConsoleLevel); - - _websocketLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultWebsocketLevel); - - _errorLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultErrorLogLevel); - - _fileLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel); - - _websocketSink = new DebugWebsocketSink(new JsonFormatter(renderMessage: true)); - - var logFilePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? - $@"{Directory.GetApplicationRootDirectory()}{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}debug{Path.DirectorySeparatorChar}app{InitialParametersClass.ApplicationNumber}{Path.DirectorySeparatorChar}global-log.log" : - $@"{Directory.GetApplicationRootDirectory()}{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}debug{Path.DirectorySeparatorChar}room{InitialParametersClass.RoomId}{Path.DirectorySeparatorChar}global-log.log"; - - CrestronConsole.PrintLine($"Saving log files to {logFilePath}"); - - var errorLogTemplate = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance - ? "{@t:fff}ms [{@l:u4}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}" - : "[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}"; - - _defaultLoggerConfiguration = new LoggerConfiguration() - .MinimumLevel.Verbose() - .Enrich.FromLogContext() - .Enrich.With(new CrestronEnricher()) - .WriteTo.Sink(new DebugConsoleSink(new ExpressionTemplate("[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}")), levelSwitch: _consoleLoggingLevelSwitch) - .WriteTo.Sink(_websocketSink, levelSwitch: _websocketLoggingLevelSwitch) - .WriteTo.Sink(new DebugErrorLogSink(new ExpressionTemplate(errorLogTemplate)), levelSwitch: _errorLogLevelSwitch) - .WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath, - rollingInterval: RollingInterval.Day, - restrictedToMinimumLevel: LogEventLevel.Debug, - retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 30 : 60, - levelSwitch: _fileLevelSwitch - ); - - try - { - if (InitialParametersClass.NumberOfRemovableDrives > 0) - { - CrestronConsole.PrintLine("{0} RM Drive(s) Present. Initializing CrestronLogger", InitialParametersClass.NumberOfRemovableDrives); - _defaultLoggerConfiguration.WriteTo.Sink(new DebugCrestronLoggerSink()); - } - else - CrestronConsole.PrintLine("No RM Drive(s) Present. Not using Crestron Logger"); - } - catch (Exception e) - { - CrestronConsole.PrintLine("Initializing of CrestronLogger failed: {0}", e); - } - - // Instantiate the root logger - _loggerConfiguration = _defaultLoggerConfiguration; - - _logger = _loggerConfiguration.CreateLogger(); - // Get the assembly version and print it to console and the log - GetVersion(); - - string msg = $"[App {InitialParametersClass.ApplicationNumber}] Using PepperDash_Core v{PepperDashCoreVersion}"; - - if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Server) - { - msg = $"[Room {InitialParametersClass.RoomId}] Using PepperDash_Core v{PepperDashCoreVersion}"; - } - - CrestronConsole.PrintLine(msg); - - LogMessage(LogEventLevel.Information,msg); - - IncludedExcludedKeys = new Dictionary(); - - if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro) - { - // Add command to console - CrestronConsole.AddNewConsoleCommand(SetDoNotLoadOnNextBootFromConsole, "donotloadonnextboot", - "donotloadonnextboot:P [true/false]: Should the application load on next boot", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", - "appdebug:P [0-5]: Sets the application's console debug message level", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(ShowDebugLog, "appdebuglog", - "appdebuglog:P [all] Use \"all\" for full log.", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronLogger.Clear(false), "appdebugclear", - "appdebugclear:P Clears the current custom log", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(SetDebugFilterFromConsole, "appdebugfilter", - "appdebugfilter [params]", ConsoleAccessLevelEnum.AccessOperator); - } - - CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; - - LoadMemory(); - - var context = _contexts.GetOrCreateItem("DEFAULT"); - Level = context.Level; - DoNotLoadConfigOnNextBoot = context.DoNotLoadOnNextBoot; - - if(DoNotLoadConfigOnNextBoot) - CrestronConsole.PrintLine(string.Format("Program {0} will not load config after next boot. Use console command go:{0} to load the config manually", InitialParametersClass.ApplicationNumber)); - - _consoleLoggingLevelSwitch.MinimumLevelChanged += (sender, args) => - { - Console(0, "Console debug level set to {0}", _consoleLoggingLevelSwitch.MinimumLevel); - }; - } - - public static void UpdateLoggerConfiguration(LoggerConfiguration config) - { - _loggerConfiguration = config; - - _logger = config.CreateLogger(); - } - - public static void ResetLoggerConfiguration() - { - _loggerConfiguration = _defaultLoggerConfiguration; - - _logger = _loggerConfiguration.CreateLogger(); - } - - private static LogEventLevel GetStoredLogEventLevel(string levelStoreKey) - { - try - { - var result = CrestronDataStoreStatic.GetLocalIntValue(levelStoreKey, out int logLevel); - - if (result != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - { - CrestronConsole.Print($"Unable to retrieve stored log level for {levelStoreKey}.\r\nError: {result}.\r\nSetting level to {LogEventLevel.Information}\r\n"); - return LogEventLevel.Information; - } - - if(logLevel < 0 || logLevel > 5) - { - CrestronConsole.PrintLine($"Stored Log level not valid for {levelStoreKey}: {logLevel}. Setting level to {LogEventLevel.Information}"); - return LogEventLevel.Information; - } - - return (LogEventLevel)logLevel; - } catch (Exception ex) - { - CrestronConsole.PrintLine($"Exception retrieving log level for {levelStoreKey}: {ex.Message}"); - return LogEventLevel.Information; - } - } - - private static void GetVersion() - { - var assembly = Assembly.GetExecutingAssembly(); - var ver = - assembly - .GetCustomAttributes(typeof (AssemblyInformationalVersionAttribute), false); - - if (ver != null && ver.Length > 0) - { - if (ver[0] is AssemblyInformationalVersionAttribute verAttribute) - { - PepperDashCoreVersion = verAttribute.InformationalVersion; - } - } - else - { - var version = assembly.GetName().Version; - PepperDashCoreVersion = version.ToString(); - } - } - - /// - /// Used to save memory when shutting down - /// - /// - static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - - if (programEventType == eProgramStatusEventType.Stopping) - { - Log.CloseAndFlush(); - - if (_saveTimer != null) - { - _saveTimer.Stop(); - _saveTimer = null; - } - Console(0, "Saving debug settings"); - SaveMemory(); - } - } - - /// - /// Callback for console command - /// - /// - public static void SetDebugFromConsole(string levelString) - { - try - { - if (levelString.Trim() == "?") - { - CrestronConsole.ConsoleCommandResponse( - $@"Used to set the minimum level of debug messages to be printed to the console: -{_logLevels[0]} = 0 -{_logLevels[1]} = 1 -{_logLevels[2]} = 2 -{_logLevels[3]} = 3 -{_logLevels[4]} = 4 -{_logLevels[5]} = 5"); - return; - } - - if (string.IsNullOrEmpty(levelString.Trim())) - { - CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", _consoleLoggingLevelSwitch.MinimumLevel); - return; - } - - if(int.TryParse(levelString, out var levelInt)) - { - if(levelInt < 0 || levelInt > 5) - { - CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level. If using a number, value must be between 0-5"); - return; - } - SetDebugLevel((uint) levelInt); - return; - } - - if(Enum.TryParse(levelString, out var levelEnum)) - { - SetDebugLevel(levelEnum); - return; - } - - CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level"); - } - catch - { - CrestronConsole.ConsoleCommandResponse("Usage: appdebug:P [0-5]"); - } - } - - /// - /// Sets the debug level - /// - /// Valid values 0-5 - public static void SetDebugLevel(uint level) - { - if(!_logLevels.TryGetValue(level, out var logLevel)) - { - logLevel = LogEventLevel.Information; - - CrestronConsole.ConsoleCommandResponse($"{level} not valid. Setting level to {logLevel}"); - - SetDebugLevel(logLevel); - } - - SetDebugLevel(logLevel); - } - - public static void SetDebugLevel(LogEventLevel level) - { - _consoleLoggingLevelSwitch.MinimumLevel = level; - - CrestronConsole.ConsoleCommandResponse("[Application {0}], Debug level set to {1}\r\n", - InitialParametersClass.ApplicationNumber, _consoleLoggingLevelSwitch.MinimumLevel); - - CrestronConsole.ConsoleCommandResponse($"Storing level {level}:{(int) level}"); - - var err = CrestronDataStoreStatic.SetLocalIntValue(LevelStoreKey, (int) level); - - CrestronConsole.ConsoleCommandResponse($"Store result: {err}:{(int)level}"); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - CrestronConsole.PrintLine($"Error saving console debug level setting: {err}"); - } - - public static void SetWebSocketMinimumDebugLevel(LogEventLevel level) - { - _websocketLoggingLevelSwitch.MinimumLevel = level; - - var err = CrestronDataStoreStatic.SetLocalUintValue(WebSocketLevelStoreKey, (uint) level); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogMessage(LogEventLevel.Information, "Error saving websocket debug level setting: {erro}", err); - - LogMessage(LogEventLevel.Information, "Websocket debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel); - } - - public static void SetErrorLogMinimumDebugLevel(LogEventLevel level) - { - _errorLogLevelSwitch.MinimumLevel = level; - - var err = CrestronDataStoreStatic.SetLocalUintValue(ErrorLogLevelStoreKey, (uint)level); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogMessage(LogEventLevel.Information, "Error saving Error Log debug level setting: {error}", err); - - LogMessage(LogEventLevel.Information, "Error log debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel); - } - - public static void SetFileMinimumDebugLevel(LogEventLevel level) - { - _errorLogLevelSwitch.MinimumLevel = level; - - var err = CrestronDataStoreStatic.SetLocalUintValue(ErrorLogLevelStoreKey, (uint)level); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogMessage(LogEventLevel.Information, "Error saving File debug level setting: {error}", err); - - LogMessage(LogEventLevel.Information, "File debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel); - } - - /// - /// Callback for console command - /// - /// - public static void SetDoNotLoadOnNextBootFromConsole(string stateString) - { - try - { - if (string.IsNullOrEmpty(stateString.Trim())) - { - CrestronConsole.ConsoleCommandResponse("DoNotLoadOnNextBoot = {0}", DoNotLoadConfigOnNextBoot); - return; - } - - SetDoNotLoadConfigOnNextBoot(bool.Parse(stateString)); - } - catch - { - CrestronConsole.ConsoleCommandResponse("Usage: donotloadonnextboot:P [true/false]"); - } - } - - /// - /// Callback for console command - /// - /// - public static void SetDebugFilterFromConsole(string items) - { - var str = items.Trim(); - if (str == "?") - { - CrestronConsole.ConsoleCommandResponse("Usage:\r APPDEBUGFILTER key1 key2 key3....\r " + - "+all: at beginning puts filter into 'default include' mode\r" + - " All keys that follow will be excluded from output.\r" + - "-all: at beginning puts filter into 'default exclude all' mode.\r" + - " All keys that follow will be the only keys that are shown\r" + - "+nokey: Enables messages with no key (default)\r" + - "-nokey: Disables messages with no key.\r" + - "(nokey settings are independent of all other settings)"); - return; - } - var keys = Regex.Split(str, @"\s*"); - foreach (var keyToken in keys) - { - var lkey = keyToken.ToLower(); - if (lkey == "+all") - { - IncludedExcludedKeys.Clear(); - _excludeAllMode = false; - } - else if (lkey == "-all") - { - IncludedExcludedKeys.Clear(); - _excludeAllMode = true; - } - //else if (lkey == "+nokey") - //{ - // ExcludeNoKeyMessages = false; - //} - //else if (lkey == "-nokey") - //{ - // ExcludeNoKeyMessages = true; - //} - else - { - string key; - if (lkey.StartsWith("-")) - { - key = lkey.Substring(1); - // if in exclude all mode, we need to remove this from the inclusions - if (_excludeAllMode) - { - if (IncludedExcludedKeys.ContainsKey(key)) - IncludedExcludedKeys.Remove(key); - } - // otherwise include all mode, add to the exclusions - else - { - IncludedExcludedKeys[key] = new object(); - } - } - else if (lkey.StartsWith("+")) - { - key = lkey.Substring(1); - // if in exclude all mode, we need to add this as inclusion - if (_excludeAllMode) - { - - IncludedExcludedKeys[key] = new object(); - } - // otherwise include all mode, remove this from exclusions - else - { - if (IncludedExcludedKeys.ContainsKey(key)) - IncludedExcludedKeys.Remove(key); - } - } - } - } - } - - - - - /// - /// sets the settings for a device or creates a new entry - /// - /// - /// - /// - public static void SetDeviceDebugSettings(string deviceKey, object settings) - { - _contexts.SetDebugSettingsForKey(deviceKey, settings); - SaveMemoryOnTimeout(); - } - - /// - /// Gets the device settings for a device by key or returns null - /// - /// - /// - public static object GetDeviceDebugSettingsForKey(string deviceKey) - { - return _contexts.GetDebugSettingsForKey(deviceKey); - } - - /// - /// Sets the flag to prevent application starting on next boot - /// - /// - public static void SetDoNotLoadConfigOnNextBoot(bool state) - { - DoNotLoadConfigOnNextBoot = state; - _contexts.GetOrCreateItem("DEFAULT").DoNotLoadOnNextBoot = state; - SaveMemoryOnTimeout(); - - CrestronConsole.ConsoleCommandResponse("[Application {0}], Do Not Load Config on Next Boot set to {1}", - InitialParametersClass.ApplicationNumber, DoNotLoadConfigOnNextBoot); - } - - /// - /// - /// - public static void ShowDebugLog(string s) - { - var loglist = CrestronLogger.PrintTheLog(s.ToLower() == "all"); - foreach (var l in loglist) - CrestronConsole.ConsoleCommandResponse(l + CrestronEnvironment.NewLine); - } - - /// - /// Log an Exception using Serilog's default Exception logging mechanism - /// - /// Exception to log - /// Message template - /// Optional IKeyed device. If provided, the Key of the device will be added to the log message - /// Args to put into message template - public static void LogMessage(Exception ex, string message, IKeyed device = null, params object[] args) - { - using (LogContext.PushProperty("Key", device?.Key)) - { - _logger.Error(ex, message, args); - } - } - - /// - /// Log a message - /// - /// Level to log at - /// Message template - /// Optional IKeyed device. If provided, the Key of the device will be added to the log message - /// Args to put into message template - public static void LogMessage(LogEventLevel level, string message, IKeyed device=null, params object[] args) - { - using (LogContext.PushProperty("Key", device?.Key)) - { - _logger.Write(level, message, args); - } - } - - public static void LogMessage(LogEventLevel level, string message, params object[] args) - { - LogMessage(level, message, null, args); - } - - public static void LogMessage(LogEventLevel level, IKeyed keyed, string message, params object[] args) - { - LogMessage(level, message, keyed, args); - } - - - private static void LogMessage(uint level, string format, params object[] items) - { - if (!_logLevels.ContainsKey(level)) return; - - var logLevel = _logLevels[level]; - - LogMessage(logLevel, format, items); - } - - private static void LogMessage(uint level, IKeyed keyed, string format, params object[] items) - { - if (!_logLevels.ContainsKey(level)) return; - - var logLevel = _logLevels[level]; - - LogMessage(logLevel, keyed, format, items); - } - - - /// - /// Prints message to console if current debug level is equal to or higher than the level of this message. - /// Uses CrestronConsole.PrintLine. - /// - /// - /// Console format string - /// Object parameters - [Obsolete("Use LogMessage methods")] - public static void Console(uint level, string format, params object[] items) - { - - LogMessage(level, format, items); - - //if (IsRunningOnAppliance) - //{ - // CrestronConsole.PrintLine("[{0}]App {1} Lvl {2}:{3}", DateTime.Now.ToString("HH:mm:ss.fff"), - // InitialParametersClass.ApplicationNumber, - // level, - // string.Format(format, items)); - //} - } - - /// - /// Logs to Console when at-level, and all messages to error log, including device key - /// - [Obsolete("Use LogMessage methods")] - public static void Console(uint level, IKeyed dev, string format, params object[] items) - { - LogMessage(level, dev, format, items); - - //if (Level >= level) - // Console(level, "[{0}] {1}", dev.Key, message); - } - - /// - /// Prints message to console if current debug level is equal to or higher than the level of this message. Always sends message to Error Log. - /// Uses CrestronConsole.PrintLine. - /// - [Obsolete("Use LogMessage methods")] - public static void Console(uint level, IKeyed dev, ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - LogMessage(level, dev, format, items); - } - - /// - /// Logs to Console when at-level, and all messages to error log - /// - [Obsolete("Use LogMessage methods")] - public static void Console(uint level, ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - LogMessage(level, format, items); - } - - /// - /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at - /// or above the level provided, then the output will be written to both console and the log. Otherwise - /// it will only be written to the log. - /// - [Obsolete("Use LogMessage methods")] - public static void ConsoleWithLog(uint level, string format, params object[] items) - { - LogMessage(level, format, items); - - // var str = string.Format(format, items); - //if (Level >= level) - // CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); - // CrestronLogger.WriteToLog(str, level); - } - - /// - /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at - /// or above the level provided, then the output will be written to both console and the log. Otherwise - /// it will only be written to the log. - /// - [Obsolete("Use LogMessage methods")] - public static void ConsoleWithLog(uint level, IKeyed dev, string format, params object[] items) - { - LogMessage(level, dev, format, items); - - // var str = string.Format(format, items); - // CrestronLogger.WriteToLog(string.Format("[{0}] {1}", dev.Key, str), level); - } - - /// - /// Prints to log and error log - /// - /// - /// - [Obsolete("Use LogMessage methods")] - public static void LogError(ErrorLogLevel errorLogLevel, string str) - { - switch (errorLogLevel) - { - case ErrorLogLevel.Error: - LogMessage(LogEventLevel.Error, str); - break; - case ErrorLogLevel.Warning: - LogMessage(LogEventLevel.Warning, str); - break; - case ErrorLogLevel.Notice: - LogMessage(LogEventLevel.Information, str); - break; - } - } - - /// - /// Writes the memory object after timeout - /// - static void SaveMemoryOnTimeout() - { - Console(0, "Saving debug settings"); - if (_saveTimer == null) - _saveTimer = new CTimer(o => - { - _saveTimer = null; - SaveMemory(); - }, SaveTimeoutMs); - else - _saveTimer.Reset(SaveTimeoutMs); - } - - /// - /// Writes the memory - use SaveMemoryOnTimeout - /// - static void SaveMemory() - { - //var dir = @"\NVRAM\debug"; - //if (!Directory.Exists(dir)) - // Directory.Create(dir); - - var fileName = GetMemoryFileName(); - - Console(0, ErrorLogLevel.Notice, "Loading debug settings file from {0}", fileName); - - using (var sw = new StreamWriter(fileName)) - { - var json = JsonConvert.SerializeObject(_contexts); - sw.Write(json); - sw.Flush(); - } - } - - /// - /// - /// - static void LoadMemory() - { - var file = GetMemoryFileName(); - if (File.Exists(file)) - { - using (var sr = new StreamReader(file)) - { - var json = sr.ReadToEnd(); - _contexts = JsonConvert.DeserializeObject(json); - - if (_contexts != null) - { - Console(1, "Debug memory restored from file"); - return; - } - } - } - - _contexts = new DebugContextCollection(); - } - - /// - /// Helper to get the file path for this app's debug memory - /// - static string GetMemoryFileName() - { - if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance) - { - // CheckForMigration(); - return string.Format(@"\user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber); - } - - return string.Format("{0}{1}user{1}debugSettings{1}{2}.json",Directory.GetApplicationRootDirectory(), Path.DirectorySeparatorChar, InitialParametersClass.RoomId); - } - - private static void CheckForMigration() - { - var oldFilePath = String.Format(@"\nvram\debugSettings\program{0}", InitialParametersClass.ApplicationNumber); - var newFilePath = String.Format(@"\user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber); - - //check for file at old path - if (!File.Exists(oldFilePath)) - { - Console(0, ErrorLogLevel.Notice, - String.Format( - @"Debug settings file migration not necessary. Using file at \user\debugSettings\program{0}", - InitialParametersClass.ApplicationNumber)); - - return; - } - - //create the new directory if it doesn't already exist - if (!Directory.Exists(@"\user\debugSettings")) - { - Directory.CreateDirectory(@"\user\debugSettings"); - } - - Console(0, ErrorLogLevel.Notice, - String.Format( - @"File found at \nvram\debugSettings\program{0}. Migrating to \user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber)); - - //Copy file from old path to new path, then delete it. This will overwrite the existing file - File.Copy(oldFilePath, newFilePath, true); - File.Delete(oldFilePath); - - //Check if the old directory is empty, then delete it if it is - if (Directory.GetFiles(@"\nvram\debugSettings").Length > 0) - { - return; - } - - Directory.Delete(@"\nvram\debugSettings"); - } - - /// - /// Error level to for message to be logged at - /// - public enum ErrorLogLevel - { - /// - /// Error - /// - Error, - /// - /// Warning - /// - Warning, - /// - /// Notice - /// - Notice, - /// - /// None - /// - None, - } - } -} \ No newline at end of file diff --git a/src/Pepperdash.Core/Logging/DebugClient.cs b/src/Pepperdash.Core/Logging/DebugClient.cs new file mode 100644 index 0000000..5dcddef --- /dev/null +++ b/src/Pepperdash.Core/Logging/DebugClient.cs @@ -0,0 +1,62 @@ +#if NET472 +using System; +using WebSocketSharp; +using WebSocketSharp.Server; + +namespace PepperDash.Core.Logging +{ + public class DebugClient : WebSocketBehavior + { + private DateTime connectionTime; + + public TimeSpan ConnectedDuration + { + get + { + if (Context.WebSocket.IsAlive) + { + return DateTime.Now - connectionTime; + } + + return new TimeSpan(0); + } + } + + public DebugClient() + { + Debug.Console(0, "DebugClient Created"); + } + + protected override void OnOpen() + { + base.OnOpen(); + + var url = Context.WebSocket.Url; + Debug.Console(0, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url); + + connectionTime = DateTime.Now; + } + + protected override void OnMessage(MessageEventArgs e) + { + base.OnMessage(e); + + Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data); + } + + protected override void OnClose(CloseEventArgs e) + { + base.OnClose(e); + + Debug.Console(0, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason); + } + + protected override void OnError(WebSocketSharp.ErrorEventArgs e) + { + base.OnError(e); + + Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message); + } + } +} +#endif \ No newline at end of file diff --git a/src/Pepperdash.Core/Logging/DebugConsoleSink.cs b/src/Pepperdash.Core/Logging/DebugConsoleSink.cs index a6c7f89..ff8d61f 100644 --- a/src/Pepperdash.Core/Logging/DebugConsoleSink.cs +++ b/src/Pepperdash.Core/Logging/DebugConsoleSink.cs @@ -1,23 +1,21 @@ -using Crestron.SimplSharp; -using Serilog.Configuration; +using System.IO; +using System.Text; +using Crestron.SimplSharp; using Serilog; +using Serilog.Configuration; using Serilog.Core; using Serilog.Events; using Serilog.Formatting; using Serilog.Formatting.Json; -using System.IO; -using System.Text; - -namespace PepperDash.Core +namespace PepperDash.Core.Logging { public class DebugConsoleSink : ILogEventSink { - private readonly ITextFormatter _textFormatter; + private readonly ITextFormatter textFormatter; public void Emit(LogEvent logEvent) { - if (!Debug.IsRunningOnAppliance) return; /*string message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}]{logEvent.RenderMessage()}"; @@ -28,28 +26,44 @@ namespace PepperDash.Core var buffer = new StringWriter(new StringBuilder(256)); - _textFormatter.Format(logEvent, buffer); + textFormatter.Format(logEvent, buffer); var message = buffer.ToString(); CrestronConsole.PrintLine(message); } - public DebugConsoleSink(ITextFormatter formatProvider ) + public DebugConsoleSink(ITextFormatter formatProvider) { - _textFormatter = formatProvider ?? new JsonFormatter(); + textFormatter = formatProvider ?? new JsonFormatter(); } - } public static class DebugConsoleSinkExtensions { public static LoggerConfiguration DebugConsoleSink( - this LoggerSinkConfiguration loggerConfiguration, - ITextFormatter formatProvider = null) + this LoggerSinkConfiguration loggerConfiguration, + ITextFormatter formatProvider = null, + LoggingLevelSwitch levelSwitch = null) { - return loggerConfiguration.Sink(new DebugConsoleSink(formatProvider)); + var sink = new DebugConsoleSink(formatProvider); + return loggerConfiguration.Conditional(Predicate, c => c.Sink(sink, levelSwitch: levelSwitch)); + + static bool Predicate(LogEvent @event) + { + if (!Debug.IsRunningOnAppliance) + { + return false; + } + + if (@event.Properties.TryGetValue("Key", out var value) && + value is ScalarValue { Value: string rawValue }) + { + return DebugContext.DeviceExistsInContext(Debug.ConsoleLevelStoreKey, rawValue); + } + + return true; + } } } - } diff --git a/src/Pepperdash.Core/Logging/DebugContext.cs b/src/Pepperdash.Core/Logging/DebugContext.cs index 54c8741..8f9d98f 100644 --- a/src/Pepperdash.Core/Logging/DebugContext.cs +++ b/src/Pepperdash.Core/Logging/DebugContext.cs @@ -1,281 +1,107 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Crestron.SimplSharp; -using Crestron.SimplSharp.CrestronIO; using Newtonsoft.Json; +using Serilog; +using Serilog.Events; +using WebSocketSharp; - -namespace PepperDash.Core +namespace PepperDash.Core.Logging { /// /// Represents a debugging context /// - public class DebugContext + public static class DebugContext { - /// - /// Describes the folder location where a given program stores it's debug level memory. By default, the - /// file written will be named appNdebug where N is 1-10. - /// - public string Key { get; private set; } + private static readonly CTimer SaveTimer; + private static readonly Dictionary CurrentData; - ///// - ///// The name of the file containing the current debug settings. - ///// - //string FileName = string.Format(@"\nvram\debug\app{0}Debug.json", InitialParametersClass.ApplicationNumber); - - DebugContextSaveData SaveData; - - int SaveTimeoutMs = 30000; - - CTimer SaveTimer; - - - static List Contexts = new List(); + public static readonly string ApplianceFilePath = Path.Combine("user", "debug", $"app-{InitialParametersClass.ApplicationNumber}-Debug.json"); + public static readonly string ServerFilePath = Path.Combine("User", "debug", $"app-{InitialParametersClass.RoomId}-Debug.json"); /// - /// Creates or gets a debug context + /// The name of the file containing the current debug settings. /// - /// - /// - public static DebugContext GetDebugContext(string key) + public static readonly string FileName = CrestronEnvironment.DevicePlatform switch { - var context = Contexts.FirstOrDefault(c => c.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - if (context == null) - { - context = new DebugContext(key); - Contexts.Add(context); - } - return context; - } + eDevicePlatform.Appliance => ApplianceFilePath, + eDevicePlatform.Server => ServerFilePath, + _ => string.Empty + }; - /// - /// Do not use. For S+ access. - /// - public DebugContext() { } - - DebugContext(string key) + static DebugContext() { - Key = key; - if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro) + CurrentData = LoadData(); + SaveTimer = new CTimer(_ => SaveData(), Timeout.Infinite); + + CrestronEnvironment.ProgramStatusEventHandler += args => { - // Add command to console - CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", - "appdebug:P [0-2]: Sets the application's console debug message level", - ConsoleAccessLevelEnum.AccessOperator); - } - - CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; - - LoadMemory(); - } - - /// - /// Used to save memory when shutting down - /// - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType == eProgramStatusEventType.Stopping) - { - if (SaveTimer != null) + if (args == eProgramStatusEventType.Stopping) { - SaveTimer.Stop(); - SaveTimer = null; + using (SaveTimer) + { + SaveData(); + SaveTimer.Stop(); + } } - Console(0, "Saving debug settings"); - SaveMemory(); - } + }; } - /// - /// Callback for console command - /// - /// - public void SetDebugFromConsole(string levelString) + public static DebugContextData GetDataForKey(string key, LogEventLevel defaultLevel) => + CurrentData.TryGetValue(key, out var data) ? data : new DebugContextData(defaultLevel); + + public static bool DeviceExistsInContext(string contextKey, string deviceKey) => + CurrentData.TryGetValue(contextKey, out var data) switch + { + true when data.Devices != null => data.Devices.Any(key => string.Equals(key, deviceKey, StringComparison.OrdinalIgnoreCase)), + _ => false + }; + + public static void SetDataForKey(string key, LogEventLevel defaultLevel, string[] devices = null) { + if (CurrentData.ContainsKey(key)) + { + CurrentData[key] = new DebugContextData(defaultLevel, devices); + } + else + { + CurrentData.Add(key, new DebugContextData(defaultLevel, devices)); + } + + SaveTimer.Reset(5000); + } + + private static void SaveData() + { + Log.Information("Saving debug data to file:{File}", FileName); + try { - if (string.IsNullOrEmpty(levelString.Trim())) - { - CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", SaveData.Level); - return; - } - - SetDebugLevel(Convert.ToInt32(levelString)); + var json = JsonConvert.SerializeObject(CurrentData); + File.WriteAllText(FileName, json); } - catch + catch (Exception e) { - CrestronConsole.PrintLine("Usage: appdebug:P [0-2]"); + Log.Error(e, "Failed to save debug data"); } } - /// - /// Sets the debug level - /// - /// Valid values 0 (no debug), 1 (critical), 2 (all messages) - public void SetDebugLevel(int level) + private static Dictionary LoadData() { - if (level <= 2) + if (!File.Exists(FileName)) { - SaveData.Level = level; - SaveMemoryOnTimeout(); - - CrestronConsole.PrintLine("[Application {0}], Debug level set to {1}", - InitialParametersClass.ApplicationNumber, SaveData.Level); + return new Dictionary(); } - } - /// - /// Prints message to console if current debug level is equal to or higher than the level of this message. - /// Uses CrestronConsole.PrintLine. - /// - /// - /// Console format string - /// Object parameters - public void Console(uint level, string format, params object[] items) - { - if (SaveData.Level >= level) - CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, - string.Format(format, items)); - } - - /// - /// Appends a device Key to the beginning of a message - /// - public void Console(uint level, IKeyed dev, string format, params object[] items) - { - if (SaveData.Level >= level) - Console(level, "[{0}] {1}", dev.Key, string.Format(format, items)); - } - - /// - /// - /// - /// - /// - /// - /// - /// - public void Console(uint level, IKeyed dev, Debug.ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - if (SaveData.Level >= level) - { - var str = string.Format("[{0}] {1}", dev.Key, string.Format(format, items)); - Console(level, str); - LogError(errorLogLevel, str); - } - } - - /// - /// - /// - /// - /// - /// - /// - public void Console(uint level, Debug.ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - if (SaveData.Level >= level) - { - var str = string.Format(format, items); - Console(level, str); - LogError(errorLogLevel, str); - } - } - - /// - /// - /// - /// - /// - public void LogError(Debug.ErrorLogLevel errorLogLevel, string str) - { - string msg = string.Format("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); - switch (errorLogLevel) - { - case Debug.ErrorLogLevel.Error: - ErrorLog.Error(msg); - break; - case Debug.ErrorLogLevel.Warning: - ErrorLog.Warn(msg); - break; - case Debug.ErrorLogLevel.Notice: - ErrorLog.Notice(msg); - break; - } - } - - /// - /// Writes the memory object after timeout - /// - void SaveMemoryOnTimeout() - { - if (SaveTimer == null) - SaveTimer = new CTimer(o => - { - SaveTimer = null; - SaveMemory(); - }, SaveTimeoutMs); - else - SaveTimer.Reset(SaveTimeoutMs); - } - - /// - /// Writes the memory - use SaveMemoryOnTimeout - /// - void SaveMemory() - { - using (StreamWriter sw = new StreamWriter(GetMemoryFileName())) - { - var json = JsonConvert.SerializeObject(SaveData); - sw.Write(json); - sw.Flush(); - } - } - - /// - /// - /// - void LoadMemory() - { - var file = GetMemoryFileName(); - if (File.Exists(file)) - { - using (StreamReader sr = new StreamReader(file)) - { - var data = JsonConvert.DeserializeObject(sr.ReadToEnd()); - if (data != null) - { - SaveData = data; - Debug.Console(1, "Debug memory restored from file"); - return; - } - else - SaveData = new DebugContextSaveData(); - } - } - } - - /// - /// Helper to get the file path for this app's debug memory - /// - string GetMemoryFileName() - { - return string.Format(@"\NVRAM\debugSettings\program{0}-{1}", InitialParametersClass.ApplicationNumber, Key); + var json = File.ReadAllText(FileName); + return JsonConvert.DeserializeObject>(json); } } /// /// /// - public class DebugContextSaveData - { - /// - /// - /// - public int Level { get; set; } - } + public record DebugContextData(LogEventLevel Level, string[] Devices = null); } \ No newline at end of file diff --git a/src/Pepperdash.Core/Logging/DebugErrorLogSink.cs b/src/Pepperdash.Core/Logging/DebugErrorLogSink.cs index 3885982..d3a0c99 100644 --- a/src/Pepperdash.Core/Logging/DebugErrorLogSink.cs +++ b/src/Pepperdash.Core/Logging/DebugErrorLogSink.cs @@ -5,9 +5,7 @@ using Serilog.Formatting; using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; namespace PepperDash.Core.Logging { @@ -24,6 +22,7 @@ namespace PepperDash.Core.Logging {LogEventLevel.Error, (msg) => ErrorLog.Error(msg) }, {LogEventLevel.Fatal, (msg) => ErrorLog.Error(msg) } }; + public void Emit(LogEvent logEvent) { string message; diff --git a/src/Pepperdash.Core/Logging/DebugExtensions.cs b/src/Pepperdash.Core/Logging/DebugExtensions.cs index e37e6d9..46c8d8e 100644 --- a/src/Pepperdash.Core/Logging/DebugExtensions.cs +++ b/src/Pepperdash.Core/Logging/DebugExtensions.cs @@ -1,4 +1,5 @@ -using Serilog; +using System; +using Serilog; using Serilog.Events; using Log = PepperDash.Core.Debug; @@ -30,10 +31,5 @@ namespace PepperDash.Core.Logging { Log.LogMessage(LogEventLevel.Error, device, message, args); } - - public static void LogFatal(this IKeyed device, string message, params object[] args) - { - Log.LogMessage(LogEventLevel.Fatal, device, message, args); - } } } diff --git a/src/Pepperdash.Core/Logging/DebugMemory.cs b/src/Pepperdash.Core/Logging/DebugMemory.cs deleted file mode 100644 index a5737af..0000000 --- a/src/Pepperdash.Core/Logging/DebugMemory.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Collections.Generic; -using Crestron.SimplSharp; -using Newtonsoft.Json; - -namespace PepperDash.Core.Logging -{ - /// - /// Class to persist current Debug settings across program restarts - /// - public class DebugContextCollection - { - /// - /// To prevent threading issues with the DeviceDebugSettings collection - /// - private readonly CCriticalSection _deviceDebugSettingsLock; - - [JsonProperty("items")] private readonly Dictionary _items; - - /// - /// Collection of the debug settings for each device where the dictionary key is the device key - /// - [JsonProperty("deviceDebugSettings")] - private Dictionary DeviceDebugSettings { get; set; } - - - /// - /// Default constructor - /// - public DebugContextCollection() - { - _deviceDebugSettingsLock = new CCriticalSection(); - DeviceDebugSettings = new Dictionary(); - _items = new Dictionary(); - } - - /// - /// Sets the level of a given context item, and adds that item if it does not - /// exist - /// - /// - /// - public void SetLevel(string contextKey, int level) - { - if (level < 0 || level > 2) - return; - GetOrCreateItem(contextKey).Level = level; - } - - /// - /// Gets a level or creates it if not existing - /// - /// - /// - public DebugContextItem GetOrCreateItem(string contextKey) - { - if (!_items.ContainsKey(contextKey)) - _items[contextKey] = new DebugContextItem { Level = 0 }; - return _items[contextKey]; - } - - - /// - /// sets the settings for a device or creates a new entry - /// - /// - /// - /// - public void SetDebugSettingsForKey(string deviceKey, object settings) - { - try - { - _deviceDebugSettingsLock.Enter(); - - if (DeviceDebugSettings.ContainsKey(deviceKey)) - { - DeviceDebugSettings[deviceKey] = settings; - } - else - DeviceDebugSettings.Add(deviceKey, settings); - } - finally - { - _deviceDebugSettingsLock.Leave(); - } - } - - /// - /// Gets the device settings for a device by key or returns null - /// - /// - /// - public object GetDebugSettingsForKey(string deviceKey) - { - return DeviceDebugSettings[deviceKey]; - } - } - - /// - /// Contains information about - /// - public class DebugContextItem - { - /// - /// The level of debug messages to print - /// - [JsonProperty("level")] - public int Level { get; set; } - - /// - /// Property to tell the program not to intitialize when it boots, if desired - /// - [JsonProperty("doNotLoadOnNextBoot")] - public bool DoNotLoadOnNextBoot { get; set; } - } -} \ No newline at end of file diff --git a/src/Pepperdash.Core/Logging/DebugNet472WebSocket.cs b/src/Pepperdash.Core/Logging/DebugNet472WebSocket.cs new file mode 100644 index 0000000..e211ffa --- /dev/null +++ b/src/Pepperdash.Core/Logging/DebugNet472WebSocket.cs @@ -0,0 +1,72 @@ +#if NET472 + +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using WebSocketSharp.Net; +using WebSocketSharp.Server; + +namespace PepperDash.Core.Logging +{ + internal class DebugNet472WebSocket : DebugWebSocket + { + private const string Path = "/debug/join/"; + + private readonly WebSocketServer server; + + public DebugNet472WebSocket(string certPath = "") : base(certPath) + { + server = new WebSocketServer(Port, IsSecure); + + if (IsSecure) + { + server.SslConfiguration = new ServerSslConfiguration(new X509Certificate2(certPath, CertificatePassword)) + { + ClientCertificateRequired = false, + CheckCertificateRevocation = false, + EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, + + ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => + { + Debug.LogInformation("HTTPS ClientCertificateValidation Callback triggered"); + return true; + } + }; + } + + server.AddWebSocketService(Path); + server.Start(); + } + + public override bool IsListening => server.IsListening; + + public override void Broadcast(string message) => server.WebSocketServices.Broadcast(message); + } +} +#else + +using System; +using System.Net; + +namespace PepperDash.Core.Logging +{ + internal class DebugNetWebSocket : DebugWebSocket + { + private const string Path = "/debug/join/"; + + private readonly HttpListener server = new(); + + public DebugNetWebSocket(int port, string certPath = "") : base(certPath) + { + server.AuthenticationSchemeSelectorDelegate = (request) => AuthenticationSchemes.Anonymous; + server.Prefixes.Add("wss://*:" + port + Path); + } + + public override bool IsListening => server.IsListening; + + public override void Broadcast(string message) => throw new NotImplementedException(); + } +} + +#endif + + diff --git a/src/Pepperdash.Core/Logging/DebugWebSocket.cs b/src/Pepperdash.Core/Logging/DebugWebSocket.cs new file mode 100644 index 0000000..311e0c9 --- /dev/null +++ b/src/Pepperdash.Core/Logging/DebugWebSocket.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using Crestron.SimplSharp; +using Org.BouncyCastle.Asn1.X509; + +namespace PepperDash.Core.Logging +{ + public abstract class DebugWebSocket + { + public const string CertificateName = "selfCres"; + public const string CertificatePassword = "cres12345"; + + protected DebugWebSocket(string certPath = "") + { + IsSecure = !string.IsNullOrEmpty(certPath); + + if (!IsSecure) + { + return; + } + + if (!File.Exists(certPath)) + { + CreateCert(certPath); + } + } + + protected bool IsSecure { get; } + + private static void CreateCert(string filePath) + { + try + { + var utility = new BouncyCertificate(); + var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); + var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); + var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0); + var certificate = utility.CreateSelfSignedCertificate($"CN={hostName}.{domainName}", [$"{hostName}.{domainName}", ipAddress], new[] { KeyPurposeID.IdKPServerAuth, KeyPurposeID.IdKPClientAuth }); + + utility.CertificatePassword = CertificatePassword; + utility.WriteCertificate(certificate, filePath, CertificateName); + } + catch (Exception ex) + { + Debug.LogError(ex, "WSS failed to create cert"); + } + } + + public abstract bool IsListening { get; } + + public abstract void Broadcast(string message); + + public int Port { get; } = new Random().Next(65435, 65535); + } +} diff --git a/src/Pepperdash.Core/Logging/DebugWebsocketSink.cs b/src/Pepperdash.Core/Logging/DebugWebsocketSink.cs index f24a585..f138790 100644 --- a/src/Pepperdash.Core/Logging/DebugWebsocketSink.cs +++ b/src/Pepperdash.Core/Logging/DebugWebsocketSink.cs @@ -1,271 +1,40 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.IO; using Serilog; +using Serilog.Configuration; using Serilog.Core; using Serilog.Events; -using Serilog.Configuration; -using WebSocketSharp.Server; -using Crestron.SimplSharp; -using WebSocketSharp; -using System.Security.Authentication; -using WebSocketSharp.Net; -using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; -using System.IO; -using Org.BouncyCastle.Asn1.X509; using Serilog.Formatting; -using Newtonsoft.Json.Linq; using Serilog.Formatting.Json; -namespace PepperDash.Core +namespace PepperDash.Core.Logging { public class DebugWebsocketSink : ILogEventSink { - private HttpServer _httpsServer; - - private string _path = "/debug/join/"; - private const string _certificateName = "selfCres"; - private const string _certificatePassword = "cres12345"; + private readonly DebugWebSocket webSocket; + private readonly ITextFormatter textFormatter; - public int Port - { get - { - - if(_httpsServer == null) return 0; - return _httpsServer.Port; - } - } - - public string Url + public DebugWebsocketSink(DebugWebSocket webSocket, ITextFormatter formatProvider = null) { - get - { - if (_httpsServer == null) return ""; - return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{_httpsServer.WebSocketServices[_path].Path}"; - } + this.webSocket = webSocket; + textFormatter = formatProvider ?? new JsonFormatter(); } - public bool IsRunning { get => _httpsServer?.IsListening ?? false; } - - - private readonly ITextFormatter _textFormatter; - - public DebugWebsocketSink(ITextFormatter formatProvider) - { - - _textFormatter = formatProvider ?? new JsonFormatter(); - - if (!File.Exists($"\\user\\{_certificateName}.pfx")) - CreateCert(null); - - CrestronEnvironment.ProgramStatusEventHandler += type => - { - if (type == eProgramStatusEventType.Stopping) - { - StopServer(); - } - }; - } - - private void CreateCert(string[] args) - { - try - { - //Debug.Console(0,"CreateCert Creating Utility"); - CrestronConsole.PrintLine("CreateCert Creating Utility"); - //var utility = new CertificateUtility(); - var utility = new BouncyCertificate(); - //Debug.Console(0, "CreateCert Calling CreateCert"); - CrestronConsole.PrintLine("CreateCert Calling CreateCert"); - //utility.CreateCert(); - var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); - var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); - var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0); - - //Debug.Console(0, "DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress); - CrestronConsole.PrintLine(string.Format("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress)); - - var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), new[] { string.Format("{0}.{1}", hostName, domainName), ipAddress }, new[] { KeyPurposeID.IdKPServerAuth, KeyPurposeID.IdKPClientAuth }); - //Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested - //Debug.Print($"CreateCert Storing Certificate To My.LocalMachine"); - //utility.AddCertToStore(certificate, StoreName.My, StoreLocation.LocalMachine); - //Debug.Console(0, "CreateCert Saving Cert to \\user\\"); - CrestronConsole.PrintLine("CreateCert Saving Cert to \\user\\"); - utility.CertificatePassword = _certificatePassword; - utility.WriteCertificate(certificate, @"\user\", _certificateName); - //Debug.Console(0, "CreateCert Ending CreateCert"); - CrestronConsole.PrintLine("CreateCert Ending CreateCert"); - } - catch (Exception ex) - { - //Debug.Console(0, "WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace); - CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace)); - } - } public void Emit(LogEvent logEvent) { - if (_httpsServer == null || !_httpsServer.IsListening) return; + if (!webSocket.IsListening) return; - var sw = new StringWriter(); - _textFormatter.Format(logEvent, sw); - - _httpsServer.WebSocketServices.Broadcast(sw.ToString()); - - } - - public void StartServerAndSetPort(int port) - { - Debug.Console(0, "Starting Websocket Server on port: {0}", port); - - - Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword); - } - - private void Start(int port, string certPath = "", string certPassword = "") - { - try - { - _httpsServer = new HttpServer(port, true); - - - if (!string.IsNullOrWhiteSpace(certPath)) - { - Debug.Console(0, "Assigning SSL Configuration"); - _httpsServer.SslConfiguration = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword)) - { - ClientCertificateRequired = false, - CheckCertificateRevocation = false, - EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, - //this is just to test, you might want to actually validate - ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => - { - Debug.Console(0, "HTTPS ClientCerticateValidation Callback triggered"); - return true; - } - }; - } - Debug.Console(0, "Adding Debug Client Service"); - _httpsServer.AddWebSocketService(_path); - Debug.Console(0, "Assigning Log Info"); - _httpsServer.Log.Level = LogLevel.Trace; - _httpsServer.Log.Output = (d, s) => - { - uint level; - - switch(d.Level) - { - case WebSocketSharp.LogLevel.Fatal: - level = 3; - break; - case WebSocketSharp.LogLevel.Error: - level = 2; - break; - case WebSocketSharp.LogLevel.Warn: - level = 1; - break; - case WebSocketSharp.LogLevel.Info: - level = 0; - break; - case WebSocketSharp.LogLevel.Debug: - level = 4; - break; - case WebSocketSharp.LogLevel.Trace: - level = 5; - break; - default: - level = 4; - break; - } - - Debug.Console(level, "{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s); - }; - Debug.Console(0, "Starting"); - - _httpsServer.Start(); - Debug.Console(0, "Ready"); - } - catch (Exception ex) - { - Debug.Console(0, "WebSocket Failed to start {0}", ex.Message); - } - } - - public void StopServer() - { - Debug.Console(0, "Stopping Websocket Server"); - _httpsServer?.Stop(); - - _httpsServer = null; + using var sw = new StringWriter(); + textFormatter.Format(logEvent, sw); + webSocket.Broadcast(sw.ToString()); } } - public static class DebugWebsocketSinkExtensions + public class NoopSink : ILogEventSink { - public static LoggerConfiguration DebugWebsocketSink( - this LoggerSinkConfiguration loggerConfiguration, - ITextFormatter formatProvider = null) + public static readonly NoopSink Instance = new NoopSink(); + public void Emit(LogEvent logEvent) { - return loggerConfiguration.Sink(new DebugWebsocketSink(formatProvider)); - } - } - - public class DebugClient : WebSocketBehavior - { - private DateTime _connectionTime; - - public TimeSpan ConnectedDuration - { - get - { - if (Context.WebSocket.IsAlive) - { - return DateTime.Now - _connectionTime; - } - else - { - return new TimeSpan(0); - } - } - } - - public DebugClient() - { - Debug.Console(0, "DebugClient Created"); - } - - protected override void OnOpen() - { - base.OnOpen(); - - var url = Context.WebSocket.Url; - Debug.Console(0, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url); - - _connectionTime = DateTime.Now; - } - - protected override void OnMessage(MessageEventArgs e) - { - base.OnMessage(e); - - Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data); - } - - protected override void OnClose(CloseEventArgs e) - { - base.OnClose(e); - - Debug.Console(0, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason); - - } - - protected override void OnError(WebSocketSharp.ErrorEventArgs e) - { - base.OnError(e); - - Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message); } } } diff --git a/src/Pepperdash.Core/PepperDash.Core.csproj b/src/Pepperdash.Core/PepperDash.Core.csproj index c257ad1..fc5d431 100644 --- a/src/Pepperdash.Core/PepperDash.Core.csproj +++ b/src/Pepperdash.Core/PepperDash.Core.csproj @@ -3,6 +3,7 @@ PepperDash.Core PepperDashCore net472;net6.0;net8.0 + latest true en bin\$(Configuration)\ @@ -40,6 +41,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +