From a893431e1c1e58d500b840cc415e39212c8256f9 Mon Sep 17 00:00:00 2001 From: rohit-joy Date: Thu, 23 Mar 2017 14:07:44 -0700 Subject: [PATCH] Adding code for RInterop v2.0 (#2) * Adding code for RInterop v2.0 * Updates * Configuration updates * Readme update. * Readme update. * Update README.md * Update README.md --- Logging/ILogger.cs | 9 +++ Logging/Logging.csproj | 58 +++++++++++++++ Logging/Properties/AssemblyInfo.cs | 35 +++++++++ {RInterop => Logging}/TraceLogger.cs | 72 +++++++++--------- README.md | 67 ++++++++++------- RInterop.sln | 6 ++ RInterop/Bootstrapper.cs | 105 +++++++++++++++++++++++++++ RInterop/CommandLineOptions.cs | 45 +++++++++--- RInterop/Config.cs | 2 +- RInterop/DependencyFactory.cs | 27 ++----- RInterop/GlobalAssemblyInfo.cs | 4 +- RInterop/IBootstrapper.cs | 7 ++ RInterop/ILogger.cs | 9 --- RInterop/ILoggerFactory.cs | 9 --- RInterop/IR.cs | 10 +-- RInterop/Models.bond | 21 ++++++ RInterop/Program.cs | 95 ++---------------------- RInterop/Properties/AssemblyInfo.cs | 6 +- RInterop/R.cs | 72 +++++++++--------- RInterop/REngineWrapper.cs | 84 +++++++++++++++------ RInterop/RInterop.csproj | 62 +++++++++------- RInterop/SerializationTypeMaps.bond | 9 --- RInterop/Service.cs | 64 ++++++++++++++++ RInterop/TraceLoggerFactory.cs | 15 ---- RInterop/app.config | 56 ++++++++++++++ RInterop/packages.config | 8 +- 26 files changed, 626 insertions(+), 331 deletions(-) create mode 100644 Logging/ILogger.cs create mode 100644 Logging/Logging.csproj create mode 100644 Logging/Properties/AssemblyInfo.cs rename {RInterop => Logging}/TraceLogger.cs (84%) create mode 100644 RInterop/Bootstrapper.cs create mode 100644 RInterop/IBootstrapper.cs delete mode 100644 RInterop/ILogger.cs delete mode 100644 RInterop/ILoggerFactory.cs create mode 100644 RInterop/Models.bond delete mode 100644 RInterop/SerializationTypeMaps.bond create mode 100644 RInterop/Service.cs delete mode 100644 RInterop/TraceLoggerFactory.cs diff --git a/Logging/ILogger.cs b/Logging/ILogger.cs new file mode 100644 index 0000000..583030f --- /dev/null +++ b/Logging/ILogger.cs @@ -0,0 +1,9 @@ +namespace Logging +{ + public interface ILogger + { + void LogInformation(string message); + void LogError(string message); + void Close(); + } +} diff --git a/Logging/Logging.csproj b/Logging/Logging.csproj new file mode 100644 index 0000000..6db58e9 --- /dev/null +++ b/Logging/Logging.csproj @@ -0,0 +1,58 @@ + + + + + Debug + AnyCPU + {DA0E6A71-5A37-4EE6-87A4-C82C93EF908D} + Library + Properties + Logging + Logging + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + GlobalAssemblyInfo.cs + + + + + + + + \ No newline at end of file diff --git a/Logging/Properties/AssemblyInfo.cs b/Logging/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ec7b086 --- /dev/null +++ b/Logging/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Logging")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Logging")] +//[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("da0e6a71-5a37-4ee6-87a4-c82c93ef908d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +//[assembly: AssemblyVersion("1.0.0.0")] +//[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/RInterop/TraceLogger.cs b/Logging/TraceLogger.cs similarity index 84% rename from RInterop/TraceLogger.cs rename to Logging/TraceLogger.cs index e704bfa..ef4fc80 100644 --- a/RInterop/TraceLogger.cs +++ b/Logging/TraceLogger.cs @@ -6,12 +6,10 @@ using System.IO; using System.Security.AccessControl; using System.Security.Principal; -namespace RInterop +namespace Logging { public class TraceLogger : ILogger { - private TraceSource TraceSource { get; } - public TraceLogger(string rootFolder, string prefix) { TraceSource = new TraceSource(prefix, SourceLevels.All); @@ -21,8 +19,8 @@ namespace RInterop string logFilePath = Path.Combine(rootFolder, string.Format(CultureInfo.InvariantCulture, "{0}_{1}.log", - prefix, - DateTime.UtcNow.Ticks)); + prefix, + DateTime.UtcNow.Ticks)); Trace.AutoFlush = true; @@ -65,7 +63,9 @@ namespace RInterop newLogFilename = f.FullName; break; } - catch { } // This log file is in use -- try next + catch + { + } // This log file is in use -- try next } // If we didn't find a reusable log file, we'll create a new using the next available log file number @@ -77,17 +77,20 @@ namespace RInterop Path.GetDirectoryName(Path.Combine(Environment.CurrentDirectory, logFilePath)), Path.GetFileNameWithoutExtension(logFilePath), maxLogFileNumber.ToString("000", CultureInfo.InvariantCulture), - Path.GetExtension(logFilePath)); // e.g. "logFilename.001.log" + Path.GetExtension(logFilePath)); // e.g. "logFilename.001.log" } - if (File.Exists(newLogFilename)) { File.Delete(newLogFilename); } + if (File.Exists(newLogFilename)) + { + File.Delete(newLogFilename); + } // Hook up the log file as a trace listener FileStream fstream = new FileStream(newLogFilename, FileMode.Create, FileSystemRights.WriteData, FileShare.Read, - (int)Math.Pow(2, 10), + (int) Math.Pow(2, 10), FileOptions.None, GetFileSecurity()); @@ -96,6 +99,26 @@ namespace RInterop TraceSource.Listeners.Add(traceListener); } + private TraceSource TraceSource { get; } + + public void LogInformation(string message) + { + TraceSource.TraceEvent(TraceEventType.Information, 0, string.Format(CultureInfo.InvariantCulture, "{0:O} {1}", DateTime.UtcNow, + message)); + } + + public void LogError(string message) + { + TraceSource.TraceEvent(TraceEventType.Error, 0, string.Format(CultureInfo.InvariantCulture, "{0:O} {1}", DateTime.UtcNow, + message)); + } + + public void Close() + { + TraceSource.Flush(); + TraceSource.Close(); + } + public static FileSecurity GetFileSecurity() { var fss = new FileSecurity(); @@ -154,41 +177,18 @@ namespace RInterop return fss; } - - public void LogInformation(string message, params object[] parameters) - { - TraceSource.TraceInformation(string.Format(CultureInfo.InvariantCulture, "{0:O} {1}", DateTime.UtcNow, message, parameters)); - } - - public void Close() - { - TraceSource.Flush(); - TraceSource.Close(); - } - - private string GetCallingFunction() - { - StackFrame frame = new StackTrace().GetFrame(3); - var declaringType = frame.GetMethod().DeclaringType; - if (declaringType != null) - { - string callingFunction = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", declaringType.FullName, frame.GetMethod().Name); - return callingFunction; - } - return string.Empty; - } } /// /// Used to sort an array of FileInfo objects by filename. /// - class CompareFileInfoNames : IComparer + internal class CompareFileInfoNames : IComparer { public int Compare(object x, object y) { - FileInfo firstFile = (FileInfo)x; - FileInfo secondFile = (FileInfo)y; + FileInfo firstFile = (FileInfo) x; + FileInfo secondFile = (FileInfo) y; return string.CompareOrdinal(firstFile.FullName, secondFile.FullName); } } -} +} \ No newline at end of file diff --git a/README.md b/README.md index 964ff57..c5341c5 100644 --- a/README.md +++ b/README.md @@ -2,50 +2,63 @@ .NET managed abstraction layer for communicating with R ## Installation -#### [x86]: -#### [x64]: +Please note that there are breaking changes in terms of how RInterop works as well as command line arguments it takes in to start up. +#### ## Command-line arguments -```sh +``` --schema Path to schema binary file containing types to serialize and deserialize input data and output data sent to and received from the R package, respectively --s Same as -schema --rpackage Path to R package file containing statistical functions (optional if packages are already installed) --r Same as --rpackage + +--typemap Mapping from R function to input and output type schema. +--t Same as --typemap ``` ## Example -```sh -RInterop.exe --r C:\RPackages\MyStatsPackage_0.1.zip --s C:\RPackages\Schemas.dll +``` +RInterop.exe --r C:\RPackages\MyStatsPackage_0.1.zip --s C:\RPackages\Schemas.dll --t C:\RPackages\TypeMap.json ``` ## Overview R Interop starts a WCF service with named-pipe endpoints for inter process communication. Your application talks to R Interop through the named pipe. The following are the steps involved: -1. Client application sends request to R Interop as an Input object -2. R Interop serializes the input object and sends the request to R package loaded in R -3. R function evaluates the input object and returns the output -4. R Interop deserializes the output and returns to client application +1. Client application sends request to R Interop as an Input object. +2. R Interop serializes the input object and sends the request to R package loaded in R. +3. R function evaluates the input object and returns the output. +4. R Interop deserializes the output and returns it to client application. + +Note that both the client application and RInterop utilize the same Schemas library so that they can understand each other. ## Named pipe endpoints -R Interop makes available 3 endpoints. +R Interop makes available 2 endpoints. #### net.pipe://RInterop/ -Metadata endpoint for generating a service reference - -#### net.pipe://RInterop/Initialize -R Interop communicates with R through a simple JSON serialization/deserialization contract. The contract with R package expects an input type, an output type and the R function name, at minimum. There are two dictionaries - one for Input and one for Output - that enable this contract to be successfully created between the client application and R Interop. The key for the dictionary is the R function name. The value is the type provided in the class library passed as the --schema parameter. - -For example, use the following code to initialize the type mapping for the R function. -```sh -Dictionary inputMap["DistributionTest"] = "Schemas.TTest.Input"; -Dictionary outputMap["DistributionTest"] = "Schemas.TTest.Output"; - -RClient client = new RClient("NetNamedPipeBinding_IR1"); -client.Initialize(inputMap, outputMap); -client.Close(); -``` - -Call the Initialize endpoint to pass the input and output type Dictionary mapping for each of your R functions. +Metadata exchange (MEX) endpoint for generating a service reference within the client or caller application. #### net.pipe://RInterop/Execute -Executes the R function provided in the Input object with members describing the parameters. Returns the result as the Output type. The types are as described in the dictionaries initialized when calling the endpoint net.pipe://RInterop/Initialize +Executes the R function provided in the Input object with members describing the parameters. Returns the result as the corresponding Output type. The types are as described in the dictionaries provided in TypeMap.json that map to the types in the Schemas assembly. + +The format of TypeMap.json is as follows: + +``` +{ + "Mapping": [ + { + "Function": "TTest", + "InputType": "Schemas.TTest.Input", + "OutputType": "Schemas.TTest.Output" + }, + { + "Function": "ChiSquareTest", + "InputType": "Schemas.ChiSquareTest.Input", + "OutputType": "Schemas.ChiSquareTest.Output" + } + ] +} +``` + +## Windows events for developers +When RInterop starts up successfully, it will fire an event named ```Global\RInteropStarted```. +When RInterop is unable to start up successfully, it will fire an event named ```Global\RInteropStartupError``` and exit. The logs located in the ```%temp%\RInterop``` folder will help diagnose and/or resolve issues. Feel free to post questions on Github. diff --git a/RInterop.sln b/RInterop.sln index 67a2773..e5152af 100644 --- a/RInterop.sln +++ b/RInterop.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RInterop", "RInterop\RInterop.csproj", "{EC24722E-AEC0-42D9-B5ED-300137236355}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{DA0E6A71-5A37-4EE6-87A4-C82C93EF908D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {EC24722E-AEC0-42D9-B5ED-300137236355}.Debug|Any CPU.Build.0 = Debug|Any CPU {EC24722E-AEC0-42D9-B5ED-300137236355}.Release|Any CPU.ActiveCfg = Release|Any CPU {EC24722E-AEC0-42D9-B5ED-300137236355}.Release|Any CPU.Build.0 = Release|Any CPU + {DA0E6A71-5A37-4EE6-87A4-C82C93EF908D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA0E6A71-5A37-4EE6-87A4-C82C93EF908D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA0E6A71-5A37-4EE6-87A4-C82C93EF908D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA0E6A71-5A37-4EE6-87A4-C82C93EF908D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/RInterop/Bootstrapper.cs b/RInterop/Bootstrapper.cs new file mode 100644 index 0000000..a6ca275 --- /dev/null +++ b/RInterop/Bootstrapper.cs @@ -0,0 +1,105 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using Logging; +using Newtonsoft.Json; + +namespace RInterop +{ + public class Bootstrapper : IBootstrapper + { + private readonly ILogger _logger = DependencyFactory.Resolve(); + + public void Start(string[] args) + { + if (args == null) throw new ArgumentNullException(nameof(args)); + + _logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Working directory: {0}", + Environment.CurrentDirectory)); + _logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Arguments: {0}", string.Join(" ", args))); + + var options = new CommandLineOptions(); + if (!CommandLineOptions.ParseArguments(args, options)) + { + _logger.LogError(string.Format(CultureInfo.InvariantCulture, "Invalid arguments {0}", + string.Join(" ", args))); + LogUsage(); + return; + } + + Config.SchemaBinaryPath = options.SchemaBinaryPath; + Assembly assembly = Assembly.LoadFrom(Config.SchemaBinaryPath); + + if (!string.IsNullOrEmpty(options.RPackagePath)) + { + try + { + var filename = Path.GetFileName(options.RPackagePath); + if (!string.IsNullOrEmpty(filename)) + Config.RPackageName = filename.Substring(0, filename.IndexOf("_", StringComparison.Ordinal)); + } + catch (Exception exception) + { + _logger.LogError(string.Format(CultureInfo.InvariantCulture, + "Exiting. Exception while extracting R package name: {0}", exception)); + LogUsage(); + return; + } + + try + { + REngineWrapper.InstallPackages(options.RPackagePath); + } + catch (Exception exception) + { + EventWaitHandle startupErrorEvent = new EventWaitHandle(false, EventResetMode.ManualReset, + @"Global\RInteropStartupError"); + startupErrorEvent.Set(); + _logger.LogError(string.Format(CultureInfo.InvariantCulture, + "Exiting. Exception while installing packages: {0}", exception)); + return; + } + } + + if (!string.IsNullOrEmpty(options.TypeMapJsonPath)) + { + Config.SerializationTypeMap = new SerializationTypeMap(); + if (!string.IsNullOrEmpty(options.TypeMapJsonPath)) + { + TypeMap o = + JsonConvert.DeserializeObject(new StreamReader(options.TypeMapJsonPath).ReadToEnd()); + + foreach (Map i in o.Mapping) + { + DependencyFactory.Resolve() + .LogInformation(string.Format(CultureInfo.InvariantCulture, + "Got input type mapping: {0} > {1} > {2}", i.Function, i.InputType, i.OutputType)); + Config.SerializationTypeMap.InputTypeMap[i.Function] = assembly + .GetTypes() + .First(a => a.FullName.Equals(i.InputType)); + Config.SerializationTypeMap.OutputTypeMap[i.Function] = assembly + .GetTypes() + .First(a => a.FullName.Equals(i.OutputType)); + } + } + } + + using (var service = new Service()) + { + service.Start(); + service.StartedEvent.WaitOne(); + } + } + + private void LogUsage() + { + _logger.LogInformation( + @"Usage: RInterop.exe --s """" --r """""); + _logger.LogInformation( + @"Example: RInterop.exe --s ""C:\Temp\Schemas.dll"" --r ""C:\Temp\RPackage.zip"" --t ""C:\Temp\TypeMap.json"""); + } + } +} \ No newline at end of file diff --git a/RInterop/CommandLineOptions.cs b/RInterop/CommandLineOptions.cs index 19fe999..34d6e61 100644 --- a/RInterop/CommandLineOptions.cs +++ b/RInterop/CommandLineOptions.cs @@ -1,44 +1,65 @@ -using CommandLine; +using System; using System.Globalization; using System.IO; +using CommandLine; +using Logging; namespace RInterop { public class CommandLineOptions { [Option('s', "schema", Required = true, - HelpText = "Path to schema binary file containing types to serialize and deserialize input data and output data sent to and received from the R package, respectively")] + HelpText = + "Path to schema binary file containing types to serialize and deserialize input data and output data sent to and received from the R package, respectively" + )] public string SchemaBinaryPath { get; set; } - [Option('r', "rpackage", Required = true, - HelpText = "Path to R package file containing statistical functions (optional if packages are already installed)")] + [Option('r', "rpackage", Required = false, + HelpText = + "Path to R package file containing statistical functions (optional if packages are already installed)")] public string RPackagePath { get; set; } + [Option('t', "typemap", Required = false, + HelpText = "Path to JSON file with type map (optional)")] + public string TypeMapJsonPath { get; set; } + // Omitting long name, default --verbose [Option(HelpText = "Prints all messages to standard output.")] public bool Verbose { get; set; } public static bool ParseArguments(string[] args, CommandLineOptions options) { + if (args == null) throw new ArgumentNullException(nameof(args)); + if (options == null) throw new ArgumentNullException(nameof(options)); + var logger = DependencyFactory.Resolve(); if (!Parser.Default.ParseArguments(args, options)) { return false; } - - if (!File.Exists(options.RPackagePath)) - { - logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "RPackage {0} not found", options.RPackagePath)); - return false; - } if (!File.Exists(options.SchemaBinaryPath)) { - logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Schema binary {0} not found", options.SchemaBinaryPath)); + logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Schema binary {0} not found", + options.SchemaBinaryPath)); + return false; + } + + if (!string.IsNullOrEmpty(options.RPackagePath) && !File.Exists(options.RPackagePath)) + { + logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "R Package {0} not found", + options.RPackagePath)); + return false; + } + + if (!string.IsNullOrEmpty(options.TypeMapJsonPath) && !File.Exists(options.TypeMapJsonPath)) + { + logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Type Map JSON file {0} not found", + options.TypeMapJsonPath)); return false; } return true; } } -} +} \ No newline at end of file diff --git a/RInterop/Config.cs b/RInterop/Config.cs index 98fbd70..695fd57 100644 --- a/RInterop/Config.cs +++ b/RInterop/Config.cs @@ -6,6 +6,6 @@ public static string RPackageName { get; set; } - public static SerializationTypeMaps SerializationTypeMaps { get; set; } + public static SerializationTypeMap SerializationTypeMap { get; set; } } } diff --git a/RInterop/DependencyFactory.cs b/RInterop/DependencyFactory.cs index 8ee9952..ae32ee8 100644 --- a/RInterop/DependencyFactory.cs +++ b/RInterop/DependencyFactory.cs @@ -1,36 +1,25 @@ -using Microsoft.Practices.Unity; -using Microsoft.Practices.Unity.Configuration; -using System.Configuration; +using Logging; +using Microsoft.Practices.Unity; namespace RInterop { public class DependencyFactory { - public static IUnityContainer Container { get; private set; } - - static DependencyFactory() + public static void Initialize() { var container = new UnityContainer(); - - var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity"); - section?.Configure(container); Container = container; - - Container.RegisterType("TraceLoggerFactory", - new InjectionFactory(c => new TraceLoggerFactory())); - Container.RegisterInstance( - Container - .Resolve("TraceLoggerFactory") - .Create("RInterop")); - + Container.RegisterInstance(new TraceLogger("%temp%", "RInterop")); Resolve().LogInformation("Completed registering dependencies"); } + public static IUnityContainer Container { get; private set; } + public static T Resolve() { T ret = default(T); - if (Container.IsRegistered(typeof(T))) + if (Container.IsRegistered(typeof (T))) { ret = Container.Resolve(); } @@ -38,4 +27,4 @@ namespace RInterop return ret; } } -} +} \ No newline at end of file diff --git a/RInterop/GlobalAssemblyInfo.cs b/RInterop/GlobalAssemblyInfo.cs index 8f0115d..496245f 100644 --- a/RInterop/GlobalAssemblyInfo.cs +++ b/RInterop/GlobalAssemblyInfo.cs @@ -10,7 +10,7 @@ [assembly: System.Reflection.AssemblyCopyright("Copyright (c) Microsoft Corporation 2016")] [assembly: System.CLSCompliant(false)] -[assembly: System.Reflection.AssemblyVersion("1.1.41577.4")] -[assembly: System.Reflection.AssemblyFileVersion("1.1.41577.4")] +[assembly: System.Reflection.AssemblyVersion("1.2.3.4")] +[assembly: System.Reflection.AssemblyFileVersion("1.2.3.4")] diff --git a/RInterop/IBootstrapper.cs b/RInterop/IBootstrapper.cs new file mode 100644 index 0000000..e5a7292 --- /dev/null +++ b/RInterop/IBootstrapper.cs @@ -0,0 +1,7 @@ +namespace RInterop +{ + public interface IBootstrapper + { + void Start(string[] args); + } +} \ No newline at end of file diff --git a/RInterop/ILogger.cs b/RInterop/ILogger.cs deleted file mode 100644 index 985f5cc..0000000 --- a/RInterop/ILogger.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace RInterop -{ - public interface ILogger - { - void LogInformation(string message, params object[] parameters); - - void Close(); - } -} diff --git a/RInterop/ILoggerFactory.cs b/RInterop/ILoggerFactory.cs deleted file mode 100644 index 50b2f03..0000000 --- a/RInterop/ILoggerFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace RInterop -{ - public interface ILoggerFactory - { - ILogger Create(string prefix); - - ILogger Create(string rootFolder, string prefix); - } -} diff --git a/RInterop/IR.cs b/RInterop/IR.cs index 0128cc9..187aab9 100644 --- a/RInterop/IR.cs +++ b/RInterop/IR.cs @@ -1,16 +1,12 @@ -using System.Collections.Generic; -using System.ServiceModel; +using System.ServiceModel; namespace RInterop { [ServiceContract] - [ServiceKnownType("GetKnownTypes", typeof(KnownTypesProvider))] + [ServiceKnownType("GetKnownTypes", typeof (KnownTypesProvider))] public interface IR { [OperationContract] object Execute(dynamic input); - - [OperationContract] - void Initialize(Dictionary inputTypeMap, Dictionary outputTypeMap); } -} +} \ No newline at end of file diff --git a/RInterop/Models.bond b/RInterop/Models.bond new file mode 100644 index 0000000..c2d6009 --- /dev/null +++ b/RInterop/Models.bond @@ -0,0 +1,21 @@ +namespace RInterop + +using Type = int64; + +struct SerializationTypeMap +{ + 0: map InputTypeMap; + 1: map OutputTypeMap; +} + +struct Map +{ + 0: string Function; + 1: string InputType; + 2: string OutputType; +} + +struct TypeMap +{ + 0: vector Mapping; +} \ No newline at end of file diff --git a/RInterop/Program.cs b/RInterop/Program.cs index 0ded1bf..b790147 100644 --- a/RInterop/Program.cs +++ b/RInterop/Program.cs @@ -1,96 +1,13 @@ -using System; -using System.Globalization; -using System.IO; -using System.ServiceModel; -using System.ServiceModel.Description; -using System.Threading; - -namespace RInterop +namespace RInterop { - class Program + public class Program { public static void Main(string[] args) { - ILogger logger = DependencyFactory.Resolve(); + DependencyFactory.Initialize(); - logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Working directory: {0}", Environment.CurrentDirectory)); - logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Arguments: {0}", string.Join(" ", args))); - - var options = new CommandLineOptions(); - if (!CommandLineOptions.ParseArguments(args, options)) - { - DependencyFactory.Resolve().LogInformation(string.Format(CultureInfo.InvariantCulture, "Invalid arguments {0}", string.Join(" ", args))); - - logger.LogInformation(@"Usage: RInterop.exe --s """" --r """""); - logger.LogInformation(@"Example: RInterop.exe --s ""C:\Temp\Schemas.dll"" --r ""C:\Temp\RPackage.zip"""); - logger.LogInformation("Press any key to continue..."); - Console.ReadLine(); - return; - } - - try - { - var filename = Path.GetFileName(options.RPackagePath); - Config.RPackageName = filename.Substring(0, filename.IndexOf("_", StringComparison.Ordinal)); - } - catch (Exception exception) - { - logger.LogInformation("Exception while extracting R package name: {0}", exception); - return; - } - - Config.SchemaBinaryPath = options.SchemaBinaryPath; - REngineWrapper.InstallPackages(options.RPackagePath, options.SchemaBinaryPath); - - StartService(); - } - - private static void StartService() - { - EventWaitHandle startedEvent = new EventWaitHandle(false, EventResetMode.ManualReset, @"Global\RInteropStarted"); - - using (ServiceHost host = new ServiceHost(typeof(R), new Uri("net.pipe://RInterop"))) - { - NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None); - binding.MaxReceivedMessageSize = Int32.MaxValue; - binding.MaxBufferSize = Int32.MaxValue; - - host.AddServiceEndpoint(typeof(IR), - binding, - "Execute"); - - host.AddServiceEndpoint(typeof(IR), - binding, - "Initialize"); - - // Check to see if the service host already has a ServiceMetadataBehavior - ServiceMetadataBehavior smb = host.Description.Behaviors.Find(); - // If not, add one - if (smb == null) - smb = new ServiceMetadataBehavior(); - - smb.HttpGetEnabled = false; - smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy15; - host.Description.Behaviors.Add(smb); - - // Add MEX endpoint - host.AddServiceEndpoint( - ServiceMetadataBehavior.MexContractName, - MetadataExchangeBindings.CreateMexNamedPipeBinding(), - "mex"); - - // Add application endpoint - host.AddServiceEndpoint(typeof(IR), new NetNamedPipeBinding(), ""); - - host.Open(); - - startedEvent.Set(); - - DependencyFactory.Resolve().LogInformation("Service is available. Press to exit."); - Console.ReadLine(); - - host.Close(); - } + var bootstrapper = new Bootstrapper(); + bootstrapper.Start(args); } } -} +} \ No newline at end of file diff --git a/RInterop/Properties/AssemblyInfo.cs b/RInterop/Properties/AssemblyInfo.cs index cb07273..cb31569 100644 --- a/RInterop/Properties/AssemblyInfo.cs +++ b/RInterop/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Microsoft")] [assembly: AssemblyProduct("RInterop")] -//[assembly: AssemblyCopyright("Copyright © 2016")] // Added in generated GlobalAssemblyInfo.cs file +//[assembly: AssemblyCopyright("Copyright © 2016")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -31,5 +31,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -//[assembly: AssemblyVersion("0.4.7.0")] // Added in generated GlobalAssemblyInfo.cs file -//[assembly: AssemblyFileVersion("0.4.7.0")] // Added in generated GlobalAssemblyInfo.cs file +//[assembly: AssemblyVersion("0.4.7.0")] +//[assembly: AssemblyFileVersion("0.4.7.0")] diff --git a/RInterop/R.cs b/RInterop/R.cs index 1232fa8..e53d379 100644 --- a/RInterop/R.cs +++ b/RInterop/R.cs @@ -1,16 +1,17 @@ -using Newtonsoft.Json; -using RDotNet; -using System; +using System; using System.Collections.Generic; +using System.Configuration; using System.Globalization; using System.Linq; -using System.Reflection; using System.ServiceModel; +using Logging; +using Newtonsoft.Json; +using RDotNet; namespace RInterop { [ServiceBehavior( - ConcurrencyMode = ConcurrencyMode.Multiple, + ConcurrencyMode = ConcurrencyMode.Multiple, IncludeExceptionDetailInFaults = true, InstanceContextMode = InstanceContextMode.PerCall)] public class R : IR @@ -21,72 +22,65 @@ namespace RInterop private readonly ILogger _logger; + private readonly bool _shouldLogCommand; + private readonly bool _shouldLogResult; + public R() : this(DependencyFactory.Resolve()) { _engine = REngineWrapper.REngine; } - + public R(ILogger logger) { _logger = logger; + _shouldLogCommand = Convert.ToBoolean(ConfigurationManager.AppSettings["LogCommand"]); + _shouldLogResult = Convert.ToBoolean(ConfigurationManager.AppSettings["LogResult"]); } public object Execute(dynamic input) { // R.Net's REngine cannot perform parallel evaluation of R scripts - lock(LockObject) + lock (LockObject) { try { - Type inputType = Config.SerializationTypeMaps.InputTypeMap[input.config.fn]; - Type outputType = Config.SerializationTypeMaps.OutputTypeMap[input.config.fn]; + Type inputType = Config.SerializationTypeMap.InputTypeMap[input.config.fn]; + Type outputType = Config.SerializationTypeMap.OutputTypeMap[input.config.fn]; object deserializedInput = Convert.ChangeType(input, inputType); var command = string.Format(CultureInfo.InvariantCulture, - "{0}::run(\"{1}\")", + @"{0}::run(""{1}"")", Config.RPackageName, JsonConvert.SerializeObject(deserializedInput).Replace("\"", "\\\"")); + + if (_shouldLogCommand) _logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Command: {0}", command)); - _logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Evaluating: {0}", command)); - - var result = _engine - .Evaluate(command) + var result = _engine.Evaluate(command) .AsList() .First() .AsCharacter() .First(); - _logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Result: {0}", result)); + if (_shouldLogResult) _logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Result: {0}", result)); return JsonConvert.DeserializeObject(result, outputType); } + catch (KeyNotFoundException e) + { + string reason = string.Format(CultureInfo.InvariantCulture, + @"Could not find function ""{0}"" in the TypeMap.json configuration. Exception: {1}", + input.config.fn, e); + _logger.LogError(reason); + throw new FaultException(new FaultReason(reason)); + } catch (Exception e) { - _logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Exception during evaluation {0}", e)); - return null; + string reason = string.Format(CultureInfo.InvariantCulture, + "Evaluation failed. Exception: {0}", e); + _logger.LogError(reason); + throw new FaultException(new FaultReason(reason)); } } } - - public void Initialize(Dictionary inputTypeMap, Dictionary outputTypeMap) - { - Assembly assembly = Assembly.LoadFrom(Config.SchemaBinaryPath); - Config.SerializationTypeMaps = new SerializationTypeMaps(); - foreach (string key in inputTypeMap.Keys) - { - Config.SerializationTypeMaps.InputTypeMap[key] = assembly - .GetTypes() - .First(a => a.FullName.Equals(inputTypeMap[key])); - _logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Got input type mapping: {0} > {1}", key, inputTypeMap[key])); - } - - foreach (string key in outputTypeMap.Keys) - { - Config.SerializationTypeMaps.OutputTypeMap[key] = assembly - .GetTypes() - .First(a => a.FullName.Equals(outputTypeMap[key])); - _logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Got output type mapping: {0} > {1}", key, outputTypeMap[key])); - } - } } -} +} \ No newline at end of file diff --git a/RInterop/REngineWrapper.cs b/RInterop/REngineWrapper.cs index 1edf24a..7613e1f 100644 --- a/RInterop/REngineWrapper.cs +++ b/RInterop/REngineWrapper.cs @@ -1,6 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; +using Logging; using RDotNet; namespace RInterop @@ -10,7 +13,7 @@ namespace RInterop private static REngine _engine; private static readonly object LockObject = new object(); - + public static REngine REngine { get @@ -21,8 +24,6 @@ namespace RInterop { if (_engine == null) { - var logger = DependencyFactory.Resolve(); - _engine = REngine.GetInstance(); } } @@ -32,34 +33,71 @@ namespace RInterop } } - public static void InstallPackages(string rPackagePath, string schemaBinaryPath) + public static void InstallPackages(string rPackagePath) { - var engine = REngineWrapper.REngine; + var engine = REngine; var logger = DependencyFactory.Resolve(); - engine.Evaluate(string.Format( - CultureInfo.InvariantCulture, + var userDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "DEARPackages").Replace("\\", "/"); + if (!Directory.Exists(userDirectory)) Directory.CreateDirectory(userDirectory); + engine.Evaluate($@".libPaths(""{userDirectory}"")"); + engine.Evaluate(@".libPaths()"); + engine.Evaluate(string.Format(CultureInfo.InvariantCulture, @"install.packages(""{0}"", verbose = TRUE, dependencies = TRUE, type = ""win.binary"")", rPackagePath.Replace(@"\", @"\\"))); - logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Installed R package {0}", rPackagePath)); - - List imports = engine - .Evaluate(@"packageDescription(""" + Config.RPackageName + @""")$Imports") - .AsCharacter() - .First() - .Replace(" ", string.Empty) - .Split(',') - .ToList(); + logger.LogInformation(string.Format(CultureInfo.InvariantCulture, @"Installed R package ""{0}""", rPackagePath)); + + List imports; + try + { + imports = engine + .Evaluate(string.Format(CultureInfo.InvariantCulture, + @"packageDescription(""{0}"")$Imports", + Config.RPackageName)) + .AsCharacter() + .FirstOrDefault()? + .Replace(" ", string.Empty) + .Split(',') + .ToList(); + } + catch (Exception e) + { + logger.LogInformation(string.Format(CultureInfo.InvariantCulture, + @"Could not get the dependent packages from provided package. Please manually install package using the ""install.packages"" command. Exception: {0}", + e)); + throw; + } + + if (imports == null) return; foreach (string packageName in imports) { - logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Installing dependent R package {0}", packageName)); - engine.Evaluate(string.Format(CultureInfo.InvariantCulture, - @"if (!require(""{0}"")) -install.packages(""{0}"", repo = ""https://cran.rstudio.com/"", dependencies = TRUE)", + logger.LogInformation(string.Format(CultureInfo.InvariantCulture, + @"Installing dependent R package ""{0}""", packageName)); - logger.LogInformation(string.Format(CultureInfo.InvariantCulture, "Installed R package {0}", packageName)); + + try + { + engine.Evaluate(string.Format(CultureInfo.InvariantCulture, + @"if (!require(""{0}"")) +install.packages(""{0}"", repo = ""https://cran.rstudio.com/"", dependencies = TRUE)", + packageName)); + engine.Evaluate(string.Format(CultureInfo.InvariantCulture, + @"if (!require(""{0}"")) stop(""Package {0} did not install correctly."")", + packageName)); + } + catch (Exception e) + { + logger.LogInformation(string.Format(CultureInfo.InvariantCulture, + @"Could not automatically install dependent package ""{0}"". Please manually install R package using the following command: install.packages(""{0}"") +Exception: {1}", + packageName, + e)); + throw; + } + + logger.LogInformation(string.Format(CultureInfo.InvariantCulture, @"Installed dependent R package ""{0}""", packageName)); } } } -} +} \ No newline at end of file diff --git a/RInterop/RInterop.csproj b/RInterop/RInterop.csproj index bc72485..88e81c8 100644 --- a/RInterop/RInterop.csproj +++ b/RInterop/RInterop.csproj @@ -1,6 +1,6 @@  - + Debug @@ -27,6 +27,7 @@ true ManagedMinimumRules.ruleset false + false pdbonly @@ -41,19 +42,23 @@ - ..\packages\Bond.Core.CSharp.5.0.0\lib\net45\Bond.dll + ..\packages\Bond.Core.CSharp.5.2.0\lib\net45\Bond.dll True - ..\packages\Bond.Core.CSharp.5.0.0\lib\net45\Bond.Attributes.dll + ..\packages\Bond.Core.CSharp.5.2.0\lib\net45\Bond.Attributes.dll True - ..\packages\Bond.Core.CSharp.5.0.0\lib\net45\Bond.IO.dll + ..\packages\Bond.Core.CSharp.5.2.0\lib\net45\Bond.IO.dll True - ..\packages\Bond.Runtime.CSharp.5.0.0\lib\net45\Bond.JSON.dll + ..\packages\Bond.Runtime.CSharp.5.2.0\lib\net45\Bond.JSON.dll + True + + + ..\packages\Bond.Core.CSharp.5.2.0\lib\net45\Bond.Reflection.dll True @@ -108,30 +113,34 @@ + - - + - - + Designer - PreserveNewest Designer - + + + + + {da0e6a71-5a37-4ee6-87a4-c82c93ef908d} + Logging + @@ -142,36 +151,35 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + - - 1 - 1 - - - - $([MSBuild]::Modulo($([System.DateTime]::UtcNow.ToString("yyyyMMdd")), 65536)) - 4 - $(Build).$(Revision) - - + 2 + 0 $([MSBuild]::Modulo($([System.Math]::Floor($(BUILD_BUILDNUMBER))), 65536)) $(BUILD_BUILDNUMBER.Substring($([MSBuild]::Add($(BUILD_BUILDNUMBER.IndexOf('.')), 1)))) + + + 1 + 2 + 3 + 4 + + - - + +