feature: first
All checks were successful
Build / Compute Version (push) Successful in 4s
Build / Build Nuget package (CringeBootstrap.Abstractions) (push) Successful in 2m47s
Build / Build Nuget package (CringePlugins) (push) Successful in 5m31s
Build / Build Nuget package (NuGet) (push) Successful in 6m2s
Build / Build Nuget package (SharedCringe) (push) Successful in 7m25s
Build / Build Launcher (push) Successful in 9m11s

This commit is contained in:
zznty
2022-10-28 01:58:54 +07:00
commit aa979e9519
81 changed files with 6162 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
using System.Reflection;
using System.Runtime.Loader;
using CringeBootstrap.Abstractions;
using SharedCringe.Loader;
namespace CringePlugins.Loader;
internal class PluginAssemblyLoadContext : DerivedAssemblyLoadContext
{
private readonly string _entrypointPath;
private readonly AssemblyDependencyResolver _dependencyResolver;
private Assembly? _assembly;
internal PluginAssemblyLoadContext(ICoreLoadContext parentContext, string entrypointPath) : base(parentContext, $"Plugin Context {Path.GetFileNameWithoutExtension(entrypointPath)}")
{
_entrypointPath = entrypointPath;
_dependencyResolver = new(entrypointPath);
}
public Assembly LoadEntrypoint() => _assembly ??= LoadFromAssemblyPath(_entrypointPath);
protected override Assembly? Load(AssemblyName assemblyName)
{
if (_dependencyResolver.ResolveAssemblyToPath(assemblyName) is { } path)
return LoadFromAssemblyPath(path);
return base.Load(assemblyName);
}
protected override nint LoadUnmanagedDll(string unmanagedDllName)
{
if (_dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName) is { } path)
return LoadUnmanagedDllFromPath(path);
return base.LoadUnmanagedDll(unmanagedDllName);
}
}

View File

@@ -0,0 +1,55 @@
using System.Runtime.Loader;
using CringeBootstrap.Abstractions;
using CringePlugins.Utils;
using VRage.Plugins;
namespace CringePlugins.Loader;
internal sealed class PluginInstance
{
private readonly string _entrypointPath;
private PluginAssemblyLoadContext? _context;
private IPlugin? _instance;
private IHandleInputPlugin? _handleInputInstance;
public PluginMetadata Metadata { get; }
public PluginInstance(PluginMetadata metadata, string entrypointPath)
{
_entrypointPath = entrypointPath;
Metadata = metadata;
}
public PluginInstance(string entrypointPath) : this(PluginMetadata.ReadFromEntrypoint(entrypointPath), entrypointPath)
{
}
public void Instantiate()
{
if (AssemblyLoadContext.GetLoadContext(typeof(PluginInstance).Assembly) is not ICoreLoadContext parentContext)
throw new NotSupportedException("Plugin instantiation is not supported in this context");
_context = new PluginAssemblyLoadContext(parentContext, _entrypointPath);
var entrypoint = _context.LoadEntrypoint();
var plugins = IntrospectionContext.Global.CollectDerivedTypes<IPlugin>(entrypoint.GetMainModule()).ToArray();
if (plugins.Length == 0)
throw new InvalidOperationException("Entrypoint does not contain any plugins");
if (plugins.Length > 1)
throw new InvalidOperationException("Entrypoint contains multiple plugins");
_instance = (IPlugin) Activator.CreateInstance(plugins[0])!;
_handleInputInstance = _instance as IHandleInputPlugin;
}
public void RegisterLifetime()
{
if (_instance is null)
throw new InvalidOperationException("Must call Instantiate first");
MyPlugins.m_plugins.Add(_instance);
if (_handleInputInstance is not null)
MyPlugins.m_handleInputPlugins.Add(_handleInputInstance);
}
}

View File

@@ -0,0 +1,21 @@
using System.Reflection;
using dnlib.DotNet;
namespace CringePlugins.Loader;
public record PluginMetadata(string Name, Version Version)
{
public static PluginMetadata ReadFromEntrypoint(string entrypointPath)
{
var module = ModuleDefMD.Load(entrypointPath);
var titleAttribute = module.CustomAttributes.Find(typeof(AssemblyTitleAttribute).FullName);
var versionAttribute = module.CustomAttributes.Find(typeof(AssemblyVersionAttribute).FullName);
var name = titleAttribute?.ConstructorArguments[0].Value as string ?? module.FullName;
if (!Version.TryParse(versionAttribute?.ConstructorArguments[0].Value as string ?? "0.0.0.0", out var version))
version = new();
return new(name, version);
}
}

