Skip to content

Commit 0cd6390

Browse files
authored
Implement dependency loading (#469)
* Update PackedPluginProvider.cs * Update PackedPluginProvider.cs * Load depends properly * We don't need to load the assembly of a dependency. * This might fix depends loading... * Fix * Update PluginManager.cs * Okay dependency loading works * Go through deps.json instead and load up necessary assemblies * Update PackedPluginProvider.cs
1 parent bd371f1 commit 0cd6390

File tree

7 files changed

+161
-64
lines changed

7 files changed

+161
-64
lines changed

Obsidian.API/Obsidian.API.csproj

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
<AdditionalFiles Include="..\Obsidian\Assets\tags.json" Link="Assets\tags.json" />
4545
<AdditionalFiles Include="..\Obsidian\Assets\enums.json" Link="Assets\enums.json" />
4646
<AdditionalFiles Include="..\Obsidian\Assets\item_components.json" Link="Assets\item_components.json" />
47-
47+
4848
<AdditionalFiles Include="..\Obsidian\Assets\Codecs\**\*.json" LinkBase="Assets\Codecs" />
4949
</ItemGroup>
5050

@@ -69,10 +69,18 @@
6969
</ItemGroup>
7070

7171
<ItemGroup>
72-
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.11.0" />
73-
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
74-
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
75-
<PackageReference Include="SharpNoise" Version="0.12.1.1" />
72+
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.11.0">
73+
<ExcludeAssets>runtime</ExcludeAssets>
74+
</PackageReference>
75+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0">
76+
<ExcludeAssets>runtime</ExcludeAssets>
77+
</PackageReference>
78+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0">
79+
<ExcludeAssets>runtime</ExcludeAssets>
80+
</PackageReference>
81+
<PackageReference Include="SharpNoise" Version="0.12.1.1">
82+
<ExcludeAssets>runtime</ExcludeAssets>
83+
</PackageReference>
7684
</ItemGroup>
7785

7886
<ItemGroup>

Obsidian.ConsoleApp/config/server.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
"Logging": {
2525
"LogLevel": {
26-
"Default": "Information",
26+
"Default": "Warning",
2727
"Microsoft": "Warning"
2828
}
2929
}

Obsidian/Commands/Modules/MainCommandModule.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,15 @@ public async Task PluginsAsync()
131131
var info = pluginContainer.Info;
132132

133133
var plugin = new ChatMessage();
134-
var colorByState = pluginContainer.Loaded || pluginContainer.IsReady ? HexColor.Green : HexColor.Red;
134+
var colorByState = pluginContainer.Loaded ? ChatColor.BrightGreen : ChatColor.Red;
135135

136136
plugin.Text = pluginContainer.Info.Name;
137-
plugin.Color = colorByState;
137+
plugin.Color = new HexColor(colorByState.Color);
138138

139139
plugin.HoverEvent = new HoverComponent
140140
{
141141
Action = HoverAction.ShowText,
142-
Contents = new HoverChatContent { ChatMessage = $"{colorByState}{info.Name}{ChatColor.Reset}\nVersion: {colorByState}{info.Version}{ChatColor.Reset}\nAuthor(s): {colorByState}{info.Authors}{ChatColor.Reset}" }
142+
Contents = new HoverChatContent { ChatMessage = $"{colorByState}{info.Name}{ChatColor.Reset}\nVersion: {colorByState}{info.Version}{ChatColor.Reset}\nAuthor(s): {colorByState}{string.Join(", ", info.Authors)}{ChatColor.Reset}" }
143143
};
144144

145145
if (pluginContainer.Info.ProjectUrl != null)

Obsidian/Plugins/PluginContainer.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ public sealed class PluginContainer : IDisposable, IPluginContainer
3131

3232
public required string Source { get; set; }
3333
public required bool ValidSignature { get; init; }
34-
35-
public bool HasDependencies { get; private set; } = true;
36-
public bool IsReady => HasDependencies;
3734
public bool Loaded { get; internal set; }
3835

3936
~PluginContainer()

Obsidian/Plugins/PluginManager.cs

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,7 @@ public PluginManager(IServiceProvider serverProvider, IServer server,
8989

9090
public async Task LoadPluginsAsync()
9191
{
92-
//TODO talk about what format we should support
93-
// get directory surrent dll is in
94-
var acceptedKeysPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "accepted_keys");
92+
var acceptedKeysPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "accepted_keys");
9593
var acceptedKeyFiles = Directory.GetFiles(acceptedKeysPath);
9694

9795
using var rsa = RSA.Create();
@@ -113,20 +111,20 @@ public async Task LoadPluginsAsync()
113111
if (pluginContainer is null)
114112
continue;
115113

116-
foreach (var canLoad in waitingForDepend.Where(x => x.IsDependency(pluginContainer.Info.Id)).ToList())
117-
{
118-
packedPluginProvider.InitializePlugin(canLoad);
119-
120-
//Add dependency to plugin
121-
canLoad.AddDependency(pluginContainer.LoadContext);
114+
if (pluginContainer.Plugin is null)
115+
waitingForDepend.Add(pluginContainer);
116+
}
122117

123-
await this.HandlePluginAsync(canLoad);
118+
foreach (var canLoad in waitingForDepend)
119+
{
120+
packedPluginProvider.InitializePlugin(canLoad);
121+
packedPluginProvider.HandlePlugin(canLoad, canLoad.PluginAssembly);
124122

125-
waitingForDepend.Remove(canLoad);
126-
}
123+
var depends = canLoad.Info.Dependencies.Select(x => x.Id).SelectMany(x => this.Plugins.Where(p => p.Info.Id == x));
124+
foreach (var depend in depends)
125+
canLoad.AddDependency(depend.LoadContext);
127126

128-
if (pluginContainer.Plugin is null)
129-
waitingForDepend.Add(pluginContainer);
127+
await this.HandlePluginAsync(canLoad);
130128
}
131129

