Skip to content
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions src/Core/ExtensionsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,36 @@
using SwarmUI.Utils;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;

namespace SwarmUI.Core;

public class SwarmExtensionLoadContext(string name, string extensionDir) : AssemblyLoadContext(name, isCollectible: false)
{
/// <summary>Host wins, then we probe the extension's folder for private deps.</summary>
protected override Assembly Load(AssemblyName name)
{
string candidate = Path.Combine(extensionDir, name.Name + ".dll");
// Host wins to keep type identity intact. If the extension also shipped its own copy, that's almost always a csproj misconfig (missing Private=false), so warn.
try
{
Default.LoadFromAssemblyName(name);
if (File.Exists(candidate))
{
Logs.Warning($"Extension {Name} ships {name.Name}.dll but host already has it; using host copy. Set Private=false on that reference.");
}
return null;
}
catch (FileNotFoundException) { }
if (!File.Exists(candidate))
{
return null;
}
Logs.Debug($"Extension {Name} loading private dep {name.Name} from {candidate}");
return LoadFromAssemblyPath(candidate);
}
}

public class ExtensionsManager
{
/// <summary>All extensions currently loaded.</summary>
Expand Down Expand Up @@ -45,6 +72,15 @@ public static HtmlString HtmlTags(string[] tags)
/// <summary>List of known online available extensions.</summary>
public List<ExtensionInfo> KnownExtensions = [];

/// <summary>Per-extension load contexts, keyed by extension DLL name. Each extension gets its own <see cref="SwarmExtensionLoadContext"/> so private dependencies stay isolated from other extensions.</summary>
public ConcurrentDictionary<string, SwarmExtensionLoadContext> ExtensionLoadContexts = new();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a retained dict on its own? It doesn't look like there's any reuse, just a single grab on load

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair critique. I've reduced this further.


private Assembly LoadInExtensionContext(string dllName, string targetPath)
{
SwarmExtensionLoadContext ctx = ExtensionLoadContexts.GetOrAdd(dllName, n => new SwarmExtensionLoadContext(n, Path.GetDirectoryName(targetPath)));
return ctx.LoadFromAssemblyPath(targetPath);
}

public static string ReferenceCsproj =
"""
<Project Sdk="Microsoft.NET.Sdk.Web">
Expand Down Expand Up @@ -181,7 +217,7 @@ public async Task<Assembly> BuildExtension(string folder, string projFile)
if (File.Exists(target) && !Program.IsDevMode)
{
Logs.Debug($"Don't need to rebuild extension {projFile}, already built.");
return Assembly.LoadFile(Path.GetFullPath(target));
return LoadInExtensionContext(dllName, Path.GetFullPath(target));
}
Logs.Debug($"Building extension project: {projFile}...");
string buildParam = $"-p:BaseIntermediateOutputPath={Path.GetFullPath($"./src/obj/extensions/{dllName}/")};TargetName={dllName}-{hash}";
Expand All @@ -195,7 +231,7 @@ public async Task<Assembly> BuildExtension(string folder, string projFile)
{
Logs.Debug($"Successful build output for extension project {projFile}:\n{output}");
}
return Assembly.LoadFile(Path.GetFullPath(target));
return LoadInExtensionContext(dllName, Path.GetFullPath(target));
}

public void PrepExtension(Type extType, bool isCore, string[] possible)
Expand Down
Loading