View File

@@ -0,0 +1,142 @@
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using System.Text.Json;
using CringePlugins.Config;
using CringePlugins.Resolver;
using CringePlugins.Splash;
using NLog;
using NuGet;
using NuGet.Deps;
using NuGet.Frameworks;
using NuGet.Versioning;
namespace CringePlugins.Loader;
public class PluginsLifetime : ILoadingStage
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
public string Name => "Loading Plugins";
private ImmutableArray<PluginInstance> _plugins = [];
private readonly DirectoryInfo _dir = Directory.CreateDirectory(Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CringeLauncher"));
private readonly NuGetFramework _runtimeFramework = NuGetFramework.ParseFolder("net8.0-windows10.0.19041.0");
public async ValueTask Load(ISplashProgress progress)
{
progress.DefineStepsCount(6);
progress.Report("Discovering local plugins");
DiscoverLocalPlugins(_dir.CreateSubdirectory("plugins"));
progress.Report("Loading config");
PackagesConfig? packagesConfig = null;
var configPath = Path.Join(_dir.FullName, "packages.json");
if (File.Exists(configPath))
await using (var stream = File.OpenRead(configPath))
packagesConfig = JsonSerializer.Deserialize<PackagesConfig>(stream, NuGetClient.SerializerOptions)!;
if (packagesConfig == null)
{
packagesConfig = PackagesConfig.Default;
await using var stream = File.Create(configPath);
await JsonSerializer.SerializeAsync(stream, packagesConfig, NuGetClient.SerializerOptions);
}
progress.Report("Resolving packages");
var sourceMapping = new PackageSourceMapping(packagesConfig.Sources);
var resolver = new PackageResolver(_runtimeFramework, packagesConfig.Packages, sourceMapping);
var packages = await resolver.ResolveAsync();
progress.Report("Downloading packages");
var cachedPackages = await resolver.DownloadPackagesAsync(_dir.CreateSubdirectory("cache"), packages, progress);
progress.Report("Loading plugins");
await LoadPlugins(cachedPackages, sourceMapping);
progress.Report("Registering plugins");
RegisterLifetime();
}
private void RegisterLifetime()
{
foreach (var instance in _plugins)
{
try
{
instance.Instantiate();
instance.RegisterLifetime();
}
catch (Exception e)
{
Log.Error(e, "Failed to instantiate plugin {Plugin}", instance.Metadata);
}
}
}
private async Task LoadPlugins(IReadOnlySet<CachedPackage> packages, PackageSourceMapping sourceMapping)
{
var plugins = _plugins.ToBuilder();
var packageVersions = BuiltInPackages.GetPackages(_runtimeFramework)
.ToImmutableDictionary(b => b.Package.Id, b => b.Package.Version,
StringComparer.OrdinalIgnoreCase);
packageVersions = packageVersions.AddRange(packages.Select(b =>
new KeyValuePair<string, NuGetVersion>(b.Package.Id, b.Package.Version)));
var manifestBuilder = new DependencyManifestBuilder(_dir.CreateSubdirectory("cache"), sourceMapping,
dependency => packageVersions.TryGetValue(dependency.Id, out var version) && version.Major != 99
? version
: dependency.Range.MinVersion ?? dependency.Range.MaxVersion);
foreach (var package in packages)
{
var dir = Path.Join(package.Directory.FullName, "lib", package.ResolvedFramework.GetShortFolderName());
await using (var stream = File.Create(Path.Join(dir, $"{package.Package.Id}.deps.json")))
await manifestBuilder.WriteDependencyManifestAsync(stream, package.Entry, _runtimeFramework);
LoadComponent(plugins, Path.Join(dir, $"{package.Package.Id}.dll"));
}
_plugins = plugins.ToImmutable();
}
private void DiscoverLocalPlugins(DirectoryInfo dir)
{
var plugins = ImmutableArray<PluginInstance>.Empty.ToBuilder();
foreach (var directory in dir.EnumerateDirectories())
{
var files = directory.GetFiles("*.deps.json");
if (files.Length != 1) continue;
var path = files[0].FullName[..^".deps.json".Length] + ".dll";
LoadComponent(plugins, path);
}
_plugins = plugins.ToImmutable();
}
private static void LoadComponent(ImmutableArray<PluginInstance>.Builder plugins, string path)
{
try
{
plugins.Add(new PluginInstance(path));
}
catch (Exception e)
{
Log.Error(e, "Failed to load plugin {PluginPath}", path);
}
}
}