132130
DirectoryWatcher.Watch("plugins");
@@ -238,46 +236,62 @@ private void ConfigureInitialServices()
238236

239237
private async ValueTask<PluginContainer> HandlePluginAsync(PluginContainer pluginContainer)
240238
{
241-
//The plugin still hasn't fully loaded. Probably due to it having a hard dependency
239+
// If the plugin is already loaded or staged, skip loading it again
240+
if (pluginContainer.Loaded || stagedPlugins.Contains(pluginContainer))
241+
return pluginContainer;
242+
243+
// The plugin still hasn't fully loaded. Probably due to it having a hard dependency
242244
if (pluginContainer.Plugin is null)
243245
return pluginContainer;
244246

245-
//Inject first wave of services (services initialized by obsidian e.x IServerConfiguration)
247+
// Inject first wave of services (services initialized by Obsidian e.g., IServerConfiguration)
246248
PluginServiceHandler.InjectServices(this.serverProvider, pluginContainer, this.logger);
247249

248-
if (pluginContainer.IsReady)
250+
// Check if the plugin has dependencies that are not loaded yet
251+
var missingDependencies = pluginContainer.Info.Dependencies
252+
.Where(dep => !Plugins.Any(p => p.Info.Id == dep.Id && p.Loaded));
253+
254+
if (missingDependencies.Any())
249255
{
250-
lock (plugins)
256+
// Stage the plugin if dependencies are missing
257+
lock (stagedPlugins)
251258
{
252-
plugins.Add(pluginContainer);
259+
stagedPlugins.Add(pluginContainer);
253260
}
254261

255-
pluginContainer.Plugin.ConfigureServices(this.pluginServiceDescriptors);
256-
pluginContainer.Plugin.ConfigureRegistry(this.pluginRegistry);
262+
logger.LogWarning("Plugin {name} staged, missing dependencies.", pluginContainer.Info.Name);
257263

258-
pluginContainer.Loaded = true;
264+
return pluginContainer;
265+
}
259266

260-
await pluginContainer.Plugin.OnLoadedAsync(this.server);
267+
// Now we can load the plugin since all its dependencies are either loaded or are being loaded
268+
lock (plugins)
269+
{
270+
plugins.Add(pluginContainer);
261271
}
262-
else
272+
273+
pluginContainer.Plugin.ConfigureServices(this.pluginServiceDescriptors);
274+
pluginContainer.Plugin.ConfigureRegistry(this.pluginRegistry);
275+
276+
pluginContainer.Loaded = true;
277+
278+
await pluginContainer.Plugin.OnLoadedAsync(this.server);
279+
280+
// Check and resolve any other plugins that were staged and waiting on dependencies
281+
foreach (var stagedPlugin in stagedPlugins.ToList())
263282
{
264-
lock (stagedPlugins)
265-
{
266-
stagedPlugins.Add(pluginContainer);
267-
}
283+
// Check if all dependencies of this staged plugin are loaded now
284+
var missingDepsForStaged = stagedPlugin.Info.Dependencies
285+
.Where(dep => !Plugins.Any(p => p.Info.Id == dep.Id && p.Loaded));
268286

269-
if (logger != null)
287+
if (!missingDepsForStaged.Any())
270288
{
271-
var stageMessage = new System.Text.StringBuilder(50);
272-
stageMessage.Append($"Plugin {pluginContainer.Info.Name} staged");
273-
if (!pluginContainer.HasDependencies)
274-
stageMessage.Append(", missing dependencies");
275-
276-
logger.LogWarning("{}", stageMessage.ToString());
289+
stagedPlugins.Remove(stagedPlugin);
290+
await HandlePluginAsync(stagedPlugin); // Recurse and attempt to load this staged plugin now
277291
}
278292
}
279293

280-
logger?.LogInformation("Loading finished!");
294+
logger.LogInformation("Loaded {name}.", pluginContainer.Info.Name);
281295

282296
return pluginContainer;
283297
}

