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;