Merge pull request #84 from ktrance/master
Added support for running C# code in a dotnet core environment
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
bin/*
|
||||||
|
obj/*
|
||||||
|
out/*
|
||||||
|
.vscode/*
|
||||||
|
project.lock*
|
||||||
|
TODO_dotnet
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
FROM microsoft/dotnet:1.1.0-runtime
|
||||||
|
|
||||||
|
WORKDIR /fission-workdir
|
||||||
|
COPY out .
|
||||||
|
EXPOSE 8888
|
||||||
|
|
||||||
|
ENTRYPOINT ["dotnet"]
|
||||||
|
|
||||||
|
CMD ["fission-dotnet.dll"]
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using Fission.DotNetCore.Compiler;
|
||||||
|
using Fission.DotNetCore.Api;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Nancy;
|
||||||
|
using System.IO;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Fission.DotNetCore
|
||||||
|
{
|
||||||
|
public class ExecutorModule : NancyModule
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
private const string CODE_PATH = "/tmp/func.cs";
|
||||||
|
#else
|
||||||
|
private const string CODE_PATH = "/userfunc/user";
|
||||||
|
#endif
|
||||||
|
private static Function _userFunc;
|
||||||
|
private static Logger _logger = new Logger();
|
||||||
|
|
||||||
|
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<string>();
|
||||||
|
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);
|
||||||
|
_logger.WriteError(errstr);
|
||||||
|
var response = (Response)errstr;
|
||||||
|
response.StatusCode = HttpStatusCode.InternalServerError;
|
||||||
|
return response;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var errstr = $"Unable to locate code at '{CODE_PATH}'";
|
||||||
|
_logger.WriteError(errstr);
|
||||||
|
var response = (Response)errstr;
|
||||||
|
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();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _userFunc.Invoke(new FissionContext(args, new Logger()));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.WriteError(e.ToString());
|
||||||
|
var response = (Response)e.Message;
|
||||||
|
response.StatusCode = HttpStatusCode.BadRequest;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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<string> errors)
|
||||||
|
{
|
||||||
|
errors = new List<string>();
|
||||||
|
|
||||||
|
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code);
|
||||||
|
|
||||||
|
string assemblyName = Path.GetRandomFileName();
|
||||||
|
|
||||||
|
var coreDir = Directory.GetParent(typeof(Enumerable).GetTypeInfo().Assembly.Location);
|
||||||
|
|
||||||
|
List<MetadataReference> references = new List<MetadataReference>
|
||||||
|
{
|
||||||
|
MetadataReference.CreateFromFile(coreDir.FullName + Path.DirectorySeparatorChar + "mscorlib.dll"),
|
||||||
|
MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
|
||||||
|
MetadataReference.CreateFromFile(Assembly.GetEntryAssembly().Location)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var referencedAssembly in Assembly.GetEntryAssembly().GetReferencedAssemblies())
|
||||||
|
{
|
||||||
|
var assembly = Assembly.Load(referencedAssembly);
|
||||||
|
references.Add(MetadataReference.CreateFromFile(assembly.Location));
|
||||||
|
}
|
||||||
|
|
||||||
|
CSharpCompilation compilation = CSharpCompilation.Create(
|
||||||
|
assemblyName,
|
||||||
|
syntaxTrees: new[] { syntaxTree },
|
||||||
|
references: references,
|
||||||
|
options: new CSharpCompilationOptions(
|
||||||
|
OutputKind.DynamicallyLinkedLibrary,
|
||||||
|
optimizationLevel: OptimizationLevel.Release));
|
||||||
|
|
||||||
|
using (var ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
EmitResult result = compilation.Emit(ms);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
|
||||||
|
diagnostic.IsWarningAsError ||
|
||||||
|
diagnostic.Severity == DiagnosticSeverity.Error).ToList();
|
||||||
|
|
||||||
|
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("FissionFunction");
|
||||||
|
var info = type.GetMember("Execute").First() as MethodInfo;
|
||||||
|
return new Function(assembly, type, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Fission.DotNetCore.Api
|
||||||
|
{
|
||||||
|
public class FissionContext
|
||||||
|
{
|
||||||
|
public FissionContext(Dictionary<string, object> args, Logger logger)
|
||||||
|
{
|
||||||
|
if (args == null) throw new ArgumentNullException(nameof(args));
|
||||||
|
if (logger == null) throw new ArgumentNullException(nameof(logger));
|
||||||
|
Arguments = args;
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, object> Arguments { get; private set; }
|
||||||
|
public Logger Logger { get; private set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Logger
|
||||||
|
{
|
||||||
|
public void Write(Severity severity, string format, params object[] args)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{DateTime.Now.ToString("MM/dd/yy H:mm:ss zzz")} {severity}: " + format, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteInfo(string format, params object[] args){
|
||||||
|
Write(Severity.Info, format, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteWarning(string format, params object[] args){
|
||||||
|
Write(Severity.Warning, format, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteError(string format, params object[] args){
|
||||||
|
Write(Severity.Error, format, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteCritical(string format, params object[] args){
|
||||||
|
Write(Severity.Critical, format, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteVerbose(string format, params object[] args){
|
||||||
|
Write(Severity.Verbose, format, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Severity
|
||||||
|
{
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
Critical,
|
||||||
|
Verbose
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using Fission.DotNetCore.Api;
|
||||||
|
|
||||||
|
namespace Fission.DotNetCore.Compiler
|
||||||
|
{
|
||||||
|
class Function
|
||||||
|
{
|
||||||
|
private readonly Assembly _assembly;
|
||||||
|
private readonly Type _type;
|
||||||
|
private readonly MethodInfo _info;
|
||||||
|
|
||||||
|
public Function(Assembly assembly, Type type, MethodInfo info)
|
||||||
|
{
|
||||||
|
if (info == null) throw new ArgumentNullException(nameof(info));
|
||||||
|
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||||
|
if (type == null) throw new ArgumentNullException(nameof(type));
|
||||||
|
_assembly = assembly;
|
||||||
|
_type = type;
|
||||||
|
_info = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Invoke(FissionContext context)
|
||||||
|
{
|
||||||
|
return _info.Invoke(_assembly.CreateInstance(_type.FullName), new[] { context });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
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")
|
||||||
|
.Configure(app => app.UseOwin(x => x.UseNancy()))
|
||||||
|
.Build();
|
||||||
|
host.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# 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 FissionFunction which has a method named Execute taking a single
|
||||||
|
parameter, a FissionContext object.
|
||||||
|
|
||||||
|
The FissionContext object gives access to the arguments and other items
|
||||||
|
like logging. Please see FissionContext.cs for public API.
|
||||||
|
|
||||||
|
Example of simplest possible class to be executed:
|
||||||
|
|
||||||
|
```
|
||||||
|
using System;
|
||||||
|
using Fission.DotNetCore.Api;
|
||||||
|
|
||||||
|
public class FissionFunction {
|
||||||
|
public string Execute(FissionContext context) {
|
||||||
|
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 Fission.DotNetCore.Api;
|
||||||
|
|
||||||
|
public class FissionFunction
|
||||||
|
{
|
||||||
|
public string Execute(FissionContext context){
|
||||||
|
context.Logger.WriteInfo("executing.. {0}", context.Arguments["text"]);
|
||||||
|
return (string)context.Arguments["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 Fission.DotNetCore.Api;
|
||||||
|
|
||||||
|
public class FissionFunction
|
||||||
|
{
|
||||||
|
public string Execute(FissionContext context){
|
||||||
|
var x = Convert.ToInt32(context.Arguments["x"]);
|
||||||
|
var y = Convert.ToInt32(context.Arguments["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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing/debugging the enviroment locally
|
||||||
|
|
||||||
|
The easiest way to debug the environment is to open the directory in
|
||||||
|
Visual Studio Code (VSCode) as that will setup debugger for you the
|
||||||
|
first time.
|
||||||
|
|
||||||
|
Remember to install the excellent extension
|
||||||
|
"C# for Visual Studio Code(powered by OmniSharp)" to get statement completion
|
||||||
|
|
||||||
|
The class ExecutorModule contain preprocessor directive overriding where
|
||||||
|
the input code file should be found:
|
||||||
|
|
||||||
|
```
|
||||||
|
#if DEBUG
|
||||||
|
private const string CODE_PATH = "/tmp/func.cs";
|
||||||
|
#else
|
||||||
|
private const string CODE_PATH = "/userfunc/user";
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
So what you need to do is:
|
||||||
|
1. Open the directory in VSCode.
|
||||||
|
This will prompt restore of packages and query is debugger setup is needed. Accept both prompts.
|
||||||
|
2. Press F5 to start the web server. Set breakpoints etc..
|
||||||
|
3. Add a code file containing valid C# at /tmp/func.cs
|
||||||
|
4. Specialize the service with curl via post
|
||||||
|
```
|
||||||
|
$ curl -XPOST http://localhost:8888/specialize
|
||||||
|
```
|
||||||
|
5. Call your function with curl
|
||||||
|
```
|
||||||
|
$ curl -XGET http://localhost:8888
|
||||||
|
```
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
dotnet restore
|
||||||
|
dotnet publish -c Release -o out
|
||||||
|
docker build -t fission/dotnet-env .
|
||||||
Executable
+24
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user