mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 10:58:28 +00:00
Merge pull request #1441 from PepperDash/feature/package-manifest-v3-routing-backport
Backport PR #1439: packageManifest CWS endpoint + v1/v2 config merge fix
This commit is contained in:
commit
dddb67317a
7 changed files with 468 additions and 33 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
<Authors>PepperDash Technology</Authors>
|
<Authors>PepperDash Technology</Authors>
|
||||||
<Company>PepperDash Technology</Company>
|
<Company>PepperDash Technology</Company>
|
||||||
<Product>PepperDash Essentials</Product>
|
<Product>PepperDash Essentials</Product>
|
||||||
<Copyright>Copyright © 2025</Copyright>
|
<Copyright>Copyright © 2026</Copyright>
|
||||||
<RepositoryUrl>https://github.com/PepperDash/Essentials</RepositoryUrl>
|
<RepositoryUrl>https://github.com/PepperDash/Essentials</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PackageTags>Crestron; 4series</PackageTags>
|
<PackageTags>Crestron; 4series</PackageTags>
|
||||||
|
|
@ -20,4 +20,7 @@
|
||||||
<None Include="..\..\LICENSE.md" Pack="true" PackagePath=""/>
|
<None Include="..\..\LICENSE.md" Pack="true" PackagePath=""/>
|
||||||
<None Include="..\..\README.md" Pack="true" PackagePath=""/>
|
<None Include="..\..\README.md" Pack="true" PackagePath=""/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyMetadata Include="PackageId" Value="$(PackageId)" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -132,12 +132,14 @@ namespace PepperDash.Essentials.Core.Config;
|
||||||
{
|
{
|
||||||
var parsedConfig = JObject.Parse(fileContents);
|
var parsedConfig = JObject.Parse(fileContents);
|
||||||
|
|
||||||
// Check if it's a v2 config (check for "version" node)
|
// A config is v1 if it has separate "system" and "template" nodes that
|
||||||
// this means it's already merged by the Portal API
|
// need to be merged. A v2 config is already merged by the Portal API and
|
||||||
// from the v2 config tool
|
// will not have "system"/"template" nodes. This is independent of whether
|
||||||
var isV2Config = parsedConfig["versions"] != null;
|
// a "versions" node is present, which only carries version metadata and
|
||||||
|
// can appear on either a v1 or v2 config.
|
||||||
|
var isV1Config = parsedConfig["system"] != null && parsedConfig["template"] != null;
|
||||||
|
|
||||||
if (isV2Config)
|
if (!isV1Config)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary.");
|
Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary.");
|
||||||
ConfigObject = parsedConfig.ToObject<EssentialsConfig>();
|
ConfigObject = parsedConfig.ToObject<EssentialsConfig>();
|
||||||
|
|
@ -145,6 +147,8 @@ namespace PepperDash.Essentials.Core.Config;
|
||||||
return ConfigObject != null;
|
return ConfigObject != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Debug.LogMessage(LogEventLevel.Information, "Config file is a v1 format, merging system and template.");
|
||||||
|
|
||||||
// Extract SystemUrl and TemplateUrl into final config output
|
// Extract SystemUrl and TemplateUrl into final config output
|
||||||
ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject<EssentialsConfig>();
|
ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject<EssentialsConfig>();
|
||||||
|
|
||||||
|
|
@ -163,6 +167,13 @@ namespace PepperDash.Essentials.Core.Config;
|
||||||
{
|
{
|
||||||
ConfigObject.TemplateUrl = parsedConfig["template_url"].Value<string>();
|
ConfigObject.TemplateUrl = parsedConfig["template_url"].Value<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MergeConfigs does not carry the "versions" node forward, so it must be
|
||||||
|
// applied separately to ensure it's preserved in the merged config.
|
||||||
|
if (parsedConfig["versions"] != null)
|
||||||
|
{
|
||||||
|
ConfigObject.Versions = parsedConfig["versions"].ToObject<VersionData>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Merged Config");
|
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Merged Config");
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,15 @@ namespace PepperDash.Essentials.Core.Config;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EssentialsConfig : BasicConfig
|
public class EssentialsConfig : BasicConfig
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the System URL for the Essentials configuration
|
||||||
|
/// </summary>
|
||||||
[JsonProperty("system_url")]
|
[JsonProperty("system_url")]
|
||||||
public string SystemUrl { get; set; }
|
public string SystemUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Template URL for the Essentials configuration
|
||||||
|
/// </summary>
|
||||||
[JsonProperty("template_url")]
|
[JsonProperty("template_url")]
|
||||||
public string TemplateUrl { get; set; }
|
public string TemplateUrl { get; set; }
|
||||||
|
|
||||||
|
|
@ -74,14 +80,21 @@ public class EssentialsConfig : BasicConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of rooms (device configurations)
|
||||||
|
/// </summary>
|
||||||
[JsonProperty("rooms")]
|
[JsonProperty("rooms")]
|
||||||
public List<DeviceConfig> Rooms { get; set; }
|
public List<DeviceConfig> Rooms { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the Versions
|
/// Gets or sets the Versions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonProperty("versions")]
|
||||||
public VersionData Versions { get; set; }
|
public VersionData Versions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="EssentialsConfig"/> class.
|
||||||
|
/// </summary>
|
||||||
public EssentialsConfig()
|
public EssentialsConfig()
|
||||||
: base()
|
: base()
|
||||||
{
|
{
|
||||||
|
|
@ -98,13 +111,13 @@ public class VersionData
|
||||||
/// Gets or sets the Essentials version
|
/// Gets or sets the Essentials version
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("essentials")]
|
[JsonProperty("essentials")]
|
||||||
public NugetVersion Essentials { get; set; }
|
public PackageVersion Essentials { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of UserInterfaces
|
/// Gets or sets the list of UserInterfaces
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("userInterfaces")]
|
[JsonProperty("userInterfaces")]
|
||||||
public List<AppVersion> UserInterfaces { get; set; }
|
public List<PackageVersion> UserInterfaces { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the TouchpanelWrapperApp version
|
/// Gets or sets the TouchpanelWrapperApp version
|
||||||
|
|
@ -112,45 +125,29 @@ public class VersionData
|
||||||
/// to run an HTML5 user interface.
|
/// to run an HTML5 user interface.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("touchpanelWrapperApp")]
|
[JsonProperty("touchpanelWrapperApp")]
|
||||||
public AppVersion TouchpanelWrapperApp { get; set; }
|
public PackageVersion TouchpanelWrapperApp { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of Packages
|
/// Gets or sets the list of Packages
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("packages")]
|
[JsonProperty("packages")]
|
||||||
public List<NugetVersion> Packages { get; set; }
|
public List<PackageVersion> Packages { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="VersionData"/> class.
|
/// Initializes a new instance of the <see cref="VersionData"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public VersionData()
|
public VersionData()
|
||||||
{
|
{
|
||||||
UserInterfaces = new List<AppVersion>();
|
UserInterfaces = new List<PackageVersion>();
|
||||||
TouchpanelWrapperApp = new AppVersion();
|
TouchpanelWrapperApp = new PackageVersion();
|
||||||
Packages = new List<NugetVersion>();
|
Packages = new List<PackageVersion>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AppVersion
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the Version
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("version")]
|
|
||||||
public string Version { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the RepoUrl
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("repoUrl")]
|
|
||||||
public string RepoUrl { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a NugetVersion
|
/// Represents a Package Version
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class NugetVersion
|
public class PackageVersion
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the Version
|
/// Gets or sets the Version
|
||||||
|
|
@ -161,14 +158,20 @@ public class NugetVersion
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the PackageId
|
/// Gets or sets the PackageId
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("packageId")]
|
[JsonProperty("packageId", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
public string PackageId { get; set; }
|
public string PackageId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the RepoUrl
|
/// Gets or sets the RepoUrl
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("repoUrl")]
|
[JsonProperty("repoUrl", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
public string RepoUrl { get; set; }
|
public string RepoUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the human-readable name
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public string Name { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -176,6 +179,9 @@ public class NugetVersion
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SystemTemplateConfigs
|
public class SystemTemplateConfigs
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the System configuration
|
||||||
|
/// </summary>
|
||||||
public EssentialsConfig System { get; set; }
|
public EssentialsConfig System { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,11 @@ public class EssentialsWebApi : EssentialsDevice
|
||||||
Name = "ReportVersions",
|
Name = "ReportVersions",
|
||||||
RouteHandler = new ReportVersionsRequestHandler()
|
RouteHandler = new ReportVersionsRequestHandler()
|
||||||
},
|
},
|
||||||
|
new HttpCwsRoute("packageManifest")
|
||||||
|
{
|
||||||
|
Name = "GetPackageManifest",
|
||||||
|
RouteHandler = new GetPackageManifestRequestHandler()
|
||||||
|
},
|
||||||
new HttpCwsRoute("appdebug")
|
new HttpCwsRoute("appdebug")
|
||||||
{
|
{
|
||||||
Name = "AppDebug",
|
Name = "AppDebug",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Crestron.SimplSharp.WebScripting;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using PepperDash.Core.Web.RequestHandlers;
|
||||||
|
using PepperDash.Essentials.Core.Config;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials.Core.Web.RequestHandlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a GetPackageManifestRequestHandler
|
||||||
|
/// </summary>
|
||||||
|
public class GetPackageManifestRequestHandler : WebApiBaseRequestHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// base(true) enables CORS support by default
|
||||||
|
/// </remarks>
|
||||||
|
public GetPackageManifestRequestHandler()
|
||||||
|
: base(true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles GET method requests
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
protected override void HandleGet(HttpCwsContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = CloneVersionData(ConfigReader.ConfigObject?.Versions) ?? new VersionData();
|
||||||
|
|
||||||
|
PopulateEssentials(result);
|
||||||
|
PopulatePackages(result);
|
||||||
|
|
||||||
|
var js = JsonConvert.SerializeObject(result, Formatting.Indented);
|
||||||
|
|
||||||
|
context.Response.StatusCode = 200;
|
||||||
|
context.Response.StatusDescription = "OK";
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
|
||||||
|
context.Response.Write(js, false);
|
||||||
|
context.Response.End();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PepperDash.Core.Debug.LogMessage(ex, "Exception handling GET /packageManifest request");
|
||||||
|
context.Response.StatusCode = 500;
|
||||||
|
context.Response.StatusDescription = "Internal Server Error";
|
||||||
|
context.Response.End();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deep-copies the config's VersionData so the live config object is never mutated
|
||||||
|
/// </summary>
|
||||||
|
private static VersionData CloneVersionData(VersionData source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonConvert.SerializeObject(source);
|
||||||
|
return JsonConvert.DeserializeObject<VersionData>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enriches (or creates) the essentials entry from the loaded PepperDash.Essentials.Core assembly
|
||||||
|
/// </summary>
|
||||||
|
private static void PopulateEssentials(VersionData result)
|
||||||
|
{
|
||||||
|
var essentials = result.Essentials ?? new PackageVersion();
|
||||||
|
|
||||||
|
essentials.Version = Global.AssemblyVersion;
|
||||||
|
|
||||||
|
// The main program assembly (PackageId "PepperDashEssentials") is what's actually published
|
||||||
|
// to NuGet, but this handler lives in PepperDash.Essentials.Core, which can't reference that
|
||||||
|
// project's types directly (Essentials -> Core, not the reverse). PluginLoader.EssentialsAssembly.Assembly
|
||||||
|
// is unreliable (often left null - see PluginLoader.SetEssentialsAssembly), so look it up
|
||||||
|
// directly from the loaded AppDomain by its Directory.Build.props-embedded PackageId metadata
|
||||||
|
// (every project's .csproj sets its own PackageId explicitly), falling back to this handler's
|
||||||
|
// own (Core) assembly if it can't be found.
|
||||||
|
var essentialsAssembly = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.FirstOrDefault(a => string.Equals(GetAssemblyMetadataValue(a, "PackageId"), "PepperDashEssentials", StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? typeof(GetPackageManifestRequestHandler).Assembly;
|
||||||
|
|
||||||
|
var repoUrl = TrimTrailingGit(GetAssemblyMetadataValue(essentialsAssembly, "RepositoryUrl"));
|
||||||
|
if (!string.IsNullOrEmpty(repoUrl))
|
||||||
|
{
|
||||||
|
essentials.RepoUrl = repoUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = GetAssemblyProduct(essentialsAssembly);
|
||||||
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
essentials.Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the PackageId embedded via Directory.Build.props' AssemblyMetadata item (the
|
||||||
|
// authoritative source, matching the actual published PackageId) over any config-supplied
|
||||||
|
// or hardcoded value.
|
||||||
|
var reflectedPackageId = GetAssemblyMetadataValue(essentialsAssembly, "PackageId");
|
||||||
|
if (!string.IsNullOrEmpty(reflectedPackageId))
|
||||||
|
{
|
||||||
|
essentials.PackageId = reflectedPackageId;
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrEmpty(essentials.PackageId))
|
||||||
|
{
|
||||||
|
essentials.PackageId = "PepperDashEssentials";
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Essentials = essentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Merges reflection data from loaded plugin assemblies with the config's packages list
|
||||||
|
/// </summary>
|
||||||
|
private static void PopulatePackages(VersionData result)
|
||||||
|
{
|
||||||
|
// Filter out null entries defensively - the packages list is deserialized from user-editable
|
||||||
|
// config JSON, so a malformed "packages": [null, ...] shouldn't throw and 500 the endpoint.
|
||||||
|
var configPackages = (result.Packages ?? new System.Collections.Generic.List<PackageVersion>())
|
||||||
|
.Where(p => p != null)
|
||||||
|
.ToList();
|
||||||
|
var matchedConfigPackages = new System.Collections.Generic.HashSet<PackageVersion>();
|
||||||
|
var mergedPackages = new System.Collections.Generic.List<PackageVersion>();
|
||||||
|
|
||||||
|
foreach (var loaded in PluginLoader.EssentialsPluginAssemblies.Where(a => a.Assembly != null))
|
||||||
|
{
|
||||||
|
var reflectedVersion = loaded.Version;
|
||||||
|
if (string.IsNullOrEmpty(reflectedVersion))
|
||||||
|
{
|
||||||
|
// Never emit an entry with no version - the extension's parser drops entries
|
||||||
|
// whose version isn't a string.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reflectedRepoUrl = TrimTrailingGit(GetAssemblyMetadataValue(loaded.Assembly, "RepositoryUrl"));
|
||||||
|
var reflectedName = GetAssemblyProduct(loaded.Assembly);
|
||||||
|
|
||||||
|
// Plugins built from a Directory.Build.props that embeds
|
||||||
|
// <AssemblyMetadata Include="PackageId" Value="$(PackageId)" /> carry their PackageId
|
||||||
|
// directly - this is authoritative and should be preferred over the title/name fallback chain.
|
||||||
|
var reflectedPackageId = GetAssemblyMetadataValue(loaded.Assembly, "PackageId");
|
||||||
|
|
||||||
|
var assemblyTitle = GetAssemblyTitle(loaded.Assembly);
|
||||||
|
var assemblyName = loaded.Assembly.GetName().Name;
|
||||||
|
var assemblyNameNoSeriesSuffix = StripTrailingSeriesSuffix(assemblyName);
|
||||||
|
|
||||||
|
var match = configPackages.FirstOrDefault(p =>
|
||||||
|
!matchedConfigPackages.Contains(p) &&
|
||||||
|
!string.IsNullOrEmpty(p.PackageId) &&
|
||||||
|
(string.Equals(p.PackageId, reflectedPackageId, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(p.PackageId, assemblyTitle, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(p.PackageId, assemblyName, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(p.PackageId, assemblyNameNoSeriesSuffix, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
|
if (match != null)
|
||||||
|
{
|
||||||
|
matchedConfigPackages.Add(match);
|
||||||
|
|
||||||
|
mergedPackages.Add(new PackageVersion
|
||||||
|
{
|
||||||
|
Name = !string.IsNullOrEmpty(match.Name) ? match.Name : reflectedName,
|
||||||
|
RepoUrl = !string.IsNullOrEmpty(match.RepoUrl) ? match.RepoUrl : reflectedRepoUrl,
|
||||||
|
PackageId = !string.IsNullOrEmpty(reflectedPackageId) ? reflectedPackageId : match.PackageId,
|
||||||
|
Version = reflectedVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Loaded but not present (or not matched) in config - emit the reflected PackageId
|
||||||
|
// when the assembly carries one, otherwise leave it null as before.
|
||||||
|
mergedPackages.Add(new PackageVersion
|
||||||
|
{
|
||||||
|
Name = reflectedName,
|
||||||
|
RepoUrl = reflectedRepoUrl,
|
||||||
|
PackageId = reflectedPackageId,
|
||||||
|
Version = reflectedVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configured but not currently loaded - pass through unchanged, but keep the same
|
||||||
|
// "never emit an entry with no version" guarantee as the loaded-reflection branch above -
|
||||||
|
// otherwise this branch reintroduces the schema issue that guard is meant to prevent.
|
||||||
|
mergedPackages.AddRange(configPackages.Where(p =>
|
||||||
|
!matchedConfigPackages.Contains(p) && !string.IsNullOrEmpty(p.Version)));
|
||||||
|
|
||||||
|
result.Packages = mergedPackages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAssemblyMetadataValue(Assembly assembly, string key)
|
||||||
|
{
|
||||||
|
if (assembly == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = assembly.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false)
|
||||||
|
.Cast<AssemblyMetadataAttribute>()
|
||||||
|
.FirstOrDefault(a => string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
return match?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAssemblyProduct(Assembly assembly)
|
||||||
|
{
|
||||||
|
if (assembly == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attribute = assembly.GetCustomAttributes(typeof(AssemblyProductAttribute), false)
|
||||||
|
.FirstOrDefault() as AssemblyProductAttribute;
|
||||||
|
|
||||||
|
return attribute?.Product;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAssemblyTitle(Assembly assembly)
|
||||||
|
{
|
||||||
|
if (assembly == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attribute = assembly.GetCustomAttributes(typeof(AssemblyTitleAttribute), false)
|
||||||
|
.FirstOrDefault() as AssemblyTitleAttribute;
|
||||||
|
|
||||||
|
return attribute?.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StripTrailingSeriesSuffix(string assemblyName)
|
||||||
|
{
|
||||||
|
const string suffix = ".4Series";
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(assemblyName) || !assemblyName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return assemblyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return assemblyName.Substring(0, assemblyName.Length - suffix.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TrimTrailingGit(string repoUrl)
|
||||||
|
{
|
||||||
|
const string suffix = ".git";
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(repoUrl) || !repoUrl.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return repoUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return repoUrl.Substring(0, repoUrl.Length - suffix.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1006,6 +1006,14 @@ namespace PepperDash.Essentials
|
||||||
" Not Enabled in Config.\r\n"
|
" Not Enabled in Config.\r\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var expectedAppVersion = ConfigReader.ConfigObject?.Versions?.TouchpanelWrapperApp?.Version;
|
||||||
|
var userAppPath = Global.FilePathPrefix + "mcUserApp" + Global.DirectorySeparator;
|
||||||
|
var versionCheck = TouchpanelWrapperAppVersionChecker.CheckDeployedVersion(userAppPath, expectedAppVersion);
|
||||||
|
|
||||||
|
CrestronConsole.ConsoleCommandResponse(
|
||||||
|
$"\r\nUI Wrapper App Deployed Version Check:\r\n {versionCheck.Summary}\r\n"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the touchpanel wrapper app (the mobile control React app) deployed to this
|
||||||
|
/// processor's <c>mcUserApp</c> folder matches the version configured in the system config's
|
||||||
|
/// <c>versions.touchpanelWrapperApp</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
public static class TouchpanelWrapperAppVersionChecker
|
||||||
|
{
|
||||||
|
private static readonly string[] SearchPatterns = { "*.js", "*.html" };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scans the deployed touchpanel wrapper app's .js/.html files under <paramref name="appPath"/>
|
||||||
|
/// for the literal <paramref name="expectedVersion"/> string (the version build tooling bakes
|
||||||
|
/// into the app at build time).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="appPath">The path to the deployed mcUserApp folder</param>
|
||||||
|
/// <param name="expectedVersion">The expected version, from config's versions.touchpanelWrapperApp.version</param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The outcome of a <see cref="TouchpanelWrapperAppVersionChecker.CheckDeployedVersion"/> check
|
||||||
|
/// </summary>
|
||||||
|
public class TouchpanelWrapperAppVersionCheckResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The mcUserApp path that was checked
|
||||||
|
/// </summary>
|
||||||
|
public string AppPath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The expected version from config, if any was configured
|
||||||
|
/// </summary>
|
||||||
|
public string ExpectedVersion { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if any files were found deployed at <see cref="AppPath"/>
|
||||||
|
/// </summary>
|
||||||
|
public bool AppDeployed { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if <see cref="ExpectedVersion"/> was found in one of the deployed files
|
||||||
|
/// </summary>
|
||||||
|
public bool VersionMatched { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The file the expected version was found in, if <see cref="VersionMatched"/> is true
|
||||||
|
/// </summary>
|
||||||
|
public string MatchedFile { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of files scanned
|
||||||
|
/// </summary>
|
||||||
|
public int FilesScanned { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A human-readable summary of the outcome, suitable for console output
|
||||||
|
/// </summary>
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue