From 4631d3a5375728e23ddb49d93e5d612fe5ccbd85 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Fri, 10 Jul 2026 12:25:43 -0500 Subject: [PATCH] refactor: replace client-reported UI app version tracking with disk-based check Remove ConnectedClientVersionInfo and the per-client version dictionary in MobileControlSystemController, which grew unbounded and depended on a UI client connecting and self-reporting its APP_VERSION. Add TouchpanelWrapperAppVersionChecker, which instead scans the deployed mcUserApp files on disk for the configured versions.touchpanelWrapperApp version string. This works without any client connected and doesn't accumulate state. Wired into the mobileinfo console command output. --- .../ConnectedClientVersionInfo.cs | 61 -------- .../MobileControlSystemController.cs | 93 +----------- .../TouchpanelWrapperAppVersionChecker.cs | 141 ++++++++++++++++++ 3 files changed, 147 insertions(+), 148 deletions(-) delete mode 100644 src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs create mode 100644 src/PepperDash.Essentials.MobileControl/TouchpanelWrapperAppVersionChecker.cs diff --git a/src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs b/src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs deleted file mode 100644 index 68f5e36e..00000000 --- a/src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace PepperDash.Essentials -{ - /// - /// Represents the version information reported by a connected Mobile Control UI client - /// - public class ConnectedClientVersionInfo - { - /// - /// Gets or sets the client id - /// - [JsonProperty("clientId")] - public string ClientId { get; set; } - - /// - /// Gets or sets the room key the client joined - /// - [JsonProperty("roomKey")] - public string RoomKey { get; set; } - - /// - /// Gets or sets the touchpanel key the client joined as, if any - /// - [JsonProperty("touchpanelKey")] - public string TouchpanelKey { get; set; } - - /// - /// Gets or sets the app version reported by the client (e.g. the React app's build-time APP_VERSION) - /// - [JsonProperty("appVersion")] - public string AppVersion { get; set; } - - /// - /// Gets or sets the expected app version from the system config's versions.touchpanelWrapperApp, if configured - /// - [JsonProperty("expectedAppVersion")] - public string ExpectedAppVersion { get; set; } - - /// - /// Gets or sets the UTC time the client last reported this version - /// - [JsonProperty("lastSeen")] - public DateTime LastSeen { get; set; } - - /// - /// Returns a copy of this instance, safe for callers outside the owning lock to hold/mutate - /// without affecting the internally tracked instance - /// - public ConnectedClientVersionInfo Clone() => new ConnectedClientVersionInfo - { - ClientId = ClientId, - RoomKey = RoomKey, - TouchpanelKey = TouchpanelKey, - AppVersion = AppVersion, - ExpectedAppVersion = ExpectedAppVersion, - LastSeen = LastSeen - }; - } -} diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 8de9d9eb..ea3b02d1 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -58,32 +58,11 @@ namespace PepperDash.Essentials private readonly Dictionary _defaultMessengers = new Dictionary(); - private readonly Dictionary _connectedClientVersions = - new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - private readonly object _connectedClientVersionsLock = new object(); - /// /// Get the custom messengers /// public ReadOnlyDictionary Messengers => new ReadOnlyDictionary(_messengers); - /// - /// Gets the most recently reported UI app version for each connected client, keyed by clientId - /// - public ReadOnlyDictionary ConnectedClientVersions - { - get - { - lock (_connectedClientVersionsLock) - { - return new ReadOnlyDictionary( - _connectedClientVersions.ToDictionary(kv => kv.Key, kv => kv.Value.Clone()) - ); - } - } - } - /// /// Get the default messengers /// @@ -1028,27 +1007,13 @@ namespace PepperDash.Essentials ); } - var connectedClientVersions = ConnectedClientVersions; + var expectedAppVersion = ConfigReader.ConfigObject?.Versions?.TouchpanelWrapperApp?.Version; + var userAppPath = Global.FilePathPrefix + "mcUserApp" + Global.DirectorySeparator; + var versionCheck = TouchpanelWrapperAppVersionChecker.CheckDeployedVersion(userAppPath, expectedAppVersion); - if (connectedClientVersions.Count == 0) - { - CrestronConsole.ConsoleCommandResponse("\r\nUI Client App Versions: None reported yet\r\n"); - } - else - { - CrestronConsole.ConsoleCommandResponse("\r\nUI Client App Versions:\r\n"); - foreach (var kv in connectedClientVersions) - { - var v = kv.Value; - var match = string.IsNullOrEmpty(v.ExpectedAppVersion) || string.Equals(v.ExpectedAppVersion, v.AppVersion, StringComparison.OrdinalIgnoreCase); - - CrestronConsole.ConsoleCommandResponse( - $" Client: {v.ClientId} Touchpanel: {v.TouchpanelKey} Room: {v.RoomKey}\r\n" + - $" Reported: {v.AppVersion} Expected: {(string.IsNullOrEmpty(v.ExpectedAppVersion) ? "(not configured)" : v.ExpectedAppVersion)} Match: {(match ? "Yes" : "NO - MISMATCH")}\r\n" + - $" Last Seen (UTC): {v.LastSeen:yyyy-MM-dd HH:mm:ss}\r\n" - ); - } - } + CrestronConsole.ConsoleCommandResponse( + $"\r\nUI Wrapper App Deployed Version Check:\r\n {versionCheck.Summary}\r\n" + ); } /// @@ -1445,8 +1410,6 @@ namespace PepperDash.Essentials var roomKey = content["roomKey"].Value(); var touchpanelKey = content.SelectToken("touchpanelKey"); - TrackClientAppVersion(clientId, roomKey, touchpanelKey?.Value(), content.SelectToken("appVersion")?.Value()); - if (_roomCombiner == null) { var message = new MobileControlMessage @@ -1518,50 +1481,6 @@ namespace PepperDash.Essentials SendTouchpanelKey(clientId, touchpanelKey); } - /// - /// Records the app version reported by a connecting UI client (e.g. the mobile control React app's - /// build-time APP_VERSION) and compares it against the configured versions.touchpanelWrapperApp version. - /// - private void TrackClientAppVersion(string clientId, string roomKey, string touchpanelKey, string appVersion) - { - if (string.IsNullOrEmpty(appVersion)) - { - return; - } - - var expectedVersion = ConfigReader.ConfigObject?.Versions?.TouchpanelWrapperApp?.Version; - - var info = new ConnectedClientVersionInfo - { - ClientId = clientId, - RoomKey = roomKey, - TouchpanelKey = touchpanelKey, - AppVersion = appVersion, - ExpectedAppVersion = expectedVersion, - LastSeen = DateTime.UtcNow - }; - - lock (_connectedClientVersionsLock) - { - _connectedClientVersions[clientId] = info; - } - - if (!string.IsNullOrEmpty(expectedVersion) && !string.Equals(expectedVersion, appVersion, StringComparison.OrdinalIgnoreCase)) - { - this.LogWarning( - "Client {clientId} (touchpanel {touchpanelKey}) reported UI app version {appVersion}, which does not match configured versions.touchpanelWrapperApp version {expectedVersion}", - clientId, touchpanelKey, appVersion, expectedVersion - ); - } - else - { - this.LogVerbose( - "Client {clientId} (touchpanel {touchpanelKey}) reported UI app version {appVersion}", - clientId, touchpanelKey, appVersion - ); - } - } - private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken) { if (touchpanelKeyToken == null) diff --git a/src/PepperDash.Essentials.MobileControl/TouchpanelWrapperAppVersionChecker.cs b/src/PepperDash.Essentials.MobileControl/TouchpanelWrapperAppVersionChecker.cs new file mode 100644 index 00000000..1368b788 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/TouchpanelWrapperAppVersionChecker.cs @@ -0,0 +1,141 @@ +using System; +using System.IO; +using System.Linq; + +namespace PepperDash.Essentials +{ + /// + /// Determines whether the touchpanel wrapper app (the mobile control React app) deployed to this + /// processor's mcUserApp folder matches the version configured in the system config's + /// versions.touchpanelWrapperApp. + /// + /// + /// This reads the app's built .js/.html files directly from disk, so unlike the previous + /// client-self-reported-version approach, it does not depend on any UI client being connected and + /// does not accumulate per-client state over the life of the program. + /// + public static class TouchpanelWrapperAppVersionChecker + { + private static readonly string[] SearchPatterns = { "*.js", "*.html" }; + + /// + /// Scans the deployed touchpanel wrapper app's .js/.html files under + /// for the literal string (the version build tooling bakes + /// into the app at build time). + /// + /// The path to the deployed mcUserApp folder + /// The expected version, from config's versions.touchpanelWrapperApp.version + public static TouchpanelWrapperAppVersionCheckResult CheckDeployedVersion(string appPath, string expectedVersion) + { + if (string.IsNullOrEmpty(expectedVersion)) + { + return TouchpanelWrapperAppVersionCheckResult.NotConfigured(appPath); + } + + if (string.IsNullOrEmpty(appPath) || !Directory.Exists(appPath)) + { + return TouchpanelWrapperAppVersionCheckResult.AppNotDeployed(appPath, expectedVersion); + } + + try + { + var files = SearchPatterns + .SelectMany(pattern => Directory.GetFiles(appPath, pattern, SearchOption.AllDirectories)) + .ToList(); + + if (files.Count == 0) + { + return TouchpanelWrapperAppVersionCheckResult.AppNotDeployed(appPath, expectedVersion); + } + + foreach (var file in files) + { + var contents = File.ReadAllText(file); + if (contents.IndexOf(expectedVersion, StringComparison.OrdinalIgnoreCase) >= 0) + { + return TouchpanelWrapperAppVersionCheckResult.Match(appPath, expectedVersion, file); + } + } + + return TouchpanelWrapperAppVersionCheckResult.Mismatch(appPath, expectedVersion, files.Count); + } + catch (Exception ex) + { + return TouchpanelWrapperAppVersionCheckResult.Error(appPath, expectedVersion, ex.Message); + } + } + } + + /// + /// The outcome of a check + /// + public class TouchpanelWrapperAppVersionCheckResult + { + /// + /// The mcUserApp path that was checked + /// + public string AppPath { get; } + + /// + /// The expected version from config, if any was configured + /// + public string ExpectedVersion { get; } + + /// + /// True if any files were found deployed at + /// + public bool AppDeployed { get; } + + /// + /// True if was found in one of the deployed files + /// + public bool VersionMatched { get; } + + /// + /// The file the expected version was found in, if is true + /// + public string MatchedFile { get; } + + /// + /// The number of files scanned + /// + public int FilesScanned { get; } + + /// + /// A human-readable summary of the outcome, suitable for console output + /// + public string Summary { get; } + + private TouchpanelWrapperAppVersionCheckResult(string appPath, string expectedVersion, bool appDeployed, + bool versionMatched, string matchedFile, int filesScanned, string summary) + { + AppPath = appPath; + ExpectedVersion = expectedVersion; + AppDeployed = appDeployed; + VersionMatched = versionMatched; + MatchedFile = matchedFile; + FilesScanned = filesScanned; + Summary = summary; + } + + internal static TouchpanelWrapperAppVersionCheckResult NotConfigured(string appPath) => + new TouchpanelWrapperAppVersionCheckResult(appPath, null, false, false, null, 0, + "versions.touchpanelWrapperApp.version is not configured; skipping check"); + + internal static TouchpanelWrapperAppVersionCheckResult AppNotDeployed(string appPath, string expectedVersion) => + new TouchpanelWrapperAppVersionCheckResult(appPath, expectedVersion, false, false, null, 0, + $"No app files found at '{appPath}'; expected version {expectedVersion}"); + + internal static TouchpanelWrapperAppVersionCheckResult Match(string appPath, string expectedVersion, string matchedFile) => + new TouchpanelWrapperAppVersionCheckResult(appPath, expectedVersion, true, true, matchedFile, 1, + $"Deployed app matches configured version {expectedVersion} (found in '{matchedFile}')"); + + internal static TouchpanelWrapperAppVersionCheckResult Mismatch(string appPath, string expectedVersion, int filesScanned) => + new TouchpanelWrapperAppVersionCheckResult(appPath, expectedVersion, true, false, null, filesScanned, + $"Deployed app at '{appPath}' does NOT contain expected version {expectedVersion} ({filesScanned} file(s) scanned) - MISMATCH"); + + internal static TouchpanelWrapperAppVersionCheckResult Error(string appPath, string expectedVersion, string errorMessage) => + new TouchpanelWrapperAppVersionCheckResult(appPath, expectedVersion, false, false, null, 0, + $"Error checking deployed app version at '{appPath}': {errorMessage}"); + } +}