|
|
|
@@ -0,0 +1,102 @@
|
|
|
|
|
#nullable enable
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Reflection;
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
using ShrinkContext;
|
|
|
|
|
using ShrinkEventBus;
|
|
|
|
|
using ShrinkNetwork;
|
|
|
|
|
using ShrinkCommand;
|
|
|
|
|
|
|
|
|
|
namespace ShrinkSDK.Inspection
|
|
|
|
|
{
|
|
|
|
|
public sealed class ShrinkInspectionRelation
|
|
|
|
|
{
|
|
|
|
|
public string From = "";
|
|
|
|
|
public string To = "";
|
|
|
|
|
public string Kind = "";
|
|
|
|
|
public string Scope = "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Explicit, on-demand snapshot. Call from the owning host thread while registration/composition is stable.</summary>
|
|
|
|
|
public sealed class ShrinkInspectionSnapshot
|
|
|
|
|
{
|
|
|
|
|
public int FormatVersion = 1;
|
|
|
|
|
public string BuildId = "";
|
|
|
|
|
public string CapturedAtUtc = DateTime.UtcNow.ToString("O");
|
|
|
|
|
public readonly List<string> Coverage = new();
|
|
|
|
|
public readonly List<ShrinkInspectionRelation> Relations = new();
|
|
|
|
|
public readonly Dictionary<string, object> State = new();
|
|
|
|
|
|
|
|
|
|
public static string BuildIdentity(IEnumerable<string> assemblyNames)
|
|
|
|
|
{
|
|
|
|
|
var loaded = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).ToArray();
|
|
|
|
|
var identity = string.Join(";", assemblyNames.Distinct().OrderBy(n => n, StringComparer.Ordinal).Select(name =>
|
|
|
|
|
{
|
|
|
|
|
var candidates = loaded.Where(a => a.GetName().Name == name).ToArray();
|
|
|
|
|
if (candidates.Length != 1) throw new InvalidOperationException("Cannot establish unique runtime assembly identity: " + name);
|
|
|
|
|
return name + ":" + candidates[0].ManifestModule.ModuleVersionId.ToString("N");
|
|
|
|
|
}));
|
|
|
|
|
using var sha = System.Security.Cryptography.SHA256.Create();
|
|
|
|
|
return BitConverter.ToString(sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(identity))).Replace("-", "").ToLowerInvariant();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ShrinkInspectionSnapshot(string buildId) { BuildId = buildId; }
|
|
|
|
|
public string ToJson() => JsonConvert.SerializeObject(this, Formatting.Indented, new Newtonsoft.Json.Converters.StringEnumConverter());
|
|
|
|
|
|
|
|
|
|
public void AddContext(string scope, ShrinkContextRuntime runtime, ShrinkContextLoader? loader = null)
|
|
|
|
|
{
|
|
|
|
|
var snapshot = runtime.CaptureDiagnostic();
|
|
|
|
|
State.Add(scope, new { snapshot.BindingCount, snapshot.Notifications,
|
|
|
|
|
Fibers = snapshot.Fibers.Select(f => new { f.Uid, f.Name, f.Status, f.Retired, f.InTransition, f.Inject, f.Provide, f.Dependencies, f.ErrorType }).ToArray(),
|
|
|
|
|
Transaction = loader?.LastTransaction is { } t ? new { t.Generation, t.Phase, t.CurrentEntryId, t.ErrorType, t.RestoreAttempted, t.PreviousCompositionRestored, t.RestoreErrorType } : null });
|
|
|
|
|
foreach (var fiber in snapshot.Fibers.Where(f => !f.Retired))
|
|
|
|
|
foreach (var dependency in fiber.Dependencies)
|
|
|
|
|
{
|
|
|
|
|
Add("fiber:" + scope + ":" + fiber.Uid, "key:" + dependency.Key, "requires@" + dependency.Realm, scope);
|
|
|
|
|
if (dependency.CurrentProviderUid.HasValue)
|
|
|
|
|
Add("fiber:" + scope + ":" + dependency.CurrentProviderUid, "fiber:" + scope + ":" + fiber.Uid, "provider:" + dependency.Key + "@" + dependency.Realm, scope);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void AddNetwork(string scope, ShrinkNetworkService service)
|
|
|
|
|
{
|
|
|
|
|
State.Add(scope, new { Metrics = service.GetDiagnosticsSnapshot(), Queue = service.DispatchScheduler is ShrinkNetworkDispatchQueue queued ? queued.CaptureDiagnostics() : (service.DispatchScheduler as ShrinkNetworkInlineDispatchScheduler)?.CaptureDiagnostics() });
|
|
|
|
|
foreach (var meta in service.MessageRegistry.Registrations)
|
|
|
|
|
{
|
|
|
|
|
Add("opcode:" + meta.Opcode, "type:" + TypeName(meta.MessageType), "message", scope);
|
|
|
|
|
if (!string.IsNullOrEmpty(meta.Route)) Add("route:" + meta.Route, "type:" + TypeName(meta.MessageType), "message", scope);
|
|
|
|
|
}
|
|
|
|
|
foreach (var pair in service.Router.CaptureBindings()) Add("type:" + TypeName(pair.Key), MethodId(pair.Value), "handler", scope);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void AddEventBus(string scope, IShrinkEventBus bus)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var bindings = EventBus.CaptureBindings(bus);
|
|
|
|
|
State.Add(scope, new { Bus = bus.Key.ToString(), Subscribers = bindings.Count });
|
|
|
|
|
foreach (var binding in bindings) Add("type:" + TypeName(binding.EventType), MethodId(binding.Handler), "subscribes", scope);
|
|
|
|
|
}
|
|
|
|
|
catch (NotSupportedException) { Coverage.Add(scope + ": custom EventBus does not expose bindings."); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void AddCommands(string scope, ShrinkCommandService service)
|
|
|
|
|
{
|
|
|
|
|
State.Add(scope, new { Registrations = service.Commands.Select(c => new { c.Path, c.Aliases, c.Permission, c.SourceKind }).ToArray() });
|
|
|
|
|
foreach (var binding in service.CaptureBindings()) Add("command:" + binding.Path, MethodId(binding.Handler), "command", scope);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Add(string from, string to, string kind, string scope) => Relations.Add(new ShrinkInspectionRelation { From = from, To = to, Kind = kind, Scope = scope });
|
|
|
|
|
private static string TypeName(Type type) => (type.FullName ?? type.Name).Replace('+', '.');
|
|
|
|
|
public static string MethodId(MethodInfo method)
|
|
|
|
|
{
|
|
|
|
|
// Generated wrappers and generic signatures remain runtime-only endpoints rather than guessed source matches.
|
|
|
|
|
if (method.IsGenericMethod || method.DeclaringType?.IsGenericType == true || method.GetParameters().Any(p => p.ParameterType.IsGenericType || p.ParameterType.IsArray || p.ParameterType.IsByRef))
|
|
|
|
|
return "runtime-method:" + method.Module.ModuleVersionId.ToString("N") + ":" + method.MetadataToken;
|
|
|
|
|
var parameters = method.GetParameters();
|
|
|
|
|
return "M:" + TypeName(method.DeclaringType!) + "." + method.Name + (parameters.Length == 0 ? "" : "(" + string.Join(",", parameters.Select(p => TypeName(p.ParameterType))) + ")");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|