diff --git a/environments/dotnet/.gitignore b/environments/dotnet/.gitignore new file mode 100644 index 00000000..8a82e2f6 --- /dev/null +++ b/environments/dotnet/.gitignore @@ -0,0 +1,6 @@ +bin/* +obj/* +out/* +.vscode/* +project.lock* +TODO_dotnet \ No newline at end of file diff --git a/environments/dotnet/ExecutorModule.cs b/environments/dotnet/ExecutorModule.cs index bb90cade..ef1d7fd2 100644 --- a/environments/dotnet/ExecutorModule.cs +++ b/environments/dotnet/ExecutorModule.cs @@ -1,4 +1,5 @@ using Fission.DotNetCore.Compiler; +using Fission.DotNetCore.Api; using System.Collections.Generic; using Nancy; using System.IO; @@ -8,9 +9,13 @@ 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() { @@ -21,7 +26,6 @@ namespace Fission.DotNetCore Head("/", _ => Run()); Options("/", _ => Run()); Delete("/", _ => Run()); - } private object Specialize() @@ -34,7 +38,7 @@ namespace Fission.DotNetCore if (_userFunc == null) { var errstr = string.Join(Environment.NewLine, errors); - Console.WriteLine(errstr); + _logger.WriteError(errstr); var response = (Response)errstr; response.StatusCode = HttpStatusCode.InternalServerError; return response; @@ -44,7 +48,9 @@ namespace Fission.DotNetCore } else { - var response = (Response)"Unable to locate code"; + var errstr = $"Unable to locate code at '{CODE_PATH}'"; + _logger.WriteError(errstr); + var response = (Response)errstr; response.StatusCode = HttpStatusCode.InternalServerError; return response; } @@ -59,7 +65,18 @@ namespace Fission.DotNetCore return response; } var args = ((DynamicDictionary)Request.Query).ToDictionary(); - return _userFunc.Invoke(args); + 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; + } } } } + diff --git a/environments/dotnet/FissionCompiler.cs b/environments/dotnet/FissionCompiler.cs index 495a3ae1..99f236cd 100644 --- a/environments/dotnet/FissionCompiler.cs +++ b/environments/dotnet/FissionCompiler.cs @@ -12,31 +12,46 @@ 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) { + 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[] + + var coreDir = Directory.GetParent(typeof(Enumerable).GetTypeInfo().Assembly.Location); + + List references = new List { - MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location) + 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)); - + options: new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + optimizationLevel: OptimizationLevel.Release)); + 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); + IEnumerable failures = result.Diagnostics.Where(diagnostic => + diagnostic.IsWarningAsError || + diagnostic.Severity == DiagnosticSeverity.Error).ToList(); foreach (Diagnostic diagnostic in failures) { @@ -46,14 +61,14 @@ namespace Fission.DotNetCore.Compiler 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); + var type = assembly.GetType("FissionFunction"); + var info = type.GetMember("Execute").First() as MethodInfo; + return new Function(assembly, type, info); } } return null; } } -} \ No newline at end of file +} diff --git a/environments/dotnet/FissionContext.cs b/environments/dotnet/FissionContext.cs new file mode 100644 index 00000000..977444c7 --- /dev/null +++ b/environments/dotnet/FissionContext.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace Fission.DotNetCore.Api +{ + public class FissionContext + { + public FissionContext(Dictionary 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 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 + } +} \ No newline at end of file diff --git a/environments/dotnet/Function.cs b/environments/dotnet/Function.cs index f60f6928..f8b5f724 100644 --- a/environments/dotnet/Function.cs +++ b/environments/dotnet/Function.cs @@ -1,21 +1,28 @@ using System; using System.Reflection; -using System.Collections.Generic; +using Fission.DotNetCore.Api; namespace Fission.DotNetCore.Compiler { class Function { + private readonly Assembly _assembly; + private readonly Type _type; private readonly MethodInfo _info; - public Function(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(Dictionary args) + public object Invoke(FissionContext context) { - return _info.Invoke(null, new[] { args }); + return _info.Invoke(_assembly.CreateInstance(_type.FullName), new[] { context }); } } -} \ No newline at end of file +} diff --git a/environments/dotnet/Program.cs b/environments/dotnet/Program.cs index 9206ddf5..85dc1628 100755 --- a/environments/dotnet/Program.cs +++ b/environments/dotnet/Program.cs @@ -13,18 +13,9 @@ namespace Fission.DotNetCore .UseContentRoot(Directory.GetCurrentDirectory()) .UseKestrel() .UseUrls("http://*:8888") - .UseStartup() + .Configure(app => app.UseOwin(x => x.UseNancy())) .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 index 85313a00..9f012558 100644 --- a/environments/dotnet/README.md +++ b/environments/dotnet/README.md @@ -12,19 +12,22 @@ 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. +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 System.Collections.Generic; +using Fission.DotNetCore.Api; -public class Fission { - public static string Run(Dictionary args) { - return null; - } +public class FissionFunction { + public string Execute(FissionContext context) { + return null; + } } ``` @@ -96,12 +99,14 @@ Secondly you need to create a file /tmp/func.cs containing the following code: ``` using System; -using System.Collections.Generic; +using Fission.DotNetCore.Api; -public class Fission { - public static string Run(Dictionary args) { - return (string)args["text"]; - } +public class FissionFunction +{ + public string Execute(FissionContext context){ + context.Logger.WriteInfo("executing.. {0}", context.Arguments["text"]); + return (string)context.Arguments["text"]; + } } ``` ### Run the example @@ -132,14 +137,15 @@ Secondly you need to create a file /tmp/func.cs containing the following code: ``` using System; -using System.Collections.Generic; +using Fission.DotNetCore.Api; -public class Fission { - public static string Run(Dictionary args) { - var x = Convert.ToInt32(args["x"]); - var y = Convert.ToInt32(args["y"]); +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 @@ -156,3 +162,37 @@ $ 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 +``` diff --git a/environments/dotnet/build.sh b/environments/dotnet/build.sh index dc391208..b6622ac3 100755 --- a/environments/dotnet/build.sh +++ b/environments/dotnet/build.sh @@ -1,3 +1,3 @@ dotnet restore dotnet publish -c Release -o out -docker build -t fission/dotnet-env . \ No newline at end of file +docker build -t fission/dotnet-env .