fix: add AssemblyResolve handler for reflection diagnostics

Previous version failed GetExportedTypes() because Assembly.Load(bytes)
couldn't resolve Disco.Services.dll from the PowerShell process. Now
uses LoadFrom() with an AssemblyResolve handler that probes both the
build output and Disco bin directories. Also adds ReflectionTypeLoad
exception detail logging and proper handler cleanup in finally block.
This commit is contained in:
jessikitty committed 2026-04-29 09:36:17 +10:00
1 parent 7cf07b50d2
commit df6671d729
1 file changed
+1 -479
+1 -479
View File
@@ -1,479 +1 @@
# Build-Plugin.ps1 IyBCdWlsZC1QbHVnaW4ucHMxCiMgQnVpbGRzIGFuZCBwYWNrYWdlcyB0aGUgQUQgQ29tcGFyZSBwbHVnaW4gZm9yIERpc2NvIElDVCBpbXBvcnQKIwojIFVzYWdlOgojICAgLlxCdWlsZC1QbHVnaW4ucHMxIC1EaXNjb0JpblBhdGggIkM6XFByb2dyYW0gRmlsZXNcRGlzY29cV2ViQXBwXGJpbiIKCnBhcmFtKAogICAgW1BhcmFtZXRlcihNYW5kYXRvcnk9JHRydWUpXQogICAgW3N0cmluZ10kRGlzY29CaW5QYXRoLAoKICAgIFtzdHJpbmddJENvbmZpZ3VyYXRpb24gPSAiUmVsZWFzZSIKKQoKJEVycm9yQWN0aW9uUHJlZmVyZW5jZSA9ICJTdG9wIgokUGx1Z2luRGlyID0gJFBTU2NyaXB0Um9vdAokUGx1Z2luTmFtZSA9ICJEaXNjby5QbHVnaW5zLkFEQ29tcGFyZSIKCldyaXRlLUhvc3QgIj09PSBBRCBDb21wYXJlIFBsdWdpbiBCdWlsZGVyID09PSIgLUZvcmVncm91bmRDb2xvciBDeWFuCldyaXRlLUhvc3QgIkRpc2NvIGJpbiBwYXRoOiAkRGlzY29CaW5QYXRoIiAtRm9yZWdyb3VuZENvbG9yIEdyYXkKCiMgLS0tIFZhbGlkYXRlIC0tLQppZiAoLW5vdCAoVGVzdC1QYXRoICREaXNjb0JpblBhdGgpKSB7CiAgICBXcml0ZS1FcnJvciAiRGlzY28gYmluIHBhdGggbm90IGZvdW5kOiAkRGlzY29CaW5QYXRoIgogICAgZXhpdCAxCn0KCiRyZXF1aXJlZERsbHMgPSBAKCJEaXNjby5TZXJ2aWNlcy5kbGwiLCAiRGlzY28uTW9kZWxzLmRsbCIsICJEaXNjby5EYXRhLmRsbCIpCmZvcmVhY2ggKCRkbGwgaW4gJHJlcXVpcmVkRGxscykgewogICAgJGRsbFBhdGggPSBKb2luLVBhdGggJERpc2NvQmluUGF0aCAkZGxsCiAgICBpZiAoLW5vdCAoVGVzdC1QYXRoICRkbGxQYXRoKSkgewogICAgICAgIFdyaXRlLUVycm9yICJSZXF1aXJlZCBhc3NlbWJseSBub3QgZm91bmQ6ICRkbGxQYXRoIgogICAgICAgIGV4aXQgMQogICAgfQp9CldyaXRlLUhvc3QgIkFsbCByZXF1aXJlZCBEaXNjbyBhc3NlbWJsaWVzIGZvdW5kIiAtRm9yZWdyb3VuZENvbG9yIEdyZWVuCgojIC0tLSBTaG93IERpc2NvIEFzc2VtYmx5IFZlcnNpb25zIC0tLQpXcml0ZS1Ib3N0ICJgbi0tLSBEaXNjbyBBc3NlbWJseSBWZXJzaW9ucyAtLS0iIC1Gb3JlZ3JvdW5kQ29sb3IgRGFya0dyYXkKZm9yZWFjaCAoJGRsbCBpbiAkcmVxdWlyZWREbGxzKSB7CiAgICAkZGxsUGF0aCA9IEpvaW4tUGF0aCAkRGlzY29CaW5QYXRoICRkbGwKICAgIHRyeSB7CiAgICAgICAgJGFzbU5hbWUgPSBbU3lzdGVtLlJlZmxlY3Rpb24uQXNzZW1ibHlOYW1lXTo6R2V0QXNzZW1ibHlOYW1lKCRkbGxQYXRoKQogICAgICAgIFdyaXRlLUhvc3QgIiAgJGRsbCA6IHYkKCRhc21OYW1lLlZlcnNpb24pIiAtRm9yZWdyb3VuZENvbG9yIEdyYXkKICAgIH0gY2F0Y2ggewogICAgICAgIFdyaXRlLVdhcm5pbmcgIiAgJGRsbCA6IGNvdWxkIG5vdCByZWFkIHZlcnNpb24gLSAkKCRfLkV4Y2VwdGlvbi5NZXNzYWdlKSIKICAgIH0KfQoKIyAtLS0gRmluZCBNU0J1aWxkIC0tLQokbXNidWlsZCA9ICRudWxsCiR2c1doZXJlID0gIiR7ZW52OlByb2dyYW1GaWxlcyh4ODYpfVxNaWNyb3NvZnQgVmlzdWFsIFN0dWRpb1xJbnN0YWxsZXJcdnN3aGVyZS5leGUiCmlmIChUZXN0LVBhdGggJHZzV2hlcmUpIHsKICAgICRtc2J1aWxkID0gJiAkdnNXaGVyZSAtbGF0ZXN0IC1yZXF1aXJlcyBNaWNyb3NvZnQuQ29tcG9uZW50Lk1TQnVpbGQgLWZpbmQgIk1TQnVpbGRcKipcQmluXE1TQnVpbGQuZXhlIiAyPiRudWxsIHwgU2VsZWN0LU9iamVjdCAtRmlyc3QgMQp9CmlmICgtbm90ICRtc2J1aWxkIC1vciAtbm90IChUZXN0LVBhdGggJG1zYnVpbGQpKSB7CiAgICAkbXNidWlsZCA9ICJDOlxXaW5kb3dzXE1pY3Jvc29mdC5ORVRcRnJhbWV3b3JrNjRcdjQuMC4zMDMxOVxNU0J1aWxkLmV4ZSIKfQppZiAoLW5vdCAoVGVzdC1QYXRoICRtc2J1aWxkKSkgewogICAgV3JpdGUtRXJyb3IgIk1TQnVpbGQgbm90IGZvdW5kLiIKICAgIGV4aXQgMQp9CldyaXRlLUhvc3QgIlVzaW5nIE1TQnVpbGQ6ICRtc2J1aWxkIiAtRm9yZWdyb3VuZENvbG9yIEdyYXkKCiMgLS0tIFJlc29sdmUgYWxsIEhpbnRQYXRocyB0byBEaXNjbyBiaW4gLS0tCiRjc3Byb2ogPSBKb2luLVBhdGggJFBsdWdpbkRpciAiJFBsdWdpbk5hbWUuY3Nwcm9qIgokY3Nwcm9qQ29udGVudCA9IEdldC1Db250ZW50ICRjc3Byb2ogLVJhdwokY3Nwcm9qQ29udGVudCA9IFtyZWdleF06OlJlcGxhY2UoJGNzcHJvakNvbnRlbnQsICc8SGludFBhdGg+W148XStcXChbXlxcPF0rXC5kbGwpPC9IaW50UGF0aD4nLCAiPEhpbnRQYXRoPiREaXNjb0JpblBhdGhcYCQxPC9IaW50UGF0aD4iKQoKJHRlbXBDc3Byb2ogPSBKb2luLVBhdGggJFBsdWdpbkRpciAiJFBsdWdpbk5hbWUuYnVpbGQuY3Nwcm9qIgokY3Nwcm9qQ29udGVudCB8IFNldC1Db250ZW50ICR0ZW1wQ3Nwcm9qIC1FbmNvZGluZyBVVEY4CldyaXRlLUhvc3QgIkFzc2VtYmx5IHJlZmVyZW5jZXMgcmVzb2x2ZWQgdG8gJERpc2NvQmluUGF0aCIgLUZvcmVncm91bmRDb2xvciBHcmVlbgoKIyAtLS0gQnVpbGQgLS0tCldyaXRlLUhvc3QgImBuQnVpbGRpbmcgJFBsdWdpbk5hbWUgKCRDb25maWd1cmF0aW9uKS4uLiIgLUZvcmVncm91bmRDb2xvciBZZWxsb3cKJGJ1aWxkT3V0cHV0ID0gSm9pbi1QYXRoICRQbHVnaW5EaXIgImJpblwkQ29uZmlndXJhdGlvbiIKCiYgJG1zYnVpbGQgJHRlbXBDc3Byb2ogL3A6Q29uZmlndXJhdGlvbj0kQ29uZmlndXJhdGlvbiAvcDpPdXRwdXRQYXRoPSRidWlsZE91dHB1dCAvdDpCdWlsZCAvdjptaW5pbWFsCgppZiAoJExBU1RFWElUQ09ERSAtbmUgMCkgewogICAgUmVtb3ZlLUl0ZW0gJHRlbXBDc3Byb2ogLUVycm9yQWN0aW9uIFNpbGVudGx5Q29udGludWUKICAgIFdyaXRlLUVycm9yICJCdWlsZCBmYWlsZWQhIgogICAgZXhpdCAxCn0KUmVtb3ZlLUl0ZW0gJHRlbXBDc3Byb2ogLUVycm9yQWN0aW9uIFNpbGVudGx5Q29udGludWUKCiRwbHVnaW5EbGwgPSBKb2luLVBhdGggJGJ1aWxkT3V0cHV0ICIkUGx1Z2luTmFtZS5kbGwiCmlmICgtbm90IChUZXN0LVBhdGggJHBsdWdpbkRsbCkpIHsKICAgIFdyaXRlLUVycm9yICJCdWlsZCBvdXRwdXQgbm90IGZvdW5kOiAkcGx1Z2luRGxsIgogICAgZXhpdCAxCn0KV3JpdGUtSG9zdCAiQnVpbGQgc3VjY2Vzc2Z1bDogJHBsdWdpbkRsbCIgLUZvcmVncm91bmRDb2xvciBHcmVlbgoKIyAtLS0gQ29weSBEZXBlbmRlbmNpZXMgZm9yIFJlZmxlY3Rpb24gYW5kIE1hbmlmZXN0R2VuZXJhdG9yIC0tLQpmb3JlYWNoICgkZGxsIGluICRyZXF1aXJlZERsbHMpIHsKICAgIENvcHktSXRlbSAoSm9pbi1QYXRoICREaXNjb0JpblBhdGggJGRsbCkgJGJ1aWxkT3V0cHV0IC1Gb3JjZSAtRXJyb3JBY3Rpb24gU2lsZW50bHlDb250aW51ZQp9CiRleHRyYURsbHMgPSBAKCJFbnRpdHlGcmFtZXdvcmsuZGxsIiwgIlN5c3RlbS5XZWIuTXZjLmRsbCIsICJTeXN0ZW0uV2ViLldlYlBhZ2VzLmRsbCIsICJTeXN0ZW0uV2ViLlJhem9yLmRsbCIsICJSYXpvckdlbmVyYXRvci5NdmMuZGxsIiwgIk5ld3RvbnNvZnQuSnNvbi5kbGwiKQpmb3JlYWNoICgkZGVwIGluICRleHRyYURsbHMpIHsKICAgICRkZXBQYXRoID0gSm9pbi1QYXRoICREaXNjb0JpblBhdGggJGRlcAogICAgaWYgKFRlc3QtUGF0aCAkZGVwUGF0aCkgeyBDb3B5LUl0ZW0gJGRlcFBhdGggJGJ1aWxkT3V0cHV0IC1Gb3JjZSAtRXJyb3JBY3Rpb24gU2lsZW50bHlDb250aW51ZSB9Cn0KCiMgLS0tIFJlZmxlY3Rpb24tQmFzZWQgRmVhdHVyZSBEaWFnbm9zdGljcyAtLS0KV3JpdGUtSG9zdCALine truncated
# Builds and packages the AD Compare plugin for Disco ICT import
#
# Usage:
# .\Build-Plugin.ps1 -DiscoBinPath "C:\Program Files\Disco\WebApp\bin"
param(
[Parameter(Mandatory=$true)]
[string]$DiscoBinPath,
[string]$Configuration = "Release"
)
$ErrorActionPreference = "Stop"
$PluginDir = $PSScriptRoot
$PluginName = "Disco.Plugins.ADCompare"
Write-Host "=== AD Compare Plugin Builder ===" -ForegroundColor Cyan
Write-Host "Disco bin path: $DiscoBinPath" -ForegroundColor Gray
# --- Validate ---
if (-not (Test-Path $DiscoBinPath)) {
Write-Error "Disco bin path not found: $DiscoBinPath"
exit 1
}
$requiredDlls = @("Disco.Services.dll", "Disco.Models.dll", "Disco.Data.dll")
foreach ($dll in $requiredDlls) {
$dllPath = Join-Path $DiscoBinPath $dll
if (-not (Test-Path $dllPath)) {
Write-Error "Required assembly not found: $dllPath"
exit 1
}
}
Write-Host "All required Disco assemblies found" -ForegroundColor Green
# --- Show Disco Assembly Versions ---
Write-Host "`n--- Disco Assembly Versions ---" -ForegroundColor DarkGray
foreach ($dll in $requiredDlls) {
$dllPath = Join-Path $DiscoBinPath $dll
try {
$asmName = [System.Reflection.AssemblyName]::GetAssemblyName($dllPath)
Write-Host " $dll : v$($asmName.Version)" -ForegroundColor Gray
} catch {
Write-Warning " $dll : could not read version - $($_.Exception.Message)"
}
}
# --- Find MSBuild ---
$msbuild = $null
$vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vsWhere) {
$msbuild = & $vsWhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" 2>$null | Select-Object -First 1
}
if (-not $msbuild -or -not (Test-Path $msbuild)) {
$msbuild = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe"
}
if (-not (Test-Path $msbuild)) {
Write-Error "MSBuild not found."
exit 1
}
Write-Host "Using MSBuild: $msbuild" -ForegroundColor Gray
# --- Resolve all HintPaths to Disco bin ---
$csproj = Join-Path $PluginDir "$PluginName.csproj"
$csprojContent = Get-Content $csproj -Raw
$csprojContent = [regex]::Replace($csprojContent, '<HintPath>[^<]+\\([^\\<]+\.dll)</HintPath>', "<HintPath>$DiscoBinPath\`$1</HintPath>")
$tempCsproj = Join-Path $PluginDir "$PluginName.build.csproj"
$csprojContent | Set-Content $tempCsproj -Encoding UTF8
Write-Host "Assembly references resolved to $DiscoBinPath" -ForegroundColor Green
# --- Build ---
Write-Host "`nBuilding $PluginName ($Configuration)..." -ForegroundColor Yellow
$buildOutput = Join-Path $PluginDir "bin\$Configuration"
& $msbuild $tempCsproj /p:Configuration=$Configuration /p:OutputPath=$buildOutput /t:Build /v:minimal
if ($LASTEXITCODE -ne 0) {
Remove-Item $tempCsproj -ErrorAction SilentlyContinue
Write-Error "Build failed!"
exit 1
}
Remove-Item $tempCsproj -ErrorAction SilentlyContinue
$pluginDll = Join-Path $buildOutput "$PluginName.dll"
if (-not (Test-Path $pluginDll)) {
Write-Error "Build output not found: $pluginDll"
exit 1
}
Write-Host "Build successful: $pluginDll" -ForegroundColor Green
# --- Copy Dependencies for Reflection and ManifestGenerator ---
foreach ($dll in $requiredDlls) {
Copy-Item (Join-Path $DiscoBinPath $dll) $buildOutput -Force -ErrorAction SilentlyContinue
}
$extraDlls = @("EntityFramework.dll", "System.Web.Mvc.dll", "System.Web.WebPages.dll", "System.Web.Razor.dll", "RazorGenerator.Mvc.dll", "Newtonsoft.Json.dll")
foreach ($dep in $extraDlls) {
$depPath = Join-Path $DiscoBinPath $dep
if (Test-Path $depPath) { Copy-Item $depPath $buildOutput -Force -ErrorAction SilentlyContinue }
}
# --- Reflection-Based Feature Diagnostics ---
Write-Host "`n--- Plugin Assembly Diagnostics ---" -ForegroundColor Yellow
$reflectionOk = $true
try {
$pluginAsmBytes = [System.IO.File]::ReadAllBytes($pluginDll)
$pluginAsm = [System.Reflection.Assembly]::Load($pluginAsmBytes)
Write-Host " Plugin assembly loaded: $($pluginAsm.FullName)" -ForegroundColor Gray
# Find all types that extend UIExtensionFeature<T>
$uiExtTypes = @()
foreach ($t in $pluginAsm.GetExportedTypes()) {
$baseType = $t.BaseType
if ($baseType -ne $null -and $baseType.IsGenericType) {
$genDef = $baseType.GetGenericTypeDefinition()
if ($genDef.FullName -eq 'Disco.Services.Plugins.Features.UIExtension.UIExtensionFeature`1') {
$uiExtTypes += $t
}
}
}
if ($uiExtTypes.Count -eq 0) {
Write-Warning " NO UIExtensionFeature<T> implementations found in assembly!"
Write-Warning " ExecuteAction will never be called without UIExtension features."
$reflectionOk = $false
} else {
Write-Host " Found $($uiExtTypes.Count) UIExtension feature(s):" -ForegroundColor Green
}
foreach ($t in $uiExtTypes) {
$baseType = $t.BaseType
$genArgs = $baseType.GetGenericArguments()
$modelType = $genArgs[0]
$genDefFullName = $baseType.GetGenericTypeDefinition().FullName
Write-Host "`n Feature: $($t.FullName)" -ForegroundColor Cyan
Write-Host " Base type: $($baseType.FullName)" -ForegroundColor Gray
Write-Host " Generic definition: $genDefFullName" -ForegroundColor Gray
Write-Host " Model type: $($modelType.FullName)" -ForegroundColor Gray
Write-Host " Model assembly: $($modelType.Assembly.GetName().Name) v$($modelType.Assembly.GetName().Version)" -ForegroundColor Gray
# Verify [PluginFeature] attribute
$attrs = $t.GetCustomAttributes($true) | Where-Object { $_.GetType().Name -eq 'PluginFeatureAttribute' }
if ($attrs) {
foreach ($attr in $attrs) {
Write-Host " [PluginFeature] Id: $($attr.Id)" -ForegroundColor Gray
Write-Host " [PluginFeature] Name: $($attr.Name)" -ForegroundColor Gray
}
} else {
Write-Warning " MISSING [PluginFeature] attribute! Disco cannot discover this feature."
$reflectionOk = $false
}
# Verify ExecuteAction is overridden (not just the abstract base)
$execMethod = $t.GetMethod('ExecuteAction', [System.Reflection.BindingFlags]'Public,Instance')
if ($execMethod -and $execMethod.DeclaringType -eq $t) {
Write-Host " ExecuteAction: overridden (OK)" -ForegroundColor Green
} elseif ($execMethod) {
Write-Warning " ExecuteAction: NOT overridden (declared in $($execMethod.DeclaringType.Name))"
$reflectionOk = $false
} else {
Write-Warning " ExecuteAction: NOT FOUND"
$reflectionOk = $false
}
# Verify Initialize is overridden and calls Register()
$initMethod = $t.GetMethod('Initialize', [System.Reflection.BindingFlags]'Public,Instance', $null, @([Type]'Disco.Data.Repository.DiscoDataContext'), $null)
if ($initMethod -and $initMethod.DeclaringType -eq $t) {
Write-Host " Initialize: overridden (OK)" -ForegroundColor Green
} else {
Write-Warning " Initialize: NOT overridden - Register() will never be called!"
$reflectionOk = $false
}
# Check that the model type implements BaseUIModel
$baseUIModel = $modelType.GetInterface('BaseUIModel')
if ($baseUIModel) {
Write-Host " BaseUIModel: $($modelType.Name) implements BaseUIModel (OK)" -ForegroundColor Green
} else {
Write-Warning " BaseUIModel: $($modelType.Name) does NOT implement BaseUIModel!"
$reflectionOk = $false
}
}
# Check the main plugin class
$pluginClass = $pluginAsm.GetExportedTypes() | Where-Object {
$_.BaseType -ne $null -and $_.BaseType.FullName -eq 'Disco.Services.Plugins.Plugin'
}
if ($pluginClass) {
Write-Host "`n Plugin class: $($pluginClass.FullName)" -ForegroundColor Cyan
$pluginAttrs = $pluginClass.GetCustomAttributes($true) | Where-Object { $_.GetType().Name -eq 'PluginAttribute' }
if ($pluginAttrs) {
foreach ($attr in $pluginAttrs) {
Write-Host " [Plugin] Id: $($attr.Id)" -ForegroundColor Gray
Write-Host " [Plugin] Name: $($attr.Name)" -ForegroundColor Gray
}
} else {
Write-Warning " MISSING [Plugin] attribute!"
}
}
} catch {
Write-Warning " Reflection diagnostics failed: $($_.Exception.Message)"
Write-Warning " This may indicate assembly load issues (missing dependencies, version mismatches)."
$reflectionOk = $false
}
if ($reflectionOk) {
Write-Host "`n All reflection checks PASSED" -ForegroundColor Green
} else {
Write-Host "`n Some reflection checks FAILED - see warnings above" -ForegroundColor Red
}
# --- Generate Manifest ---
Write-Host "`n--- Manifest Generation ---" -ForegroundColor Yellow
$useManifestGen = $false
$manifestGenExe = Join-Path $DiscoBinPath "Disco.Services.Plugins.ManifestGenerator.exe"
if (-not (Test-Path $manifestGenExe)) {
$manifestGenExe = Join-Path (Split-Path $DiscoBinPath -Parent) "Disco.Services.Plugins.ManifestGenerator.exe"
}
if (Test-Path $manifestGenExe) {
Write-Host "Found ManifestGenerator: $manifestGenExe" -ForegroundColor Gray
$useManifestGen = $true
# Capture stdout and stderr separately for diagnostics
$manifestGenOutput = & $manifestGenExe $pluginDll 2>&1
$manifestGenExit = $LASTEXITCODE
if ($manifestGenOutput) {
Write-Host " ManifestGenerator output:" -ForegroundColor DarkGray
$manifestGenOutput | ForEach-Object {
$line = $_.ToString()
if ($_ -is [System.Management.Automation.ErrorRecord]) {
Write-Host " [STDERR] $line" -ForegroundColor Red
} else {
Write-Host " $line" -ForegroundColor DarkGray
}
}
}
if ($manifestGenExit -ne 0) {
Write-Warning "ManifestGenerator exited with code $manifestGenExit, falling back to manual manifest..."
$useManifestGen = $false
} else {
Write-Host "ManifestGenerator completed successfully" -ForegroundColor Green
}
} else {
Write-Host "ManifestGenerator not found at:" -ForegroundColor DarkGray
Write-Host " $DiscoBinPath" -ForegroundColor DarkGray
Write-Host " $(Split-Path $DiscoBinPath -Parent)" -ForegroundColor DarkGray
Write-Host "Falling back to manual manifest generation..." -ForegroundColor Yellow
}
if (-not $useManifestGen) {
$version = [System.Reflection.AssemblyName]::GetAssemblyName($pluginDll).Version
# CategoryTypeName must match the .NET generic type definition FullName.
# For UIExtensionFeature<T>, this is UIExtensionFeature`1 (one backtick, then 1).
# In PowerShell double-quoted strings, `` (two backticks) produces one literal backtick.
$uiExtCategoryTypeName = "Disco.Services.Plugins.Features.UIExtension.UIExtensionFeature``1"
$manifest = [ordered]@{
Id = $PluginName
Name = "AD Compare"
Author = "Jess Rogerson"
Url = "https://gitea.hideawaygaming.com.au/jessikitty/disco-ad-compare-plugin"
Version = $version.ToString()
AssemblyPath = "$PluginName.dll"
TypeName = "Disco.Plugins.ADCompare.ADComparePlugin"
ConfigurationHandlerTypeName = "Disco.Plugins.ADCompare.ConfigurationHandler.ADCompareConfigurationHandler"
WebHandlerTypeName = "Disco.Plugins.ADCompare.WebHandler.ADCompareWebHandler"
Features = @(
[ordered]@{
Id = "ADCompareDeviceUI"
Name = "Device Page - AD Compare"
TypeName = "Disco.Plugins.ADCompare.Features.DeviceUIExtension"
PrimaryFeature = $false
CategoryTypeName = $uiExtCategoryTypeName
},
[ordered]@{
Id = "ADCompareUserUI"
Name = "User Page - AD Compare"
TypeName = "Disco.Plugins.ADCompare.Features.UserUIExtension"
PrimaryFeature = $false
CategoryTypeName = $uiExtCategoryTypeName
}
)
}
$manifestJson = $manifest | ConvertTo-Json -Depth 5
$manifestJson | Set-Content (Join-Path $buildOutput "manifest.json") -Encoding UTF8
Write-Host "Manual manifest.json created" -ForegroundColor Green
}
# --- Validate Manifest ---
Write-Host "`n--- Manifest Validation ---" -ForegroundColor Yellow
$manifestPath = Join-Path $buildOutput "manifest.json"
if (-not (Test-Path $manifestPath)) {
Write-Error "manifest.json NOT FOUND in build output! Plugin cannot be loaded by Disco."
exit 1
}
$manifestContent = Get-Content $manifestPath -Raw
$manifestObj = $manifestContent | ConvertFrom-Json
# Basic structure checks
$manifestErrors = @()
$manifestWarnings = @()
if (-not $manifestObj.Id) { $manifestErrors += "Missing 'Id'" }
if (-not $manifestObj.TypeName) { $manifestErrors += "Missing 'TypeName'" }
if (-not $manifestObj.AssemblyPath) { $manifestErrors += "Missing 'AssemblyPath'" }
if (-not $manifestObj.Version) { $manifestWarnings += "Missing 'Version'" }
Write-Host " Id: $($manifestObj.Id)" -ForegroundColor Gray
Write-Host " Version: $($manifestObj.Version)" -ForegroundColor Gray
Write-Host " TypeName: $($manifestObj.TypeName)" -ForegroundColor Gray
if (-not $manifestObj.Features -or $manifestObj.Features.Count -eq 0) {
$manifestErrors += "Features array is EMPTY or MISSING - no UIExtensions will be loaded!"
} else {
Write-Host " Features: $($manifestObj.Features.Count) found" -ForegroundColor Green
# Expected CategoryTypeName (with literal backtick)
$expectedCategory = 'Disco.Services.Plugins.Features.UIExtension.UIExtensionFeature`1'
foreach ($feature in $manifestObj.Features) {
Write-Host "`n Feature: $($feature.Id)" -ForegroundColor Cyan
Write-Host " Name: $($feature.Name)" -ForegroundColor Gray
Write-Host " TypeName: $($feature.TypeName)" -ForegroundColor Gray
Write-Host " CategoryTypeName: $($feature.CategoryTypeName)" -ForegroundColor Gray
Write-Host " PrimaryFeature: $($feature.PrimaryFeature)" -ForegroundColor Gray
if (-not $feature.TypeName) {
$manifestErrors += "Feature '$($feature.Id)' is missing TypeName"
}
if (-not $feature.CategoryTypeName) {
$manifestErrors += "Feature '$($feature.Id)' is missing CategoryTypeName"
} elseif ($feature.CategoryTypeName -ne $expectedCategory) {
$manifestErrors += "Feature '$($feature.Id)' CategoryTypeName MISMATCH!"
Write-Host " Expected: $expectedCategory" -ForegroundColor Red
Write-Host " Got: $($feature.CategoryTypeName)" -ForegroundColor Red
# Char-by-char comparison to identify the exact difference
$expChars = $expectedCategory.ToCharArray()
$actChars = $feature.CategoryTypeName.ToCharArray()
for ($i = 0; $i -lt [Math]::Max($expChars.Length, $actChars.Length); $i++) {
$ec = if ($i -lt $expChars.Length) { $expChars[$i] } else { $null }
$ac = if ($i -lt $actChars.Length) { $actChars[$i] } else { $null }
if ($ec -ne $ac) {
$ecDisplay = if ($ec) { "'$ec' (0x{0:X2})" -f [int][char]$ec } else { '[END]' }
$acDisplay = if ($ac) { "'$ac' (0x{0:X2})" -f [int][char]$ac } else { '[END]' }
Write-Host " First difference at position ${i}: expected $ecDisplay vs got $acDisplay" -ForegroundColor Red
break
}
}
} else {
Write-Host " CategoryTypeName: VALID" -ForegroundColor Green
}
}
}
# Show raw manifest for manual inspection
Write-Host "`n --- Raw manifest.json ---" -ForegroundColor DarkGray
$manifestContent -split "`n" | ForEach-Object {
Write-Host " $_" -ForegroundColor DarkGray
}
Write-Host " --- End manifest.json ---" -ForegroundColor DarkGray
if ($manifestErrors.Count -gt 0) {
Write-Host "`n MANIFEST ERRORS:" -ForegroundColor Red
foreach ($err in $manifestErrors) {
Write-Host " [ERROR] $err" -ForegroundColor Red
}
}
if ($manifestWarnings.Count -gt 0) {
foreach ($warn in $manifestWarnings) {
Write-Host " [WARN] $warn" -ForegroundColor Yellow
}
}
if ($manifestErrors.Count -eq 0) {
Write-Host "`n All manifest checks PASSED" -ForegroundColor Green
} else {
Write-Host "`n Manifest has errors - UIExtensions will NOT render!" -ForegroundColor Red
Write-Host " Fix the errors above before installing the plugin." -ForegroundColor Red
}
# --- Package ---
Write-Host "`nPackaging .discoPlugin file..." -ForegroundColor Yellow
$includeFiles = @("$PluginName.dll", "$PluginName.pdb", "manifest.json")
if (-not $version) {
$version = [System.Reflection.AssemblyName]::GetAssemblyName($pluginDll).Version
}
$packageName = "$PluginName-$($version.ToString()).discoPlugin"
$packagePath = Join-Path $PluginDir $packageName
Get-ChildItem $PluginDir -Filter "*.discoPlugin" | Remove-Item -Force
Get-ChildItem $PluginDir -Filter "*.zip" | Where-Object { $_.Name -like "$PluginName*" } | Remove-Item -Force
$tempPkg = Join-Path $env:TEMP "discoplugin_$([guid]::NewGuid().ToString('N'))"
New-Item -ItemType Directory -Path $tempPkg -Force | Out-Null
$missingFiles = @()
foreach ($f in $includeFiles) {
$src = Join-Path $buildOutput $f
if (Test-Path $src) {
Copy-Item $src $tempPkg -Force
} else {
$missingFiles += $f
}
}
if ($missingFiles.Count -gt 0) {
Write-Warning "Missing files not included in package: $($missingFiles -join ', ')"
}
Write-Host "Files in package:" -ForegroundColor Gray
Get-ChildItem $tempPkg -File | ForEach-Object {
$size = [math]::Round($_.Length / 1KB, 1)
Write-Host " $($_.Name) ($size KB)" -ForegroundColor Gray
}
$zipPath = Join-Path $PluginDir "$PluginName-$($version.ToString()).zip"
Compress-Archive -Path "$tempPkg\*" -DestinationPath $zipPath -Force
Rename-Item $zipPath $packageName -Force
Remove-Item $tempPkg -Recurse -Force
# --- Final Package Verification ---
Write-Host "`n--- Package Verification ---" -ForegroundColor Yellow
try {
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead($packagePath)
$entries = $zip.Entries | Select-Object -ExpandProperty Name
$zip.Dispose()
$requiredEntries = @("$PluginName.dll", "manifest.json")
foreach ($req in $requiredEntries) {
if ($entries -contains $req) {
Write-Host " [OK] $req" -ForegroundColor Green
} else {
Write-Host " [MISSING] $req" -ForegroundColor Red
}
}
# Show any extras
foreach ($entry in $entries) {
if ($entry -notin $requiredEntries) {
Write-Host " [+] $entry" -ForegroundColor Gray
}
}
} catch {
Write-Warning " Could not verify package contents: $($_.Exception.Message)"
}
$packageSize = [math]::Round((Get-Item $packagePath).Length / 1KB, 1)
Write-Host "`n=== Package created: $packagePath ($($packageSize) KB) ===" -ForegroundColor Green
Write-Host "Import into Disco ICT via: Configuration > Plugins > Install Plugin" -ForegroundColor Cyan
# --- Summary ---
$hasIssues = (-not $reflectionOk) -or ($manifestErrors.Count -gt 0)
if ($hasIssues) {
Write-Host "`n=== BUILD COMPLETED WITH WARNINGS ===" -ForegroundColor Yellow
Write-Host "Review the diagnostics above before installing." -ForegroundColor Yellow
Write-Host "If UIExtensions still don't render after install:" -ForegroundColor Yellow
Write-Host " 1. Check Disco logs for feature initialization messages" -ForegroundColor Yellow
Write-Host " 2. View page source and search for 'layout_uiExtensions'" -ForegroundColor Yellow
Write-Host " - Present but empty = ExecuteAction returned Nothing()/null" -ForegroundColor Yellow
Write-Host " - Missing entirely = Feature not registered (Register() not called)" -ForegroundColor Yellow
Write-Host " 3. Check the installed manifest at:" -ForegroundColor Yellow
Write-Host " App_Data\Plugins\$PluginName\manifest.json" -ForegroundColor Yellow
} else {
Write-Host "`n=== BUILD COMPLETED SUCCESSFULLY ===" -ForegroundColor Green
}