Client LuaCsForBarotrauma
MemoryFileAssemblyContextLoader.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Collections.Immutable;
4 using System.Diagnostics.CodeAnalysis;
5 using System.IO;
6 using System.Linq;
7 using System.Reflection;
8 using System.Runtime.CompilerServices;
9 using System.Runtime.Loader;
10 using Microsoft.CodeAnalysis;
11 using Microsoft.CodeAnalysis.CSharp;
12 // ReSharper disable ConditionIsAlwaysTrueOrFalse
13 
14 [assembly: InternalsVisibleTo("CompiledAssembly")]
15 
16 namespace Barotrauma;
17 
23 public class MemoryFileAssemblyContextLoader : AssemblyLoadContext
24 {
25  // public
26  public string FriendlyName { get; set; }
27  // ReSharper disable MemberCanBePrivate.Global
28  public Assembly CompiledAssembly { get; private set; }
29  public byte[] CompiledAssemblyImage { get; private set; }
30  // ReSharper restore MemberCanBePrivate.Global
31  // internal
32  private readonly Dictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new(); // path-folder, resolver
33  protected bool IsResolving; //this is to avoid circular dependency lookup.
34  private AssemblyManager _assemblyManager;
35  public bool IsTemplateMode { get; set; }
36  public bool IsDisposed { get; private set; }
37 
38  public MemoryFileAssemblyContextLoader(AssemblyManager assemblyManager) : base(isCollectible: true)
39  {
40  this._assemblyManager = assemblyManager;
41  this.IsDisposed = false;
42  base.Unloading += OnUnload;
43  }
44 
45 
50  public AssemblyLoadingSuccessState LoadFromFiles([NotNull] IEnumerable<string> assemblyFilePaths)
51  {
52  if (assemblyFilePaths is null)
53  throw new ArgumentNullException(
54  $"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(LoadFromFiles)}() | The supplied filepath list is null.");
55 
56  foreach (string filepath in assemblyFilePaths)
57  {
58  // path verification
59  if (filepath.IsNullOrWhiteSpace())
60  continue;
61  string sanitizedFilePath = System.IO.Path.GetFullPath(filepath.CleanUpPath());
62  string directoryKey = System.IO.Path.GetDirectoryName(sanitizedFilePath);
63 
64  if (directoryKey is null)
65  return AssemblyLoadingSuccessState.BadFilePath;
66 
67  // setup dep resolver if not available
68  if (!_dependencyResolvers.ContainsKey(directoryKey) || _dependencyResolvers[directoryKey] is null)
69  {
70  _dependencyResolvers[directoryKey] = new AssemblyDependencyResolver(sanitizedFilePath); // supply the first assembly to be loaded
71  }
72 
73  // try loading the assemblies
74  try
75  {
76  LoadFromAssemblyPath(sanitizedFilePath);
77  }
78  // on fail of any we're done because we assume that loaded files are related. This ACL needs to be unloaded and collected.
79  catch (ArgumentNullException ane)
80  {
81  ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {ane.Message} | {ane.StackTrace}");
82  return AssemblyLoadingSuccessState.BadFilePath;
83  }
84  catch (ArgumentException ae)
85  {
86  ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {ae.Message} | {ae.StackTrace}");
87  return AssemblyLoadingSuccessState.BadFilePath;
88  }
89  catch (FileLoadException fle)
90  {
91  ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {fle.Message} | {fle.StackTrace}");
92  return AssemblyLoadingSuccessState.CannotLoadFile;
93  }
94  catch (FileNotFoundException fnfe)
95  {
96  ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {fnfe.Message} | {fnfe.StackTrace}");
97  return AssemblyLoadingSuccessState.NoAssemblyFound;
98  }
99  catch (BadImageFormatException bife)
100  {
101  ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {bife.Message} | {bife.StackTrace}");
102  return AssemblyLoadingSuccessState.InvalidAssembly;
103  }
104  catch (Exception e)
105  {
106 #if SERVER
107  LuaCsLogger.LogError($"Unable to load dependency assembly file at {filepath.CleanUpPath()} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}");
108 #elif CLIENT
109  LuaCsLogger.ShowErrorOverlay($"Unable to load dependency assembly file at {filepath} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}");
110 #endif
111  return AssemblyLoadingSuccessState.ACLLoadFailure;
112  }
113  }
114 
115  return AssemblyLoadingSuccessState.Success;
116  }
117 
118 
135  public AssemblyLoadingSuccessState CompileAndLoadScriptAssembly(
136  [NotNull] string assemblyName,
137  [NotNull] IEnumerable<SyntaxTree> syntaxTrees,
138  IEnumerable<MetadataReference> externMetadataReferences,
139  [NotNull] CSharpCompilationOptions compilationOptions,
140  out string compilationMessages,
141  IEnumerable<Assembly> externFileAssemblyReferences = null)
142  {
143  compilationMessages = "";
144 
145  if (this.CompiledAssembly is not null)
146  {
147  return AssemblyLoadingSuccessState.AlreadyLoaded;
148  }
149 
150  var externAssemblyRefs = externFileAssemblyReferences is not null ? externFileAssemblyReferences.ToImmutableList() : ImmutableList<Assembly>.Empty;
151  var externAssemblyNames = externAssemblyRefs.Any() ? externAssemblyRefs
152  .Where(a => a.FullName is not null)
153  .Select(a => a.FullName).ToImmutableHashSet()
154  : ImmutableHashSet<string>.Empty;
155 
156  // verifications
157  if (assemblyName.IsNullOrWhiteSpace())
158  throw new ArgumentNullException(
159  $"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied assembly name is null!");
160 
161  if (syntaxTrees is null)
162  throw new ArgumentNullException(
163  $"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied syntax tree is null!");
164 
165  // add external references
166  List<MetadataReference> metadataReferences = new();
167  if (externMetadataReferences is not null)
168  metadataReferences.AddRange(externMetadataReferences);
169 
170  // build metadata refs from default where not an in-memory compiled assembly and not the same assembly as supplied.
171  metadataReferences.AddRange(AssemblyLoadContext.Default.Assemblies
172  .Where(a =>
173  {
174  if (a.IsDynamic || string.IsNullOrWhiteSpace(a.Location) || a.Location.Contains("xunit"))
175  return false;
176  if (a.FullName is null)
177  return true;
178  return !externAssemblyNames.Contains(a.FullName); // exclude duplicates
179  })
180  .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
181  .Union(externAssemblyRefs // add custom supplied assemblies
182  .Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit")))
183  .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
184  ).ToList());
185 
186  ImmutableList<AssemblyManager.LoadedACL> loadedAcls = _assemblyManager.GetAllLoadedACLs().ToImmutableList();
187  if (loadedAcls.Any())
188  {
189  // build metadata refs from ACL assemblies from files/disk.
190  foreach (AssemblyManager.LoadedACL loadedAcl in loadedAcls)
191  {
192  if(loadedAcl?.Acl is null || loadedAcl.Acl.IsTemplateMode || loadedAcl.Acl.IsDisposed)
193  continue;
194  metadataReferences.AddRange(loadedAcl.Acl.Assemblies
195  .Where(a =>
196  {
197  if (a.IsDynamic || string.IsNullOrWhiteSpace(a.Location) || a.Location.Contains("xunit"))
198  return false;
199  if (a.FullName is null)
200  return true;
201  return !externAssemblyNames.Contains(a.FullName); // exclude duplicates
202  })
203  .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
204  .Union(externAssemblyRefs // add custom supplied assemblies
205  .Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit")))
206  .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
207  ).ToList());
208  }
209 
210  // build metadata refs from in-memory images
211  foreach (var loadedAcl in loadedAcls)
212  {
213  if (loadedAcl?.Acl?.CompiledAssemblyImage is null || loadedAcl.Acl.CompiledAssemblyImage.Length == 0)
214  continue;
215  metadataReferences.Add(MetadataReference.CreateFromImage(loadedAcl.Acl.CompiledAssemblyImage));
216  }
217  }
218 
219  // Change inaccessible options to allow public access to restricted members
220  var topLevelBinderFlagsProperty = typeof(CSharpCompilationOptions).GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic);
221  topLevelBinderFlagsProperty?.SetValue(compilationOptions, (uint)1 << 22);
222 
223  // begin compilation
224  using var memoryCompilation = new MemoryStream();
225  // compile, emit
226  var result = CSharpCompilation.Create(assemblyName, syntaxTrees, metadataReferences, compilationOptions).Emit(memoryCompilation);
227  // check for errors
228  if (!result.Success)
229  {
230  IEnumerable<Diagnostic> failures = result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error);
231  foreach (Diagnostic diagnostic in failures)
232  {
233  compilationMessages += $"\n{diagnostic}";
234  }
235 
236  return AssemblyLoadingSuccessState.InvalidAssembly;
237  }
238 
239  // read compiled assembly from memory stream into an in-memory assembly & image
240  memoryCompilation.Seek(0, SeekOrigin.Begin); // reset
241  try
242  {
243  CompiledAssembly = LoadFromStream(memoryCompilation);
244  CompiledAssemblyImage = memoryCompilation.ToArray();
245  }
246  catch (Exception e)
247  {
248 #if SERVER
249  LuaCsLogger.LogError($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}");
250 #elif CLIENT
251  LuaCsLogger.ShowErrorOverlay($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}");
252 #endif
253  return AssemblyLoadingSuccessState.CannotLoadFromStream;
254  }
255 
256  return AssemblyLoadingSuccessState.Success;
257  }
258 
259  [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")]
260  protected override Assembly Load(AssemblyName assemblyName)
261  {
262  if (IsResolving)
263  return null; //circular resolution fast exit.
264 
265  try
266  {
267  IsResolving = true;
268 
269  // resolve self collection
270  Assembly ass = this.Assemblies.FirstOrDefault(a =>
271  a.FullName is not null && a.FullName.Equals(assemblyName.FullName), null);
272  if (ass is not null)
273  return ass;
274 
275  // resolve to local folders
276  foreach (KeyValuePair<string,AssemblyDependencyResolver> pair in _dependencyResolvers)
277  {
278  var asspath = pair.Value.ResolveAssemblyToPath(assemblyName);
279  if (asspath is null)
280  continue;
281  ass = LoadFromAssemblyPath(asspath);
282  // ReSharper disable once ConditionIsAlwaysTrueOrFalse
283  if (ass is not null)
284  return ass;
285  }
286 
287  //try resolve against other loaded alcs
288  ImmutableList<AssemblyManager.LoadedACL> list;
289  try
290  {
291  list = _assemblyManager.UnsafeGetAllLoadedACLs();
292  }
293  catch
294  {
295  list = ImmutableList<AssemblyManager.LoadedACL>.Empty;
296  }
297 
298  if (!list.IsEmpty)
299  {
300  foreach (var loadedAcL in list)
301  {
302  if (loadedAcL.Acl is null || loadedAcL.Acl.IsTemplateMode || loadedAcL.Acl.IsDisposed)
303  continue;
304 
305  try
306  {
307  ass = loadedAcL.Acl.LoadFromAssemblyName(assemblyName);
308  if (ass is not null)
309  return ass;
310  }
311  catch
312  {
313  // LoadFromAssemblyName throws, no need to propagate
314  }
315  }
316  }
317 
318  ass = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName);
319  if (ass is not null)
320  return ass;
321  }
322  finally
323  {
324  IsResolving = false;
325  }
326 
327  return null;
328  }
329 
330 
331  private void OnUnload(AssemblyLoadContext alc)
332  {
333  CompiledAssembly = null;
334  CompiledAssemblyImage = null;
335  _dependencyResolvers.Clear();
336  _assemblyManager = null;
337  base.Unloading -= OnUnload;
338  this.IsDisposed = true;
339  }
340 }
MemoryFileAssemblyContextLoader Acl
Provides functionality for the loading, unloading and management of plugins implementing IAssemblyPlu...
IEnumerable< LoadedACL > GetAllLoadedACLs()
Returns a list of all loaded ACLs. WARNING: References to these ACLs outside of the AssemblyManager s...
AssemblyLoadContext to compile from syntax trees in memory and to load from disk/file....
MemoryFileAssemblyContextLoader(AssemblyManager assemblyManager)
AssemblyLoadingSuccessState CompileAndLoadScriptAssembly([NotNull] string assemblyName, [NotNull] IEnumerable< SyntaxTree > syntaxTrees, IEnumerable< MetadataReference > externMetadataReferences, [NotNull] CSharpCompilationOptions compilationOptions, out string compilationMessages, IEnumerable< Assembly > externFileAssemblyReferences=null)
Compiles the supplied syntaxtrees and options into an in-memory assembly image. Builds metadata from ...
override Assembly Load(AssemblyName assemblyName)
AssemblyLoadingSuccessState LoadFromFiles([NotNull] IEnumerable< string > assemblyFilePaths)
Try to load the list of disk-file assemblies.