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}"); + } +}