Endeavours With scriptcs.hosting
One of the most promising perspectives in scriptcs, in my opinion, is hosting. Basically hosting is the complete package of scriptcs, just put inside your own application. It can be pretty mind blowing to think about this. Hosting makes it possible execute C# scripts inside of any standard .Net application.
With my scriptcs.rebus project, I wanted to add a feature, which would use scriptcs.hosting for executing scripts inside of a host. The script was supposed to be send over either MSMQ, RabbitMQ, or Azure Service Bus. When the host received the script it would be executed. It should also be possible to added NuGet references, local dependencies, and import namespaces to be used by the script.
I had read the excellent blog post by Filip Wojcieszyn on extending Glimpse using scriptcs. This blog post gives a small hint on the perspectives for scriptcs hosting. When I came up the idea of combining scriptcs hosting with messaging, I had a Jabbr session with Glenn Block, who guided me in the right direction.
The end of the session with Glenn, was that all I needed to do was reference scriptcs.hosting
, and I should be given all the dependencies needed.
Initial Attempt
I started looking into how the internals of scriptcs is working, and how hosting in particular is working. I soon realized that there is a number of different dependencies at work, and quickly came up with idea of using AutoFac, which is also used inside of scriptcs hosting. I wrote a rather nice script module, on which I registered all my dependencies from scriptcs.hosting
.
This script module would be registered in my ScriptHandler
. The ScriptHandler subscribes to messages of type Script
:
public class ScriptHandler : IHandleMessages<Script>
{
public void Handle(Script message)
{
var builder = new ContainerBuilder();
builder.RegisterModule(new ScriptModule());
using (var container = builder.Build())
{
using (var scope = container.BeginLifetimeScope())
{
var logger = scope.Resolve<ILog>();
var executor = scope.Resolve<ScriptExecutor>();
scope.Resolve<IInstallationProvider>().Initialize();
try
{
executor.Execute(message.ScriptContent, message.Dependencies);
}
catch (Exception ex)
{
logger.Error(ex);
throw;
}
}
}
}
}
So upon receiving a script, the script module is registered, and I can resolve the logger and ScriptExecutor
. I thought this was a pretty cool setup.
For completeness my ScriptExecutor
would look like this:
public class ScriptExecutor
{
private readonly ILog _logger;
private readonly IFileSystem _fileSystem;
private readonly IPackageAssemblyResolver _packageAssemblyResolver;
private readonly IPackageInstaller _packageInstaller;
private readonly IScriptPackResolver _scriptPackResolver;
private readonly IScriptExecutor _scriptExecutor;
public ScriptExecutor(ILog logger, IFileSystem fileSystem,
IPackageAssemblyResolver packageAssemblyResolver,
IPackageInstaller packageInstaller, IScriptPackResolver scriptPackResolver,
IScriptExecutor scriptExecutor)
{
_logger = logger;
_fileSystem = fileSystem;
_packageAssemblyResolver = packageAssemblyResolver;
_packageInstaller = packageInstaller;
_scriptPackResolver = scriptPackResolver;
_scriptExecutor = scriptExecutor;
}
public void Execute(string script, string[] dependencies)
{
// set current dicrectory, import for NuGet.
Environment.CurrentDirectory = AppDomain.CurrentDomain.BaseDirectory;
// prepare NuGet dependencies, download them if required
var nuGetReferences = PreparePackages(_fileSystem, _packageAssemblyResolver,
_packageInstaller, PrepareAdditionalPackages(dependencies));
// get script packs: not fully tested yet
var scriptPacks = _scriptPackResolver.GetPacks();
// execute script from file
_scriptExecutor.Initialize(nuGetReferences, scriptPacks);
var scriptResult = _scriptExecutor.ExecuteScript(script);
if (scriptResult != null)
if (scriptResult.CompileExceptionInfo != null)
if (scriptResult.CompileExceptionInfo.SourceException != null)
_logger.Debug(scriptResult.CompileExceptionInfo.SourceException.Message);
}
private IEnumerable<IPackageReference> PrepareAdditionalPackages(string[] dependencies)
{
return from dep in dependencies
select new PackageReference(dep, new FrameworkName(".NETFramework,Version=v4.0"), string.Empty);
}
// prepare NuGet dependencies, download them if required
private static IEnumerable<string> PreparePackages(IFileSystem fileSystem, IPackageAssemblyResolver packageAssemblyResolver,
IPackageInstaller packageInstaller, IEnumerable<IPackageReference> additionalReferences)
{
var workingDirectory = Environment.CurrentDirectory;
var packages = packageAssemblyResolver.GetPackages(workingDirectory);
packages = packages.Concat(additionalReferences);
packageInstaller.InstallPackages(
packages,
allowPreRelease: true);
return packageAssemblyResolver.GetAssemblyNames(workingDirectory);
}
}
The Execute()
method will take the script to be executed and a number NuGet dependencies. Setting the Environment.CurrentDirectory
is required for NuGet to be able to determine which packages are already installed, and where to download and install the new ones. The PreparePacakages()
method will download and install NuGet packages, if they are not already installed. If there is any script packs installed, they'll be referenced, using the ScriptPackResolver
. The ScriptExecutor
is initialized with the NuGet references and the script packs. ExecuteScript()
is called on the ScriptExecutor
. The result is verified and logged if relevant.
This is solution that I released in version 0.4.0. Despite missing a few essential scriptcs.hosting features, like importing namespaces and reference local dependencies, I decided to release it anyway.
On of the key features of the upcoming 0.5.0 release is Mono support. I'd like to add the options to use the Mono compiler inside of the hosting pieces. During this work I ran into an issue with the way I wired up my dependencies. It was rather trivial to set an option for using the Mono compiler, and then replace it with the dependency to Roslyn. It worked so far that I could compile plain C#, just like the Roslyn compiler, but if I wanted to compile C# with some features that is not supported by Roslyn yet, like dynamic
or async/await
, the Mono compiler would throw me an error. At first I was convinced that this must be a scriptcs bug, and there called out for help in the scriptcs community. Once again, I got in contact with Glenn Block. So, one morning, we toke a one-to-one on Skype, were I showed Glenn my code and put him into my mindset of what I'd like to achieve. Glenn is a nice guy, so he said, this cool, but you're doing it wrong. So with Glenn looking me over my shoulder, I re-implemented the Execute()
of my ScriptExecutor
.
Actual Approach
Glenn told me that all my wiring using AutoFac wasn't necessary, there is already a built-in helper class for that. It's called ScriptServicesBuilder
. What ScriptServicesBuilder does is wire up the dependencies for you. It then serves you a ScriptServices
instance, which makes it trivial to override the dependencies.
I therefore added a small method to my ScriptExecutor
class, which I borrowed some pieces from here:
private ScriptServices CreateScriptServices(bool useMono, bool useLogging)
{
var console = new ScriptConsole();
var configurator = new LoggerConfigurator(useLogging ? LogLevel.Debug : LogLevel.Info);
configurator.Configure(console);
var logger = configurator.GetLogger();
var builder = new ScriptServicesBuilder(console, logger);
if (useMono)
{
builder.ScriptEngine<MonoScriptEngine>();
}
else
{
builder.ScriptEngine<RoslynScriptEngine>();
}
builder.FileSystem<ScriptFileSystem>();
return builder.Build();
}
I do some configuration of the console and the logger. The log level is set depending on whether the user would like some debugging output. Whether the user would like to use the Mono compiler, I override the ScriptEngine
. Finally, I override the logical file system, which scriptcs uses as its environment. I override it with this:
/// <summary>
/// By default, the scriptcs.hosting inspects the bin folder for references.
/// </summary>
public class ScriptFileSystem : FileSystem
{
public override string BinFolder
{
get { return AppDomain.CurrentDomain.BaseDirectory; }
}
}
All this customization does is that it tells scriptcs that my dependencies are located in the same folder as the script is executed. Otherwise, I'd have to copy all my dependencies over to a \bin
folder.
So after some iterations my Execute()
method, ended up like this:
public void Execute(Script script)
{
// set current dicrectory, import for NuGet.
Environment.CurrentDirectory = AppDomain.CurrentDomain.BaseDirectory;
var services = CreateScriptServices(script.UseMono, script.UseLogging);
var scriptExecutor = services.Executor;
var scriptPackResolver = services.ScriptPackResolver;
services.InstallationProvider.Initialize();
// prepare NuGet dependencies, download them if required
var assemblyPaths = PreparePackages(services.PackageAssemblyResolver,
services.PackageInstaller, PrepareAdditionalPackages(script.NuGetDependencies), script.LocalDependencies, services.Logger);
scriptExecutor.Initialize(assemblyPaths, scriptPackResolver.GetPacks());
scriptExecutor.ImportNamespaces(script.Namespaces);
scriptExecutor.AddReferences(script.LocalDependencies);
var scriptResult = scriptExecutor.ExecuteScript(script.ScriptContent, string.Empty);
if (script.UseLogging && scriptResult != null)
if (scriptResult.CompileExceptionInfo != null)
if (scriptResult.CompileExceptionInfo.SourceException != null)
services.Logger.Debug(scriptResult.CompileExceptionInfo.SourceException.Message);
scriptExecutor.Terminate();
}
I get the scriptcs dependencies wired up and served with CreateScriptServices()
. I pull out the ScriptExecutor
and the ScriptPackResolver
. One, important step, is to initialize the InstallationProvider
. In my case, the InstallationProvider
is a NuGetInstallationProvider
. If you forget to initialize you are given a ArgumentNullException
if you try to download a NuGet package with the PackageInstaller
. I still have my PreparePackages()
which looks like this. The functionality is the same, I just added some more logging and error handling.
I then initialize the ScriptExecutor
with the path to the assemblies, and the resolved script packs. I import additional namespaces and adds a reference to some local assemblies if necessary.
Finally, I call ExecuteScript()
on the ScriptExecutor
. If we're using logging, the result, errors or exceptions are sent to the console.
My Execute()
method now holds a lot more code, than in my initial attempt, but on the other hand I can save a bunch of code and complexity with this solution.
This, final solution, is how it is implemented in the upcoming scriptcs.rebus 0.5.0.
Tweetcomments powered by Disqus