Obsidian/Plugins/PluginProviders/PackedPluginProvider.cs

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using Microsoft.Extensions.Logging;
22
using Obsidian.API.Plugins;
3+
using Obsidian.Entities;
34
using Org.BouncyCastle.Crypto;
45
using System.Collections.Frozen;
56
using System.IO;
67
using System.Reflection;
8+
using System.Runtime.Loader;
79
using System.Security.Cryptography;
10+
using System.Text.Json;
811

912
namespace Obsidian.Plugins.PluginProviders;
1013
public sealed class PackedPluginProvider(PluginManager pluginManager, ILogger logger)
@@ -52,12 +55,20 @@ public sealed class PackedPluginProvider(PluginManager pluginManager, ILogger lo
5255
//Can't load until those plugins are loaded
5356
if (partialContainer.Info.Dependencies.Any(x => x.Required && !this.pluginManager.Plugins.Any(d => d.Info.Id == x.Id)))
5457
{
55-
var str = partialContainer.Info.Dependencies.Length > 1 ? "has multiple hard dependencies." :
58+
var str = partialContainer.Info.Dependencies.Length > 1 ? "has multiple hard dependencies." :
5659
$"has a hard dependency on {partialContainer.Info.Dependencies.First().Id}.";
5760
this.logger.LogWarning("{name} {message}. Will Attempt to load after.", partialContainer.Info.Name, str);
5861
return partialContainer;
5962
}
6063

64+
foreach (var depends in partialContainer.Info.Dependencies)
65+
{
66+
var plugin = this.pluginManager.Plugins.FirstOrDefault(x => x.Info.Id == depends.Id);
67+
68+
partialContainer.AddDependency(plugin.LoadContext);
69+
this.logger.LogInformation("Added {depends} as a dependency for {name}", plugin.Info.Name, partialContainer.Info.Name);
70+
}
71+
6172
var mainAssembly = this.InitializePlugin(partialContainer);
6273

6374
return HandlePlugin(partialContainer, mainAssembly);
@@ -221,29 +232,98 @@ private List<string> ProcessEntries(PluginContainer pluginContainer)
221232
var pluginAssembly = pluginContainer.LoadContext.Name;
222233

223234
var libsWithSymbols = new List<string>();
224-
foreach (var (_, entry) in pluginContainer.FileEntries)
235+
236+
using var dependenciesData = new MemoryStream(pluginContainer.GetFileData($"{pluginAssembly}.deps.json"));
237+
var dependencies = JsonSerializer.Deserialize<DotNetDeps>(dependenciesData, JsonSerializerOptions.Web);
238+
var targets = dependencies.Targets[dependencies.RuntimeTarget.Name].Deserialize<Dictionary<string, DotNetTarget>>(JsonSerializerOptions.Web);
239+
240+
foreach (var (key, target) in targets)
225241
{
226-
var actualBytes = entry.GetData();
242+
var deps = target.Dependencies;
243+
var runtimes = target.Runtime;
227244

228-
var name = Path.GetFileNameWithoutExtension(entry.Name);
229-
//Don't load this assembly wait
230-
if (name == pluginAssembly)
245+
if (runtimes == null)//We don't care if its null
231246
continue;
232247

233-
//TODO LOAD OTHER FILES SOMEWHERE
234-
if (entry.Name.EndsWith(".dll"))
248+
foreach (var (dll, runtimeElement) in runtimes)
235249
{
236-
if (pluginContainer.FileEntries.ContainsKey(entry.Name.Replace(".dll", ".pdb")))
250+
var sanitizedDll = dll;
251+
var split = dll.Split('/');
252+
if (split.Length > 1)
253+
sanitizedDll = split.Last();
254+
255+
var name = sanitizedDll[..sanitizedDll.IndexOf(".dll")];
256+
257+
if (name == pluginAssembly || runtimeElement.ToString() == "{}")
258+
continue;
259+
260+
var runtime = runtimeElement.Deserialize<DependencyRuntime>(JsonSerializerOptions.Web);
261+
var assemblyName = new AssemblyName
262+
{
263+
Name = name,
264+
Version = new(runtime.AssemblyVersion),
265+
};
266+
267+
var depends = pluginContainer.Info.Dependencies.Select(x => x.Id)
268+
.SelectMany(x => this.pluginManager.Plugins.Where(p => p.Info.Id == x));
269+
270+
//TODO allow users to define what version of a dependency is allowed/valid e.g version: >1.0.0 or >=1.0.0 or =1.0.0.
271+
var dependency = depends.FirstOrDefault(x => x.PluginAssembly.GetName().Name == name && x.Info.Version == assemblyName.Version);
272+
273+
//we don't need to load this into the context.
274+
if (dependency != null)
275+
continue;
276+
277+
if (pluginContainer.FileEntries.ContainsKey($"${name}.pdb"))
237278
{
238279
//Library has debug symbols load in last
239-
libsWithSymbols.Add(entry.Name.Replace(".dll", ".pdb"));
280+
libsWithSymbols.Add(name);
240281
continue;
241282
}
242283

243-
pluginContainer.LoadContext.LoadAssembly(actualBytes);
284+
try
285+
{
286+
//Check to see if this assembly already exists in the shared context.
287+
var sharedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName);
288+
if (sharedAssembly != null)
289+
continue;
290+
}
291+
catch { }
292+
293+
var data = pluginContainer.GetFileData(sanitizedDll);
294+
pluginContainer.LoadContext.LoadAssembly(data);
244295
}
245296
}
246297

