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;
///
/// Represents a GetPackageManifestRequestHandler
///
public class GetPackageManifestRequestHandler : WebApiBaseRequestHandler
{
///
/// Constructor
///
///
/// base(true) enables CORS support by default
///
public GetPackageManifestRequestHandler()
: base(true)
{
}
///
/// Handles GET method requests
///
///
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();
}
}
///
/// Deep-copies the config's VersionData so the live config object is never mutated
///
private static VersionData CloneVersionData(VersionData source)
{
if (source == null)
{
return null;
}
var json = JsonConvert.SerializeObject(source);
return JsonConvert.DeserializeObject(json);
}
///
/// Enriches (or creates) the essentials entry from the loaded PepperDash.Essentials.Core assembly
///
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;
}
///
/// Merges reflection data from loaded plugin assemblies with the config's packages list
///
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())
.Where(p => p != null)
.ToList();
var matchedConfigPackages = new System.Collections.Generic.HashSet();
var mergedPackages = new System.Collections.Generic.List();
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
// 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
mergedPackages.AddRange(configPackages.Where(p => !matchedConfigPackages.Contains(p)));
result.Packages = mergedPackages;
}
private static string GetAssemblyMetadataValue(Assembly assembly, string key)
{
if (assembly == null)
{
return null;
}
var match = assembly.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false)
.Cast()
.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);
}
}