feat: logging refactor

- added ability to debug by key
- started on being able to switch out the websockete based on target env
- added in comprehensive log methods to debug class
This commit is contained in:
Nick Genovese
2024-11-01 15:00:03 -04:00
parent 56263df116
commit de5b5eafff
13 changed files with 1001 additions and 1534 deletions

View File

@@ -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
{
/// <summary>
/// Contains debug commands for use in various situations
/// </summary>
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<int, LogEventLevel> 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);
/// <summary>
/// When this is true, the configuration file will NOT be loaded until triggered by either a console command or a signal
/// </summary>
public static bool DoNotLoadConfigOnNextBoot =>
CrestronDataStoreStatic.GetLocalBoolValue(DoNotLoadConfigOnNextBootKey, out var value) switch
{
CrestronDataStore.CDS_ERROR.CDS_SUCCESS => value,
_ => false
};
/// <summary>
/// Version for the currently loaded PepperDashCore dll
/// </summary>
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");
}
}
/// <summary>
/// Callback for console command
/// </summary>
/// <param name="stateString"></param>
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]");
}
}
/// <summary>
/// Sets the flag to prevent application starting on next boot
/// </summary>
/// <param name="state"></param>
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);
}
/// <summary>
/// Prints message to console if current debug level is equal to or higher than the level of this message.
/// Uses CrestronConsole.PrintLine.
/// </summary>
/// <param name="level"></param>
/// <param name="format">Console format string</param>
/// <param name="items">Object parameters</param>
[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));
//}
}
/// <summary>
/// Logs to Console when at-level, and all messages to error log, including device key
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// Logs to Console when at-level, and all messages to error log
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
///
/// </summary>
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>(T device) where T : IKeyed =>
Log.ForContext<T>().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<T>(LogEventLevel level, IKeyed device, string message, params object[] args)
{
Log.ForContext("Key", device?.Key).ForContext<T>().Write(level, message, args);
}
public static void LogMessage<T>(LogEventLevel level, string message, params object[] args)
{
Log.ForContext<T>().Write(level, message, args);
}
public static void LogMessage<T>(LogEventLevel level, IKeyed device, Exception ex, string message, params object[] args)
{
Log.ForContext<T>().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<T>(string message, params object[] args)
{
Log.ForContext<T>().Error(message, args);
}
public static void LogError<T>(Exception ex, string message, params object[] args)
{
Log.ForContext<T>().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<T>(IKeyed device, string message, params object[] args)
{
Log.ForContext<T>().ForContext("Key", device?.Key).Error(message, args);
}
public static void LogError<T>(IKeyed device, Exception ex, string message, params object[] args)
{
Log.ForContext<T>().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<T>(string message, params object[] args)
{
Log.ForContext<T>().Verbose(message, args);
}
public static void LogVerbose<T>(Exception ex, string message, params object[] args)
{
Log.ForContext<T>().ForContext<T>().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<T>(IKeyed device, string message, params object[] args)
{
Log.ForContext("Key", device?.Key).ForContext<T>().Verbose(message, args);
}
public static void LogVerbose<T>(IKeyed device, Exception ex, string message, params object[] args)
{
Log.ForContext<T>().ForContext<T>().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<T>(IKeyed device, string message, params object[] args)
{
Log.ForContext("Key", device?.Key).ForContext<T>().Debug(message, args);
}
public static void LogDebug<T>(IKeyed device, Exception ex, string message, params object[] args)
{
Log.ForContext<T>().ForContext<T>().Debug(ex, message, args);
}
public static void LogDebug(string message, params object[] args)
{
Log.Debug(message, args);
}
public static void LogDebug<T>(string message, params object[] args)
{
Log.ForContext<T>().Debug(message, args);
}
public static void LogDebug(Exception ex, string message, params object[] args)
{
Log.Debug(ex, message, args);
}
public static void LogDebug<T>(Exception ex, string message, params object[] args)
{
Log.ForContext<T>().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<T>(IKeyed device, string message, params object[] args)
{
Log.ForContext("Key", device?.Key).ForContext<T>().Information(message, args);
}
public static void LogInformation<T>(IKeyed device, Exception ex, string message, params object[] args)
{
Log.ForContext<T>().ForContext<T>().Information(ex, message, args);
}
public static void LogInformation(string message, params object[] args)
{
Log.Information(message, args);
}
public static void LogInformation<T>(string message, params object[] args)
{
Log.ForContext<T>().Information(message, args);
}
public static void LogInformation(Exception ex, string message, params object[] args)
{
Log.Information(ex, message, args);
}
public static void LogInformation<T>(Exception ex, string message, params object[] args)
{
Log.ForContext<T>().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<T>(IKeyed device, string message, params object[] args)
{
Log.ForContext("Key", device?.Key).ForContext<T>().Warning(message, args);
}
public static void LogWarning<T>(IKeyed device, Exception ex, string message, params object[] args)
{
Log.ForContext<T>().ForContext<T>().Warning(ex, message, args);
}
public static void LogWarning(string message, params object[] args)
{
Log.Warning(message, args);
}
public static void LogWarning<T>(string message, params object[] args)
{
Log.ForContext<T>().Warning(message, args);
}
public static void LogWarning(Exception ex, string message, params object[] args)
{
Log.Warning(ex, message, args);
}
public static void LogWarning<T>(Exception ex, string message, params object[] args)
{
Log.ForContext<T>().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<T>(IKeyed device, string message, params object[] args)
{
Log.ForContext("Key", device?.Key).ForContext<T>().Fatal(message, args);
}
public static void LogFatal<T>(IKeyed device, Exception ex, string message, params object[] args)
{
Log.ForContext<T>().ForContext<T>().Fatal(ex, message, args);
}
public static void LogFatal(string message, params object[] args)
{
Log.Fatal(message, args);
}
public static void LogFatal<T>(string message, params object[] args)
{
Log.ForContext<T>().Fatal(message, args);
}
public static void LogFatal(Exception ex, string message, params object[] args)
{
Log.Fatal(ex, message, args);
}
public static void LogFatal<T>(Exception ex, string message, params object[] args)
{
Log.ForContext<T>().Fatal(ex, message, args);
}
public enum ErrorLogLevel
{
/// <summary>
/// Error
/// </summary>
Error,
/// <summary>
/// Warning
/// </summary>
Warning,
/// <summary>
/// Notice
/// </summary>
Notice,
/// <summary>
/// None
/// </summary>
None,
}
}
}

View File

@@ -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);
}

View File

@@ -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
{
/// <summary>
/// Contains debug commands for use in various situations
/// </summary>
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<uint, LogEventLevel> _logLevels = new Dictionary<uint, LogEventLevel>()
{
{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; }
}
/// <summary>
/// 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.
/// </summary>
public static string OldFilePathPrefix = @"\nvram\debug\";
/// <summary>
/// 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.
/// </summary>
public static string NewFilePathPrefix = @"\user\debug\";
/// <summary>
/// The name of the file containing the current debug settings.
/// </summary>
public static string FileName = string.Format(@"app{0}Debug.json", InitialParametersClass.ApplicationNumber);
/// <summary>
/// Debug level to set for a given program.
/// </summary>
public static int Level { get; private set; }
/// <summary>
/// When this is true, the configuration file will NOT be loaded until triggered by either a console command or a signal
/// </summary>
public static bool DoNotLoadConfigOnNextBoot { get; private set; }
private static DebugContextCollection _contexts;
private const int SaveTimeoutMs = 30000;
public static bool IsRunningOnAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance;
/// <summary>
/// Version for the currently loaded PepperDashCore dll
/// </summary>
public static string PepperDashCoreVersion { get; private set; }
private static CTimer _saveTimer;
/// <summary>
/// When true, the IncludedExcludedKeys dict will contain keys to include.
/// When false (default), IncludedExcludedKeys will contain keys to exclude.
/// </summary>
private static bool _excludeAllMode;
//static bool ExcludeNoKeyMessages;
private static readonly Dictionary<string, object> 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<string, object>();
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();
}
}
/// <summary>
/// Used to save memory when shutting down
/// </summary>
/// <param name="programEventType"></param>
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();
}
}
/// <summary>
/// Callback for console command
/// </summary>
/// <param name="levelString"></param>
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<LogEventLevel>(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]");
}
}
/// <summary>
/// Sets the debug level
/// </summary>
/// <param name="level"> Valid values 0-5</param>
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);
}
/// <summary>
/// Callback for console command
/// </summary>
/// <param name="stateString"></param>
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]");
}
}
/// <summary>
/// Callback for console command
/// </summary>
/// <param name="items"></param>
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);
}
}
}
}
}
/// <summary>
/// sets the settings for a device or creates a new entry
/// </summary>
/// <param name="deviceKey"></param>
/// <param name="settings"></param>
/// <returns></returns>
public static void SetDeviceDebugSettings(string deviceKey, object settings)
{
_contexts.SetDebugSettingsForKey(deviceKey, settings);
SaveMemoryOnTimeout();
}
/// <summary>
/// Gets the device settings for a device by key or returns null
/// </summary>
/// <param name="deviceKey"></param>
/// <returns></returns>
public static object GetDeviceDebugSettingsForKey(string deviceKey)
{
return _contexts.GetDebugSettingsForKey(deviceKey);
}
/// <summary>
/// Sets the flag to prevent application starting on next boot
/// </summary>
/// <param name="state"></param>
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);
}
/// <summary>
///
/// </summary>
public static void ShowDebugLog(string s)
{
var loglist = CrestronLogger.PrintTheLog(s.ToLower() == "all");
foreach (var l in loglist)
CrestronConsole.ConsoleCommandResponse(l + CrestronEnvironment.NewLine);
}
/// <summary>
/// Log an Exception using Serilog's default Exception logging mechanism
/// </summary>
/// <param name="ex">Exception to log</param>
/// <param name="message">Message template</param>
/// <param name="device">Optional IKeyed device. If provided, the Key of the device will be added to the log message</param>
/// <param name="args">Args to put into message template</param>
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);
}
}
/// <summary>
/// Log a message
/// </summary>
/// <param name="level">Level to log at</param>
/// <param name="message">Message template</param>
/// <param name="device">Optional IKeyed device. If provided, the Key of the device will be added to the log message</param>
/// <param name="args">Args to put into message template</param>
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);
}
/// <summary>
/// Prints message to console if current debug level is equal to or higher than the level of this message.
/// Uses CrestronConsole.PrintLine.
/// </summary>
/// <param name="level"></param>
/// <param name="format">Console format string</param>
/// <param name="items">Object parameters</param>
[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));
//}
}
/// <summary>
/// Logs to Console when at-level, and all messages to error log, including device key
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
[Obsolete("Use LogMessage methods")]
public static void Console(uint level, IKeyed dev, ErrorLogLevel errorLogLevel,
string format, params object[] items)
{
LogMessage(level, dev, format, items);
}
/// <summary>
/// Logs to Console when at-level, and all messages to error log
/// </summary>
[Obsolete("Use LogMessage methods")]
public static void Console(uint level, ErrorLogLevel errorLogLevel,
string format, params object[] items)
{
LogMessage(level, format, items);
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// Prints to log and error log
/// </summary>
/// <param name="errorLogLevel"></param>
/// <param name="str"></param>
[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;
}
}
/// <summary>
/// Writes the memory object after timeout
/// </summary>
static void SaveMemoryOnTimeout()
{
Console(0, "Saving debug settings");
if (_saveTimer == null)
_saveTimer = new CTimer(o =>
{
_saveTimer = null;
SaveMemory();
}, SaveTimeoutMs);
else
_saveTimer.Reset(SaveTimeoutMs);
}
/// <summary>
/// Writes the memory - use SaveMemoryOnTimeout
/// </summary>
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();
}
}
/// <summary>
///
/// </summary>
static void LoadMemory()
{
var file = GetMemoryFileName();
if (File.Exists(file))
{
using (var sr = new StreamReader(file))
{
var json = sr.ReadToEnd();
_contexts = JsonConvert.DeserializeObject<DebugContextCollection>(json);
if (_contexts != null)
{
Console(1, "Debug memory restored from file");
return;
}
}
}
_contexts = new DebugContextCollection();
}
/// <summary>
/// Helper to get the file path for this app's debug memory
/// </summary>
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");
}
/// <summary>
/// Error level to for message to be logged at
/// </summary>
public enum ErrorLogLevel
{
/// <summary>
/// Error
/// </summary>
Error,
/// <summary>
/// Warning
/// </summary>
Warning,
/// <summary>
/// Notice
/// </summary>
Notice,
/// <summary>
/// None
/// </summary>
None,
}
}
}