247298
return libsWithSymbols;
248299
}
249300
}
301+
302+
303+
public readonly struct DotNetDeps
304+
{
305+
public required RuntimeTarget RuntimeTarget { get; init; }
306+
307+
public required Dictionary<string, JsonElement> Targets { get; init; }
308+
}
309+
310+
public readonly struct DotNetTarget
311+
{
312+
public Dictionary<string, string>? Dependencies { get; init; }
313+
314+
public Dictionary<string, JsonElement>? Runtime { get; init; }
315+
}
316+
317+
public readonly struct DependencyRuntime
318+
{
319+
public required string AssemblyVersion { get; init; }
320+
321+
public required string FileVersion { get; init; }
322+
}
323+
324+
public readonly struct RuntimeTarget
325+
{
326+
public required string Name { get; init; }
327+
328+
public required string Signature { get; init; }
329+
}

Obsidian/Plugins/PluginProviders/PluginLoadContext.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,15 @@ public sealed class PluginLoadContext(string name) : AssemblyLoadContext(name: n
1313
using var mainStream = new MemoryStream(mainBytes, false);
1414
using var pbdStream = pbdBytes != null ? new MemoryStream(pbdBytes, false) : null;
1515

16-
var asm = this.LoadFromStream(mainStream, pbdStream);
17-
18-
return asm;
16+
return this.LoadFromStream(mainStream, pbdStream);
1917
}
2018

2119
public void AddDependency(PluginLoadContext context) => this.Dependencies.Add(context);
2220

2321
protected override Assembly? Load(AssemblyName assemblyName)
2422
{
25-
var assembly = this.Assemblies.FirstOrDefault(x => x.GetName() == assemblyName);
23+
var assembly = this.Assemblies.FirstOrDefault(x => x.FullName == assemblyName.FullName);
2624

27-
return assembly ?? this.Dependencies.Select(x => x.Load(assemblyName)).FirstOrDefault(x => x != null);
25+
return this.Dependencies.Select(x => x.Load(assemblyName)).FirstOrDefault(x => x != null) ?? assembly;
2826
}
2927
}

0 commit comments

Comments
 (0)