Skip to content

Commit aecc8d6

Browse files
committed
Implemented full assembly isolation support at API level.
1 parent d8f58a0 commit aecc8d6

12 files changed

+265
-38
lines changed

src/Dotnet.Script.Core/Commands/ExecuteCodeCommand.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ public async Task<TReturn> Execute<TReturn>(ExecuteCodeCommandOptions options)
2222
{
2323
var sourceText = SourceText.From(options.Code);
2424
var context = new ScriptContext(sourceText, options.WorkingDirectory ?? Directory.GetCurrentDirectory(), options.Arguments, null, options.OptimizationLevel, ScriptMode.Eval, options.PackageSources);
25-
var compiler = GetScriptCompiler(!options.NoCache, _logFactory);
25+
var compiler = new ScriptCompiler(_logFactory, !options.NoCache)
26+
{
27+
#if NETCOREAPP
28+
AssemblyLoadContext = options.AssemblyLoadContext
29+
#endif
30+
};
2631
var runner = new ScriptRunner(compiler, _logFactory, _scriptConsole)
2732
{
2833
#if NETCOREAPP
@@ -31,11 +36,5 @@ public async Task<TReturn> Execute<TReturn>(ExecuteCodeCommandOptions options)
3136
};
3237
return await runner.Execute<TReturn>(context);
3338
}
34-
35-
private static ScriptCompiler GetScriptCompiler(bool useRestoreCache, LogFactory logFactory)
36-
{
37-
var compiler = new ScriptCompiler(logFactory, useRestoreCache);
38-
return compiler;
39-
}
4039
}
4140
}