View File

@@ -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

View File

@@ -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;
}
}
}
}

View File

@@ -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
{
/// <summary>
/// Represents a debugging context
/// </summary>
public class DebugContext
public static class DebugContext
{
/// <summary>
/// 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.
/// </summary>
public string Key { get; private set; }
private static readonly CTimer SaveTimer;
private static readonly Dictionary<string, DebugContextData> CurrentData;
///// <summary>
///// The name of the file containing the current debug settings.
///// </summary>
//string FileName = string.Format(@"\nvram\debug\app{0}Debug.json", InitialParametersClass.ApplicationNumber);
DebugContextSaveData SaveData;
int SaveTimeoutMs = 30000;
CTimer SaveTimer;
static List<DebugContext> Contexts = new List<DebugContext>();
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");
/// <summary>
/// Creates or gets a debug context
/// The name of the file containing the current debug settings.
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
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
};
/// <summary>
/// Do not use. For S+ access.
/// </summary>
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();
}
/// <summary>
/// Used to save memory when shutting down
/// </summary>
/// <param name="programEventType"></param>
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();
}
};
}
/// <summary>
/// Callback for console command
/// </summary>
/// <param name="levelString"></param>
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");
}
}
/// <summary>
/// Sets the debug level
/// </summary>
/// <param name="level"> Valid values 0 (no debug), 1 (critical), 2 (all messages)</param>
public void SetDebugLevel(int level)
private static Dictionary<string, DebugContextData> 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<string, DebugContextData>();
}
}
/// <summary>
/// Prints message to console if current debug level is equal to or higher than the level of this message.
/// Uses CrestronConsole.PrintLine.
/// </summary>
/// <param name="level"></param>
/// <param name="format">Console format string</param>
/// <param name="items">Object parameters</param>
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));
}
/// <summary>
/// Appends a device Key to the beginning of a message
/// </summary>
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));
}
/// <summary>
///
/// </summary>
/// <param name="level"></param>
/// <param name="dev"></param>
/// <param name="errorLogLevel"></param>
/// <param name="format"></param>
/// <param name="items"></param>
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);
}
}
/// <summary>
///
/// </summary>
/// <param name="level"></param>
/// <param name="errorLogLevel"></param>
/// <param name="format"></param>
/// <param name="items"></param>
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);
}
}
/// <summary>
///
/// </summary>
/// <param name="errorLogLevel"></param>
/// <param name="str"></param>
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;
}
}
/// <summary>
/// Writes the memory object after timeout
/// </summary>
void SaveMemoryOnTimeout()
{
if (SaveTimer == null)
SaveTimer = new CTimer(o =>
{
SaveTimer = null;
SaveMemory();
}, SaveTimeoutMs);
else
SaveTimer.Reset(SaveTimeoutMs);
}
/// <summary>
/// Writes the memory - use SaveMemoryOnTimeout
/// </summary>
void SaveMemory()
{
using (StreamWriter sw = new StreamWriter(GetMemoryFileName()))
{
var json = JsonConvert.SerializeObject(SaveData);
sw.Write(json);
sw.Flush();
}
}
/// <summary>
///
/// </summary>
void LoadMemory()
{
var file = GetMemoryFileName();
if (File.Exists(file))
{
using (StreamReader sr = new StreamReader(file))
{
var data = JsonConvert.DeserializeObject<DebugContextSaveData>(sr.ReadToEnd());
if (data != null)
{
SaveData = data;
Debug.Console(1, "Debug memory restored from file");
return;
}
else
SaveData = new DebugContextSaveData();
}
}
}
/// <summary>
/// Helper to get the file path for this app's debug memory
/// </summary>
string GetMemoryFileName()
{
return string.Format(@"\NVRAM\debugSettings\program{0}-{1}", InitialParametersClass.ApplicationNumber, Key);
var json = File.ReadAllText(FileName);
return JsonConvert.DeserializeObject<Dictionary<string, DebugContextData>>(json);
}
}
/// <summary>
///
/// </summary>
public class DebugContextSaveData
{
/// <summary>
///
/// </summary>
public int Level { get; set; }
}
public record DebugContextData(LogEventLevel Level, string[] Devices = null);
}

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -1,115 +0,0 @@
using System.Collections.Generic;
using Crestron.SimplSharp;
using Newtonsoft.Json;
namespace PepperDash.Core.Logging
{
/// <summary>
/// Class to persist current Debug settings across program restarts
/// </summary>
public class DebugContextCollection
{
/// <summary>
/// To prevent threading issues with the DeviceDebugSettings collection
/// </summary>
private readonly CCriticalSection _deviceDebugSettingsLock;
[JsonProperty("items")] private readonly Dictionary<string, DebugContextItem> _items;
/// <summary>
/// Collection of the debug settings for each device where the dictionary key is the device key
/// </summary>
[JsonProperty("deviceDebugSettings")]
private Dictionary<string, object> DeviceDebugSettings { get; set; }
/// <summary>
/// Default constructor
/// </summary>
public DebugContextCollection()
{
_deviceDebugSettingsLock = new CCriticalSection();
DeviceDebugSettings = new Dictionary<string, object>();
_items = new Dictionary<string, DebugContextItem>();
}
/// <summary>
/// Sets the level of a given context item, and adds that item if it does not
/// exist
/// </summary>
/// <param name="contextKey"></param>
/// <param name="level"></param>
public void SetLevel(string contextKey, int level)
{
if (level < 0 || level > 2)
return;
GetOrCreateItem(contextKey).Level = level;
}
/// <summary>
/// Gets a level or creates it if not existing
/// </summary>
/// <param name="contextKey"></param>
/// <returns></returns>
public DebugContextItem GetOrCreateItem(string contextKey)
{
if (!_items.ContainsKey(contextKey))
_items[contextKey] = new DebugContextItem { Level = 0 };
return _items[contextKey];
}
/// <summary>
/// sets the settings for a device or creates a new entry
/// </summary>
/// <param name="deviceKey"></param>
/// <param name="settings"></param>
/// <returns></returns>
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();
}
}
/// <summary>
/// Gets the device settings for a device by key or returns null
/// </summary>
/// <param name="deviceKey"></param>
/// <returns></returns>
public object GetDebugSettingsForKey(string deviceKey)
{
return DeviceDebugSettings[deviceKey];
}
}
/// <summary>
/// Contains information about
/// </summary>
public class DebugContextItem
{
/// <summary>
/// The level of debug messages to print
/// </summary>
[JsonProperty("level")]
public int Level { get; set; }
/// <summary>
/// Property to tell the program not to intitialize when it boots, if desired
/// </summary>
[JsonProperty("doNotLoadOnNextBoot")]
public bool DoNotLoadOnNextBoot { get; set; }
}
}

View File

@@ -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<DebugWebsocketSink>("HTTPS ClientCertificateValidation Callback triggered");
return true;
}
};
}
server.AddWebSocketService<DebugClient>(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

View File

@@ -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);
}
}

View File

@@ -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<DebugClient>(_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);
}
}
}

View File

@@ -3,6 +3,7 @@
<RootNamespace>PepperDash.Core</RootNamespace>
<AssemblyName>PepperDashCore</AssemblyName>
<TargetFrameworks>net472;net6.0;net8.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Deterministic>true</Deterministic>
<NeutralLanguage>en</NeutralLanguage>
<OutputPath>bin\$(Configuration)\</OutputPath>
@@ -40,6 +41,10 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
<PackageReference Include="Microsoft.Net.Http" Version="2.2.29" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">