diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs
index 2dfa7698..cf166c78 100644
--- a/src/PepperDash.Core/Logging/Debug.cs
+++ b/src/PepperDash.Core/Logging/Debug.cs
@@ -114,7 +114,7 @@ public static class Debug
///
public static string PepperDashCoreVersion { get; private set; }
- private static Timer _saveTimer;
+ // private static Timer _saveTimer;
private const int defaultConsoleDebugTimeoutMin = 120;
@@ -235,7 +235,7 @@ public static class Debug
"appdebugfilter [params]", ConsoleAccessLevelEnum.AccessOperator);
}
- CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
+ // CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
DoNotLoadConfigOnNextBoot = GetDoNotLoadOnNextBoot();
@@ -249,7 +249,8 @@ public static class Debug
}
catch (Exception ex)
{
- LogError(ex, "Exception in Debug static constructor: {message}", ex.Message);
+ // _logger may not have been initialized yet — do not call LogError here.
+ CrestronConsole.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}");
}
}
@@ -337,26 +338,26 @@ public static class Debug
}
}
- ///
- /// Used to save memory when shutting down
- ///
- ///
- static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
- {
+ // ///
+ // /// Used to save memory when shutting down
+ // ///
+ // ///
+ // static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
+ // {
- if (programEventType == eProgramStatusEventType.Stopping)
- {
- Log.CloseAndFlush();
+ // if (programEventType == eProgramStatusEventType.Stopping)
+ // {
+ // Log.CloseAndFlush();
- if (_saveTimer != null)
- {
- _saveTimer.Stop();
- _saveTimer = null;
- }
- LogMessage(LogEventLevel.Information, "Saving debug settings");
- SaveMemory();
- }
- }
+ // if (_saveTimer != null)
+ // {
+ // _saveTimer.Stop();
+ // _saveTimer = null;
+ // }
+ // LogMessage(LogEventLevel.Information, "Saving debug settings");
+ // // SaveMemory();
+ // }
+ // }
///
/// Callback for console command
@@ -632,7 +633,7 @@ public static class Debug
public static void SetDeviceDebugSettings(string deviceKey, object settings)
{
_contexts.SetDebugSettingsForKey(deviceKey, settings);
- SaveMemoryOnTimeout();
+ // SaveMemoryOnTimeout();
}
///
@@ -1005,83 +1006,82 @@ public static class Debug
}
- ///
- /// Writes the memory object after timeout
- ///
- static void SaveMemoryOnTimeout()
- {
- LogInformation("Saving debug settings");
- if (_saveTimer == null)
- {
- _saveTimer = new Timer(SaveTimeoutMs) { AutoReset = false };
- _saveTimer.Elapsed += (s, e) =>
- {
- _saveTimer = null;
- SaveMemory();
- };
- _saveTimer.Start();
- }
- else
- {
- _saveTimer.Stop();
- _saveTimer.Interval = SaveTimeoutMs;
- _saveTimer.Start();
- }
- }
+ // ///
+ // /// Writes the memory object after timeout
+ // ///
+ // static void SaveMemoryOnTimeout()
+ // {
+ // LogInformation("Saving debug settings");
+ // if (_saveTimer == null)
+ // {
+ // _saveTimer = new Timer(SaveTimeoutMs) { AutoReset = false };
+ // _saveTimer.Elapsed += (s, e) =>
+ // {
+ // _saveTimer = null;
+ // SaveMemory();
+ // };
+ // _saveTimer.Start();
+ // }
+ // else
+ // {
+ // _saveTimer.Stop();
+ // _saveTimer.Interval = SaveTimeoutMs;
+ // _saveTimer.Start();
+ // }
+ // }
- ///
- /// Writes the memory - use SaveMemoryOnTimeout
- ///
- static void SaveMemory()
- {
- //var dir = @"\NVRAM\debug";
- //if (!Directory.Exists(dir))
- // Directory.Create(dir);
+ // ///
+ // /// Writes the memory - use SaveMemoryOnTimeout
+ // ///
+ // static void SaveMemory()
+ // {
+ // //var dir = @"\NVRAM\debug";
+ // //if (!Directory.Exists(dir))
+ // // Directory.Create(dir);
- try
- {
- var fileName = GetMemoryFileName();
+ // try
+ // {
+ // var fileName = GetMemoryFileName();
- LogInformation("Loading debug settings file from {fileName}", fileName);
+ // LogInformation("Loading debug settings file from {fileName}", fileName);
- using (var sw = new StreamWriter(fileName))
- {
- var json = JsonConvert.SerializeObject(_contexts);
- sw.Write(json);
- sw.Flush();
- }
- }
- catch (Exception ex)
- {
- ErrorLog.Error("Exception saving debug settings: {message}", ex);
- CrestronConsole.PrintLine("Exception saving debug settings: {message}", ex.Message);
- return;
- }
- }
+ // using (var sw = new StreamWriter(fileName))
+ // {
+ // var json = JsonConvert.SerializeObject(_contexts);
+ // sw.Write(json);
+ // sw.Flush();
+ // }
+ // }
+ // catch (Exception ex)
+ // {
+ // LogError("Exception saving debug settings: {message}", ex);
+ // return;
+ // }
+ // }
- ///
- ///
- ///
- static void LoadMemory()
- {
- var file = GetMemoryFileName();
- if (File.Exists(file))
- {
- using (var sr = new StreamReader(file))
- {
- var json = sr.ReadToEnd();
- _contexts = JsonConvert.DeserializeObject(json);
+ // ///
+ // ///
+ // ///
+ // 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)
- {
- LogMessage(LogEventLevel.Debug, "Debug memory restored from file");
- return;
- }
- }
- }
+ // if (_contexts != null)
+ // {
+ // LogMessage(LogEventLevel.Debug, "Debug memory restored from file");
+ // return;
+ // }
+ // }
+ // }
- _contexts = new DebugContextCollection();
- }
+ // _contexts = new DebugContextCollection();
+ // }
///
/// Helper to get the file path for this app's debug memory
diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs
index b9c74e68..6f1aee18 100644
--- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs
+++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs
@@ -1,8 +1,10 @@
extern alias NewtonsoftJson;
using System;
-using Crestron.SimplSharp;
-using Org.BouncyCastle.Asn1.X509;
+using System.Security.Authentication;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using Crestron.SimplSharp;
using Serilog;
using Serilog.Configuration;
using Serilog.Core;
@@ -11,10 +13,8 @@ using Serilog.Formatting;
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using Serilog.Formatting.Json;
using System.IO;
-using System.Security.Authentication;
using WebSocketSharp;
using WebSocketSharp.Server;
-using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
using WebSocketSharp.Net;
namespace PepperDash.Core;
@@ -29,21 +29,24 @@ namespace PepperDash.Core;
public class DebugWebsocketSink : ILogEventSink, IKeyed
{
private HttpServer _httpsServer;
-
+
private readonly string _path = "/debug/join/";
private const string _certificateName = "selfCres";
private const string _certificatePassword = "cres12345";
+ private static string CertPath =>
+ $"{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}{_certificateName}.pfx";
///
/// Gets the port number on which the HTTPS server is currently running.
///
- public int Port
- { get
- {
-
- if(_httpsServer == null) return 0;
+ public int Port
+ {
+ get
+ {
+
+ if (_httpsServer == null) return 0;
return _httpsServer.Port;
- }
+ }
}
///
@@ -55,15 +58,17 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
{
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}";
+ if (_httpsServer == null || !_httpsServer.IsListening) return "";
+ var service = _httpsServer.WebSocketServices[_path];
+ if (service == null) return "";
+ return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{service.Path}";
}
}
///
/// Gets a value indicating whether the HTTPS server is currently listening for incoming connections.
///
- public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
+ public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
///
public string Key => "DebugWebsocketSink";
@@ -83,7 +88,7 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
_textFormatter = formatProvider ?? new JsonFormatter();
- if (!File.Exists($"\\user\\{_certificateName}.pfx"))
+ if (!File.Exists(CertPath))
CreateCert();
CrestronEnvironment.ProgramStatusEventHandler += type =>
@@ -97,29 +102,65 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
private static void CreateCert()
{
+ // NOTE: This method is called from the constructor, which is itself called during Debug's static
+ // constructor before _logger is assigned. Do NOT call any Debug.Log* methods here — use
+ // CrestronConsole.PrintLine only, to avoid a NullReferenceException that would poison the Debug type.
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);
- Debug.LogInformation("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress);
+ CrestronConsole.PrintLine(string.Format("CreateCert: DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress));
- var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), [string.Format("{0}.{1}", hostName, domainName), ipAddress], [KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth]);
+ var subjectName = string.Format("CN={0}.{1}", hostName, domainName);
+ var fqdn = string.Format("{0}.{1}", hostName, domainName);
- //Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested
+ using var rsa = RSA.Create(2048);
+
+ var request = new CertificateRequest(
+ subjectName,
+ rsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ // Subject Key Identifier
+ request.CertificateExtensions.Add(
+ new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
+
+ // Extended Key Usage: server + client auth
+ request.CertificateExtensions.Add(
+ new X509EnhancedKeyUsageExtension(
+ new OidCollection
+ {
+ new Oid("1.3.6.1.5.5.7.3.1"), // id-kp-serverAuth
+ new Oid("1.3.6.1.5.5.7.3.2") // id-kp-clientAuth
+ },
+ false));
+
+ // Subject Alternative Names: DNS + IP
+ var sanBuilder = new SubjectAlternativeNameBuilder();
+ sanBuilder.AddDnsName(fqdn);
+ if (System.Net.IPAddress.TryParse(ipAddress, out var ip))
+ sanBuilder.AddIpAddress(ip);
+ request.CertificateExtensions.Add(sanBuilder.Build());
+
+ var notBefore = DateTimeOffset.UtcNow;
+ var notAfter = notBefore.AddYears(2);
+
+ using var cert = request.CreateSelfSigned(notBefore, notAfter);
var separator = Path.DirectorySeparatorChar;
-
- utility.CertificatePassword = _certificatePassword;
- utility.WriteCertificate(certificate, @$"{separator}user{separator}", _certificateName);
+ var outputPath = string.Format("{0}user{1}{2}.pfx", separator, separator, _certificateName);
+
+ var pfxBytes = cert.Export(X509ContentType.Pfx, _certificatePassword);
+ File.WriteAllBytes(outputPath, pfxBytes);
+
+ CrestronConsole.PrintLine(string.Format("CreateCert: Certificate written to {0}", outputPath));
}
catch (Exception ex)
{
- Debug.LogError(ex, "WSS CreateCert Failed: {0}", ex.Message);
- Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
+ CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed: {0}\r\n{1}", ex.Message, ex.StackTrace));
}
}
@@ -137,7 +178,7 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
var sw = new StringWriter();
_textFormatter.Format(logEvent, sw);
- _httpsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString());
+ _httpsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString());
}
///
@@ -152,20 +193,39 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
Debug.LogInformation("Starting Websocket Server on port: {0}", port);
- Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword);
+ Start(port, CertPath, _certificatePassword);
+ }
+
+ private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
+ {
+ try
+ {
+ // EphemeralKeySet is required on Linux/OpenSSL (Crestron 4-series) to avoid
+ // key-container persistence failures, and avoids the private key export restriction.
+ return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
+ }
+ catch (Exception ex)
+ {
+ // Cert is stale or was generated by an incompatible library (e.g. old BouncyCastle output).
+ // Delete it, regenerate with the BCL path, and retry once.
+ CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message));
+ try { File.Delete(certPath); } catch { }
+ CreateCert();
+ return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
+ }
}
private void Start(int port, string certPath = "", string certPassword = "")
{
try
{
- _httpsServer = new HttpServer(port, true);
+ _httpsServer = new HttpServer(port, true);
if (!string.IsNullOrWhiteSpace(certPath))
{
Debug.LogInformation("Assigning SSL Configuration");
- _httpsServer.SslConfiguration.ServerCertificate = new X509Certificate2(certPath, certPassword);
+ _httpsServer.SslConfiguration.ServerCertificate = LoadOrRecreateCert(certPath, certPassword);
_httpsServer.SslConfiguration.ClientCertificateRequired = false;
_httpsServer.SslConfiguration.CheckCertificateRevocation = false;
_httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12;
@@ -180,36 +240,7 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
_httpsServer.AddWebSocketService(_path);
Debug.LogInformation("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.LogInformation("{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s);
- };
+ _httpsServer.Log.Output = WriteWebSocketInternalLog;
Debug.LogInformation("Starting");
_httpsServer.Start();
@@ -219,6 +250,8 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
{
Debug.LogError(ex, "WebSocket Failed to start {0}", ex.Message);
Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
+ // Null out the server so callers can detect failure via IsRunning / Url null guards.
+ _httpsServer = null;
}
}
@@ -230,10 +263,69 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
public void StopServer()
{
Debug.LogInformation("Stopping Websocket Server");
- _httpsServer?.Stop();
- _httpsServer = null;
+ try
+ {
+ if (_httpsServer == null || !_httpsServer.IsListening)
+ {
+ return;
+ }
+
+ // Prevent close-sequence internal websocket logs from re-entering the logging pipeline.
+ _httpsServer.Log.Output = (d, s) => { };
+
+ var serviceHost = _httpsServer.WebSocketServices[_path];
+
+ if (serviceHost == null)
+ {
+ _httpsServer.Stop();
+ _httpsServer = null;
+ return;
+ }
+
+ serviceHost.Sessions.Broadcast("Server is stopping");
+
+ foreach (var session in serviceHost.Sessions.Sessions)
+ {
+ if (session?.Context?.WebSocket != null && session.Context.WebSocket.IsAlive)
+ {
+ session.Context.WebSocket.Close(1001, "Server is stopping");
+ }
+ }
+
+ _httpsServer.Stop();
+
+ _httpsServer = null;
+
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError(ex, "WebSocket Failed to stop gracefully {0}", ex.Message);
+ Debug.LogVerbose("Stack Trace\r\n{0}", ex.StackTrace);
+ }
}
+
+ private static void WriteWebSocketInternalLog(LogData data, string supplemental)
+ {
+ try
+ {
+ if (data == null)
+ {
+ return;
+ }
+
+ var message = string.IsNullOrWhiteSpace(data.Message) ? "" : data.Message;
+ var details = string.IsNullOrWhiteSpace(supplemental) ? string.Empty : string.Format(" | details: {0}", supplemental);
+
+ // Use direct console output to avoid recursive log sink calls.
+ CrestronConsole.PrintLine(string.Format("WS[{0}] {1} | message: {2}{3}", data.Level, data.Date, message, details));
+ }
+ catch
+ {
+ // Never throw from websocket log callback.
+ }
+ }
+
}
///
@@ -295,7 +387,7 @@ public class DebugClient : WebSocketBehavior
{
Debug.LogInformation("DebugClient Created");
}
-
+
///
protected override void OnOpen()
{
diff --git a/src/PepperDash.Core/Web/WebApiServer.cs b/src/PepperDash.Core/Web/WebApiServer.cs
index 099817c7..df8e4b58 100644
--- a/src/PepperDash.Core/Web/WebApiServer.cs
+++ b/src/PepperDash.Core/Web/WebApiServer.cs
@@ -1,14 +1,9 @@
-extern alias NewtonsoftJson;
-
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;
-using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting;
-using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert;
-using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Core.Logging;
@@ -76,10 +71,13 @@ public class WebApiServer : IKeyName
Name = string.IsNullOrEmpty(name) ? DefaultName : name;
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
+ this.LogInformation("Creating Web API Server with Key: {Key}, Name: {Name}, BasePath: {BasePath}", Key, Name, BasePath);
+
if (_server == null) _server = new HttpCwsServer(BasePath);
_server.setProcessName(Key);
_server.HttpRequestHandler = new DefaultRequestHandler();
+ _server.ReceivedRequestEvent += ReceivedRequestEventHandler;
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler;
@@ -104,8 +102,12 @@ public class WebApiServer : IKeyName
///
void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs)
{
- // Re-enable the server if the link comes back up and the status should be connected
- if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
+ if (ethernetEventArgs.EthernetEventType != eEthernetEventType.LinkUp)
+ {
+ return;
+ }
+
+ if (IsRegistered)
{
this.LogInformation("Ethernet link up. Server is already registered.");
return;
@@ -116,14 +118,14 @@ public class WebApiServer : IKeyName
Start();
}
- ///
- /// Initialize method
- ///
- public void Initialize(string key, string basePath)
- {
- Key = key;
- BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
- }
+ // ///
+ // /// Initialize method
+ // ///
+ // public void Initialize(string key, string basePath)
+ // {
+ // Key = key;
+ // BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
+ // }
///
/// Adds a route to CWS
@@ -214,12 +216,10 @@ public class WebApiServer : IKeyName
return;
}
- IsRegistered = _server.Unregister() == false;
+ var unregistered = _server.Unregister();
+ IsRegistered = !unregistered;
- this.LogDebug("Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful");
-
- _server.Dispose();
- _server = null;
+ this.LogDebug("Stopping server, unregistration {0}", unregistered ? "was successful" : "failed");
}
catch (Exception ex)
{
@@ -240,13 +240,12 @@ public class WebApiServer : IKeyName
{
try
{
- var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented);
- this.LogVerbose("RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
+ var req = args.Context?.Request;
+ this.LogVerbose("ReceivedRequestEventHandler: {Method} {Path}", req?.HttpMethod, req?.Path);
}
catch (Exception ex)
{
this.LogException(ex, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message);
- this.LogVerbose("ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace);
}
}
}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs
index 071e5a20..362ab9f0 100644
--- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs
+++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs
@@ -123,68 +123,48 @@ namespace PepperDash.Essentials.Core.Config;
{
Debug.LogMessage(LogEventLevel.Information, "Loading config file: '{0}'", filePath);
+ var fileContents = fs.ReadToEnd();
+
if (localConfigFound)
{
- ConfigObject = JObject.Parse(fs.ReadToEnd()).ToObject();
-
- if (localConfigFound)
- {
- ConfigObject = JObject.Parse(fs.ReadToEnd()).ToObject();
-
- Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Local Config");
-
- return true;
- }
- else
- {
- var parsedConfig = JObject.Parse(fs.ReadToEnd());
-
- // Check if it's a v2 config (check for "version" node)
- // this means it's already merged by the Portal API
- // from the v2 config tool
- var isV2Config = parsedConfig["versions"] != null;
-
- if (isV2Config)
- {
- Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary.");
- ConfigObject = parsedConfig.ToObject();
- Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded v2 Config");
- return true;
- }
-
- // Extract SystemUrl and TemplateUrl into final config output
- ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject();
-
- if (parsedConfig["system_url"] != null)
- {
- ConfigObject.SystemUrl = parsedConfig["system_url"].Value();
- }
-
- if (parsedConfig["template_url"] != null)
- {
- ConfigObject.TemplateUrl = parsedConfig["template_url"].Value();
- }
- }
-
- Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Merged Config");
-
- return true;
+ ConfigObject = JObject.Parse(fileContents).ToObject();
+ Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Local Config");
+ return ConfigObject != null;
}
else
{
- var doubleObj = JObject.Parse(fs.ReadToEnd());
- ConfigObject = PortalConfigReader.MergeConfigs(doubleObj).ToObject();
+ var parsedConfig = JObject.Parse(fileContents);
- // Extract SystemUrl and TemplateUrl into final config output
+ // Check if it's a v2 config (check for "version" node)
+ // this means it's already merged by the Portal API
+ // from the v2 config tool
+ var isV2Config = parsedConfig["versions"] != null;
- if (doubleObj["system_url"] != null)
+ if (isV2Config)
{
- ConfigObject.SystemUrl = doubleObj["system_url"].Value();
+ Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary.");
+ ConfigObject = parsedConfig.ToObject();
+ Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded v2 Config");
+ return ConfigObject != null;
}
- if (doubleObj["template_url"] != null)
+ // Extract SystemUrl and TemplateUrl into final config output
+ ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject();
+
+ if (ConfigObject == null)
{
- ConfigObject.TemplateUrl = doubleObj["template_url"].Value();
+ Debug.LogMessage(LogEventLevel.Warning, "Config merge produced a null ConfigObject.");
+ return false;
+ }
+
+ if (parsedConfig["system_url"] != null)
+ {
+ ConfigObject.SystemUrl = parsedConfig["system_url"].Value();
+ }
+
+ if (parsedConfig["template_url"] != null)
+ {
+ ConfigObject.TemplateUrl = parsedConfig["template_url"].Value();
}
}
diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs
index f73b3f2c..237edd62 100644
--- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs
+++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;
using PepperDash.Core;
@@ -26,10 +27,6 @@ public class EssentialsWebApi : EssentialsDevice
? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber)
: "/api";
- private const int DebugTrace = 0;
- private const int DebugInfo = 1;
- private const int DebugVerbose = 2;
-
///
/// Gets or sets the BasePath
///
@@ -230,7 +227,7 @@ public class EssentialsWebApi : EssentialsDevice
_server.Start();
- GetPaths();
+ PrintPaths();
return;
}
@@ -240,11 +237,11 @@ public class EssentialsWebApi : EssentialsDevice
_server.Start();
- GetPaths();
+ PrintPaths();
}
///
- /// Print the available pahts
+ /// Print the available paths
///
///
/// http(s)://{ipaddress}/cws/{basePath}
@@ -253,7 +250,7 @@ public class EssentialsWebApi : EssentialsDevice
///
/// GetPaths method
///
- public void GetPaths()
+ public void PrintPaths()
{
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
@@ -267,7 +264,7 @@ public class EssentialsWebApi : EssentialsDevice
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{BasePath}"
: $"https://{currentIp}/cws{BasePath}";
- Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path);
+ Debug.LogMessage(LogEventLevel.Information, this, "Server: {path:l}", path);
var routeCollection = _server.GetRouteCollection();
if (routeCollection == null)
diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs
index 4dbd00a5..c0dee8bb 100644
--- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs
+++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs
@@ -6,18 +6,23 @@ using PepperDash.Core.Web.RequestHandlers;
using Serilog.Events;
using System;
using System.Text;
+using System.Threading.Tasks;
namespace PepperDash.Essentials.Core.Web.RequestHandlers;
+///
+/// Represents a DebugSessionRequestHandler
+///
public class DebugSessionRequestHandler : WebApiBaseRequestHandler
-{
- private readonly DebugWebsocketSink _sink = new DebugWebsocketSink();
+{
+ ///
+ /// Constructor
+ ///
public DebugSessionRequestHandler()
: base(true)
{
}
-
///
/// Gets details for a debug session
///
@@ -41,21 +46,30 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler
var port = 0;
- if (!_sink.IsRunning)
+ if (!Debug.WebsocketSink.IsRunning)
{
Debug.LogMessage(LogEventLevel.Information, "Starting WS Server");
// Generate a random port within a specified range
port = new Random().Next(65435, 65535);
// Start the WS Server
- _sink.StartServerAndSetPort(port);
+ Debug.WebsocketSink.StartServerAndSetPort(port);
Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose);
}
- var url = _sink.Url;
+ if (!Debug.WebsocketSink.IsRunning)
+ {
+ context.Response.StatusCode = 500;
+ context.Response.StatusDescription = "Internal Server Error";
+ context.Response.Write(JsonConvert.SerializeObject(new { error = "Failed to start WebSocket debug server. Check logs for details." }), false);
+ context.Response.End();
+ return;
+ }
+
+ var url = Debug.WebsocketSink.Url;
object data = new
{
- url = _sink.Url
+ url = Debug.WebsocketSink.Url
};
Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url);
@@ -82,7 +96,8 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler
///
protected override void HandlePost(HttpCwsContext context)
{
- _sink.StopServer();
+
+ Task.Run(() => Debug.WebsocketSink.StopServer());
context.Response.StatusCode = 200;
context.Response.StatusDescription = "OK";
diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs
index fcb48fdd..63f4160e 100644
--- a/src/PepperDash.Essentials/ControlSystem.cs
+++ b/src/PepperDash.Essentials/ControlSystem.cs
@@ -15,6 +15,7 @@ using System.Threading;
using Timeout = Crestron.SimplSharp.Timeout;
using Serilog.Events;
using System.Threading.Tasks;
+using PepperDash.Essentials.Core.Web;
namespace PepperDash.Essentials;
@@ -56,7 +57,15 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
}
catch (Exception e)
{
- Debug.LogError(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state");
+ try
+ {
+ Debug.LogError(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state");
+ }
+ catch
+ {
+ // Debug may not be initialized (e.g. its own static ctor failed); fall back to console.
+ CrestronConsole.PrintLine($"FATAL INITIALIZE ERROR. System is in an inconsistent state\r\n{e.Message}\r\n{e.StackTrace}");
+ }
}
}
@@ -152,6 +161,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts,
"getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator);
+ DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API"));
if (!Debug.DoNotLoadConfigOnNextBoot)
{
@@ -260,10 +270,10 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
PluginLoader.LoadPlugins();
Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config...");
- if (!ConfigReader.LoadConfig2())
+ if (!ConfigReader.LoadConfig2() || ConfigReader.ConfigObject == null)
{
- Debug.LogMessage(LogEventLevel.Information, "Essentials Load complete with errors");
- return;
+ Debug.LogMessage(LogEventLevel.Warning, "Unable to load config file. Please ensure a valid config file is present and restart the program.");
+ // return;
}
Load();
@@ -385,43 +395,49 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
new Core.Monitoring.SystemMonitorController("systemMonitor"));
}
- foreach (var devConf in ConfigReader.ConfigObject.Devices)
+ if (ConfigReader.ConfigObject is not null)
{
- IKeyed newDev = null;
+ Debug.LogMessage(LogEventLevel.Warning, "LoadDevices: ConfigObject is null. Cannot load devices.");
- try
+ foreach (var devConf in ConfigReader.ConfigObject.Devices)
{
- Debug.LogMessage(LogEventLevel.Information, "Creating device '{deviceKey:l}', type '{deviceType:l}'", devConf.Key, devConf.Type);
- // Skip this to prevent unnecessary warnings
- if (devConf.Key == "processor")
+ IKeyed newDev = null;
+
+ try
{
- var prompt = Global.ControlSystem.ControllerPrompt;
+ Debug.LogMessage(LogEventLevel.Information, "Creating device '{deviceKey:l}', type '{deviceType:l}'", devConf.Key, devConf.Type);
+ // Skip this to prevent unnecessary warnings
+ if (devConf.Key == "processor")
+ {
+ var prompt = Global.ControlSystem.ControllerPrompt;
- var typeMatch = string.Equals(devConf.Type, prompt, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(devConf.Type, prompt.Replace("-", ""), StringComparison.OrdinalIgnoreCase);
+ var typeMatch = string.Equals(devConf.Type, prompt, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(devConf.Type, prompt.Replace("-", ""), StringComparison.OrdinalIgnoreCase);
- if (!typeMatch)
- Debug.LogMessage(LogEventLevel.Information,
- "WARNING: Config file defines processor type as '{deviceType:l}' but actual processor is '{processorType:l}'! Some ports may not be available",
- devConf.Type.ToUpper(), Global.ControlSystem.ControllerPrompt.ToUpper());
+ if (!typeMatch)
+ Debug.LogMessage(LogEventLevel.Information,
+ "WARNING: Config file defines processor type as '{deviceType:l}' but actual processor is '{processorType:l}'! Some ports may not be available",
+ devConf.Type.ToUpper(), Global.ControlSystem.ControllerPrompt.ToUpper());
- continue;
+ continue;
+ }
+
+
+ if (newDev == null)
+ newDev = Core.DeviceFactory.GetDevice(devConf);
+
+ if (newDev != null)
+ DeviceManager.AddDevice(newDev);
+ else
+ Debug.LogMessage(LogEventLevel.Information, "ERROR: Cannot load unknown device type '{deviceType:l}', key '{deviceKey:l}'.", devConf.Type, devConf.Key);
+ }
+ catch (Exception e)
+ {
+ Debug.LogMessage(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key });
}
-
-
- if (newDev == null)
- newDev = Core.DeviceFactory.GetDevice(devConf);
-
- if (newDev != null)
- DeviceManager.AddDevice(newDev);
- else
- Debug.LogMessage(LogEventLevel.Information, "ERROR: Cannot load unknown device type '{deviceType:l}', key '{deviceKey:l}'.", devConf.Type, devConf.Key);
- }
- catch (Exception e)
- {
- Debug.LogMessage(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key });
}
+
}
Debug.LogMessage(LogEventLevel.Information, "All Devices Loaded.");
@@ -438,7 +454,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
var tlc = TieLineCollection.Default;
- if (ConfigReader.ConfigObject.TieLines == null)
+ if (ConfigReader.ConfigObject?.TieLines == null)
{
return;
}
@@ -459,7 +475,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
///
public void LoadRooms()
{
- if (ConfigReader.ConfigObject.Rooms == null)
+ if (ConfigReader.ConfigObject?.Rooms == null)
{
Debug.LogMessage(LogEventLevel.Information, "Notice: Configuration contains no rooms - Is this intentional? This may be a valid configuration.");
return;