src/Dotnet.Script.Core/Commands/ExecuteInteractiveCommand.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ public ExecuteInteractiveCommand(ScriptConsole scriptConsole, LogFactory logFact
1818

1919
public async Task<int> Execute(ExecuteInteractiveCommandOptions options)
2020
{
21-
var compiler = new ScriptCompiler(_logFactory, useRestoreCache: false);
21+
var compiler = new ScriptCompiler(_logFactory, useRestoreCache: false)
22+
{
23+
#if NETCOREAPP
24+
AssemblyLoadContext = options.AssemblyLoadContext
25+
#endif
26+
};
2227
var runner = new InteractiveRunner(compiler, _logFactory, _scriptConsole, options.PackageSources);
2328

2429
if (options.ScriptFile == null)
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
using Microsoft.CodeAnalysis;
1+
#if NETCOREAPP
2+
using System.Runtime.Loader;
3+
#endif
24

35
namespace Dotnet.Script.Core.Commands
46
{
57
public class ExecuteInteractiveCommandOptions
68
{
7-
public ExecuteInteractiveCommandOptions(ScriptFile scriptFile, string[] arguments ,string[] packageSources)
9+
public ExecuteInteractiveCommandOptions(ScriptFile scriptFile, string[] arguments, string[] packageSources)
810
{
911
ScriptFile = scriptFile;
1012
Arguments = arguments;
@@ -14,5 +16,14 @@ public ExecuteInteractiveCommandOptions(ScriptFile scriptFile, string[] argument
1416
public ScriptFile ScriptFile { get; }
1517
public string[] Arguments { get; }
1618
public string[] PackageSources { get; }
19+
20+
#if NETCOREAPP
21+
#nullable enable
22+
/// <summary>
23+
/// Gets or sets a custom assembly load context to use for script execution.
24+
/// </summary>
25+
public AssemblyLoadContext? AssemblyLoadContext { get; init; }
26+
#nullable restore
27+
#endif
1728
}
1829
}

src/Dotnet.Script.Core/Commands/ExecuteLibraryCommand.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ public async Task<TReturn> Execute<TReturn>(ExecuteLibraryCommandOptions options
2525
}
2626

2727
var absoluteFilePath = options.LibraryPath.GetRootedPath();
28-
var compiler = GetScriptCompiler(!options.NoCache, _logFactory);
28+
var compiler = new ScriptCompiler(_logFactory, !options.NoCache)
29+
{
30+
#if NETCOREAPP
31+
AssemblyLoadContext = options.AssemblyLoadContext
32+
#endif
33+
};
2934
var runner = new ScriptRunner(compiler, _logFactory, _scriptConsole)
3035
{
3136
#if NETCOREAPP
@@ -35,11 +40,5 @@ public async Task<TReturn> Execute<TReturn>(ExecuteLibraryCommandOptions options
3540
var result = await runner.Execute<TReturn>(absoluteFilePath, options.Arguments);
3641
return result;
3742
}
38-
39-
private static ScriptCompiler GetScriptCompiler(bool useRestoreCache, LogFactory logFactory)
40-
{
41-
var compiler = new ScriptCompiler(logFactory, useRestoreCache);
42-
return compiler;
43-
}
4443
}
4544
}

src/Dotnet.Script.Core/Commands/ExecuteScriptCommand.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,12 @@ private string GetLibrary(ExecuteScriptCommandOptions executeOptions)
6464
return pathToLibrary;
6565
}
6666

67-
var options = new PublishCommandOptions(executeOptions.File, executionCacheFolder, "script", PublishType.Library, executeOptions.OptimizationLevel, executeOptions.PackageSources, null, executeOptions.NoCache);
67+
var options = new PublishCommandOptions(executeOptions.File, executionCacheFolder, "script", PublishType.Library, executeOptions.OptimizationLevel, executeOptions.PackageSources, null, executeOptions.NoCache)
68+
{
69+
#if NETCOREAPP
70+
AssemblyLoadContext = executeOptions.AssemblyLoadContext
71+
#endif
72+
};
6873
new PublishCommand(_scriptConsole, _logFactory).Execute(options);
6974
if (hash != null)
7075
{

src/Dotnet.Script.Core/Commands/PublishCommand.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ public void Execute(PublishCommandOptions options)
2828
(options.PublishType == PublishType.Library ? Path.Combine(Path.GetDirectoryName(absoluteFilePath), "publish") : Path.Combine(Path.GetDirectoryName(absoluteFilePath), "publish", options.RuntimeIdentifier));
2929

3030
var absolutePublishDirectory = publishDirectory.GetRootedPath();
31-
var compiler = GetScriptCompiler(!options.NoCache, _logFactory);
31+
var compiler = new ScriptCompiler(_logFactory, !options.NoCache)
32+
{
33+
#if NETCOREAPP
34+
AssemblyLoadContext = options.AssemblyLoadContext
35+
#endif
36+
};
3237
var scriptEmitter = new ScriptEmitter(_scriptConsole, compiler);
3338
var publisher = new ScriptPublisher(_logFactory, scriptEmitter);
3439
var code = absoluteFilePath.ToSourceText();
@@ -43,12 +48,5 @@ public void Execute(PublishCommandOptions options)
4348
publisher.CreateExecutable<int, CommandLineScriptGlobals>(context, _logFactory, options.RuntimeIdentifier, options.LibraryName);
4449
}
4550
}
46-
47-
private static ScriptCompiler GetScriptCompiler(bool useRestoreCache, LogFactory logFactory)
48-
{
49-
50-
var compiler = new ScriptCompiler(logFactory, useRestoreCache);
51-
return compiler;
52-
}
5351
}
5452
}

src/Dotnet.Script.Core/Commands/PublishCommandOptions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using Dotnet.Script.DependencyModel.Environment;
22
using Microsoft.CodeAnalysis;
3+
#if NETCOREAPP
4+
using System.Runtime.Loader;
5+
#endif
36

