From f5270d97b13df1d26ffac52e98463eda31b6213d Mon Sep 17 00:00:00 2001 From: Klavs Madsen Date: Sat, 21 Jan 2017 13:34:32 +0100 Subject: [PATCH] Added support for running C# code in a dotnet core environment --- environments/dotnet/Dockerfile | 9 ++ environments/dotnet/ExecutorModule.cs | 65 ++++++++++ environments/dotnet/FissionCompiler.cs | 59 +++++++++ environments/dotnet/Function.cs | 21 ++++ environments/dotnet/Program.cs | 30 +++++ environments/dotnet/README.md | 158 +++++++++++++++++++++++++ environments/dotnet/build.sh | 3 + environments/dotnet/project.json | 24 ++++ 8 files changed, 369 insertions(+) create mode 100644 environments/dotnet/Dockerfile create mode 100644 environments/dotnet/ExecutorModule.cs create mode 100644 environments/dotnet/FissionCompiler.cs create mode 100644 environments/dotnet/Function.cs create mode 100755 environments/dotnet/Program.cs create mode 100644 environments/dotnet/README.md create mode 100755 environments/dotnet/build.sh create mode 100755 environments/dotnet/project.json diff --git a/environments/dotnet/Dockerfile b/environments/dotnet/Dockerfile new file mode 100644 index 00000000..97582361 --- /dev/null +++ b/environments/dotnet/Dockerfile @@ -0,0 +1,9 @@ +FROM microsoft/dotnet:1.1.0-runtime + +WORKDIR /fission-workdir +COPY out . +EXPOSE 8888 + +ENTRYPOINT ["dotnet"] + +CMD ["fission-dotnet.dll"] \ No newline at end of file diff --git a/environments/dotnet/ExecutorModule.cs b/environments/dotnet/ExecutorModule.cs new file mode 100644 index 00000000..bb90cade --- /dev/null +++ b/environments/dotnet/ExecutorModule.cs @@ -0,0 +1,65 @@ +using Fission.DotNetCore.Compiler; +using System.Collections.Generic; +using Nancy; +using System.IO; +using System; + +namespace Fission.DotNetCore +{ + public class ExecutorModule : NancyModule + { + private const string CODE_PATH = "/userfunc/user"; + + private static Function _userFunc; + + public ExecutorModule() + { + Post("/specialize", args => Specialize()); + Get("/", _ => Run()); + Post("/", _ => Run()); + Put("/", _ => Run()); + Head("/", _ => Run()); + Options("/", _ => Run()); + Delete("/", _ => Run()); + + } + + private object Specialize() + { + var errors = new List(); + if (File.Exists(CODE_PATH)) + { + var code = File.ReadAllText(CODE_PATH); + _userFunc = FissionCompiler.Compile(code, out errors); + if (_userFunc == null) + { + var errstr = string.Join(Environment.NewLine, errors); + Console.WriteLine(errstr); + var response = (Response)errstr; + response.StatusCode = HttpStatusCode.InternalServerError; + return response; + + } + return null; + } + else + { + var response = (Response)"Unable to locate code"; + response.StatusCode = HttpStatusCode.InternalServerError; + return response; + } + } + + private object Run() + { + if (_userFunc == null) + { + var response = (Response)"Generic container: no requests supported"; + response.StatusCode = HttpStatusCode.InternalServerError; + return response; + } + var args = ((DynamicDictionary)Request.Query).ToDictionary(); + return _userFunc.Invoke(args); + } + } +} diff --git a/environments/dotnet/FissionCompiler.cs b/environments/dotnet/FissionCompiler.cs new file mode 100644 index 00000000..495a3ae1 --- /dev/null +++ b/environments/dotnet/FissionCompiler.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; + +namespace Fission.DotNetCore.Compiler +{ + //adapted from this article http://www.tugberkugurlu.com/archive/compiling-c-sharp-code-into-memory-and-executing-it-with-roslyn + class FissionCompiler + { + public static Function Compile(string code, out List errors) { + errors = new List(); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code); + + string assemblyName = Path.GetRandomFileName(); + MetadataReference[] references = new MetadataReference[] + { + MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location) + }; + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName, + syntaxTrees: new[] { syntaxTree }, + references: references, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + using (var ms = new MemoryStream()) + { + EmitResult result = compilation.Emit(ms); + + if (!result.Success) + { + IEnumerable failures = result.Diagnostics.Where(diagnostic => + diagnostic.IsWarningAsError || + diagnostic.Severity == DiagnosticSeverity.Error); + + foreach (Diagnostic diagnostic in failures) + { + errors.Add($"{diagnostic.Id}: {diagnostic.GetMessage()}"); + } + } + else + { + ms.Seek(0, SeekOrigin.Begin); + + Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(ms); + var type = assembly.GetType("Fission"); + var info = type.GetMember("Run").First() as MethodInfo; + return new Function(info); + } + } + return null; + } + } +} \ No newline at end of file diff --git a/environments/dotnet/Function.cs b/environments/dotnet/Function.cs new file mode 100644 index 00000000..f60f6928 --- /dev/null +++ b/environments/dotnet/Function.cs @@ -0,0 +1,21 @@ +using System; +using System.Reflection; +using System.Collections.Generic; + +namespace Fission.DotNetCore.Compiler +{ + class Function + { + private readonly MethodInfo _info; + public Function(MethodInfo info) + { + if (info == null) throw new ArgumentNullException(nameof(info)); + _info = info; + } + + public object Invoke(Dictionary args) + { + return _info.Invoke(null, new[] { args }); + } + } +} \ No newline at end of file diff --git a/environments/dotnet/Program.cs b/environments/dotnet/Program.cs new file mode 100755 index 00000000..9206ddf5 --- /dev/null +++ b/environments/dotnet/Program.cs @@ -0,0 +1,30 @@ +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Nancy.Owin; + +namespace Fission.DotNetCore +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseKestrel() + .UseUrls("http://*:8888") + .UseStartup() + .Build(); + host.Run(); + } + + + } + public class Startup + { + public void Configure(IApplicationBuilder app) + { + app.UseOwin(x => x.UseNancy()); + } + } +} \ No newline at end of file diff --git a/environments/dotnet/README.md b/environments/dotnet/README.md new file mode 100644 index 00000000..85313a00 --- /dev/null +++ b/environments/dotnet/README.md @@ -0,0 +1,158 @@ +# Fission: dotnet C# Environment + +This is a simple dotnet C# environment for Fission. + +It's a Docker image containing the dotnet 1.1.0 runtime. The image +uses Kestrel with Nancy to host the internal web server and uses +Roslyn to compile the uploaded code. + +The image supports compiling and running code with types defined in +mscorlib and does not at present support other library references. +One workaround for this would be to add the references to this project's +project.json file and rebuild the container. + +The environment works via convention where you create a C# class +called Fission which has a static method named Run taking a single +parameter, a dictionary containing any querystring parameters. + +Example of simplest possible class to be executed: + +``` +using System; +using System.Collections.Generic; + +public class Fission { + public static string Run(Dictionary args) { + return null; + } +} +``` + +Please see examples below. + +## Rebuilding and pushing the image + +To rebuild the image you need either a computer with dotnet 1.1.0 +installed or else you will have to map the source directory into a +container containing the dotnet 1.1.0 environment. + +### Locally installed Dotnet 1.1.0 + +Simply move to the source directory in a terminal and run the ./build.sh script. + +The script will restore dependencies, compile a release build and +and build the container. If you need to change the name of the container +simply change it in the script. + +After the build finishes push the new image to a Docker registry using the +standard procedure. + +### Build in a container + +Move to the directory containing the source and start the Docker container +with dotnet and mount the current directory to a build location: + +``` +docker run -it --rm -v $PWD:/build microsoft/dotnet +``` + +Move to the build directory inside the container and restore the packages: + +``` +cd /build +dotnet restore +log : Restoring packages for /source/project.json... +log : Installing System.Net.WebSockets 4.0.0. +log : Installing runtime.native.System.IO.Compression 4.1.0. +... +``` + +Compile and publish a release build of the source to the 'out' folder: + +``` +dotnet publish -c Release -o out +Publishing source for .NETCoreApp,Version +... +``` +Exit the build container and build the Docker container on the local host: + +``` +exit +docker build -t USER/dotnet-env . +``` +After the build finishes push the new image to a Docker registry using the +standard procedure. + +## Echo example + +### Setup fission environment +First you need to setup the fission according to your cluster setup as +specified here: https://github.com/fission/fission + + +### Create the class to run + +Secondly you need to create a file /tmp/func.cs containing the following code: + +``` +using System; +using System.Collections.Generic; + +public class Fission { + public static string Run(Dictionary args) { + return (string)args["text"]; + } +} +``` +### Run the example + +Lastly to run the example: + +``` +$ fission env create --name dotnet --image fission/dotnet-env + +$ fission function create --name echo --env dotnet --code /tmp/func.cs + +$ fission route create --method GET --url /echo --function echo + +$ curl http://$FISSION_ROUTER/echo?text=hello%20world! + hello world +``` + +## Addition service example + +### Setup fission environment +First you need to setup the fission according to your cluster setup as +specified here: https://github.com/fission/fission + + +### Create the class to run + +Secondly you need to create a file /tmp/func.cs containing the following code: + +``` +using System; +using System.Collections.Generic; + +public class Fission { + public static string Run(Dictionary args) { + var x = Convert.ToInt32(args["x"]); + var y = Convert.ToInt32(args["y"]); + return (x+y).ToString(); + } +} +``` +### Run the example + +Lastly to run the example: + +``` +$ fission env create --name dotnet --image fission/dotnet-env + +$ fission function create --name addition --env dotnet --code /tmp/func.cs + +$ fission route create --method GET --url /add --function addition + +$ curl "http://$FISSION_ROUTER/add?x=30&y=12" + 42 +``` diff --git a/environments/dotnet/build.sh b/environments/dotnet/build.sh new file mode 100755 index 00000000..dc391208 --- /dev/null +++ b/environments/dotnet/build.sh @@ -0,0 +1,3 @@ +dotnet restore +dotnet publish -c Release -o out +docker build -t fission/dotnet-env . \ No newline at end of file diff --git a/environments/dotnet/project.json b/environments/dotnet/project.json new file mode 100755 index 00000000..2c1ea729 --- /dev/null +++ b/environments/dotnet/project.json @@ -0,0 +1,24 @@ +{ + "version": "1.0.0-*", + "buildOptions": { + "emitEntryPoint": true, + "outputName": "fission-dotnet" + }, + + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.1.0", + "type": "platform" + }, + "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.Owin": "1.0.0", + "Nancy": "2.0.0-barneyrubble", + "Microsoft.CodeAnalysis.CSharp": "1.3.0-*", + "System.Runtime.Loader": "4.0.0-*", + }, + "frameworks": { + "netcoreapp1.0": { + "imports": "dnxcore50" + } + } +} \ No newline at end of file