From f1907e1627be46c0210c47841a6f83ad8b4d0b0f Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Wed, 18 Aug 2021 21:21:09 +0300 Subject: [PATCH 01/18] AssemblyLoadPal is used instead of AppDomain in order to cover both AppDomain and AssemblyLoadContext scenarios. --- .../Dotnet.Script.Core.csproj | 1 + src/Dotnet.Script.Core/ScriptRunner.cs | 18 ++++++++++-------- src/Dotnet.Script.Tests/ScriptRunnerTests.cs | 3 ++- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj index 24a97b2f..665368de 100644 --- a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj +++ b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Dotnet.Script.Core/ScriptRunner.cs b/src/Dotnet.Script.Core/ScriptRunner.cs index 38940695..d4efedfe 100644 --- a/src/Dotnet.Script.Core/ScriptRunner.cs +++ b/src/Dotnet.Script.Core/ScriptRunner.cs @@ -7,6 +7,7 @@ using Dotnet.Script.DependencyModel.Environment; using Dotnet.Script.DependencyModel.Logging; using Dotnet.Script.DependencyModel.Runtime; +using Gapotchenko.FX.Reflection; using Microsoft.CodeAnalysis.CSharp.Scripting.Hosting; using Microsoft.CodeAnalysis.Scripting; using Microsoft.CodeAnalysis.Scripting.Hosting; @@ -30,13 +31,15 @@ public ScriptRunner(ScriptCompiler scriptCompiler, LogFactory logFactory, Script public async Task Execute(string dllPath, IEnumerable commandLineArgs) { + var assemblyLoaderPal = AssemblyLoadPal.ForCurrentAppDomain; + var runtimeDeps = ScriptCompiler.RuntimeDependencyResolver.GetDependenciesForLibrary(dllPath); var runtimeDepsMap = ScriptCompiler.CreateScriptDependenciesMap(runtimeDeps); - var assembly = Assembly.LoadFrom(dllPath); // this needs to be called prior to 'AppDomain.CurrentDomain.AssemblyResolve' event handler added + var assembly = assemblyLoaderPal.LoadFrom(dllPath); // this needs to be called prior to 'AppDomain.CurrentDomain.AssemblyResolve' event handler added - Assembly OnResolve(object sender, ResolveEventArgs args) => ResolveAssembly(args, runtimeDepsMap); + Assembly OnResolve(AssemblyLoadPal sender, AssemblyLoadPal.ResolvingEventArgs args) => ResolveAssembly(sender, args, runtimeDepsMap); - AppDomain.CurrentDomain.AssemblyResolve += OnResolve; + assemblyLoaderPal.Resolving += OnResolve; try { var type = assembly.GetType("Submission#0"); @@ -64,7 +67,7 @@ public async Task Execute(string dllPath, IEnumerable } finally { - AppDomain.CurrentDomain.AssemblyResolve -= OnResolve; + assemblyLoaderPal.Resolving -= OnResolve; } } @@ -97,12 +100,11 @@ public virtual async Task Execute(ScriptCompilationCont return ProcessScriptState(scriptResult); } - internal Assembly ResolveAssembly(ResolveEventArgs args, Dictionary runtimeDepsMap) + internal Assembly ResolveAssembly(AssemblyLoadPal sender, AssemblyLoadPal.ResolvingEventArgs args, Dictionary runtimeDepsMap) { - var assemblyName = new AssemblyName(args.Name); - var result = runtimeDepsMap.TryGetValue(assemblyName.Name, out RuntimeAssembly runtimeAssembly); + var result = runtimeDepsMap.TryGetValue(args.Name.Name, out RuntimeAssembly runtimeAssembly); if (!result) return null; - var loadedAssembly = Assembly.LoadFrom(runtimeAssembly.Path); + var loadedAssembly = sender.LoadFrom(runtimeAssembly.Path); return loadedAssembly; } diff --git a/src/Dotnet.Script.Tests/ScriptRunnerTests.cs b/src/Dotnet.Script.Tests/ScriptRunnerTests.cs index 913ad33d..b97197f5 100644 --- a/src/Dotnet.Script.Tests/ScriptRunnerTests.cs +++ b/src/Dotnet.Script.Tests/ScriptRunnerTests.cs @@ -3,6 +3,7 @@ using Dotnet.Script.Core; using Dotnet.Script.DependencyModel.Runtime; using Dotnet.Script.Shared.Tests; +using Gapotchenko.FX.Reflection; using Moq; using Xunit; @@ -15,7 +16,7 @@ public void ResolveAssembly_ReturnsNull_WhenRuntimeDepsMapDoesNotContainAssembly { var scriptRunner = CreateScriptRunner(); - var result = scriptRunner.ResolveAssembly(new ResolveEventArgs("AnyAssemblyName"), new Dictionary()); + var result = scriptRunner.ResolveAssembly(AssemblyLoadPal.ForCurrentAppDomain, new AssemblyLoadPal.ResolvingEventArgs(null, "AnyAssemblyName"), new Dictionary()); Assert.Null(result); } From 79066f115fe0e89c9bb1ea480ef02c570ec0b37a Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Thu, 19 Aug 2021 11:06:52 +0300 Subject: [PATCH 02/18] ScriptRunner can be configured with a custom AssemblyLoadContext. --- .../Dotnet.Script.Core.csproj | 4 +++- src/Dotnet.Script.Core/ScriptRunner.cs | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj index 665368de..4080e72c 100644 --- a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj +++ b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj @@ -4,7 +4,7 @@ A cross platform library allowing you to run C# (CSX) scripts with support for debugging and inline NuGet packages. Based on Roslyn. 1.1.0 filipw - netstandard2.0 + netstandard2.0;netcoreapp2.1 Dotnet.Script.Core Dotnet.Script.Core script;csx;csharp;roslyn @@ -16,6 +16,7 @@ false false false + 9.0 true ../dotnet-script.snk @@ -23,6 +24,7 @@ + diff --git a/src/Dotnet.Script.Core/ScriptRunner.cs b/src/Dotnet.Script.Core/ScriptRunner.cs index d4efedfe..260b8127 100644 --- a/src/Dotnet.Script.Core/ScriptRunner.cs +++ b/src/Dotnet.Script.Core/ScriptRunner.cs @@ -3,6 +3,9 @@ using System.Collections.Immutable; using System.Linq; using System.Reflection; +#if NETCOREAPP +using System.Runtime.Loader; +#endif using System.Threading.Tasks; using Dotnet.Script.DependencyModel.Environment; using Dotnet.Script.DependencyModel.Logging; @@ -29,9 +32,23 @@ public ScriptRunner(ScriptCompiler scriptCompiler, LogFactory logFactory, Script _scriptEnvironment = ScriptEnvironment.Default; } +#if NETCOREAPP +#nullable enable + /// + /// Gets or sets a custom assembly load context to use for script execution. + /// + public AssemblyLoadContext? AssemblyLoadContext { get; init; } +#nullable restore +#endif + public async Task Execute(string dllPath, IEnumerable commandLineArgs) { - var assemblyLoaderPal = AssemblyLoadPal.ForCurrentAppDomain; +#if NETCOREAPP + var assemblyLoadContext = AssemblyLoadContext; + var assemblyLoaderPal = assemblyLoadContext != null ? new AssemblyLoadPal(assemblyLoadContext) : AssemblyLoadPal.ForCurrentAppDomain; +#else + var assemblyLoaderPal = AssemblyLoadPal.ForCurrentAppDomain; +#endif var runtimeDeps = ScriptCompiler.RuntimeDependencyResolver.GetDependenciesForLibrary(dllPath); var runtimeDepsMap = ScriptCompiler.CreateScriptDependenciesMap(runtimeDeps); From 11e800cb27455784dba53c45d90368ae7656ddf9 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Thu, 19 Aug 2021 11:09:11 +0300 Subject: [PATCH 03/18] ScriptRunner refactoring related to AssemblyLoadContext support. --- src/Dotnet.Script.Core/ScriptRunner.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Dotnet.Script.Core/ScriptRunner.cs b/src/Dotnet.Script.Core/ScriptRunner.cs index 260b8127..31d09583 100644 --- a/src/Dotnet.Script.Core/ScriptRunner.cs +++ b/src/Dotnet.Script.Core/ScriptRunner.cs @@ -45,18 +45,18 @@ public async Task Execute(string dllPath, IEnumerable { #if NETCOREAPP var assemblyLoadContext = AssemblyLoadContext; - var assemblyLoaderPal = assemblyLoadContext != null ? new AssemblyLoadPal(assemblyLoadContext) : AssemblyLoadPal.ForCurrentAppDomain; + var assemblyLoadPal = assemblyLoadContext != null ? new AssemblyLoadPal(assemblyLoadContext) : AssemblyLoadPal.ForCurrentAppDomain; #else - var assemblyLoaderPal = AssemblyLoadPal.ForCurrentAppDomain; + var assemblyLoadPal = AssemblyLoadPal.ForCurrentAppDomain; #endif var runtimeDeps = ScriptCompiler.RuntimeDependencyResolver.GetDependenciesForLibrary(dllPath); var runtimeDepsMap = ScriptCompiler.CreateScriptDependenciesMap(runtimeDeps); - var assembly = assemblyLoaderPal.LoadFrom(dllPath); // this needs to be called prior to 'AppDomain.CurrentDomain.AssemblyResolve' event handler added + var assembly = assemblyLoadPal.LoadFrom(dllPath); // this needs to be called prior to 'AssemblyLoadPal.Resolving' event handler added Assembly OnResolve(AssemblyLoadPal sender, AssemblyLoadPal.ResolvingEventArgs args) => ResolveAssembly(sender, args, runtimeDepsMap); - assemblyLoaderPal.Resolving += OnResolve; + assemblyLoadPal.Resolving += OnResolve; try { var type = assembly.GetType("Submission#0"); @@ -84,7 +84,7 @@ public async Task Execute(string dllPath, IEnumerable } finally { - assemblyLoaderPal.Resolving -= OnResolve; + assemblyLoadPal.Resolving -= OnResolve; } } @@ -117,11 +117,11 @@ public virtual async Task Execute(ScriptCompilationCont return ProcessScriptState(scriptResult); } - internal Assembly ResolveAssembly(AssemblyLoadPal sender, AssemblyLoadPal.ResolvingEventArgs args, Dictionary runtimeDepsMap) + internal Assembly ResolveAssembly(AssemblyLoadPal pal, AssemblyLoadPal.ResolvingEventArgs args, Dictionary runtimeDepsMap) { var result = runtimeDepsMap.TryGetValue(args.Name.Name, out RuntimeAssembly runtimeAssembly); if (!result) return null; - var loadedAssembly = sender.LoadFrom(runtimeAssembly.Path); + var loadedAssembly = pal.LoadFrom(runtimeAssembly.Path); return loadedAssembly; } From 5b765606f0aacf4f77710847a1655540b5db9079 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Thu, 19 Aug 2021 13:30:40 +0300 Subject: [PATCH 04/18] AssemblyLoadContext: assembly loader, downstream propagation via contextual reflection for .NET Core 3.0+ hosts. --- src/Dotnet.Script.Core/Dotnet.Script.Core.csproj | 2 +- src/Dotnet.Script.Core/ScriptRunner.cs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj index 4080e72c..f2749093 100644 --- a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj +++ b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj @@ -4,7 +4,7 @@ A cross platform library allowing you to run C# (CSX) scripts with support for debugging and inline NuGet packages. Based on Roslyn. 1.1.0 filipw - netstandard2.0;netcoreapp2.1 + netstandard2.0;netcoreapp2.1;netcoreapp3.1 Dotnet.Script.Core Dotnet.Script.Core script;csx;csharp;roslyn diff --git a/src/Dotnet.Script.Core/ScriptRunner.cs b/src/Dotnet.Script.Core/ScriptRunner.cs index 31d09583..0667c5c0 100644 --- a/src/Dotnet.Script.Core/ScriptRunner.cs +++ b/src/Dotnet.Script.Core/ScriptRunner.cs @@ -54,6 +54,15 @@ public async Task Execute(string dllPath, IEnumerable var runtimeDepsMap = ScriptCompiler.CreateScriptDependenciesMap(runtimeDeps); var assembly = assemblyLoadPal.LoadFrom(dllPath); // this needs to be called prior to 'AssemblyLoadPal.Resolving' event handler added +#if NETCOREAPP + using var assemblyAutoLoader = new AssemblyAutoLoader(assemblyLoadContext); + assemblyAutoLoader.AddAssembly(assembly); +#endif + +#if NETCOREAPP3_0_OR_GREATER + using var contextualReflectionScope = assemblyLoadContext.EnterContextualReflection(); +#endif + Assembly OnResolve(AssemblyLoadPal sender, AssemblyLoadPal.ResolvingEventArgs args) => ResolveAssembly(sender, args, runtimeDepsMap); assemblyLoadPal.Resolving += OnResolve; From a81c8ef0c41bfdc2660432463998c2e03b9e93f2 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Thu, 19 Aug 2021 13:37:11 +0300 Subject: [PATCH 05/18] Custom assembly load context can be null and should not be used in this case. --- src/Dotnet.Script.Core/ScriptRunner.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Dotnet.Script.Core/ScriptRunner.cs b/src/Dotnet.Script.Core/ScriptRunner.cs index 0667c5c0..23d6ba1a 100644 --- a/src/Dotnet.Script.Core/ScriptRunner.cs +++ b/src/Dotnet.Script.Core/ScriptRunner.cs @@ -55,12 +55,12 @@ public async Task Execute(string dllPath, IEnumerable var assembly = assemblyLoadPal.LoadFrom(dllPath); // this needs to be called prior to 'AssemblyLoadPal.Resolving' event handler added #if NETCOREAPP - using var assemblyAutoLoader = new AssemblyAutoLoader(assemblyLoadContext); - assemblyAutoLoader.AddAssembly(assembly); + using var assemblyAutoLoader = assemblyLoadContext != null ? new AssemblyAutoLoader(assemblyLoadContext) : null; + assemblyAutoLoader?.AddAssembly(assembly); #endif #if NETCOREAPP3_0_OR_GREATER - using var contextualReflectionScope = assemblyLoadContext.EnterContextualReflection(); + using var contextualReflectionScope = assemblyLoadContext != null ? assemblyLoadContext.EnterContextualReflection() : default; #endif Assembly OnResolve(AssemblyLoadPal sender, AssemblyLoadPal.ResolvingEventArgs args) => ResolveAssembly(sender, args, runtimeDepsMap); From e71073995eb8d2a7b99d8a216a999fc56104ea30 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Thu, 19 Aug 2021 14:06:43 +0300 Subject: [PATCH 06/18] Gluing and propagating custom AssemblyLoadContext through the higher API levels. --- .../Commands/ExecuteCodeCommand.cs | 7 ++++++- .../Commands/ExecuteCodeCommandOptions.cs | 12 ++++++++++++ .../Commands/ExecuteLibraryCommand.cs | 7 ++++++- .../Commands/ExecuteLibraryCommandOptions.cs | 13 +++++++++++++ .../Commands/ExecuteScriptCommand.cs | 15 ++++++++------- .../Commands/ExecuteScriptCommandOptions.cs | 12 ++++++++++++ 6 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/Dotnet.Script.Core/Commands/ExecuteCodeCommand.cs b/src/Dotnet.Script.Core/Commands/ExecuteCodeCommand.cs index 688540b0..b8b1b754 100644 --- a/src/Dotnet.Script.Core/Commands/ExecuteCodeCommand.cs +++ b/src/Dotnet.Script.Core/Commands/ExecuteCodeCommand.cs @@ -23,7 +23,12 @@ public async Task Execute(ExecuteCodeCommandOptions options) var sourceText = SourceText.From(options.Code); var context = new ScriptContext(sourceText, options.WorkingDirectory ?? Directory.GetCurrentDirectory(), options.Arguments, null, options.OptimizationLevel, ScriptMode.Eval, options.PackageSources); var compiler = GetScriptCompiler(!options.NoCache, _logFactory); - var runner = new ScriptRunner(compiler, _logFactory, _scriptConsole); + var runner = new ScriptRunner(compiler, _logFactory, _scriptConsole) + { +#if NETCOREAPP + AssemblyLoadContext = options.AssemblyLoadContext +#endif + }; return await runner.Execute(context); } diff --git a/src/Dotnet.Script.Core/Commands/ExecuteCodeCommandOptions.cs b/src/Dotnet.Script.Core/Commands/ExecuteCodeCommandOptions.cs index b1bdf40b..11ebf3a7 100644 --- a/src/Dotnet.Script.Core/Commands/ExecuteCodeCommandOptions.cs +++ b/src/Dotnet.Script.Core/Commands/ExecuteCodeCommandOptions.cs @@ -1,4 +1,7 @@ using Microsoft.CodeAnalysis; +#if NETCOREAPP +using System.Runtime.Loader; +#endif namespace Dotnet.Script.Core.Commands { @@ -20,5 +23,14 @@ public ExecuteCodeCommandOptions(string code, string workingDirectory, string[] public OptimizationLevel OptimizationLevel { get; } public bool NoCache { get; } public string[] PackageSources { get; } + +#if NETCOREAPP +#nullable enable + /// + /// Gets or sets a custom assembly load context to use for script execution. + /// + public AssemblyLoadContext? AssemblyLoadContext { get; init; } +#nullable restore +#endif } } \ No newline at end of file diff --git a/src/Dotnet.Script.Core/Commands/ExecuteLibraryCommand.cs b/src/Dotnet.Script.Core/Commands/ExecuteLibraryCommand.cs index d7350e82..c406287d 100644 --- a/src/Dotnet.Script.Core/Commands/ExecuteLibraryCommand.cs +++ b/src/Dotnet.Script.Core/Commands/ExecuteLibraryCommand.cs @@ -26,7 +26,12 @@ public async Task Execute(ExecuteLibraryCommandOptions options var absoluteFilePath = options.LibraryPath.GetRootedPath(); var compiler = GetScriptCompiler(!options.NoCache, _logFactory); - var runner = new ScriptRunner(compiler, _logFactory, _scriptConsole); + var runner = new ScriptRunner(compiler, _logFactory, _scriptConsole) + { +#if NETCOREAPP + AssemblyLoadContext = options.AssemblyLoadContext +#endif + }; var result = await runner.Execute(absoluteFilePath, options.Arguments); return result; } diff --git a/src/Dotnet.Script.Core/Commands/ExecuteLibraryCommandOptions.cs b/src/Dotnet.Script.Core/Commands/ExecuteLibraryCommandOptions.cs index 2c0f4796..a3c3cb7e 100644 --- a/src/Dotnet.Script.Core/Commands/ExecuteLibraryCommandOptions.cs +++ b/src/Dotnet.Script.Core/Commands/ExecuteLibraryCommandOptions.cs @@ -1,3 +1,7 @@ +#if NETCOREAPP +using System.Runtime.Loader; +#endif + namespace Dotnet.Script.Core.Commands { public class ExecuteLibraryCommandOptions @@ -12,5 +16,14 @@ public ExecuteLibraryCommandOptions(string libraryPath, string[] arguments, bool public string LibraryPath { get; } public string[] Arguments { get; } public bool NoCache { get; } + +#if NETCOREAPP +#nullable enable + /// + /// Gets or sets a custom assembly load context to use for script execution. + /// + public AssemblyLoadContext? AssemblyLoadContext { get; init; } +#nullable restore +#endif } } \ No newline at end of file diff --git a/src/Dotnet.Script.Core/Commands/ExecuteScriptCommand.cs b/src/Dotnet.Script.Core/Commands/ExecuteScriptCommand.cs index 470a4e52..278ff42c 100644 --- a/src/Dotnet.Script.Core/Commands/ExecuteScriptCommand.cs +++ b/src/Dotnet.Script.Core/Commands/ExecuteScriptCommand.cs @@ -32,7 +32,14 @@ public async Task Run(ExecuteScriptCommandOptions optio } var pathToLibrary = GetLibrary(options); - return await ExecuteLibrary(pathToLibrary, options.Arguments, options.NoCache); + + var libraryOptions = new ExecuteLibraryCommandOptions(pathToLibrary, options.Arguments, options.NoCache) + { +#if NETCOREAPP + AssemblyLoadContext = options.AssemblyLoadContext +#endif + }; + return await new ExecuteLibraryCommand(_scriptConsole, _logFactory).Execute(libraryOptions); } private async Task DownloadAndRunCode(ExecuteScriptCommandOptions executeOptions) @@ -124,11 +131,5 @@ public bool TryGetHash(string cacheFolder, out string hash) hash = File.ReadAllText(pathToHashFile); return true; } - - private async Task ExecuteLibrary(string pathToLibrary, string[] arguments, bool noCache) - { - var options = new ExecuteLibraryCommandOptions(pathToLibrary, arguments, noCache); - return await new ExecuteLibraryCommand(_scriptConsole, _logFactory).Execute(options); - } } } \ No newline at end of file diff --git a/src/Dotnet.Script.Core/Commands/ExecuteScriptCommandOptions.cs b/src/Dotnet.Script.Core/Commands/ExecuteScriptCommandOptions.cs index 656cb74d..93577f01 100644 --- a/src/Dotnet.Script.Core/Commands/ExecuteScriptCommandOptions.cs +++ b/src/Dotnet.Script.Core/Commands/ExecuteScriptCommandOptions.cs @@ -1,4 +1,7 @@ using Microsoft.CodeAnalysis; +#if NETCOREAPP +using System.Runtime.Loader; +#endif namespace Dotnet.Script.Core.Commands { @@ -20,5 +23,14 @@ public ExecuteScriptCommandOptions(ScriptFile file, string[] arguments, Optimiza public string[] PackageSources { get; } public bool IsInteractive { get; } public bool NoCache { get; } + +#if NETCOREAPP +#nullable enable + /// + /// Gets or sets a custom assembly load context to use for script execution. + /// + public AssemblyLoadContext? AssemblyLoadContext { get; init; } +#nullable restore +#endif } } \ No newline at end of file From d8f58a083dfa9af899a435d60f5ee83de16292a3 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Thu, 19 Aug 2021 18:01:38 +0300 Subject: [PATCH 07/18] Script files are executed in isolated assembly load context. --- .gitignore | 1 + src/Dotnet.Script/IsolatedAssemblyLoadContext.cs | 10 ++++++++++ src/Dotnet.Script/Program.cs | 5 ++++- 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 src/Dotnet.Script/IsolatedAssemblyLoadContext.cs diff --git a/.gitignore b/.gitignore index e9ec90a2..860e9507 100644 --- a/.gitignore +++ b/.gitignore @@ -265,3 +265,4 @@ project.json /build/dotnet-script /dotnet-script /.vscode +/src/Dotnet.Script/Properties/launchSettings.json diff --git a/src/Dotnet.Script/IsolatedAssemblyLoadContext.cs b/src/Dotnet.Script/IsolatedAssemblyLoadContext.cs new file mode 100644 index 00000000..67ebe677 --- /dev/null +++ b/src/Dotnet.Script/IsolatedAssemblyLoadContext.cs @@ -0,0 +1,10 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Dotnet.Script +{ + sealed class IsolatedAssemblyLoadContext : AssemblyLoadContext + { + protected override Assembly Load(AssemblyName assemblyName) => null; + } +} diff --git a/src/Dotnet.Script/Program.cs b/src/Dotnet.Script/Program.cs index 54b2c196..62f3e6a2 100644 --- a/src/Dotnet.Script/Program.cs +++ b/src/Dotnet.Script/Program.cs @@ -245,7 +245,10 @@ private static int Wain(string[] args) packageSources.Values?.ToArray(), interactive.HasValue(), nocache.HasValue() - ); + ) + { + AssemblyLoadContext = new IsolatedAssemblyLoadContext() + }; var fileCommand = new ExecuteScriptCommand(ScriptConsole.Default, logFactory); return await fileCommand.Run(fileCommandOptions); From aecc8d626a364d6064b12786acf8f2336de2941a Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Fri, 20 Aug 2021 19:02:38 +0300 Subject: [PATCH 08/18] Implemented full assembly isolation support at API level. --- .../Commands/ExecuteCodeCommand.cs | 13 +- .../Commands/ExecuteInteractiveCommand.cs | 7 +- .../ExecuteInteractiveCommandOptions.cs | 15 +- .../Commands/ExecuteLibraryCommand.cs | 13 +- .../Commands/ExecuteScriptCommand.cs | 7 +- .../Commands/PublishCommand.cs | 14 +- .../Commands/PublishCommandOptions.cs | 12 ++ .../Dotnet.Script.Core.csproj | 2 +- .../ScriptAssemblyLoadContext.cs | 140 ++++++++++++++++++ src/Dotnet.Script.Core/ScriptCompiler.cs | 26 +++- src/Dotnet.Script.Core/ScriptRunner.cs | 51 +++++-- src/Dotnet.Script.Tests/ScriptRunnerTests.cs | 3 +- 12 files changed, 265 insertions(+), 38 deletions(-) create mode 100644 src/Dotnet.Script.Core/ScriptAssemblyLoadContext.cs diff --git a/src/Dotnet.Script.Core/Commands/ExecuteCodeCommand.cs b/src/Dotnet.Script.Core/Commands/ExecuteCodeCommand.cs index b8b1b754..b9b6b039 100644 --- a/src/Dotnet.Script.Core/Commands/ExecuteCodeCommand.cs +++ b/src/Dotnet.Script.Core/Commands/ExecuteCodeCommand.cs @@ -22,7 +22,12 @@ public async Task Execute(ExecuteCodeCommandOptions options) { var sourceText = SourceText.From(options.Code); var context = new ScriptContext(sourceText, options.WorkingDirectory ?? Directory.GetCurrentDirectory(), options.Arguments, null, options.OptimizationLevel, ScriptMode.Eval, options.PackageSources); - var compiler = GetScriptCompiler(!options.NoCache, _logFactory); + var compiler = new ScriptCompiler(_logFactory, !options.NoCache) + { +#if NETCOREAPP + AssemblyLoadContext = options.AssemblyLoadContext +#endif + }; var runner = new ScriptRunner(compiler, _logFactory, _scriptConsole) { #if NETCOREAPP @@ -31,11 +36,5 @@ public async Task Execute(ExecuteCodeCommandOptions options) }; return await runner.Execute(context); } - - private static ScriptCompiler GetScriptCompiler(bool useRestoreCache, LogFactory logFactory) - { - var compiler = new ScriptCompiler(logFactory, useRestoreCache); - return compiler; - } } } \ No newline at end of file diff --git a/src/Dotnet.Script.Core/Commands/ExecuteInteractiveCommand.cs b/src/Dotnet.Script.Core/Commands/ExecuteInteractiveCommand.cs index 17d4f3cf..a883fce2 100644 --- a/src/Dotnet.Script.Core/Commands/ExecuteInteractiveCommand.cs +++ b/src/Dotnet.Script.Core/Commands/ExecuteInteractiveCommand.cs @@ -18,7 +18,12 @@ public ExecuteInteractiveCommand(ScriptConsole scriptConsole, LogFactory logFact public async Task Execute(ExecuteInteractiveCommandOptions options) { - var compiler = new ScriptCompiler(_logFactory, useRestoreCache: false); + var compiler = new ScriptCompiler(_logFactory, useRestoreCache: false) + { +#if NETCOREAPP + AssemblyLoadContext = options.AssemblyLoadContext +#endif + }; var runner = new InteractiveRunner(compiler, _logFactory, _scriptConsole, options.PackageSources); if (options.ScriptFile == null) diff --git a/src/Dotnet.Script.Core/Commands/ExecuteInteractiveCommandOptions.cs b/src/Dotnet.Script.Core/Commands/ExecuteInteractiveCommandOptions.cs index 5b189d06..bb7658ca 100644 --- a/src/Dotnet.Script.Core/Commands/ExecuteInteractiveCommandOptions.cs +++ b/src/Dotnet.Script.Core/Commands/ExecuteInteractiveCommandOptions.cs @@ -1,10 +1,12 @@ -using Microsoft.CodeAnalysis; +#if NETCOREAPP +using System.Runtime.Loader; +#endif namespace Dotnet.Script.Core.Commands { public class ExecuteInteractiveCommandOptions { - public ExecuteInteractiveCommandOptions(ScriptFile scriptFile, string[] arguments ,string[] packageSources) + public ExecuteInteractiveCommandOptions(ScriptFile scriptFile, string[] arguments, string[] packageSources) { ScriptFile = scriptFile; Arguments = arguments; @@ -14,5 +16,14 @@ public ExecuteInteractiveCommandOptions(ScriptFile scriptFile, string[] argument public ScriptFile ScriptFile { get; } public string[] Arguments { get; } public string[] PackageSources { get; } + +#if NETCOREAPP +#nullable enable + /// + /// Gets or sets a custom assembly load context to use for script execution. + /// + public AssemblyLoadContext? AssemblyLoadContext { get; init; } +#nullable restore +#endif } } \ No newline at end of file diff --git a/src/Dotnet.Script.Core/Commands/ExecuteLibraryCommand.cs b/src/Dotnet.Script.Core/Commands/ExecuteLibraryCommand.cs index c406287d..f2a9fd37 100644 --- a/src/Dotnet.Script.Core/Commands/ExecuteLibraryCommand.cs +++ b/src/Dotnet.Script.Core/Commands/ExecuteLibraryCommand.cs @@ -25,7 +25,12 @@ public async Task Execute(ExecuteLibraryCommandOptions options } var absoluteFilePath = options.LibraryPath.GetRootedPath(); - var compiler = GetScriptCompiler(!options.NoCache, _logFactory); + var compiler = new ScriptCompiler(_logFactory, !options.NoCache) + { +#if NETCOREAPP + AssemblyLoadContext = options.AssemblyLoadContext +#endif + }; var runner = new ScriptRunner(compiler, _logFactory, _scriptConsole) { #if NETCOREAPP @@ -35,11 +40,5 @@ public async Task Execute(ExecuteLibraryCommandOptions options var result = await runner.Execute(absoluteFilePath, options.Arguments); return result; } - - private static ScriptCompiler GetScriptCompiler(bool useRestoreCache, LogFactory logFactory) - { - var compiler = new ScriptCompiler(logFactory, useRestoreCache); - return compiler; - } } } \ No newline at end of file diff --git a/src/Dotnet.Script.Core/Commands/ExecuteScriptCommand.cs b/src/Dotnet.Script.Core/Commands/ExecuteScriptCommand.cs index 278ff42c..4b3b176a 100644 --- a/src/Dotnet.Script.Core/Commands/ExecuteScriptCommand.cs +++ b/src/Dotnet.Script.Core/Commands/ExecuteScriptCommand.cs @@ -64,7 +64,12 @@ private string GetLibrary(ExecuteScriptCommandOptions executeOptions) return pathToLibrary; } - var options = new PublishCommandOptions(executeOptions.File, executionCacheFolder, "script", PublishType.Library, executeOptions.OptimizationLevel, executeOptions.PackageSources, null, executeOptions.NoCache); + var options = new PublishCommandOptions(executeOptions.File, executionCacheFolder, "script", PublishType.Library, executeOptions.OptimizationLevel, executeOptions.PackageSources, null, executeOptions.NoCache) + { +#if NETCOREAPP + AssemblyLoadContext = executeOptions.AssemblyLoadContext +#endif + }; new PublishCommand(_scriptConsole, _logFactory).Execute(options); if (hash != null) { diff --git a/src/Dotnet.Script.Core/Commands/PublishCommand.cs b/src/Dotnet.Script.Core/Commands/PublishCommand.cs index 684911c0..7e5311eb 100644 --- a/src/Dotnet.Script.Core/Commands/PublishCommand.cs +++ b/src/Dotnet.Script.Core/Commands/PublishCommand.cs @@ -28,7 +28,12 @@ public void Execute(PublishCommandOptions options) (options.PublishType == PublishType.Library ? Path.Combine(Path.GetDirectoryName(absoluteFilePath), "publish") : Path.Combine(Path.GetDirectoryName(absoluteFilePath), "publish", options.RuntimeIdentifier)); var absolutePublishDirectory = publishDirectory.GetRootedPath(); - var compiler = GetScriptCompiler(!options.NoCache, _logFactory); + var compiler = new ScriptCompiler(_logFactory, !options.NoCache) + { +#if NETCOREAPP + AssemblyLoadContext = options.AssemblyLoadContext +#endif + }; var scriptEmitter = new ScriptEmitter(_scriptConsole, compiler); var publisher = new ScriptPublisher(_logFactory, scriptEmitter); var code = absoluteFilePath.ToSourceText(); @@ -43,12 +48,5 @@ public void Execute(PublishCommandOptions options) publisher.CreateExecutable(context, _logFactory, options.RuntimeIdentifier, options.LibraryName); } } - - private static ScriptCompiler GetScriptCompiler(bool useRestoreCache, LogFactory logFactory) - { - - var compiler = new ScriptCompiler(logFactory, useRestoreCache); - return compiler; - } } } \ No newline at end of file diff --git a/src/Dotnet.Script.Core/Commands/PublishCommandOptions.cs b/src/Dotnet.Script.Core/Commands/PublishCommandOptions.cs index a82fa295..7dd44e1c 100644 --- a/src/Dotnet.Script.Core/Commands/PublishCommandOptions.cs +++ b/src/Dotnet.Script.Core/Commands/PublishCommandOptions.cs @@ -1,5 +1,8 @@ using Dotnet.Script.DependencyModel.Environment; using Microsoft.CodeAnalysis; +#if NETCOREAPP +using System.Runtime.Loader; +#endif namespace Dotnet.Script.Core.Commands { @@ -25,6 +28,15 @@ public PublishCommandOptions(ScriptFile file, string outputDirectory, string lib public string[] PackageSources { get; } public string RuntimeIdentifier { get; } public bool NoCache { get; } + +#if NETCOREAPP +#nullable enable + /// + /// Gets or sets a custom assembly load context to use for script isolation. + /// + public AssemblyLoadContext? AssemblyLoadContext { get; init; } +#nullable restore +#endif } public enum PublishType diff --git a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj index f2749093..fcb3b5a4 100644 --- a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj +++ b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/Dotnet.Script.Core/ScriptAssemblyLoadContext.cs b/src/Dotnet.Script.Core/ScriptAssemblyLoadContext.cs new file mode 100644 index 00000000..fcd83e31 --- /dev/null +++ b/src/Dotnet.Script.Core/ScriptAssemblyLoadContext.cs @@ -0,0 +1,140 @@ +#if NETCOREAPP + +using System; +using System.Reflection; +using System.Runtime.Loader; + +#nullable enable + +namespace Dotnet.Script.Core +{ + /// + /// Represents assembly load context for a script with full and automatic assembly isolation. + /// + public class ScriptAssemblyLoadContext : AssemblyLoadContext + { + /// + /// + /// Gets the value indicating whether a specified assembly is homogeneous. + /// + /// + /// Homogeneous assemblies are those shared by both host and scripts. + /// + /// + /// The assembly name. + /// true if the specified assembly is homogeneous; otherwise, false. + protected internal virtual bool IsHomogeneousAssembly(AssemblyName assemblyName) => + assemblyName.Name switch + { + "mscorlib" or + "Microsoft.CodeAnalysis.Scripting" => true, + _ => false + }; + + /// + protected override Assembly? Load(AssemblyName assemblyName) => InvokeLoading(assemblyName); + + /// + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) => InvokeLoadingUnmanagedDll(unmanagedDllName); + + /// + /// Provides data for the event. + /// + public sealed class LoadingEventArgs : EventArgs + { + public LoadingEventArgs(AssemblyName assemblyName) + { + Name = assemblyName; + } + + public AssemblyName Name { get; } + } + + /// + /// Represents a method that handles the event. + /// + /// The sender. + /// The arguments. + /// The loaded assembly or null if the assembly cannot be resolved. + internal delegate Assembly? LoadingEventHandler(ScriptAssemblyLoadContext sender, LoadingEventArgs args); + + LoadingEventHandler? m_Loading; + + /// + /// Occurs when an assembly is being loaded. + /// + internal event LoadingEventHandler Loading + { + add => m_Loading += value; + remove => m_Loading -= value; + } + + Assembly? InvokeLoading(AssemblyName assemblyName) + { + var eh = m_Loading; + if (eh != null) + { + var args = new LoadingEventArgs(assemblyName); + foreach (LoadingEventHandler handler in eh.GetInvocationList()) + { + var assembly = handler(this, args); + if (assembly != null) + return assembly; + } + } + return null; + } + + /// + /// Provides data for the event. + /// + internal sealed class LoadingUnmanagedDllEventArgs : EventArgs + { + public LoadingUnmanagedDllEventArgs(string unmanagedDllName, Func loadUnmanagedDllFromPath) + { + UnmanagedDllName = unmanagedDllName; + LoadUnmanagedDllFromPath = loadUnmanagedDllFromPath; + } + + public string UnmanagedDllName { get; } + public Func LoadUnmanagedDllFromPath { get; } + } + + /// + /// Represents a method that handles the event. + /// + /// The sender. + /// The arguments. + /// The loaded DLL or if the DLL cannot be resolved. + internal delegate IntPtr LoadingUnmanagedDllEventHandler(ScriptAssemblyLoadContext sender, LoadingUnmanagedDllEventArgs args); + + LoadingUnmanagedDllEventHandler? m_LoadingUnmanagedDll; + + /// + /// Occurs when an unmanaged DLL is being loaded. + /// + internal event LoadingUnmanagedDllEventHandler LoadingUnmanagedDll + { + add => m_LoadingUnmanagedDll += value; + remove => m_LoadingUnmanagedDll -= value; + } + + IntPtr InvokeLoadingUnmanagedDll(string unmanagedDllName) + { + var eh = m_LoadingUnmanagedDll; + if (eh != null) + { + var args = new LoadingUnmanagedDllEventArgs(unmanagedDllName, LoadUnmanagedDllFromPath); + foreach (LoadingUnmanagedDllEventHandler handler in eh.GetInvocationList()) + { + var dll = handler(this, args); + if (dll != IntPtr.Zero) + return dll; + } + } + return IntPtr.Zero; + } + } +} + +#endif diff --git a/src/Dotnet.Script.Core/ScriptCompiler.cs b/src/Dotnet.Script.Core/ScriptCompiler.cs index 994cb39f..459f1603 100644 --- a/src/Dotnet.Script.Core/ScriptCompiler.cs +++ b/src/Dotnet.Script.Core/ScriptCompiler.cs @@ -3,6 +3,9 @@ using System.Collections.Immutable; using System.Linq; using System.Reflection; +#if NETCOREAPP +using System.Runtime.Loader; +#endif using System.Text; using System.Threading.Tasks; using Dotnet.Script.Core.Internal; @@ -78,6 +81,15 @@ private ScriptCompiler(LogFactory logFactory, RuntimeDependencyResolver runtimeD } } +#if NETCOREAPP +#nullable enable + /// + /// Gets or sets a custom assembly load context to use for script execution. + /// + public AssemblyLoadContext? AssemblyLoadContext { get; init; } +#nullable restore +#endif + public virtual ScriptOptions CreateScriptOptions(ScriptContext context, IList runtimeDependencies) { var scriptMap = runtimeDependencies.ToDictionary(rdt => rdt.Name, rdt => rdt.Scripts); @@ -176,7 +188,19 @@ private ScriptOptions AddScriptReferences(ScriptOptions scriptOptions, Dictionar { foreach (var runtimeAssembly in scriptDependenciesMap.Values) { - loadedAssembliesMap.TryGetValue(runtimeAssembly.Name.Name, out var loadedAssembly); + bool homogenization; +#if NETCOREAPP + homogenization = + AssemblyLoadContext is not ScriptAssemblyLoadContext salc || + salc.IsHomogeneousAssembly(runtimeAssembly.Name); +#else + homogenization = true; +#endif + + Assembly loadedAssembly = null; + if (homogenization) + loadedAssembliesMap.TryGetValue(runtimeAssembly.Name.Name, out loadedAssembly); + if (loadedAssembly == null) { _logger.Trace("Adding reference to a runtime dependency => " + runtimeAssembly); diff --git a/src/Dotnet.Script.Core/ScriptRunner.cs b/src/Dotnet.Script.Core/ScriptRunner.cs index 23d6ba1a..b154d054 100644 --- a/src/Dotnet.Script.Core/ScriptRunner.cs +++ b/src/Dotnet.Script.Core/ScriptRunner.cs @@ -57,15 +57,43 @@ public async Task Execute(string dllPath, IEnumerable #if NETCOREAPP using var assemblyAutoLoader = assemblyLoadContext != null ? new AssemblyAutoLoader(assemblyLoadContext) : null; assemblyAutoLoader?.AddAssembly(assembly); + + Assembly OnLoading(ScriptAssemblyLoadContext sender, ScriptAssemblyLoadContext.LoadingEventArgs args) + { + var assemblyName = args.Name; + + if (sender.IsHomogeneousAssembly(assemblyName)) + { + // The default assembly loader will take care of it. + return null; + } + + return ResolveAssembly(assemblyLoadPal, assemblyName, runtimeDepsMap); + } + + IntPtr OnLoadingUnmanagedDll(ScriptAssemblyLoadContext sender, ScriptAssemblyLoadContext.LoadingUnmanagedDllEventArgs args) + { + string dllPath = assemblyAutoLoader.ResolveUnmanagedDllPath(args.UnmanagedDllName); + if (dllPath == null) + return IntPtr.Zero; + return args.LoadUnmanagedDllFromPath(dllPath); + } + + var scriptAssemblyLoadContext = assemblyLoadContext as ScriptAssemblyLoadContext; + if (scriptAssemblyLoadContext != null) + { + scriptAssemblyLoadContext.Loading += OnLoading; + scriptAssemblyLoadContext.LoadingUnmanagedDll += OnLoadingUnmanagedDll; + } #endif #if NETCOREAPP3_0_OR_GREATER using var contextualReflectionScope = assemblyLoadContext != null ? assemblyLoadContext.EnterContextualReflection() : default; #endif - Assembly OnResolve(AssemblyLoadPal sender, AssemblyLoadPal.ResolvingEventArgs args) => ResolveAssembly(sender, args, runtimeDepsMap); + Assembly OnResolving(AssemblyLoadPal sender, AssemblyLoadPal.ResolvingEventArgs args) => ResolveAssembly(sender, args.Name, runtimeDepsMap); - assemblyLoadPal.Resolving += OnResolve; + assemblyLoadPal.Resolving += OnResolving; try { var type = assembly.GetType("Submission#0"); @@ -78,22 +106,27 @@ public async Task Execute(string dllPath, IEnumerable var submissionStates = new object[2]; submissionStates[0] = globals; - var resultTask = method.Invoke(null, new[] { submissionStates }) as Task; try { - _ = await resultTask; + var resultTask = (Task)method.Invoke(null, new[] { submissionStates }); + return await resultTask; } catch (System.Exception ex) { ScriptConsole.WriteError(ex.ToString()); throw new ScriptRuntimeException("Script execution resulted in an exception.", ex); } - - return await resultTask; } finally { - assemblyLoadPal.Resolving -= OnResolve; + assemblyLoadPal.Resolving -= OnResolving; +#if NETCOREAPP + if (scriptAssemblyLoadContext != null) + { + scriptAssemblyLoadContext.LoadingUnmanagedDll -= OnLoadingUnmanagedDll; + scriptAssemblyLoadContext.Loading -= OnLoading; + } +#endif } } @@ -126,9 +159,9 @@ public virtual async Task Execute(ScriptCompilationCont return ProcessScriptState(scriptResult); } - internal Assembly ResolveAssembly(AssemblyLoadPal pal, AssemblyLoadPal.ResolvingEventArgs args, Dictionary runtimeDepsMap) + internal Assembly ResolveAssembly(AssemblyLoadPal pal, AssemblyName assemblyName, Dictionary runtimeDepsMap) { - var result = runtimeDepsMap.TryGetValue(args.Name.Name, out RuntimeAssembly runtimeAssembly); + var result = runtimeDepsMap.TryGetValue(assemblyName.Name, out RuntimeAssembly runtimeAssembly); if (!result) return null; var loadedAssembly = pal.LoadFrom(runtimeAssembly.Path); return loadedAssembly; diff --git a/src/Dotnet.Script.Tests/ScriptRunnerTests.cs b/src/Dotnet.Script.Tests/ScriptRunnerTests.cs index b97197f5..bebc1aea 100644 --- a/src/Dotnet.Script.Tests/ScriptRunnerTests.cs +++ b/src/Dotnet.Script.Tests/ScriptRunnerTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection; using Dotnet.Script.Core; using Dotnet.Script.DependencyModel.Runtime; using Dotnet.Script.Shared.Tests; @@ -16,7 +17,7 @@ public void ResolveAssembly_ReturnsNull_WhenRuntimeDepsMapDoesNotContainAssembly { var scriptRunner = CreateScriptRunner(); - var result = scriptRunner.ResolveAssembly(AssemblyLoadPal.ForCurrentAppDomain, new AssemblyLoadPal.ResolvingEventArgs(null, "AnyAssemblyName"), new Dictionary()); + var result = scriptRunner.ResolveAssembly(AssemblyLoadPal.ForCurrentAppDomain, new AssemblyName("AnyAssemblyName"), new Dictionary()); Assert.Null(result); } From 056901d990c74a796b075608d787cfab6c2685e2 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Fri, 20 Aug 2021 19:03:45 +0300 Subject: [PATCH 09/18] dotnet-script now uses full assembly isolation for script file execution. --- src/Dotnet.Script/IsolatedAssemblyLoadContext.cs | 10 ---------- src/Dotnet.Script/Program.cs | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 src/Dotnet.Script/IsolatedAssemblyLoadContext.cs diff --git a/src/Dotnet.Script/IsolatedAssemblyLoadContext.cs b/src/Dotnet.Script/IsolatedAssemblyLoadContext.cs deleted file mode 100644 index 67ebe677..00000000 --- a/src/Dotnet.Script/IsolatedAssemblyLoadContext.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Reflection; -using System.Runtime.Loader; - -namespace Dotnet.Script -{ - sealed class IsolatedAssemblyLoadContext : AssemblyLoadContext - { - protected override Assembly Load(AssemblyName assemblyName) => null; - } -} diff --git a/src/Dotnet.Script/Program.cs b/src/Dotnet.Script/Program.cs index 62f3e6a2..bd2fd1d3 100644 --- a/src/Dotnet.Script/Program.cs +++ b/src/Dotnet.Script/Program.cs @@ -247,7 +247,7 @@ private static int Wain(string[] args) nocache.HasValue() ) { - AssemblyLoadContext = new IsolatedAssemblyLoadContext() + AssemblyLoadContext = new ScriptAssemblyLoadContext() }; var fileCommand = new ExecuteScriptCommand(ScriptConsole.Default, logFactory); From 11a3aa3ca5ed2b589fa34d2afeae9ae05226f867 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Fri, 20 Aug 2021 19:04:30 +0300 Subject: [PATCH 10/18] Implemented 'ShouldIsolateScriptAssemblies' test for full assembly isolation. --- src/Dotnet.Script.Tests/ScriptExecutionTests.cs | 6 ++++++ .../TestFixtures/Isolation/Isolation.csx | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 src/Dotnet.Script.Tests/TestFixtures/Isolation/Isolation.csx diff --git a/src/Dotnet.Script.Tests/ScriptExecutionTests.cs b/src/Dotnet.Script.Tests/ScriptExecutionTests.cs index f13fd397..112403b4 100644 --- a/src/Dotnet.Script.Tests/ScriptExecutionTests.cs +++ b/src/Dotnet.Script.Tests/ScriptExecutionTests.cs @@ -472,6 +472,12 @@ public void ShouldIgnoreGlobalJsonInScriptFolder() Assert.Contains("Hello world!", result.output); } + [Fact] + public void ShouldIsolateScriptAssemblies() + { + var result = ScriptTestRunner.Default.ExecuteFixture("Isolation"); + Assert.Contains("10.0.0.0", result.output); + } private static string CreateTestScript(string scriptFolder) { diff --git a/src/Dotnet.Script.Tests/TestFixtures/Isolation/Isolation.csx b/src/Dotnet.Script.Tests/TestFixtures/Isolation/Isolation.csx new file mode 100644 index 00000000..5acb385b --- /dev/null +++ b/src/Dotnet.Script.Tests/TestFixtures/Isolation/Isolation.csx @@ -0,0 +1,6 @@ +#r "nuget:Newtonsoft.Json, 10.0.1" + +using Newtonsoft.Json; + +var version = typeof(JsonConvert).Assembly.GetName().Version; +Console.WriteLine(version); From 68c5645ec3cbc69a879187226fdd6dff3d26c765 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Fri, 20 Aug 2021 21:03:31 +0300 Subject: [PATCH 11/18] Fixed library lookup issues with loading of unmanaged DLLs. --- src/Dotnet.Script.Core/Dotnet.Script.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj index fcb3b5a4..c8b5af13 100644 --- a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj +++ b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj @@ -25,7 +25,7 @@ - + From fad86f6e624ca69740600151b12c1804407d5e73 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Fri, 20 Aug 2021 22:26:54 +0300 Subject: [PATCH 12/18] Enabled assembly isolation for interactive script execution. --- src/Dotnet.Script/Program.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Dotnet.Script/Program.cs b/src/Dotnet.Script/Program.cs index bd2fd1d3..a12cb30b 100644 --- a/src/Dotnet.Script/Program.cs +++ b/src/Dotnet.Script/Program.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Runtime.Loader; using System.Threading.Tasks; namespace Dotnet.Script @@ -247,7 +248,7 @@ private static int Wain(string[] args) nocache.HasValue() ) { - AssemblyLoadContext = new ScriptAssemblyLoadContext() + AssemblyLoadContext = CreateAssemblyLoadContext() }; var fileCommand = new ExecuteScriptCommand(ScriptConsole.Default, logFactory); @@ -265,16 +266,24 @@ private static int Wain(string[] args) private static async Task RunInteractive(bool useRestoreCache, LogFactory logFactory, string[] packageSources) { - var options = new ExecuteInteractiveCommandOptions(null, Array.Empty(), packageSources); + var options = new ExecuteInteractiveCommandOptions(null, Array.Empty(), packageSources) + { + AssemblyLoadContext = CreateAssemblyLoadContext() + }; await new ExecuteInteractiveCommand(ScriptConsole.Default, logFactory).Execute(options); return 0; } private async static Task RunInteractiveWithSeed(string file, LogFactory logFactory, string[] arguments, string[] packageSources) { - var options = new ExecuteInteractiveCommandOptions(new ScriptFile(file), arguments, packageSources); + var options = new ExecuteInteractiveCommandOptions(new ScriptFile(file), arguments, packageSources) + { + AssemblyLoadContext = CreateAssemblyLoadContext() + }; await new ExecuteInteractiveCommand(ScriptConsole.Default, logFactory).Execute(options); return 0; } + + static AssemblyLoadContext CreateAssemblyLoadContext() => new ScriptAssemblyLoadContext(); } } From 436bbe96584178e489b7588b23885e33ed00f2d9 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Fri, 20 Aug 2021 22:32:27 +0300 Subject: [PATCH 13/18] Inheritance-friendly ScriptAssemblyLoadContext constructors. --- .../ScriptAssemblyLoadContext.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Dotnet.Script.Core/ScriptAssemblyLoadContext.cs b/src/Dotnet.Script.Core/ScriptAssemblyLoadContext.cs index fcd83e31..a362e190 100644 --- a/src/Dotnet.Script.Core/ScriptAssemblyLoadContext.cs +++ b/src/Dotnet.Script.Core/ScriptAssemblyLoadContext.cs @@ -13,6 +13,36 @@ namespace Dotnet.Script.Core /// public class ScriptAssemblyLoadContext : AssemblyLoadContext { + /// + /// Initializes a new instance of the class. + /// + public ScriptAssemblyLoadContext() + { + } + +#if NETCOREAPP3_0_OR_GREATER + /// + /// Initializes a new instance of the class + /// with a name and a value that indicates whether unloading is enabled. + /// + /// + /// + public ScriptAssemblyLoadContext(string? name, bool isCollectible = false) : + base(name, isCollectible) + { + } + + /// + /// Initializes a new instance of the class + /// with a value that indicates whether unloading is enabled. + /// + /// + protected ScriptAssemblyLoadContext(bool isCollectible) : + base(isCollectible) + { + } +#endif + /// /// /// Gets the value indicating whether a specified assembly is homogeneous. From e6aef4713e3ab8910834705f6f348caae5481c0c Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Sun, 22 Aug 2021 13:18:53 +0300 Subject: [PATCH 14/18] Removed a no longer needed workaround for the issue #268. Reference to a more polished version of Gapotchenko.FX.Reflection.Loader. --- src/Dotnet.Script.Core/Dotnet.Script.Core.csproj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj index c8b5af13..b69bf38d 100644 --- a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj +++ b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj @@ -25,17 +25,16 @@ - + - - From dcc3a89d396b9929e831ea736f4c3248816b3f41 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Sun, 22 Aug 2021 13:26:36 +0300 Subject: [PATCH 15/18] Removed a no longer needed workaround for the issue #166. --- src/Dotnet.Script.Core/Dotnet.Script.Core.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj index b69bf38d..871b5499 100644 --- a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj +++ b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj @@ -32,10 +32,6 @@ - - From 6a7c2b36d499d7b4c5d8c9d9978f2fe86743539f Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Sun, 22 Aug 2021 15:32:27 +0300 Subject: [PATCH 16/18] Use a release version of Gapotchenko.FX.Reflection.Loader package. --- src/Dotnet.Script.Core/Dotnet.Script.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj index 871b5499..960a4db7 100644 --- a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj +++ b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj @@ -25,7 +25,7 @@ - + From 2610c6e93ec2328e4d10b4a2ba02696da2d1b185 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Sun, 22 Aug 2021 16:31:16 +0300 Subject: [PATCH 17/18] Assembly name comparison should be case-insensitive. Fixed internal class visibility. --- .../ScriptAssemblyLoadContext.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Dotnet.Script.Core/ScriptAssemblyLoadContext.cs b/src/Dotnet.Script.Core/ScriptAssemblyLoadContext.cs index a362e190..f5691074 100644 --- a/src/Dotnet.Script.Core/ScriptAssemblyLoadContext.cs +++ b/src/Dotnet.Script.Core/ScriptAssemblyLoadContext.cs @@ -53,13 +53,13 @@ protected ScriptAssemblyLoadContext(bool isCollectible) : /// /// The assembly name. /// true if the specified assembly is homogeneous; otherwise, false. - protected internal virtual bool IsHomogeneousAssembly(AssemblyName assemblyName) => - assemblyName.Name switch - { - "mscorlib" or - "Microsoft.CodeAnalysis.Scripting" => true, - _ => false - }; + protected internal virtual bool IsHomogeneousAssembly(AssemblyName assemblyName) + { + var name = assemblyName.Name; + return + string.Equals(name, "mscorlib", StringComparison.OrdinalIgnoreCase) || + string.Equals(name, "Microsoft.CodeAnalysis.Scripting", StringComparison.OrdinalIgnoreCase); + } /// protected override Assembly? Load(AssemblyName assemblyName) => InvokeLoading(assemblyName); @@ -70,7 +70,7 @@ protected internal virtual bool IsHomogeneousAssembly(AssemblyName assemblyName) /// /// Provides data for the event. /// - public sealed class LoadingEventArgs : EventArgs + internal sealed class LoadingEventArgs : EventArgs { public LoadingEventArgs(AssemblyName assemblyName) { From 353609e2a116f1126f75c6da02373a051331de73 Mon Sep 17 00:00:00 2001 From: hrumhurum Date: Mon, 23 Aug 2021 20:52:08 +0300 Subject: [PATCH 18/18] Dropped support for .NET Core 2.1. --- src/Dotnet.Script.Core/Dotnet.Script.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj index 08426a75..a58748f6 100644 --- a/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj +++ b/src/Dotnet.Script.Core/Dotnet.Script.Core.csproj @@ -4,7 +4,7 @@ A cross platform library allowing you to run C# (CSX) scripts with support for debugging and inline NuGet packages. Based on Roslyn. 1.2.0 filipw - netstandard2.0;netcoreapp2.1;netcoreapp3.1 + netstandard2.0;netcoreapp3.1 Dotnet.Script.Core Dotnet.Script.Core script;csx;csharp;roslyn