47
namespace Dotnet.Script.Core.Commands
58
{
@@ -25,6 +28,15 @@ public PublishCommandOptions(ScriptFile file, string outputDirectory, string lib
2528
public string[] PackageSources { get; }
2629
public string RuntimeIdentifier { get; }
2730
public bool NoCache { get; }
31+
32+
#if NETCOREAPP
33+
#nullable enable
34+
/// <summary>
35+
/// Gets or sets a custom assembly load context to use for script isolation.
36+
/// </summary>
37+
public AssemblyLoadContext? AssemblyLoadContext { get; init; }
38+
#nullable restore
39+
#endif
2840
}
2941

3042
public enum PublishType

src/Dotnet.Script.Core/Dotnet.Script.Core.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
</ItemGroup>
2626
<ItemGroup>
2727
<PackageReference Include="Gapotchenko.FX" Version="2021.1.5" />
28-
<PackageReference Include="Gapotchenko.FX.Reflection.Loader" Version="2021.2.6-beta" />
28+
<PackageReference Include="Gapotchenko.FX.Reflection.Loader" Version="2021.2.7-beta" />
2929
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.9.0" />
3030
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
3131
<PackageReference Include="ReadLine" Version="2.0.1" />
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#if NETCOREAPP
2+
3+
using System;
4+
using System.Reflection;
5+
using System.Runtime.Loader;
6+
7+
#nullable enable
8+
9+
namespace Dotnet.Script.Core
10+
{
11+
/// <summary>
12+
/// Represents assembly load context for a script with full and automatic assembly isolation.
13+
/// </summary>
14+
public class ScriptAssemblyLoadContext : AssemblyLoadContext
15+
{
16+
/// <summary>
17+
/// <para>
18+
/// Gets the value indicating whether a specified assembly is homogeneous.
19+
/// </para>
20+
/// <para>
21+
/// Homogeneous assemblies are those shared by both host and scripts.
22+
/// </para>
23+
/// </summary>
24+
/// <param name="assemblyName">The assembly name.</param>
25+
/// <returns><c>true</c> if the specified assembly is homogeneous; otherwise, <c>false</c>.</returns>
26+
protected internal virtual bool IsHomogeneousAssembly(AssemblyName assemblyName) =>
27+
assemblyName.Name switch
28+
{
29+
"mscorlib" or
30+
"Microsoft.CodeAnalysis.Scripting" => true,
31+
_ => false
32+
};
33+
34+
/// <inheritdoc/>
35+
protected override Assembly? Load(AssemblyName assemblyName) => InvokeLoading(assemblyName);
36+
37+
/// <inheritdoc/>
38+
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) => InvokeLoadingUnmanagedDll(unmanagedDllName);
39+
40+
/// <summary>
41+
/// Provides data for the <see cref="Loading"/> event.
42+
/// </summary>
43+
public sealed class LoadingEventArgs : EventArgs
44+
{
45+
public LoadingEventArgs(AssemblyName assemblyName)
46+
{
47+
Name = assemblyName;
48+
}
49+
50+
public AssemblyName Name { get; }
51+
}
52+
53+
/// <summary>
54+
/// Represents a method that handles the <see cref="Loading"/> event.
55+
/// </summary>
56+
/// <param name="sender">The sender.</param>
57+
/// <param name="args">The arguments.</param>
58+
/// <returns>The loaded assembly or <c>null</c> if the assembly cannot be resolved.</returns>
59+
internal delegate Assembly? LoadingEventHandler(ScriptAssemblyLoadContext sender, LoadingEventArgs args);
60+
61+
LoadingEventHandler? m_Loading;
62+
63+
/// <summary>
64+
/// Occurs when an assembly is being loaded.
65+
/// </summary>
66+
internal event LoadingEventHandler Loading
67+
{
68+
add => m_Loading += value;
69+
remove => m_Loading -= value;
70+
}
71+
72+
Assembly? InvokeLoading(AssemblyName assemblyName)
73+
{
74+
var eh = m_Loading;
75+
if (eh != null)
76+
{
77+
var args = new LoadingEventArgs(assemblyName);
78+
foreach (LoadingEventHandler handler in eh.GetInvocationList())
79+
{
80+
var assembly = handler(this, args);
81+
if (assembly != null)
82+
return assembly;
83+
}
84+
}
85+
return null;
86+
}
87+
88+
/// <summary>
89+
/// Provides data for the <see cref="LoadingUnmanagedDll"/> event.
90+
/// </summary>
91+
internal sealed class LoadingUnmanagedDllEventArgs : EventArgs
92+
{
93+
public LoadingUnmanagedDllEventArgs(string unmanagedDllName, Func<string, IntPtr> loadUnmanagedDllFromPath)
94+
{
95+
UnmanagedDllName = unmanagedDllName;
96+
LoadUnmanagedDllFromPath = loadUnmanagedDllFromPath;
97+
}
98+
99+
public string UnmanagedDllName { get; }
100+
public Func<string, IntPtr> LoadUnmanagedDllFromPath { get; }
101+
}
102+
103+
/// <summary>
104+
/// Represents a method that handles the <see cref="LoadingUnmanagedDll"/> event.
105+
/// </summary>
106+
/// <param name="sender">The sender.</param>
107+
/// <param name="args">The arguments.</param>
108+
/// <returns>The loaded DLL or <see cref="IntPtr.Zero"/> if the DLL cannot be resolved.</returns>
109+
internal delegate IntPtr LoadingUnmanagedDllEventHandler(ScriptAssemblyLoadContext sender, LoadingUnmanagedDllEventArgs args);
110+
111+
LoadingUnmanagedDllEventHandler? m_LoadingUnmanagedDll;
112+
113+
/// <summary>
114+
/// Occurs when an unmanaged DLL is being loaded.
115+
/// </summary>
116+
internal event LoadingUnmanagedDllEventHandler LoadingUnmanagedDll
117+
{
118+
add => m_LoadingUnmanagedDll += value;
119+
remove => m_LoadingUnmanagedDll -= value;
120+
}
121+
122+
IntPtr InvokeLoadingUnmanagedDll(string unmanagedDllName)
123+
{
124+
var eh = m_LoadingUnmanagedDll;
125+
if (eh != null)
126+
{
127+
var args = new LoadingUnmanagedDllEventArgs(unmanagedDllName, LoadUnmanagedDllFromPath);
128+
foreach (LoadingUnmanagedDllEventHandler handler in eh.GetInvocationList())
129+
{
130+
var dll = handler(this, args);
131+
if (dll != IntPtr.Zero)
132+
return dll;
133+
}
134+
}
135+
return IntPtr.Zero;
136+
}
137+
}
138+
}
139+
140+
#endif

src/Dotnet.Script.Core/ScriptCompiler.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
using System.Collections.Immutable;
44
using System.Linq;
55
using System.Reflection;
6+
#if NETCOREAPP
7+
using System.Runtime.Loader;
8+
#endif
69
using System.Text;
710
using System.Threading.Tasks;
811
using Dotnet.Script.Core.Internal;
@@ -78,6 +81,15 @@ private ScriptCompiler(LogFactory logFactory, RuntimeDependencyResolver runtimeD
7881
}
7982
}
8083

84+
#if NETCOREAPP
85+
#nullable enable
86+
/// <summary>
87+
/// Gets or sets a custom assembly load context to use for script execution.
88+
/// </summary>
89+
public AssemblyLoadContext? AssemblyLoadContext { get; init; }
90+
#nullable restore
91+
#endif
92+
8193
public virtual ScriptOptions CreateScriptOptions(ScriptContext context, IList<RuntimeDependency> runtimeDependencies)
8294
{
8395
var scriptMap = runtimeDependencies.ToDictionary(rdt => rdt.Name, rdt => rdt.Scripts);
@@ -176,7 +188,19 @@ private ScriptOptions AddScriptReferences(ScriptOptions scriptOptions, Dictionar
176188
{
177189
foreach (var runtimeAssembly in scriptDependenciesMap.Values)
178190
{
179-
loadedAssembliesMap.TryGetValue(runtimeAssembly.Name.Name, out var loadedAssembly);
191+
bool homogenization;
192+
#if NETCOREAPP
193+
homogenization =
194+
AssemblyLoadContext is not ScriptAssemblyLoadContext salc ||
195+
salc.IsHomogeneousAssembly(runtimeAssembly.Name);
196+
#else
197+
homogenization = true;
198+
#endif
199+
200+
Assembly loadedAssembly = null;
201+
if (homogenization)
202+
loadedAssembliesMap.TryGetValue(runtimeAssembly.Name.Name, out loadedAssembly);
203+
180204
if (loadedAssembly == null)
181205
{
182206
_logger.Trace("Adding reference to a runtime dependency => " + runtimeAssembly);

0 commit comments

Comments
 (0)